diff --git a/pkg/sentry/fsimpl/proc/tasks_sys.go b/pkg/sentry/fsimpl/proc/tasks_sys.go index dd57fdfdb..b2322a62d 100644 --- a/pkg/sentry/fsimpl/proc/tasks_sys.go +++ b/pkg/sentry/fsimpl/proc/tasks_sys.go @@ -20,6 +20,7 @@ import ( "math" "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/atomicbitops" "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/hostarch" @@ -58,6 +59,9 @@ func (fs *filesystem) newSysDir(ctx context.Context, root *auth.Credentials, k * "ptrace_scope": fs.newYAMAPtraceScopeFile(ctx, k, root), }), }), + "fs": fs.newStaticDir(ctx, root, map[string]kernfs.Inode{ + "nr_open": fs.newInode(ctx, root, 0644, &atomicInt32File{val: &k.MaxFDLimit, min: 8, max: kernel.MaxFdLimit}), + }), "vm": fs.newStaticDir(ctx, root, map[string]kernfs.Inode{ "max_map_count": fs.newInode(ctx, root, 0444, newStaticFile("2147483647\n")), "mmap_min_addr": fs.newInode(ctx, root, 0444, &mmapMinAddrData{k: k}), @@ -486,3 +490,49 @@ func (pr *portRange) Write(ctx context.Context, _ *vfs.FileDescription, src user *pr.end = uint16(ports[1]) return n, nil } + +// atomicInt32File implements vfs.WritableDynamicBytesSource sysctls +// represented by int32 atomic objects. +// +// +stateify savable +type atomicInt32File struct { + kernfs.DynamicBytesFile + + val *atomicbitops.Int32 + min, max int32 +} + +var _ vfs.WritableDynamicBytesSource = (*atomicInt32File)(nil) + +// Generate implements vfs.DynamicBytesSource.Generate. +func (f *atomicInt32File) Generate(ctx context.Context, buf *bytes.Buffer) error { + _, err := fmt.Fprintf(buf, "%d\n", f.val.Load()) + return err +} + +// Write implements vfs.WritableDynamicBytesSource.Write. +func (f *atomicInt32File) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) { + if offset != 0 { + // Ignore partial writes. + return 0, linuxerr.EINVAL + } + if src.NumBytes() == 0 { + return 0, nil + } + + // Limit the amount of memory allocated. + src = src.TakeFirst(hostarch.PageSize - 1) + + var v int32 + n, err := usermem.CopyInt32StringInVec(ctx, src.IO, src.Addrs, &v, src.Opts) + if err != nil { + return 0, err + } + + if v < f.min || v > f.max { + return 0, linuxerr.EINVAL + } + + f.val.Store(v) + return n, nil +} diff --git a/pkg/sentry/kernel/fd_table.go b/pkg/sentry/kernel/fd_table.go index 0ac275fb4..872eef160 100644 --- a/pkg/sentry/kernel/fd_table.go +++ b/pkg/sentry/kernel/fd_table.go @@ -231,18 +231,18 @@ func (f *FDTable) NewFDs(ctx context.Context, minFD int32, files []*vfs.FileDesc } // Default limit. - end := MaxFdLimit + end := f.k.MaxFDLimit.Load() // Ensure we don't get past the provided limit. if limitSet := limits.FromContext(ctx); limitSet != nil { lim := limitSet.Get(limits.NumberOfFiles) // Only set if the limit is smaller than the max to avoid overflow. - if lim.Cur != limits.Infinity && lim.Cur < uint64(MaxFdLimit) { + if lim.Cur != limits.Infinity && lim.Cur < uint64(end) { end = int32(lim.Cur) } - if minFD+int32(len(files)) > end { - return nil, unix.EMFILE - } + } + if minFD+int32(len(files)) > end { + return nil, unix.EMFILE } f.mu.Lock() @@ -329,6 +329,9 @@ func (f *FDTable) newFDAt(ctx context.Context, fd int32, file *vfs.FileDescripti return nil, unix.EBADF } + if fd >= f.k.MaxFDLimit.Load() { + return nil, unix.EMFILE + } // Check the limit for the provided file. if limitSet := limits.FromContext(ctx); limitSet != nil { if lim := limitSet.Get(limits.NumberOfFiles); lim.Cur != limits.Infinity && uint64(fd) >= lim.Cur { diff --git a/pkg/sentry/kernel/fd_table_test.go b/pkg/sentry/kernel/fd_table_test.go index 618070522..1ed7e8d31 100644 --- a/pkg/sentry/kernel/fd_table_test.go +++ b/pkg/sentry/kernel/fd_table_test.go @@ -69,6 +69,8 @@ func runTest(t testing.TB, fn func(ctx context.Context, fdTable *FDTable, fd *vf // Create the table. fdTable := new(FDTable) + fdTable.k = &Kernel{} + fdTable.k.MaxFDLimit.Store(MaxFdLimit) fdTable.init() // Run the test. diff --git a/pkg/sentry/kernel/kernel.go b/pkg/sentry/kernel/kernel.go index 0322c5a0e..b1c364ad6 100644 --- a/pkg/sentry/kernel/kernel.go +++ b/pkg/sentry/kernel/kernel.go @@ -323,6 +323,10 @@ type Kernel struct { // userCountersMap maps auth.KUID into a set of user counters. userCountersMap map[auth.KUID]*UserCounters userCountersMapMu userCountersMutex `state:"nosave"` + + // MaxFDLimit specifies the maximum file descriptor number that can be + // used by processes. + MaxFDLimit atomicbitops.Int32 } // InitKernelArgs holds arguments to Init. @@ -367,6 +371,11 @@ type InitKernelArgs struct { // PIDNamespace is the root PID namespace. PIDNamespace *PIDNamespace + + // MaxFDLimit specifies the maximum file descriptor number that can be + // used by processes. If it is zero, the limit will be set to + // unlimited. + MaxFDLimit int32 } // Init initialize the Kernel with no tasks. @@ -420,6 +429,10 @@ func (k *Kernel) Init(args InitKernelArgs) error { k.ptraceExceptions = make(map[*Task]*Task) k.YAMAPtraceScope = atomicbitops.FromInt32(linux.YAMA_SCOPE_RELATIONAL) k.userCountersMap = make(map[auth.KUID]*UserCounters) + if args.MaxFDLimit == 0 { + args.MaxFDLimit = MaxFdLimit + } + k.MaxFDLimit.Store(args.MaxFDLimit) ctx := k.SupervisorContext() if err := k.vfs.Init(ctx); err != nil { diff --git a/pkg/sentry/syscalls/linux/sys_file.go b/pkg/sentry/syscalls/linux/sys_file.go index 328f89196..ded2c8f50 100644 --- a/pkg/sentry/syscalls/linux/sys_file.go +++ b/pkg/sentry/syscalls/linux/sys_file.go @@ -565,6 +565,9 @@ func dup3(t *kernel.Task, oldfd, newfd int32, flags uint32) (uintptr, *kernel.Sy err := t.NewFDAt(newfd, file, kernel.FDFlags{ CloseOnExec: flags&linux.O_CLOEXEC != 0, }) + if linuxerr.Equals(linuxerr.EMFILE, err) { + err = linuxerr.EBADF + } if err != nil { return 0, nil, err } diff --git a/pkg/sentry/syscalls/linux/sys_rlimit.go b/pkg/sentry/syscalls/linux/sys_rlimit.go index 293a5c974..2995ba55e 100644 --- a/pkg/sentry/syscalls/linux/sys_rlimit.go +++ b/pkg/sentry/syscalls/linux/sys_rlimit.go @@ -108,6 +108,13 @@ func prlimit64(t *kernel.Task, resource limits.LimitType, newLim *limits.Limit) return limits.Limit{}, linuxerr.EPERM } + switch resource { + case limits.NumberOfFiles: + if newLim.Max > uint64(t.Kernel().MaxFDLimit.Load()) { + return limits.Limit{}, linuxerr.EPERM + } + } + // "A privileged process (under Linux: one with the CAP_SYS_RESOURCE // capability in the initial user namespace) may make arbitrary changes // to either limit value." diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index e680b02d5..fe8c45ec1 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -21,6 +21,7 @@ import ( mrand "math/rand" "os" "runtime" + "strconv" gtime "time" specs "github.com/opencontainers/runtime-spec/specs-go" @@ -446,6 +447,19 @@ func New(args Args) (*Loader, error) { log.Infof("Setting total memory to %.2f GB", float64(args.TotalMem)/(1<<30)) } + maxFDLimit := kernel.MaxFdLimit + if args.Spec.Linux != nil && args.Spec.Linux.Sysctl != nil { + if val, ok := args.Spec.Linux.Sysctl["fs.nr_open"]; ok { + nrOpen, err := strconv.Atoi(val) + if err != nil { + return nil, fmt.Errorf("setting fs.nr_open=%s: %w", val, err) + } + if nrOpen <= 0 || nrOpen > int(kernel.MaxFdLimit) { + return nil, fmt.Errorf("setting fs.nr_open=%s", val) + } + maxFDLimit = int32(nrOpen) + } + } // Initiate the Kernel object, which is required by the Context passed // to createVFS in order to mount (among other things) procfs. if err = k.Init(kernel.InitKernelArgs{ @@ -458,6 +472,7 @@ func New(args Args) (*Loader, error) { RootUTSNamespace: kernel.NewUTSNamespace(args.Spec.Hostname, args.Spec.Hostname, creds.UserNamespace), RootIPCNamespace: kernel.NewIPCNamespace(creds.UserNamespace), PIDNamespace: kernel.NewRootPIDNamespace(creds.UserNamespace), + MaxFDLimit: maxFDLimit, }); err != nil { return nil, fmt.Errorf("initializing kernel: %w", err) } diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 3012952aa..71540490e 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -621,7 +621,9 @@ cc_binary( "//test/util:eventfd_util", "//test/util:file_descriptor", "@com_google_absl//absl/memory", + "@com_google_absl//absl/strings", gtest, + "//test/util:capability_util", "//test/util:fs_util", "//test/util:posix_error", "//test/util:temp_path", diff --git a/test/syscalls/linux/dup.cc b/test/syscalls/linux/dup.cc index e1a74b5f9..a233f82b3 100644 --- a/test/syscalls/linux/dup.cc +++ b/test/syscalls/linux/dup.cc @@ -16,13 +16,16 @@ #include #include +#include #include #include "gtest/gtest.h" #include "absl/memory/memory.h" +#include "absl/strings/str_cat.h" #include "test/util/eventfd_util.h" #include "test/util/file_descriptor.h" #include "test/util/fs_util.h" +#include "test/util/linux_capability_util.h" #include "test/util/posix_error.h" #include "test/util/temp_path.h" #include "test/util/test_util.h" @@ -140,6 +143,29 @@ TEST(DupTest, Rlimit) { EXPECT_EQ(fds.size() + used_fds, kFDLimit - fd.get() - 1); } +TEST(RlimitTest, DupLimitedByNROpenSysctl) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_RESOURCE))); + + const int kNROpen = 1 << 22; + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open("/proc/sys/fs/nr_open", O_WRONLY)); + auto data = absl::StrCat(kNROpen); + EXPECT_THAT(write(fd.get(), data.c_str(), data.size()), + SyscallSucceedsWithValue(data.size())); + + struct rlimit rl = { + .rlim_cur = kNROpen + 1, + .rlim_max = kNROpen + 1, + }; + EXPECT_THAT(setrlimit(RLIMIT_NOFILE, &rl), SyscallFailsWithErrno(EPERM)); + rl.rlim_cur = kNROpen; + rl.rlim_max = kNROpen; + ASSERT_THAT(setrlimit(RLIMIT_NOFILE, &rl), SyscallSucceeds()); + + ASSERT_THAT(dup3(fd.get(), kNROpen, 0), SyscallFailsWithErrno(EBADF)); + ASSERT_THAT(dup3(fd.get(), kNROpen - 1, 0), SyscallSucceeds()); +} + TEST(DupTest, Dup2SameFD) { auto f = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(f.path(), O_RDONLY));