diff --git a/pkg/sentry/fsimpl/proc/task_files.go b/pkg/sentry/fsimpl/proc/task_files.go index 50f6fcaf23..970eee5644 100644 --- a/pkg/sentry/fsimpl/proc/task_files.go +++ b/pkg/sentry/fsimpl/proc/task_files.go @@ -1349,6 +1349,7 @@ type mountInfoData struct { } var _ dynamicInode = (*mountInfoData)(nil) +var _ vfs.PollableDynamicBytesSource = (*mountInfoData)(nil) // Generate implements vfs.DynamicBytesSource.Generate. func (i *mountInfoData) Generate(ctx context.Context, buf *bytes.Buffer) error { @@ -1369,6 +1370,15 @@ func (i *mountInfoData) Generate(ctx context.Context, buf *bytes.Buffer) error { return i.task.Kernel().VFS().GenerateProcMountInfo(ctx, rootDir, buf) } +// GetDynamicBytesPoller implements vfs.PollableDynamicBytesSource.GetDynamicBytesPoller. +func (i *mountInfoData) GetDynamicBytesPoller(ctx context.Context) *vfs.DynamicBytesPoller { + mntns := i.task.MountNamespace() + if mntns == nil { + return nil + } + return &mntns.Poller +} + // mountsData is used to implement /proc/[pid]/mounts. // // +stateify savable @@ -1380,6 +1390,7 @@ type mountsData struct { } var _ dynamicInode = (*mountsData)(nil) +var _ vfs.PollableDynamicBytesSource = (*mountsData)(nil) // Generate implements vfs.DynamicBytesSource.Generate. func (i *mountsData) Generate(ctx context.Context, buf *bytes.Buffer) error { @@ -1400,6 +1411,15 @@ func (i *mountsData) Generate(ctx context.Context, buf *bytes.Buffer) error { return i.task.Kernel().VFS().GenerateProcMounts(ctx, rootDir, buf) } +// GetDynamicBytesPoller implements vfs.PollableDynamicBytesSource.GetDynamicBytesPoller. +func (i *mountsData) GetDynamicBytesPoller(ctx context.Context) *vfs.DynamicBytesPoller { + mntns := i.task.MountNamespace() + if mntns == nil { + return nil + } + return &mntns.Poller +} + // +stateify savable type namespaceSymlink struct { kernfs.StaticSymlink diff --git a/pkg/sentry/vfs/mount.go b/pkg/sentry/vfs/mount.go index f9a16006cb..53808ad1a9 100644 --- a/pkg/sentry/vfs/mount.go +++ b/pkg/sentry/vfs/mount.go @@ -351,12 +351,18 @@ func (vfs *VirtualFilesystem) attachTreeLocked(ctx context.Context, mnt *Mount, owner = mntns.Owner mntns.DecRef(ctx) } + notifiedNS := map[*MountNamespace]struct{}{mp.mount.ns: {}} + mp.mount.ns.notify() for pmnt := range propMnts { vfs.commitMount(ctx, pmnt) if pmnt.parent().ns.Owner != owner { vfs.lockMountTree(pmnt) } pmnt.locked = false + if _, ok := notifiedNS[pmnt.parent().ns]; !ok { + notifiedNS[pmnt.parent().ns] = struct{}{} + pmnt.parent().ns.notify() + } } return nil } @@ -782,7 +788,11 @@ func (vfs *VirtualFilesystem) RemountAt(ctx context.Context, creds *auth.Credent if !vfs.validInMountNS(ctx, mnt) { return linuxerr.EINVAL } - return mnt.setMountOptions(opts) + if err := mnt.setMountOptions(opts); err != nil { + return err + } + mnt.ns.notify() + return nil } // MountAt creates and mounts a Filesystem configured by the given arguments. @@ -927,11 +937,21 @@ func (vfs *VirtualFilesystem) umountTreeLocked(mnt *Mount, opts *umountRecursive vfs.unlockPropagationMounts(mnt) } umountMnts := mnt.submountsLocked() + // Collect affected namespaces before umounting, since ns pointers may be + // cleared during disconnect. + affectedNS := make(map[*MountNamespace]struct{}) + for _, m := range umountMnts { + affectedNS[m.ns] = struct{}{} + } for _, mnt := range umountMnts { vfs.umount(mnt) } if opts.propagate { - umountMnts = append(umountMnts, vfs.propagateUmount(umountMnts)...) + propMnts := vfs.propagateUmount(umountMnts) + for _, m := range propMnts { + affectedNS[m.ns] = struct{}{} + } + umountMnts = append(umountMnts, propMnts...) } vfs.mounts.seq.BeginWrite() @@ -965,6 +985,9 @@ func (vfs *VirtualFilesystem) umountTreeLocked(mnt *Mount, opts *umountRecursive vfs.setPropagation(mnt, linux.MS_PRIVATE) } vfs.mounts.seq.EndWrite() + for ns := range affectedNS { + ns.notify() + } } // +checklocks:vfs.mountMu @@ -989,6 +1012,7 @@ func (vfs *VirtualFilesystem) changeMountpoint(mnt *Mount, mp VirtualDentry) { mp.IncRef() vfs.connectLocked(mnt, mp, mp.mount.ns) mp.dentry.mu.Unlock() + mp.mount.ns.notify() } // migrateChildrenNs recursively migrates mnt's children into newNs. diff --git a/pkg/sentry/vfs/namespace.go b/pkg/sentry/vfs/namespace.go index a241e42fca..14799fcba4 100644 --- a/pkg/sentry/vfs/namespace.go +++ b/pkg/sentry/vfs/namespace.go @@ -76,6 +76,11 @@ type MountNamespace struct { // anonymous mount namespaces may have originatorID == 0 (such as "fresh" // trees created using fsmount(2)). originatorID uint64 + + // Poller is notified on every mount namespace change (mount, unmount, + // remount, move). Used by /proc/[pid]/mounts and /proc/[pid]/mountinfo + // to support poll(). + Poller DynamicBytesPoller } // Namespace is the namespace interface. @@ -390,3 +395,12 @@ func (mntns *MountNamespace) anonCanBeOperatedOn(by *MountNamespace) bool { return mntns.originatorID == 0 || mntns.originatorID == by.ID } + +// notify notifies poll waiters of a mount namespace change. +// Analogous to Linux's touch_mnt_namespace(). +func (mntns *MountNamespace) notify() { + if mntns == nil { + return + } + mntns.Poller.Notify() +} diff --git a/test/syscalls/linux/mount.cc b/test/syscalls/linux/mount.cc index 4f355bee97..9264aeae9b 100644 --- a/test/syscalls/linux/mount.cc +++ b/test/syscalls/linux/mount.cc @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -2898,6 +2899,181 @@ TEST(MountTest, OverlayfsOnGoferBehavior) { } } +TEST(MountTest, PollMountInfo) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + for (const char* path : {"/proc/self/mounts", "/proc/self/mountinfo"}) { + FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path, O_RDONLY)); + int epfd_raw = epoll_create1(0); + ASSERT_GE(epfd_raw, 0) << "epoll_create1 failed: " << strerror(errno); + FileDescriptor epfd(epfd_raw); + + struct epoll_event ev = {}; + ev.events = EPOLLPRI | EPOLLERR; + ev.data.fd = fd.get(); + ASSERT_THAT(epoll_ctl(epfd.get(), EPOLL_CTL_ADD, fd.get(), &ev), + SyscallSucceeds()); + + // Drains one event and assert it has EPOLLPRI or EPOLLERR. + auto expectEvent = [&](absl::string_view label) { + struct epoll_event events[1] = {}; + int nfds = epoll_wait(epfd.get(), events, 1, 0); + ASSERT_GE(nfds, 0) << label << ": epoll_wait failed: " << strerror(errno); + ASSERT_EQ(nfds, 1) << label << ": expected 1 event"; + EXPECT_TRUE(events[0].events & (EPOLLPRI | EPOLLERR)) << label; + }; + + // Asserts no events pending. + auto expectNoEvent = [&](absl::string_view label) { + struct epoll_event events[1] = {}; + int nfds = epoll_wait(epfd.get(), events, 1, 0); + ASSERT_GE(nfds, 0) << label << ": epoll_wait failed: " << strerror(errno); + EXPECT_EQ(nfds, 0) << label; + }; + + // Initially, there should be no events. + expectNoEvent("initial"); + + // mount() triggers event. + auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto mnt = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir.path(), kTmpfs, 0, "", 0)); + expectEvent("mount"); + + // Event is consumed by poll itself, no read needed. + expectNoEvent("after mount consumed"); + + // remount triggers event. + ASSERT_THAT( + mount("", dir.path().c_str(), nullptr, MS_REMOUNT | MS_RDONLY, nullptr), + SyscallSucceeds()); + expectEvent("remount"); + expectNoEvent("after remount consumed"); + + // Undo the read-only remount so umount works cleanly. + ASSERT_THAT(mount("", dir.path().c_str(), nullptr, MS_REMOUNT, nullptr), + SyscallSucceeds()); + // Drain the remount-undo event. + expectEvent("remount undo"); + + // Umount triggers event. + mnt.Release(); + ASSERT_THAT(umount2(dir.path().c_str(), 0), SyscallSucceeds()); + expectEvent("umount"); + expectNoEvent("after umount consumed"); + + // Move mount triggers event. + // + // MS_MOVE is not allowed under a MS_SHARED parent mount, so we create a + // private parent mount containing both src and dst. + auto const privateParent = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto privateMount = ASSERT_NO_ERRNO_AND_VALUE( + Mount("", privateParent.path(), kTmpfs, 0, "", 0)); + expectEvent("move setup private parent mount"); + + ASSERT_THAT(mount("", privateParent.path().c_str(), "", MS_PRIVATE, ""), + SyscallSucceeds()); + + auto const src = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(privateParent.path())); + auto const dst = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(privateParent.path())); + auto srcMnt = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", src.path(), kTmpfs, 0, "", 0)); + expectEvent("move setup src mount"); + + auto dstMnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(src.path(), dst.path(), "", MS_MOVE, "", 0)); + srcMnt.Release(); + expectEvent("move mount"); + expectNoEvent("after move consumed"); + } +} + +TEST(MountTest, MountInfoAfterUnshare) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + + for (const char* path : {"/proc/self/mounts", "/proc/self/mountinfo"}) { + int pipefd[2]; + ASSERT_THAT(pipe(pipefd), SyscallSucceeds()); + + FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path, O_RDONLY)); + int epfd_raw = epoll_create1(0); + ASSERT_GE(epfd_raw, 0) << "epoll_create1 failed: " << strerror(errno); + FileDescriptor epfd(epfd_raw); + + struct epoll_event ev = {}; + ev.events = EPOLLPRI | EPOLLERR; + ev.data.fd = fd.get(); + ASSERT_THAT(epoll_ctl(epfd.get(), EPOLL_CTL_ADD, fd.get(), &ev), + SyscallSucceeds()); + + // Initially, no events on fd. + auto expectNoEvent = [](int epfd_raw) { + struct epoll_event events[1] = {}; + int nfds = epoll_wait(epfd_raw, events, 1, 0); + return nfds == 0; + }; + ASSERT_TRUE(expectNoEvent(epfd.get())); + + pid_t child = fork(); + if (child == 0) { + close(pipefd[1]); + + auto childExpectEvent = [](int epfd_raw) { + struct epoll_event events[1] = {}; + int nfds = epoll_wait(epfd_raw, events, 1, 0); + TEST_PCHECK(nfds == 1); + TEST_CHECK(events[0].events & (EPOLLPRI | EPOLLERR)); + }; + + // Unshare into a new mount namespace. + TEST_PCHECK(unshare(CLONE_NEWNS) == 0); + + // Wait for the parent to perform a mount in the parent's mountns. + char c; + TEST_PCHECK(read(pipefd[0], &c, 1) == 1); + close(pipefd[0]); + + // The inherited fd was opened before unshare(CLONE_NEWNS), so it should + // see the mount event from the parent's mount namespace. + childExpectEvent(epfd.get()); + + // Reading from the inherited fd should still give info about the old + // mount namespace (viz., where the parent mounted dir.path()). + lseek(fd.get(), 0, SEEK_SET); + char buf[65536]; + size_t total = 0; + int n; + while ((total < sizeof(buf) - 1) && + (n = read(fd.get(), buf + total, sizeof(buf) - 1 - total)) > 0) { + total += n; + } + TEST_PCHECK(n >= 0); + TEST_CHECK(absl::StrContains(absl::string_view(buf, total), dir.path())); + + _exit(0); + } + close(pipefd[0]); + ASSERT_THAT(child, SyscallSucceeds()); + + // Perform a mount in the parent's mount namespace. + auto mnt = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir.path(), kTmpfs, 0, "", 0)); + + // Notify the child that the mount is complete. + char c = 'a'; + ASSERT_THAT(write(pipefd[1], &c, 1), SyscallSucceedsWithValue(1)); + close(pipefd[1]); + + int status; + ASSERT_THAT(waitpid(child, &status, 0), SyscallSucceedsWithValue(child)); + ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); + } +} + } // namespace } // namespace testing