Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions pkg/sentry/fsimpl/proc/task_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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
Expand Down
28 changes: 26 additions & 2 deletions pkg/sentry/vfs/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions pkg/sentry/vfs/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
}
162 changes: 162 additions & 0 deletions test/syscalls/linux/mount.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <sys/mman.h>
#include <sys/mount.h>
Expand Down Expand Up @@ -2898,6 +2899,167 @@ TEST(MountTest, OverlayfsOnGoferBehavior) {
}
}

TEST(MountTest, PollMountsAndMountinfo) {
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, PollMountsAndMountinfoWithUnshare) {
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());
_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
Expand Down
Loading