From 598fbe650377baa89de751f6cf5e771dd321e101 Mon Sep 17 00:00:00 2001 From: Lucas Manning Date: Thu, 5 Oct 2023 12:04:57 -0700 Subject: [PATCH] Implement MS_REC. PiperOrigin-RevId: 571091808 --- pkg/sentry/syscalls/linux/sys_mount.go | 11 +- pkg/sentry/vfs/mount.go | 66 +++++++--- pkg/sentry/vfs/namespace.go | 20 ++- pkg/sentry/vfs/propagation.go | 69 +++++++---- test/syscalls/linux/BUILD | 1 + test/syscalls/linux/mount.cc | 162 ++++++++++++++++++++++++- 6 files changed, 275 insertions(+), 54 deletions(-) diff --git a/pkg/sentry/syscalls/linux/sys_mount.go b/pkg/sentry/syscalls/linux/sys_mount.go index dbb6dd3a1..24d6494aa 100644 --- a/pkg/sentry/syscalls/linux/sys_mount.go +++ b/pkg/sentry/syscalls/linux/sys_mount.go @@ -46,7 +46,7 @@ func Mount(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, // Silently allow MS_NOSUID, since we don't implement set-id bits anyway. const unsupported = linux.MS_REMOUNT | linux.MS_UNBINDABLE | linux.MS_MOVE | - linux.MS_REC | linux.MS_NODIRATIME + linux.MS_NODIRATIME // Linux just allows passing any flags to mount(2) - it won't fail when // unknown or unsupported flags are passed. Since we don't implement @@ -67,7 +67,7 @@ func Mount(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, } defer target.Release(t) - if flags&linux.MS_BIND == linux.MS_BIND { + if flags&linux.MS_BIND != 0 { var sourcePath fspath.Path sourcePath, err = copyInPath(t, sourceAddr) if err != nil { @@ -79,11 +79,10 @@ func Mount(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, return 0, nil, err } defer sourceTpop.Release(t) - return 0, nil, t.Kernel().VFS().BindAt(t, creds, &sourceTpop.pop, &target.pop) + return 0, nil, t.Kernel().VFS().BindAt(t, creds, &sourceTpop.pop, &target.pop, flags&linux.MS_REC != 0) } - const propagationFlags = linux.MS_SHARED | linux.MS_PRIVATE | linux.MS_SLAVE | linux.MS_UNBINDABLE - if propFlag := flags & propagationFlags; propFlag != 0 { - return 0, nil, t.Kernel().VFS().SetMountPropagationAt(t, creds, &target.pop, uint32(propFlag)) + if flags&(linux.MS_SHARED|linux.MS_PRIVATE|linux.MS_SLAVE|linux.MS_UNBINDABLE) != 0 { + return 0, nil, t.Kernel().VFS().SetMountPropagationAt(t, creds, &target.pop, uint32(flags)) } // Only copy in source, fstype, and data if we are doing a normal mount. diff --git a/pkg/sentry/vfs/mount.go b/pkg/sentry/vfs/mount.go index 1c1dea3eb..65409e04f 100644 --- a/pkg/sentry/vfs/mount.go +++ b/pkg/sentry/vfs/mount.go @@ -118,10 +118,6 @@ type Mount struct { // Mount.EndWrite(). The MSB of writers is set if MS_RDONLY is in effect. // writers is accessed using atomic memory operations. writers atomicbitops.Int64 - - // pendingChildren is a list of new child mounts that have not yet been - // connected to this mount as the parent. - pendingChildren []*Mount } func newMount(vfs *VirtualFilesystem, fs *Filesystem, root *Dentry, mntns *MountNamespace, opts *MountOptions) *Mount { @@ -241,12 +237,12 @@ func (vfs *VirtualFilesystem) MountDisconnected(ctx context.Context, creds *auth return newMount(vfs, fs, root, nil /* mntns */, opts), nil } -// attachMountLocked attaches mnt to vd and propagates the mount to vd.mount's -// peers and followers. This method is analogous to +// attachTreeLocked attaches the mount tree at mnt to vd and propagates the +// mount to vd.mount's peers and followers. This method is analogous to // fs/namespace.c:attach_recursive_mnt() in Linux. // // +checklocks:vfs.mountMu -func (vfs *VirtualFilesystem) attachMountLocked(ctx context.Context, mnt *Mount, vd VirtualDentry) error { +func (vfs *VirtualFilesystem) attachTreeLocked(ctx context.Context, mnt *Mount, vd VirtualDentry) error { vdCleanup := cleanup.Make(func() { vd.DecRef(ctx) }) @@ -256,8 +252,11 @@ func (vfs *VirtualFilesystem) attachMountLocked(ctx context.Context, mnt *Mount, if vd.mount.neverConnected() { return linuxerr.EINVAL } - if vd.mount.ns.mounts+1 > MountMax { - return linuxerr.ENOSPC + defer func() { + vd.mount.ns.pending = 0 + }() + if err := vd.mount.ns.checkMountCount(ctx, mnt); err != nil { + return err } if vd.mount.isShared { if err := vfs.allocMountGroupIDs(mnt, true); err != nil { @@ -270,7 +269,10 @@ func (vfs *VirtualFilesystem) attachMountLocked(ctx context.Context, mnt *Mount, // force. vfs.freeMountGroupIDs(mnt.submountsLocked()) // +checklocksforce for pmnt := range propMnts { - vfs.abortTree(ctx, pmnt) // +checklocksforce + if !pmnt.parent().neverConnected() { + pmnt.parent().ns.pending -= pmnt.countSubmountsLocked() // +checklocksforce + } + vfs.abortUncommitedMount(ctx, pmnt) // +checklocksforce } }) defer cleanup.Clean() @@ -287,8 +289,9 @@ func (vfs *VirtualFilesystem) attachMountLocked(ctx context.Context, mnt *Mount, return err } cleanup.Release() + vfs.commitChildren(ctx, mnt) for pmnt := range propMnts { - vfs.commitTree(ctx, pmnt) + vfs.commitMount(ctx, pmnt) } return nil } @@ -305,7 +308,7 @@ func (vfs *VirtualFilesystem) ConnectMountAt(ctx context.Context, creds *auth.Cr } vfs.lockMounts() defer vfs.unlockMounts(ctx) - return vfs.attachMountLocked(ctx, mnt, vd) + return vfs.attachTreeLocked(ctx, mnt, vd) } // connectMountAtLocked attaches mnt at vd. This method consumes a reference on @@ -443,7 +446,8 @@ type cloneTreeNode struct { } // cloneMountTree creates a copy of mnt's tree with the specified root -// dentry at root. The new descendants are added to mnt's pending mount list. +// dentry at root. The new descendants are added to mnt's children list but are +// not connected with call to connectLocked. // `cloneFunc` is a callback that is executed for each cloned mount. // This method is analogous to fs/namespace.c:copy_tree() in Linux. // @@ -466,7 +470,7 @@ func (vfs *VirtualFilesystem) cloneMountTree(ctx context.Context, mnt *Mount, ro } m, err := vfs.cloneMount(c, c.root, nil, cloneType) if err != nil { - vfs.abortTree(ctx, clone) + vfs.abortUncommitedMount(ctx, clone) return nil, err } mp := VirtualDentry{ @@ -475,7 +479,10 @@ func (vfs *VirtualFilesystem) cloneMountTree(ctx context.Context, mnt *Mount, ro } mp.IncRef() m.setKey(mp) - p.parentMount.pendingChildren = append(p.parentMount.pendingChildren, m) + if p.parentMount.children == nil { + p.parentMount.children = make(map[*Mount]struct{}) + } + p.parentMount.children[m] = struct{}{} if len(c.children) != 0 { queue = append(queue, cloneTreeNode{c, m}) } @@ -490,9 +497,7 @@ func (vfs *VirtualFilesystem) cloneMountTree(ctx context.Context, mnt *Mount, ro // BindAt creates a clone of the source path's parent mount and mounts it at // the target path. The new mount's root dentry is one pointed to by the source // path. -// -// TODO(b/249121230): Support recursive bind mounting. -func (vfs *VirtualFilesystem) BindAt(ctx context.Context, creds *auth.Credentials, source, target *PathOperation) error { +func (vfs *VirtualFilesystem) BindAt(ctx context.Context, creds *auth.Credentials, source, target *PathOperation, recursive bool) error { sourceVd, err := vfs.GetDentryAt(ctx, creds, source, &GetDentryOptions{}) if err != nil { return err @@ -505,13 +510,22 @@ func (vfs *VirtualFilesystem) BindAt(ctx context.Context, creds *auth.Credential vfs.lockMounts() defer vfs.unlockMounts(ctx) - clone, err := vfs.cloneMount(sourceVd.mount, sourceVd.dentry, nil, 0) + var clone *Mount + if recursive { + clone, err = vfs.cloneMountTree(ctx, sourceVd.mount, sourceVd.dentry, 0, nil) + } else { + clone, err = vfs.cloneMount(sourceVd.mount, sourceVd.dentry, nil, 0) + } if err != nil { vfs.delayDecRef(targetVd) return err } vfs.delayDecRef(clone) - return vfs.attachMountLocked(ctx, clone, targetVd) + if err := vfs.attachTreeLocked(ctx, clone, targetVd); err != nil { + vfs.abortUncomittedChildren(ctx, clone) + return err + } + return nil } // MountAt creates and mounts a Filesystem configured by the given arguments. @@ -1134,6 +1148,18 @@ func (mnt *Mount) submountsLocked() []*Mount { return mounts } +// countSubmountsLocked returns mnt's total number of descendants including +// uncommitted descendants. +// +// Precondition: mnt.vfs.mountMu must be held. +func (mnt *Mount) countSubmountsLocked() uint32 { + mounts := uint32(1) + for m := range mnt.children { + mounts += m.countSubmountsLocked() + } + return mounts +} + // Root returns the mount's root. It does not take a reference on the returned // Dentry. func (mnt *Mount) Root() *Dentry { diff --git a/pkg/sentry/vfs/namespace.go b/pkg/sentry/vfs/namespace.go index a8183d941..c74271e52 100644 --- a/pkg/sentry/vfs/namespace.go +++ b/pkg/sentry/vfs/namespace.go @@ -54,6 +54,9 @@ type MountNamespace struct { // mounts is the total number of mounts in this mount namespace. mounts uint32 + + // pending is the total number of pending mounts in this mount namespace. + pending uint32 } // Namespace is the namespace interface. @@ -184,7 +187,7 @@ func (vfs *VirtualFilesystem) CloneMountNamespace( } newns.root = newRoot newns.root.ns = newns - vfs.commitPendingTree(ctx, newRoot) + vfs.commitChildren(ctx, newRoot) return newns, nil } @@ -242,3 +245,18 @@ func (mntns *MountNamespace) Root(ctx context.Context) VirtualDentry { vd.dentry.IncRef() return vd } + +func (mntns *MountNamespace) checkMountCount(ctx context.Context, mnt *Mount) error { + if mntns.mounts > MountMax { + return linuxerr.ENOSPC + } + if mntns.mounts+mntns.pending > MountMax { + return linuxerr.ENOSPC + } + mnts := mnt.countSubmountsLocked() + if mntns.mounts+mntns.pending+mnts > MountMax { + return linuxerr.ENOSPC + } + mntns.pending += mnts + return nil +} diff --git a/pkg/sentry/vfs/propagation.go b/pkg/sentry/vfs/propagation.go index aa5a5fe77..87c55ca93 100644 --- a/pkg/sentry/vfs/propagation.go +++ b/pkg/sentry/vfs/propagation.go @@ -35,18 +35,24 @@ const ( makePrivateClone // Analogous to CL_SHARED_TO_SLAVE in Linux. sharedToFollowerClone + + propagationFlags = linux.MS_SHARED | linux.MS_PRIVATE | linux.MS_SLAVE | linux.MS_UNBINDABLE ) // +checklocks:vfs.mountMu -func (vfs *VirtualFilesystem) commitPendingTree(ctx context.Context, mnt *Mount) { - for _, c := range mnt.pendingChildren { - vfs.commitTree(ctx, c) +func (vfs *VirtualFilesystem) commitChildren(ctx context.Context, mnt *Mount) { + for c := range mnt.children { + if c.neverConnected() { + vfs.commitMount(ctx, c) + } } - mnt.pendingChildren = nil } +// commitMount attaches mnt to the parent and mountpoint specified by its +// mountKey and recursively does the same for all of mnt's descendants. +// // +checklocks:vfs.mountMu -func (vfs *VirtualFilesystem) commitTree(ctx context.Context, mnt *Mount) { +func (vfs *VirtualFilesystem) commitMount(ctx context.Context, mnt *Mount) { mp := mnt.getKey() // If there is already a mount at this (parent, point), disconnect it from its @@ -66,29 +72,38 @@ func (vfs *VirtualFilesystem) commitTree(ctx context.Context, mnt *Mount) { vfs.delayDecRef(child) } vfs.mounts.seq.EndWrite() - vfs.commitPendingTree(ctx, mnt) + vfs.commitChildren(ctx, mnt) } -// abortTree releases references on a pending mount and all its pending -// descendants. -// // +checklocks:vfs.mountMu -func (vfs *VirtualFilesystem) abortTree(ctx context.Context, mnt *Mount) { +func (vfs *VirtualFilesystem) abortUncomittedChildren(ctx context.Context, mnt *Mount) { + for c := range mnt.children { + if c.neverConnected() { + vfs.abortUncommitedMount(ctx, c) + delete(mnt.children, c) + } + } +} + +// abortUncommitedMount releases references on mnt and all its descendants. +// +// Prerequisite: mnt is not connected, i.e. mnt.ns == nil. +// +checklocks:vfs.mountMu +func (vfs *VirtualFilesystem) abortUncommitedMount(ctx context.Context, mnt *Mount) { vfs.delayDecRef(mnt) vfs.delayDecRef(mnt.getKey()) mnt.setKey(VirtualDentry{}) vfs.setPropagation(mnt, linux.MS_PRIVATE) - for _, c := range mnt.pendingChildren { - vfs.abortTree(ctx, c) - } - mnt.pendingChildren = nil + vfs.abortUncomittedChildren(ctx, mnt) } // SetMountPropagationAt changes the propagation type of the mount pointed to by // pop. -func (vfs *VirtualFilesystem) SetMountPropagationAt(ctx context.Context, creds *auth.Credentials, pop *PathOperation, propFlags uint32) error { +func (vfs *VirtualFilesystem) SetMountPropagationAt(ctx context.Context, creds *auth.Credentials, pop *PathOperation, propFlag uint32) error { + recursive := propFlag&linux.MS_REC != 0 + propFlag &= propagationFlags // Check if flags is a power of 2. If not then more than one flag is set. - if !bits.IsPowerOfTwo32(propFlags) { + if !bits.IsPowerOfTwo32(propFlag) { return linuxerr.EINVAL } vd, err := vfs.GetDentryAt(ctx, creds, pop, &GetDentryOptions{}) @@ -107,20 +122,27 @@ func (vfs *VirtualFilesystem) SetMountPropagationAt(ctx context.Context, creds * } else if vd.dentry != vd.mount.root { return linuxerr.EINVAL } - vfs.SetMountPropagation(vd.mount, propFlags) + vfs.SetMountPropagation(vd.mount, propFlag, recursive) return nil } // SetMountPropagation changes the propagation type of the mount. -func (vfs *VirtualFilesystem) SetMountPropagation(mnt *Mount, propFlags uint32) error { +func (vfs *VirtualFilesystem) SetMountPropagation(mnt *Mount, propFlags uint32, recursive bool) error { vfs.lockMounts() defer vfs.unlockMounts(context.Background()) if propFlags == linux.MS_SHARED { - if err := vfs.allocMountGroupIDs(mnt, false); err != nil { + if err := vfs.allocMountGroupIDs(mnt, recursive); err != nil { return fmt.Errorf("allocMountGroupIDs: %v", err) } } - vfs.setPropagation(mnt, propFlags) + + if !recursive { + vfs.setPropagation(mnt, propFlags) + return nil + } + for _, m := range mnt.submountsLocked() { + vfs.setPropagation(m, propFlags) + } return nil } @@ -303,7 +325,7 @@ func (vfs *VirtualFilesystem) propagateMount(ctx context.Context, dstMnt *Mount, cloneType |= makeSharedClone } } - clone, err := vfs.cloneMount(state.prevSrc, state.prevSrc.root, nil, cloneType) + clone, err := vfs.cloneMountTree(ctx, state.prevSrc, state.prevSrc.root, cloneType, nil) if err != nil { return err } @@ -315,10 +337,7 @@ func (vfs *VirtualFilesystem) propagateMount(ctx context.Context, dstMnt *Mount, } state.prevDst = dstMnt state.prevSrc = clone - if uint32(len(state.propList))+dstMnt.ns.mounts > MountMax { - return linuxerr.ENOSPC - } - return nil + return dstMnt.ns.checkMountCount(ctx, clone) } // nextFollowerPeerGroup iterates through the propagation tree and returns the diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 57ac2348d..db374ae69 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -1419,6 +1419,7 @@ cc_binary( "//test/util:test_main", "//test/util:test_util", "//test/util:thread_util", + "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/strings:str_format", ], ) diff --git a/test/syscalls/linux/mount.cc b/test/syscalls/linux/mount.cc index e6d6c0d4b..b0c2b0aa5 100644 --- a/test/syscalls/linux/mount.cc +++ b/test/syscalls/linux/mount.cc @@ -43,6 +43,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/container/flat_hash_map.h" #include "absl/strings/match.h" #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" @@ -820,8 +821,8 @@ TEST(MountTest, SimpleBind) { // Write to child1 in dir1. const std::string filename = "foo.txt"; const std::string contents = "barbaz"; - ASSERT_NO_ERRNO(CreateWithContents(JoinPath(child1.path(), filename), - contents, O_WRONLY)); + ASSERT_NO_ERRNO( + CreateWithContents(JoinPath(child1.path(), filename), contents, 0666)); // Verify both directories have the same nodes. std::vector child_names = {std::string(Basename(child1.path())), std::string(Basename(child2.path()))}; @@ -1851,6 +1852,163 @@ TEST(MountTest, MaxMountsWithSlave) { } } +TEST(MountTest, SetPropagationRecursive) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + const TempPath a = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const a_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount("test", a.path(), "tmpfs", 0, "mode=0123", MNT_DETACH)); + const auto b = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(a.path())); + auto const b_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount("test", b.path(), "tmpfs", 0, "mode=0123", MNT_DETACH)); + const auto c = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(b.path())); + auto const c_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount("test", c.path(), "tmpfs", 0, "mode=0123", MNT_DETACH)); + const auto d = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(c.path())); + auto const d_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount("test", d.path(), "tmpfs", 0, "mode=0123", MNT_DETACH)); + + ASSERT_THAT(mount("", a.path().c_str(), "", MS_SHARED | MS_REC, 0), + SyscallSucceeds()); + absl::flat_hash_map> optionals = + ASSERT_NO_ERRNO_AND_VALUE(MountOptionals()); + EXPECT_NE(optionals[a.path()][0].shared, 0); + EXPECT_NE(optionals[b.path()][0].shared, 0); + EXPECT_NE(optionals[c.path()][0].shared, 0); + EXPECT_NE(optionals[d.path()][0].shared, 0); +} + +TEST(MountTest, SetSlaveRecursive) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + const auto a = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const a_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(a.path(), a.path(), "", MS_BIND, "", MNT_DETACH)); + ASSERT_THAT(mount("", a.path().c_str(), "", MS_SHARED, 0), SyscallSucceeds()); + const auto a_master = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const a_master_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(a.path(), a_master.path(), "", MS_BIND, "", MNT_DETACH)); + + const auto b = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(a.path())); + auto const b_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(a.path(), b.path(), "", MS_BIND, "", MNT_DETACH)); + const auto b_master = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const b_master_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(b.path(), b_master.path(), "", MS_BIND, "", MNT_DETACH)); + + ASSERT_THAT(mount("", a.path().c_str(), "", MS_SLAVE | MS_REC, 0), + SyscallSucceeds()); + + absl::flat_hash_map> optionals = + ASSERT_NO_ERRNO_AND_VALUE(MountOptionals()); + EXPECT_EQ(optionals[a.path()][0].master, + optionals[a_master.path()][0].shared); + EXPECT_EQ(optionals[b.path()][0].master, + optionals[b_master.path()][0].shared); +} + +TEST(MountTest, RecursiveBind) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + const auto a = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const a_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(a.path(), a.path(), "", MS_BIND, "", MNT_DETACH)); + const auto b = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(a.path())); + auto const b_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(b.path(), b.path(), "", MS_BIND, "", MNT_DETACH)); + const auto c = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(b.path())); + const auto d = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const d_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(a.path(), d.path(), "", MS_BIND | MS_REC, "", MNT_DETACH)); + + // Write to child1 in dir1. + const std::string filename = "foo.txt"; + const std::string contents = "barbaz"; + ASSERT_NO_ERRNO( + CreateWithContents(JoinPath(c.path(), filename), contents, 0666)); + // Verify both directories have the same nodes. + const std::string path = + JoinPath(d.path(), Basename(b.path()), Basename(c.path()), filename); + + std::string output; + ASSERT_NO_ERRNO(GetContents(path, &output)); + EXPECT_EQ(output, contents); +} + +TEST(MountTest, MaxRecursiveBind) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + const auto a = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const a_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(a.path(), a.path(), "", MS_BIND, "", MNT_DETACH)); + const auto b = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(a.path())); + + int mount_max = 10000; + bool mount_max_exists = + ASSERT_NO_ERRNO_AND_VALUE(Exists("/proc/sys/fs/mount-max")); + if (mount_max_exists) { + std::string mount_max_string; + ASSERT_NO_ERRNO(GetContents("/proc/sys/fs/mount-max", &mount_max_string)); + ASSERT_TRUE(absl::SimpleAtoi(mount_max_string, &mount_max)); + } + + const std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + int num_binds = static_cast(std::log2(mount_max - mounts.size())); + + for (int i = 0; i < num_binds; i++) { + ASSERT_THAT(mount(a.path().c_str(), b.path().c_str(), nullptr, + MS_BIND | MS_REC, nullptr), + SyscallSucceeds()); + } + ASSERT_THAT(mount(a.path().c_str(), b.path().c_str(), nullptr, + MS_BIND | MS_REC, nullptr), + SyscallFailsWithErrno(ENOSPC)); +} + +TEST(MountTest, RecursiveBindPropagation) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + const auto parent = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const parent_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(parent.path(), parent.path(), "", MS_BIND, "", MNT_DETACH)); + const auto a = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(parent.path())); + const auto b = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(parent.path())); + const auto c = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(parent.path())); + + auto const a_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount("test", a.path(), "tmpfs", 0, "", MNT_DETACH)); + ASSERT_THAT(mount("", a.path().c_str(), "", MS_SHARED, 0), SyscallSucceeds()); + + auto const b_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(a.path(), b.path(), "", MS_BIND, "", MNT_DETACH)); + auto const c_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(a.path(), c.path(), "", MS_BIND, "", MNT_DETACH)); + + const auto d = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(parent.path())); + const auto f = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(a.path())); + auto const d_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(d.path(), d.path(), "", MS_BIND, "", MNT_DETACH)); + + const auto e = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(d.path())); + auto const e_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(e.path(), e.path(), "", MS_BIND, "", MNT_DETACH)); + + auto const f_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount(d.path(), f.path(), "", MS_BIND | MS_REC, "", MNT_DETACH)); + + absl::flat_hash_map> optionals = + ASSERT_NO_ERRNO_AND_VALUE(MountOptionals()); + auto b_e_path = JoinPath(b.path(), Basename(f.path()), Basename(e.path())); + ASSERT_FALSE(optionals[b_e_path].empty()); + auto c_e_path = JoinPath(c.path(), Basename(f.path()), Basename(e.path())); + ASSERT_FALSE(optionals[c_e_path].empty()); +} + TEST(MountTest, MountNamespace) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));