Add a maximum to the total number of mounts allowed in a namespace.

The limit is the same as a the default for /proc/sys/fs/mount-max.

Reported-by: syzbot+ae4591a5d362a6701e40@syzkaller.appspotmail.com
PiperOrigin-RevId: 491748452
This commit is contained in:
Lucas Manning
2022-11-29 15:12:26 -08:00
committed by gVisor bot
parent bfbb9fa4cc
commit ece02b45b5
3 changed files with 80 additions and 3 deletions
+1
View File
@@ -151,6 +151,7 @@ go_library(
"//pkg/abi/linux",
"//pkg/atomicbitops",
"//pkg/bitmap",
"//pkg/cleanup",
"//pkg/context",
"//pkg/errors/linuxerr",
"//pkg/fd",
+38 -3
View File
@@ -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)
}
+41
View File
@@ -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<ProcMountInfoEntry> mounts =
ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries());
int num_binds = static_cast<int>(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)));