diff --git a/pkg/sentry/vfs/BUILD b/pkg/sentry/vfs/BUILD index b9476cce5..6f44ae392 100644 --- a/pkg/sentry/vfs/BUILD +++ b/pkg/sentry/vfs/BUILD @@ -151,6 +151,7 @@ go_library( "//pkg/abi/linux", "//pkg/atomicbitops", "//pkg/bitmap", + "//pkg/cleanup", "//pkg/context", "//pkg/errors/linuxerr", "//pkg/fd", diff --git a/pkg/sentry/vfs/mount.go b/pkg/sentry/vfs/mount.go index 2886ff7c2..3e82e7825 100644 --- a/pkg/sentry/vfs/mount.go +++ b/pkg/sentry/vfs/mount.go @@ -23,6 +23,7 @@ import ( "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/atomicbitops" + "gvisor.dev/gvisor/pkg/cleanup" "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/refsvfs2" @@ -46,6 +47,11 @@ const ( Child // Unbindable represents the unbindable propagation type. Unbindable + + // MountMax is the maximum number of mounts allowed. In Linux this can be + // configured by the user at /proc/sys/fs/mount-max, but the default is + // 100,000. We set the gVisor limit to 10,000. + MountMax = 10000 ) // PropagationTypeFromLinux returns the PropagationType corresponding to a @@ -273,6 +279,9 @@ type MountNamespace struct { // VFS.PrepareDeleteDentry() and VFS.PrepareRemoveDentry() operate // correctly on unreferenced MountNamespaces. mountpoints map[*Dentry]uint32 + + // mounts is the total number of mounts in this mount namespace. + mounts uint32 } // NewMountNamespace returns a new mount namespace with a root filesystem @@ -349,11 +358,20 @@ func (vfs *VirtualFilesystem) ConnectMountAt(ctx context.Context, creds *auth.Cr vfs.mountMu.Lock() defer vfs.mountMu.Unlock() tree := vfs.preparePropagationTree(mnt, vd) + + cleanup := cleanup.Make(func() { + vfs.abortPropagationTree(ctx, tree) // +checklocksforce + }) + defer cleanup.Clean() + // Check if the new mount + all the propagation mounts puts us over the max. + if uint32(len(tree)+1)+vd.mount.ns.mounts > MountMax { + return linuxerr.ENOSPC + } if err := vfs.connectMountAt(ctx, mnt, vd); err != nil { - vfs.abortPropagationTree(ctx, tree) return err } vfs.commitPropagationTree(ctx, tree) + cleanup.Release() return nil } @@ -572,12 +590,21 @@ func (vfs *VirtualFilesystem) BindAt(ctx context.Context, creds *auth.Credential vfs.mergePeerGroup(sourceVd.mount, clone) } } + cleanup := cleanup.Make(func() { + // Checklocks doesn't work with anon functions. + vfs.setPropagation(clone, Private) // +checklocksforce + vfs.abortPropagationTree(ctx, tree) // +checklocksforce + targetVd.DecRef(ctx) + }) + defer cleanup.Clean() + if uint32(1+len(tree))+targetVd.mount.ns.mounts > MountMax { + return nil, linuxerr.ENOSPC + } if err := vfs.connectMountAt(ctx, clone, targetVd); err != nil { - vfs.setPropagation(clone, Private) - vfs.abortPropagationTree(ctx, tree) return nil, err } vfs.commitPropagationTree(ctx, tree) + cleanup.Release() return clone, nil } @@ -795,6 +822,7 @@ func (vfs *VirtualFilesystem) connectLocked(mnt *Mount, vd VirtualDentry, mntns vd.dentry.mounts.Add(1) mnt.ns = mntns mntns.mountpoints[vd.dentry]++ + mntns.mounts++ vfs.mounts.insertSeqed(mnt) vfsmpmounts, ok := vfs.mountpoints[vd.dentry] if !ok { @@ -817,11 +845,18 @@ func (vfs *VirtualFilesystem) disconnectLocked(mnt *Mount) VirtualDentry { if vd.mount != nil { panic("VFS.disconnectLocked called on disconnected mount") } + if mnt.ns.mountpoints[vd.dentry] == 0 { + panic("VFS.disconnectLocked called on dentry with zero mountpoints.") + } + if mnt.ns.mounts == 0 { + panic("VFS.disconnectLocked called on namespace with zero mounts.") + } } mnt.loadKey(VirtualDentry{}) delete(vd.mount.children, mnt) vd.dentry.mounts.Add(math.MaxUint32) // -1 mnt.ns.mountpoints[vd.dentry]-- + mnt.ns.mounts-- if mnt.ns.mountpoints[vd.dentry] == 0 { delete(mnt.ns.mountpoints, vd.dentry) } diff --git a/test/syscalls/linux/mount.cc b/test/syscalls/linux/mount.cc index d46dadcde..096e31a30 100644 --- a/test/syscalls/linux/mount.cc +++ b/test/syscalls/linux/mount.cc @@ -35,6 +35,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/strings/match.h" +#include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" @@ -822,6 +823,46 @@ TEST(MountTest, BindToSelf) { ASSERT_TRUE(found); } +TEST(MountTest, MaxMounts) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + auto const parent = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount("", parent.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + auto const dir = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(parent.path())); + ASSERT_THAT( + mount(dir.path().c_str(), dir.path().c_str(), nullptr, MS_BIND, nullptr), + SyscallSucceeds()); + ASSERT_THAT(mount("", dir.path().c_str(), "", MS_SHARED, ""), + SyscallSucceeds()); + + // Each bind mount doubles the number of mounts in the peer group. The number + // of binds we can do before failing is log2(max_mounts-num_current_mounts). + 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(dir.path().c_str(), dir.path().c_str(), nullptr, MS_BIND, + nullptr), + SyscallSucceeds()); + } + ASSERT_THAT( + mount(dir.path().c_str(), dir.path().c_str(), nullptr, MS_BIND, nullptr), + SyscallFailsWithErrno(ENOSPC)); + umount2(parent.path().c_str(), MNT_DETACH); +} + // Tests that it is possible to make a shared mount. TEST(MountTest, MakeShared) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));