From 5c95e1d39c19a9783a919783cbf65534d30437d6 Mon Sep 17 00:00:00 2001 From: Konstantin Bogomolov Date: Mon, 28 Feb 2022 09:33:46 -0800 Subject: [PATCH] Implement close_range. Fixes #5500 PiperOrigin-RevId: 431454836 --- pkg/abi/linux/file.go | 6 + pkg/bitmap/bitmap.go | 37 ++- pkg/bitmap/bitmap_test.go | 44 ++- pkg/sentry/kernel/fd_table.go | 154 ++++++++-- pkg/sentry/kernel/fd_table_test.go | 45 +++ pkg/sentry/kernel/task_clone.go | 17 +- pkg/sentry/kernel/task_exec.go | 2 +- pkg/sentry/strace/BUILD | 1 + pkg/sentry/strace/close_range.go | 32 +++ pkg/sentry/strace/linux64_amd64.go | 1 + pkg/sentry/strace/linux64_arm64.go | 1 + pkg/sentry/strace/strace.go | 2 + pkg/sentry/strace/syscalls.go | 3 + pkg/sentry/syscalls/linux/linux64.go | 2 + pkg/sentry/syscalls/linux/sys_file.go | 56 ++++ pkg/sentry/syscalls/linux/vfs2/fd.go | 56 ++++ pkg/sentry/syscalls/linux/vfs2/vfs2.go | 2 + test/syscalls/BUILD | 5 + test/syscalls/linux/BUILD | 17 ++ test/syscalls/linux/close_range.cc | 373 +++++++++++++++++++++++++ 20 files changed, 819 insertions(+), 37 deletions(-) create mode 100644 pkg/sentry/strace/close_range.go create mode 100644 test/syscalls/linux/close_range.cc diff --git a/pkg/abi/linux/file.go b/pkg/abi/linux/file.go index 899c0ce52..535394f13 100644 --- a/pkg/abi/linux/file.go +++ b/pkg/abi/linux/file.go @@ -394,3 +394,9 @@ const ( FALLOC_FL_INSERT_RANGE = 0x20 FALLOC_FL_UNSHARE_RANGE = 0x40 ) + +// Constants related to close_range(2). Source: /include/uapi/linux/close_range.h +const ( + CLOSE_RANGE_UNSHARE = uint32(1 << 1) + CLOSE_RANGE_CLOEXEC = uint32(1 << 2) +) diff --git a/pkg/bitmap/bitmap.go b/pkg/bitmap/bitmap.go index 12d2fc2b8..cd72a4472 100644 --- a/pkg/bitmap/bitmap.go +++ b/pkg/bitmap/bitmap.go @@ -16,10 +16,15 @@ package bitmap import ( + "fmt" "math" "math/bits" ) +// MaxBitEntryLimit defines the upper limit on how many bit entries are supported by this Bitmap +// implementation. +const MaxBitEntryLimit uint32 = math.MaxInt32 + // Bitmap implements an efficient bitmap. // // +stateify savable @@ -53,21 +58,21 @@ func (b *Bitmap) Minimum() uint32 { return uint32(r + i*64) } } - return math.MaxInt32 + return MaxBitEntryLimit } // FirstZero returns the first unset bit from the range [start, ). -func (b *Bitmap) FirstZero(start uint32) uint32 { +func (b *Bitmap) FirstZero(start uint32) (bit uint32, err error) { i, nbit := int(start/64), start%64 n := len(b.bitBlock) if i >= n { - return math.MaxInt32 + return MaxBitEntryLimit, fmt.Errorf("given start of range exceeds bitmap size") } w := b.bitBlock[i] | ((1 << nbit) - 1) for { if w != ^uint64(0) { r := bits.TrailingZeros64(^w) - return uint32(r + i*64) + return uint32(r + i*64), nil } i++ if i == n { @@ -75,7 +80,29 @@ func (b *Bitmap) FirstZero(start uint32) uint32 { } w = b.bitBlock[i] } - return math.MaxInt32 + return MaxBitEntryLimit, fmt.Errorf("bitmap has no unset bits") +} + +// FirstOne returns the first set bit from the range [start, ) +func (b *Bitmap) FirstOne(start uint32) (bit uint32, err error) { + i, nbit := int(start/64), start%64 + n := len(b.bitBlock) + if i >= n { + return MaxBitEntryLimit, fmt.Errorf("given start of range exceeds bitmap size") + } + w := b.bitBlock[i] & (math.MaxUint64 << nbit) + for { + if w != uint64(0) { + r := bits.TrailingZeros64(w) + return uint32(r + i*64), nil + } + i++ + if i == n { + break + } + w = b.bitBlock[i] + } + return MaxBitEntryLimit, fmt.Errorf("bitmap has no set bits") } // Maximum return the largest value in the Bitmap. diff --git a/pkg/bitmap/bitmap_test.go b/pkg/bitmap/bitmap_test.go index 76ebd779f..1e0f40214 100644 --- a/pkg/bitmap/bitmap_test.go +++ b/pkg/bitmap/bitmap_test.go @@ -294,13 +294,49 @@ func TestBitmapNumOnes(t *testing.T) { } } +type BitmapGetFirstTestcase struct { + queryValue uint32 + expectedValue uint32 + wantErr bool +} + func TestFirstZero(t *testing.T) { bitmap := New(uint32(1000)) bitmap.FlipRange(200, 400) - for i, j := range map[uint32]uint32{0: 0, 201: 400, 200: 400, 199: 199, 400: 400, 10000: math.MaxInt32} { - v := bitmap.FirstZero(i) - if v != j { - t.Errorf("Minimum() returns: %v, wanted: %v", v, j) + testcases := []BitmapGetFirstTestcase{ + {0, 0, false}, + {201, 400, false}, + {200, 400, false}, + {199, 199, false}, + {400, 400, false}, + {10000, math.MaxInt32, true}, + } + for _, tc := range testcases { + v, err := bitmap.FirstZero(tc.queryValue) + if v != tc.expectedValue && (err != nil) == tc.wantErr { + t.Errorf("FirstZero() returns: %v, wanted: %v", v, tc.expectedValue) + } + } +} + +func TestFirstOne(t *testing.T) { + bitmap := New(uint32(1000)) + bitmap.FlipRange(200, 400) + bitmap.FlipRange(700, 701) + testcases := []BitmapGetFirstTestcase{ + {0, 200, false}, + {199, 200, false}, + {200, 200, false}, + {399, 399, false}, + {400, 700, false}, + {700, 700, false}, + {701, math.MaxInt32, true}, + {10000, math.MaxInt32, true}, + } + for _, tc := range testcases { + v, err := bitmap.FirstOne(tc.queryValue) + if v != tc.expectedValue && (err != nil) == tc.wantErr { + t.Errorf("FirstOne() returns: %v, wanted: %v", v, tc.expectedValue) } } } diff --git a/pkg/sentry/kernel/fd_table.go b/pkg/sentry/kernel/fd_table.go index eff556a0c..fc11c6a26 100644 --- a/pkg/sentry/kernel/fd_table.go +++ b/pkg/sentry/kernel/fd_table.go @@ -73,6 +73,9 @@ type descriptor struct { flags FDFlags } +// MaxFdLimit defines the upper limit on the integer value of file descriptors. +const MaxFdLimit int32 = int32(bitmap.MaxBitEntryLimit) + // FDTable is used to manage File references and flags. // // +stateify savable @@ -184,16 +187,19 @@ func (f *FDTable) DecRef(ctx context.Context) { }) } -// forEach iterates over all non-nil files in sorted order. +// forEachUpTo iterates over all non-nil files upto maxFds (non-inclusive) in sorted order. // // It is the caller's responsibility to acquire an appropriate lock. -func (f *FDTable) forEach(ctx context.Context, fn func(fd int32, file *fs.File, fileVFS2 *vfs.FileDescription, flags FDFlags)) { +func (f *FDTable) forEachUpTo(ctx context.Context, maxFds int32, fn func(fd int32, file *fs.File, fileVFS2 *vfs.FileDescription, flags FDFlags)) { // retries tracks the number of failed TryIncRef attempts for the same FD. retries := 0 fds := f.fdBitmap.ToSlice() // Iterate through the fdBitmap. for _, ufd := range fds { fd := int32(ufd) + if fd >= maxFds { + break + } file, fileVFS2, flags, ok := f.getAll(fd) if !ok { break @@ -224,6 +230,13 @@ func (f *FDTable) forEach(ctx context.Context, fn func(fd int32, file *fs.File, } } +// forEach iterates over all non-nil files upto maxFd in sorted order. +// +// It is the caller's responsibility to acquire an appropriate lock. +func (f *FDTable) forEach(ctx context.Context, fn func(fd int32, file *fs.File, fileVFS2 *vfs.FileDescription, flags FDFlags)) { + f.forEachUpTo(ctx, MaxFdLimit, fn) +} + // String is a stringer for FDTable. func (f *FDTable) String() string { var buf strings.Builder @@ -263,7 +276,7 @@ func (f *FDTable) NewFDs(ctx context.Context, minFD int32, files []*fs.File, fla } // Default limit. - end := int32(math.MaxInt32) + end := MaxFdLimit // Ensure we don't get past the provided limit. if limitSet := limits.FromContext(ctx); limitSet != nil { @@ -294,8 +307,8 @@ func (f *FDTable) NewFDs(ctx context.Context, minFD int32, files []*fs.File, fla for len(fds) < len(files) { // Try to use free bit in fdBitmap. // If all bits in fdBitmap are used, expand fd to the max. - fd := f.fdBitmap.FirstZero(uint32(minFD)) - if fd == math.MaxInt32 { + fd, err := f.fdBitmap.FirstZero(uint32(minFD)) + if err != nil { fd = uint32(max) max++ } @@ -340,7 +353,7 @@ func (f *FDTable) NewFDsVFS2(ctx context.Context, minFD int32, files []*vfs.File } // Default limit. - end := int32(math.MaxInt32) + end := MaxFdLimit // Ensure we don't get past the provided limit. if limitSet := limits.FromContext(ctx); limitSet != nil { @@ -371,8 +384,8 @@ func (f *FDTable) NewFDsVFS2(ctx context.Context, minFD int32, files []*vfs.File for len(fds) < len(files) { // Try to use free bit in fdBitmap. // If all bits in fdBitmap are used, expand fd to the max. - fd := f.fdBitmap.FirstZero(uint32(minFD)) - if fd == math.MaxInt32 { + fd, err := f.fdBitmap.FirstZero(uint32(minFD)) + if err != nil { fd = uint32(max) max++ } @@ -494,6 +507,25 @@ func (f *FDTable) SetFlags(ctx context.Context, fd int32, flags FDFlags) error { return nil } +// SetFlagsForRange sets the flags for the given range of file descriptors +// (inclusive: [startFd, endFd]). +func (f *FDTable) SetFlagsForRange(ctx context.Context, startFd int32, endFd int32, flags FDFlags) error { + if startFd < 0 || startFd > endFd { + return unix.EBADF + } + + f.mu.Lock() + defer f.mu.Unlock() + + for fd, err := f.fdBitmap.FirstOne(uint32(startFd)); err == nil && fd <= uint32(endFd); fd, err = f.fdBitmap.FirstOne(fd + 1) { + fdI32 := int32(fd) + file, _, _ := f.get(fdI32) + f.set(ctx, fdI32, file, flags) + } + + return nil +} + // SetFlagsVFS2 sets the flags for the given file descriptor. // // True is returned iff flags were changed. @@ -517,6 +549,25 @@ func (f *FDTable) SetFlagsVFS2(ctx context.Context, fd int32, flags FDFlags) err return nil } +// SetFlagsForRangeVFS2 sets the flags for the given range of file descriptors +// (inclusive: [startFd, endFd]). +func (f *FDTable) SetFlagsForRangeVFS2(ctx context.Context, startFd int32, endFd int32, flags FDFlags) error { + if startFd < 0 || startFd > endFd { + return unix.EBADF + } + + f.mu.Lock() + defer f.mu.Unlock() + + for fd, err := f.fdBitmap.FirstOne(uint32(startFd)); err == nil && fd <= uint32(endFd); fd, err = f.fdBitmap.FirstOne(fd + 1) { + fdI32 := int32(fd) + file, _, _ := f.getVFS2(fdI32) + f.setVFS2(ctx, fdI32, file, flags) + } + + return nil +} + // Get returns a reference to the file and the flags for the FD or nil if no // file is defined for the given fd. // @@ -581,12 +632,12 @@ func (f *FDTable) GetFDs(ctx context.Context) []int32 { return fds } -// Fork returns an independent FDTable. -func (f *FDTable) Fork(ctx context.Context) *FDTable { +// Fork returns an independent FDTable, cloning all FDs up to maxFds (non-inclusive). +func (f *FDTable) Fork(ctx context.Context, maxFds int32) *FDTable { clone := f.k.NewFDTable() f.mu.Lock() defer f.mu.Unlock() - f.forEach(ctx, func(fd int32, file *fs.File, fileVFS2 *vfs.FileDescription, flags FDFlags) { + f.forEachUpTo(ctx, maxFds, func(fd int32, file *fs.File, fileVFS2 *vfs.FileDescription, flags FDFlags) { // The set function here will acquire an appropriate table // reference for the clone. We don't need anything else. if df, dfVFS2 := clone.setAll(ctx, fd, file, fileVFS2, flags); df != nil || dfVFS2 != nil { @@ -597,9 +648,10 @@ func (f *FDTable) Fork(ctx context.Context) *FDTable { return clone } -// Remove removes an FD from and returns a non-file iff successful. +// Remove removes an FD from and returns a tuple where one of the files is non-nil +// iff successful. // -// N.B. Callers are required to use DecRef when they are done. +// N.B. Callers are required to use DecRef on the returned file when they are done. func (f *FDTable) Remove(ctx context.Context, fd int32) (*fs.File, *vfs.FileDescription) { if fd < 0 { return nil, nil @@ -607,30 +659,30 @@ func (f *FDTable) Remove(ctx context.Context, fd int32) (*fs.File, *vfs.FileDesc f.mu.Lock() - orig, orig2, _, _ := f.getAll(fd) + file, fileVFS2, _, _ := f.getAll(fd) // Add reference for caller. switch { - case orig != nil: - orig.IncRef() - case orig2 != nil: - orig2.IncRef() + case file != nil: + file.IncRef() + case fileVFS2 != nil: + fileVFS2.IncRef() } - if orig != nil || orig2 != nil { - orig, orig2 = f.setAll(ctx, fd, nil, nil, FDFlags{}) // Zap entry. + if file != nil || fileVFS2 != nil { + file, fileVFS2 = f.setAll(ctx, fd, nil, nil, FDFlags{}) // Zap entry. f.fdBitmap.Remove(uint32(fd)) } f.mu.Unlock() - if orig != nil { - f.drop(ctx, orig) + if file != nil { + f.drop(ctx, file) } - if orig2 != nil { - f.dropVFS2(ctx, orig2) + if fileVFS2 != nil { + f.dropVFS2(ctx, fileVFS2) } - return orig, orig2 + return file, fileVFS2 } // RemoveIf removes all FDs where cond is true. @@ -662,3 +714,55 @@ func (f *FDTable) RemoveIf(ctx context.Context, cond func(*fs.File, *vfs.FileDes f.dropVFS2(ctx, file) } } + +// RemoveNextInRange removes the next FD that falls within the given range, +// and returns a tuple where one of the files is non-nil iff successful. +// +// N.B. Callers are required to use DecRef on the returned file when they are done. +func (f *FDTable) RemoveNextInRange(ctx context.Context, startFd int32, endFd int32) (int32, *fs.File, *vfs.FileDescription) { + if startFd < 0 || startFd > endFd { + return MaxFdLimit, nil, nil + } + + f.mu.Lock() + + fdUint, err := f.fdBitmap.FirstOne(uint32(startFd)) + fd := int32(fdUint) + if err != nil || fd > endFd { + f.mu.Unlock() + return MaxFdLimit, nil, nil + } + file, fileVFS2, _, _ := f.getAll(fd) + + // Add reference for caller. + switch { + case file != nil: + file.IncRef() + case fileVFS2 != nil: + fileVFS2.IncRef() + } + + if file != nil || fileVFS2 != nil { + file, fileVFS2 = f.setAll(ctx, fd, nil, nil, FDFlags{}) // Zap entry. + f.fdBitmap.Remove(uint32(fd)) + } + f.mu.Unlock() + + if file != nil { + f.drop(ctx, file) + } + if fileVFS2 != nil { + f.dropVFS2(ctx, fileVFS2) + } + + return fd, file, fileVFS2 +} + +// GetLastFd returns the last set FD in the FDTable bitmap. +func (f *FDTable) GetLastFd() int32 { + last := f.fdBitmap.Maximum() + if last > bitmap.MaxBitEntryLimit { + return MaxFdLimit + } + return int32(last) +} diff --git a/pkg/sentry/kernel/fd_table_test.go b/pkg/sentry/kernel/fd_table_test.go index bf5460083..374ff3edb 100644 --- a/pkg/sentry/kernel/fd_table_test.go +++ b/pkg/sentry/kernel/fd_table_test.go @@ -226,3 +226,48 @@ func BenchmarkFDLookupAndDecRefConcurrent(b *testing.B) { wg.Wait() }) } + +func TestSetFlagsForRange(t *testing.T) { + type testCase struct { + name string + startFd int32 + endFd int32 + wantErr bool + } + testCases := []testCase{ + {"negative ranges", -100, -10, true}, + {"inverted positive ranges", 100, 10, true}, + {"good range", maxFD / 4, maxFD / 2, false}, + } + + for _, test := range testCases { + runTest(t, func(ctx context.Context, fdTable *FDTable, file *fs.File, _ *limits.LimitSet) { + for i := 0; i < maxFD; i++ { + if _, err := fdTable.NewFDs(ctx, 0, []*fs.File{file}, FDFlags{}); err != nil { + t.Fatalf("testCase: %v\nfdTable.NewFDs(_, 0, %+v, FDFlags{}): %d, want: nil", test, []*fs.File{file}, err) + } + } + + newFlags := FDFlags{CloseOnExec: true} + if err := fdTable.SetFlagsForRange(ctx, test.startFd, test.endFd, newFlags); (err == nil) == test.wantErr { + t.Fatalf("testCase: %v\nfdTable.SetFlagsForRange(_, %d, %d, %v): %v, waf: %t", test, test.startFd, test.endFd, newFlags, err, test.wantErr) + } + + if test.wantErr { + return + } + + testRangeFlags := func(start int32, end int32, expected FDFlags) { + for i := start; i <= end; i++ { + file, flags := fdTable.Get(i) + if file == nil || flags != expected { + t.Fatalf("testCase: %v\nfdTable.Get(%d): (%v, %v), wanted (non-nil, %v)", test, i, file, flags, expected) + } + } + } + testRangeFlags(0, test.startFd-1, FDFlags{}) + testRangeFlags(test.startFd, test.endFd, newFlags) + testRangeFlags(test.endFd+1, maxFD-1, FDFlags{}) + }) + } +} diff --git a/pkg/sentry/kernel/task_clone.go b/pkg/sentry/kernel/task_clone.go index 5084bcf03..fe43b0bd6 100644 --- a/pkg/sentry/kernel/task_clone.go +++ b/pkg/sentry/kernel/task_clone.go @@ -156,7 +156,7 @@ func (t *Task) Clone(args *linux.CloneArgs) (ThreadID, *SyscallControl, error) { var fdTable *FDTable if args.Flags&linux.CLONE_FILES == 0 { - fdTable = t.fdTable.Fork(t) + fdTable = t.fdTable.Fork(t, MaxFdLimit) } else { fdTable = t.fdTable fdTable.IncRef() @@ -480,7 +480,7 @@ func (t *Task) Unshare(flags int32) error { var oldFDTable *FDTable if flags&linux.CLONE_FILES != 0 { oldFDTable = t.fdTable - t.fdTable = oldFDTable.Fork(t) + t.fdTable = oldFDTable.Fork(t, MaxFdLimit) } var oldFSContext *FSContext if flags&linux.CLONE_FS != 0 { @@ -497,6 +497,19 @@ func (t *Task) Unshare(flags int32) error { return nil } +// UnshareFdTable unshares the FdTable that task t shares with other tasks, upto +// the maxFd. +// +// Preconditions: The caller must be running on the task goroutine. +func (t *Task) UnshareFdTable(maxFd int32) { + t.mu.Lock() + oldFDTable := t.fdTable + t.fdTable = oldFDTable.Fork(t, maxFd) + t.mu.Unlock() + + oldFDTable.DecRef(t) +} + // vforkStop is a TaskStop imposed on a task that creates a child with // CLONE_VFORK or vfork(2), that ends when the child task ceases to use its // current MM. (Normally, CLONE_VFORK is used in conjunction with CLONE_VM, so diff --git a/pkg/sentry/kernel/task_exec.go b/pkg/sentry/kernel/task_exec.go index b27fa9a6c..cadc3b848 100644 --- a/pkg/sentry/kernel/task_exec.go +++ b/pkg/sentry/kernel/task_exec.go @@ -214,7 +214,7 @@ func (r *runSyscallAfterExecStop) execute(t *Task) taskRunState { t.tg.pidns.owner.mu.Unlock() oldFDTable := t.fdTable - t.fdTable = t.fdTable.Fork(t) + t.fdTable = t.fdTable.Fork(t, int32(t.fdTable.CurrentMaxFDs())) oldFDTable.DecRef(t) // Remove FDs with the CloseOnExec flag set. diff --git a/pkg/sentry/strace/BUILD b/pkg/sentry/strace/BUILD index 369541c7a..64cf65ed6 100644 --- a/pkg/sentry/strace/BUILD +++ b/pkg/sentry/strace/BUILD @@ -7,6 +7,7 @@ go_library( srcs = [ "capability.go", "clone.go", + "close_range.go", "epoll.go", "futex.go", "linux64_amd64.go", diff --git a/pkg/sentry/strace/close_range.go b/pkg/sentry/strace/close_range.go new file mode 100644 index 000000000..ae6fb592b --- /dev/null +++ b/pkg/sentry/strace/close_range.go @@ -0,0 +1,32 @@ +// Copyright 2022 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strace + +import ( + "gvisor.dev/gvisor/pkg/abi" + "gvisor.dev/gvisor/pkg/abi/linux" +) + +// CloseRangeFlagSet is the set of close_range(2) flags. +var CloseRangeFlagSet = abi.FlagSet{ + { + Flag: uint64(linux.CLOSE_RANGE_CLOEXEC), + Name: "CLOSE_RANGE_CLOEXEC", + }, + { + Flag: uint64(linux.CLOSE_RANGE_UNSHARE), + Name: "CLOSE_RANGE_UNSHARE", + }, +} diff --git a/pkg/sentry/strace/linux64_amd64.go b/pkg/sentry/strace/linux64_amd64.go index 317c3c31c..09589fc41 100644 --- a/pkg/sentry/strace/linux64_amd64.go +++ b/pkg/sentry/strace/linux64_amd64.go @@ -372,6 +372,7 @@ var linuxAMD64 = SyscallMap{ 433: makeSyscallInfo("fspick", FD, Path, Hex), 434: makeSyscallInfo("pidfd_open", Hex, Hex), 435: makeSyscallInfo("clone3", Hex, Hex), + 436: makeSyscallInfo("close_range", FD, FD, CloseRangeFlags), 441: makeSyscallInfo("epoll_pwait2", FD, EpollEvents, Hex, Timespec, SigSet), } diff --git a/pkg/sentry/strace/linux64_arm64.go b/pkg/sentry/strace/linux64_arm64.go index 65f27c810..5f529c1f3 100644 --- a/pkg/sentry/strace/linux64_arm64.go +++ b/pkg/sentry/strace/linux64_arm64.go @@ -313,6 +313,7 @@ var linuxARM64 = SyscallMap{ 433: makeSyscallInfo("fspick", FD, Path, Hex), 434: makeSyscallInfo("pidfd_open", Hex, Hex), 435: makeSyscallInfo("clone3", Hex, Hex), + 436: makeSyscallInfo("close_range", FD, FD, CloseRangeFlags), 441: makeSyscallInfo("epoll_pwait2", FD, EpollEvents, Hex, Timespec, SigSet), } diff --git a/pkg/sentry/strace/strace.go b/pkg/sentry/strace/strace.go index 4d3f4d556..f629e0a03 100644 --- a/pkg/sentry/strace/strace.go +++ b/pkg/sentry/strace/strace.go @@ -496,6 +496,8 @@ func (i *SyscallInfo) pre(t *kernel.Task, args arch.SyscallArguments, maximumBlo output = append(output, ProtectionFlagSet.Parse(uint64(args[arg].Uint()))) case MmapFlags: output = append(output, MmapFlagSet.Parse(uint64(args[arg].Uint()))) + case CloseRangeFlags: + output = append(output, CloseRangeFlagSet.Parse(uint64(args[arg].Uint()))) case Oct: output = append(output, "0o"+strconv.FormatUint(args[arg].Uint64(), 8)) case Hex: diff --git a/pkg/sentry/strace/syscalls.go b/pkg/sentry/strace/syscalls.go index 5893443a7..a51715dc1 100644 --- a/pkg/sentry/strace/syscalls.go +++ b/pkg/sentry/strace/syscalls.go @@ -244,6 +244,9 @@ const ( // MmapFlags is the flags argument in mmap(2). MmapFlags + + // CloseRangeFlags are close_range(2) flags. + CloseRangeFlags ) // defaultFormat is the syscall argument format to use if the actual format is diff --git a/pkg/sentry/syscalls/linux/linux64.go b/pkg/sentry/syscalls/linux/linux64.go index 2046a48b9..dbc483008 100644 --- a/pkg/sentry/syscalls/linux/linux64.go +++ b/pkg/sentry/syscalls/linux/linux64.go @@ -404,6 +404,7 @@ var AMD64 = &kernel.SyscallTable{ 433: syscalls.ErrorWithEvent("fspick", linuxerr.ENOSYS, "", nil), 434: syscalls.ErrorWithEvent("pidfd_open", linuxerr.ENOSYS, "", nil), 435: syscalls.ErrorWithEvent("clone3", linuxerr.ENOSYS, "", nil), + 436: syscalls.Supported("close_range", CloseRange), 441: syscalls.Supported("epoll_pwait2", EpollPwait2), }, Emulate: map[hostarch.Addr]uintptr{ @@ -723,6 +724,7 @@ var ARM64 = &kernel.SyscallTable{ 433: syscalls.ErrorWithEvent("fspick", linuxerr.ENOSYS, "", nil), 434: syscalls.ErrorWithEvent("pidfd_open", linuxerr.ENOSYS, "", nil), 435: syscalls.ErrorWithEvent("clone3", linuxerr.ENOSYS, "", nil), + 436: syscalls.Supported("close_range", CloseRange), 441: syscalls.Supported("epoll_pwait2", EpollPwait2), }, Emulate: map[hostarch.Addr]uintptr{}, diff --git a/pkg/sentry/syscalls/linux/sys_file.go b/pkg/sentry/syscalls/linux/sys_file.go index da37a2547..1c4210cc8 100644 --- a/pkg/sentry/syscalls/linux/sys_file.go +++ b/pkg/sentry/syscalls/linux/sys_file.go @@ -15,6 +15,8 @@ package linux import ( + "math" + "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/context" @@ -801,6 +803,60 @@ func Close(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.Syscall return 0, nil, handleIOError(t, false /* partial */, err, linuxerr.EINTR, "close", file) } +// CloseRange implements linux syscall close_range(2). +func CloseRange(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) { + first := args[0].Uint() + last := args[1].Uint() + flags := args[2].Uint() + + if (first > last) || (last > math.MaxInt32) { + return 0, nil, linuxerr.EINVAL + } + + if (flags & ^(linux.CLOSE_RANGE_CLOEXEC | linux.CLOSE_RANGE_UNSHARE)) != 0 { + return 0, nil, linuxerr.EINVAL + } + + cloexec := flags & linux.CLOSE_RANGE_CLOEXEC + unshare := flags & linux.CLOSE_RANGE_UNSHARE + + if unshare != 0 { + // If possible, we don't want to copy FDs to the new unshared table, because those FDs will + // be promptly closed and no longer used. So in the case where we know the range extends all + // the way to the end of the FdTable, we can simply copy the FdTable only up to the start of + // the range that we are closing. + if cloexec == 0 && int32(last) >= t.FDTable().GetLastFd() { + t.UnshareFdTable(int32(first)) + } else { + t.UnshareFdTable(math.MaxInt32) + } + } + + if cloexec != 0 { + flagToApply := kernel.FDFlags{ + CloseOnExec: true, + } + t.FDTable().SetFlagsForRange(t.AsyncContext(), int32(first), int32(last), flagToApply) + return 0, nil, nil + } + + fdTable := t.FDTable() + fd := int32(first) + for { + fd, file, _ := fdTable.RemoveNextInRange(t, fd, int32(last)) + if file == nil { + break + } + + fd++ + // Per the close_range(2) documentation, errors upon closing file descriptors are ignored. + _ = file.Flush(t) + file.DecRef(t) + } + + return 0, nil, nil +} + // Dup implements linux syscall dup(2). func Dup(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) { fd := args[0].Int() diff --git a/pkg/sentry/syscalls/linux/vfs2/fd.go b/pkg/sentry/syscalls/linux/vfs2/fd.go index d7c58cfa4..455177fe0 100644 --- a/pkg/sentry/syscalls/linux/vfs2/fd.go +++ b/pkg/sentry/syscalls/linux/vfs2/fd.go @@ -15,6 +15,8 @@ package vfs2 import ( + "math" + "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/sentry/arch" @@ -44,6 +46,60 @@ func Close(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.Syscall return 0, nil, slinux.HandleIOErrorVFS2(t, false /* partial */, err, linuxerr.EINTR, "close", file) } +// CloseRange implements linux syscall close_range(2). +func CloseRange(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) { + first := args[0].Uint() + last := args[1].Uint() + flags := args[2].Uint() + + if (first > last) || (last > math.MaxInt32) { + return 0, nil, linuxerr.EINVAL + } + + if (flags & ^(linux.CLOSE_RANGE_CLOEXEC | linux.CLOSE_RANGE_UNSHARE)) != 0 { + return 0, nil, linuxerr.EINVAL + } + + cloexec := flags & linux.CLOSE_RANGE_CLOEXEC + unshare := flags & linux.CLOSE_RANGE_UNSHARE + + if unshare != 0 { + // If possible, we don't want to copy FDs to the new unshared table, because those FDs will + // be promptly closed and no longer used. So in the case where we know the range extends all + // the way to the end of the FdTable, we can simply copy the FdTable only up to the start of + // the range that we are closing. + if cloexec == 0 && int32(last) >= t.FDTable().GetLastFd() { + t.UnshareFdTable(int32(first)) + } else { + t.UnshareFdTable(math.MaxInt32) + } + } + + if cloexec != 0 { + flagToApply := kernel.FDFlags{ + CloseOnExec: true, + } + t.FDTable().SetFlagsForRangeVFS2(t.AsyncContext(), int32(first), int32(last), flagToApply) + return 0, nil, nil + } + + fdTable := t.FDTable() + fd := int32(first) + for { + fd, _, file := fdTable.RemoveNextInRange(t, fd, int32(last)) + if file == nil { + break + } + + fd++ + // Per the close_range(2) documentation, errors upon closing file descriptors are ignored. + _ = file.OnClose(t) + file.DecRef(t) + } + + return 0, nil, nil +} + // Dup implements Linux syscall dup(2). func Dup(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) { fd := args[0].Int() diff --git a/pkg/sentry/syscalls/linux/vfs2/vfs2.go b/pkg/sentry/syscalls/linux/vfs2/vfs2.go index 7ed020c97..8d20b7f64 100644 --- a/pkg/sentry/syscalls/linux/vfs2/vfs2.go +++ b/pkg/sentry/syscalls/linux/vfs2/vfs2.go @@ -162,6 +162,7 @@ func Override() { s.Table[327] = syscalls.Supported("preadv2", Preadv2) s.Table[328] = syscalls.Supported("pwritev2", Pwritev2) s.Table[332] = syscalls.Supported("statx", Statx) + s.Table[436] = syscalls.Supported("close_range", CloseRange) s.Table[439] = syscalls.Supported("faccessat2", Faccessat2) s.Table[441] = syscalls.Supported("epoll_pwait2", EpollPwait2) s.Init() @@ -277,6 +278,7 @@ func Override() { s.Table[286] = syscalls.Supported("preadv2", Preadv2) s.Table[287] = syscalls.Supported("pwritev2", Pwritev2) s.Table[291] = syscalls.Supported("statx", Statx) + s.Table[436] = syscalls.Supported("close_range", CloseRange) s.Table[439] = syscalls.Supported("faccessat2", Faccessat2) s.Table[441] = syscalls.Supported("epoll_pwait2", EpollPwait2) s.Init() diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD index 2011ad221..aaefbf78d 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -1078,3 +1078,8 @@ syscall_test( syscall_test( test = "//test/syscalls/linux:deleted_test", ) + +syscall_test( + size = "small", + test = "//test/syscalls/linux:close_range_test", +) diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 500980de4..ef0143343 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -4524,3 +4524,20 @@ cc_binary( "//test/util:test_util", ], ) + +cc_binary( + name = "close_range_test", + testonly = 1, + srcs = ["close_range.cc"], + linkstatic = 1, + deps = [ + "//test/util:file_descriptor", + "@com_google_absl//absl/base:core_headers", + gtest, + "//test/util:cleanup", + "//test/util:posix_error", + "//test/util:temp_path", + "//test/util:test_main", + "//test/util:test_util", + ], +) diff --git a/test/syscalls/linux/close_range.cc b/test/syscalls/linux/close_range.cc new file mode 100644 index 000000000..e4cba1e20 --- /dev/null +++ b/test/syscalls/linux/close_range.cc @@ -0,0 +1,373 @@ +// Copyright 2022 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include + +#include "gtest/gtest.h" +#include "absl/base/macros.h" +#include "test/util/file_descriptor.h" +#include "test/util/posix_error.h" +#include "test/util/temp_path.h" +#include "test/util/test_util.h" + +namespace gvisor { +namespace testing { + +namespace { + +#ifndef CLOSE_RANGE_UNSHARE +#define CLOSE_RANGE_UNSHARE (1U << 1) +#endif +#ifndef CLOSE_RANGE_CLOEXEC +#define CLOSE_RANGE_CLOEXEC (1U << 2) +#endif + +#ifndef SYS_close_range +#if defined(__x86_64__) || defined(__aarch64__) +#define SYS_close_range 436 +#else +#error "Unknown architecture" +#endif +#endif // SYS_close_range + +int close_range(unsigned int first, unsigned int last, unsigned int flags) { + return syscall(SYS_close_range, first, last, flags); +} + +class CloseRangeTest : public ::testing::Test { + public: + void CreateFiles(int num_files) { + file_names_.reserve(num_files); + for (int i = 0; i < num_files; ++i) { + file_names_.push_back(NewTempAbsPath()); + int fd; + ASSERT_THAT(fd = open(file_names_[i].c_str(), O_CREAT, 0644), + SyscallSucceeds()); + ASSERT_THAT(close(fd), SyscallSucceeds()); + } + } + + void OpenFilesRdwr() { + fds_.clear(); + fds_.reserve(file_names_.size()); + for (std::string &file_name : file_names_) { + int fd; + ASSERT_THAT(fd = open(file_name.c_str(), O_RDWR), SyscallSucceeds()); + fds_.push_back(fd); + } + } + + private: + void TearDown() override { + for (std::string &name : file_names_) { + unlink(name.c_str()); + } + } + + protected: + std::vector file_names_; + std::vector fds_; +}; + +// Base test to confirm that all files in contiguous range get closed. +TEST_F(CloseRangeTest, ContiguousRange) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 10; + unsigned int flags = 0; + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } + + EXPECT_THAT(close_range(fds_[0], fds_[num_files_in_range - 1], flags), + SyscallSucceeds()); + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, PosixErrorIs(EBADF)); + } +} + +// Test to confirm that a range with files already closed in the range still +// closes the remaining files. +TEST_F(CloseRangeTest, RangeWithHoles) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 10; + unsigned int flags = 0; + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } + + EXPECT_THAT(close(fds_[2]), SyscallSucceeds()); + EXPECT_THAT(close(fds_[7]), SyscallSucceeds()); + + EXPECT_THAT(close_range(fds_[0], fds_[num_files_in_range - 1], flags), + SyscallSucceeds()); + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, PosixErrorIs(EBADF)); + } +} + +// Test to confirm that closing a range with fds preceding and following the +// range leaves those other fds open. +TEST_F(CloseRangeTest, RangeInMiddleOfOpenFiles) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 10; + unsigned int flags = 0; + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } + + size_t slice_start = 4; + size_t slice_end = 7; + EXPECT_THAT(close_range(fds_[slice_start], fds_[slice_end], flags), + SyscallSucceeds()); + for (int fd : + std::vector(fds_.begin() + slice_start, fds_.begin() + slice_end + 1)) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, PosixErrorIs(EBADF)); + } + for (int fd : std::vector(fds_.begin(), fds_.begin() + slice_start)) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } + for (int fd : std::vector(fds_.begin() + slice_end + 1, fds_.end())) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } +} + +// Test to confirm that calling close_range on just one file succeeds. +TEST_F(CloseRangeTest, SingleFile) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 1; + unsigned int flags = 0; /* */ + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + auto ret = ReadAllFd(fds_[0]); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + + EXPECT_THAT(close_range(fds_[0], fds_[0], flags), SyscallSucceeds()); + + ret = ReadAllFd(fds_[0]); + EXPECT_THAT(ret, PosixErrorIs(EBADF)); +} + +// Test to confirm that calling close_range twice on the same range does not +// cause errors. +TEST_F(CloseRangeTest, CallCloseRangeTwice) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 10; + unsigned int flags = 0; + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } + + EXPECT_THAT(close_range(fds_[0], fds_[num_files_in_range - 1], flags), + SyscallSucceeds()); + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, PosixErrorIs(EBADF)); + } + + EXPECT_THAT(close_range(fds_[0], fds_[num_files_in_range - 1], flags), + SyscallSucceeds()); +} + +// Test that using CLOEXEC flag does not close the file for this process. +TEST_F(CloseRangeTest, CloexecFlagTest) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 10; + unsigned int flags = CLOSE_RANGE_CLOEXEC; + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } + + EXPECT_THAT(close_range(fds_[0], fds_[num_files_in_range - 1], flags), + SyscallSucceeds()); + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } +} + +// Test that using UNSHARE flag still properly closes the files. +TEST_F(CloseRangeTest, UnshareFlagTest) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 10; + unsigned int flags = CLOSE_RANGE_UNSHARE; + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } + + EXPECT_THAT(close_range(fds_[0], fds_[num_files_in_range - 1], flags), + SyscallSucceeds()); + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, PosixErrorIs(EBADF)); + } +} + +// Test that using the UNSHARE flag and closing files at the start of the range +// still leaves the latter files opened. +TEST_F(CloseRangeTest, UnshareFlagAndCloseRangeAtStart) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 10; + unsigned int flags = CLOSE_RANGE_UNSHARE; + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } + + size_t range_split = 5; + EXPECT_THAT(close_range(fds_[0], fds_[range_split - 1], flags), + SyscallSucceeds()); + for (int fd : std::vector(fds_.begin(), fds_.begin() + range_split)) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, PosixErrorIs(EBADF)); + } + for (int fd : std::vector(fds_.begin() + range_split, fds_.end())) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } +} + +// Test that using the UNSHARE flag and closing files at the end of the range +// still leaves the earlier files opened. +TEST_F(CloseRangeTest, UnshareFlagAndCloseRangeAtEnd) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 10; + unsigned int flags = CLOSE_RANGE_UNSHARE; + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } + + size_t range_split = 5; + EXPECT_THAT( + close_range(fds_[range_split], fds_[num_files_in_range - 1], flags), + SyscallSucceeds()); + for (int fd : std::vector(fds_.begin(), fds_.begin() + range_split)) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } + for (int fd : std::vector(fds_.begin() + range_split, fds_.end())) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, PosixErrorIs(EBADF)); + } +} + +// Test that using both CLOEXEC and UNSHARE flags does not close files for this +// process. +TEST_F(CloseRangeTest, CloexecAndUnshareFlagTest) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 10; + unsigned int flags = CLOSE_RANGE_CLOEXEC | CLOSE_RANGE_UNSHARE; + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } + + EXPECT_THAT(close_range(fds_[0], fds_[num_files_in_range - 1], flags), + SyscallSucceeds()); + for (int fd : fds_) { + auto ret = ReadAllFd(fd); + EXPECT_THAT(ret, IsPosixErrorOkMatcher()); + } +} + +// Test that calling with invalid range does not succeed. +TEST_F(CloseRangeTest, RangeFirstGreaterThanLast) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 10; + unsigned int flags = 0; + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + EXPECT_THAT(close_range(fds_[num_files_in_range - 1], fds_[0], flags), + SyscallFailsWithErrno(EINVAL)); +} + +// Test that calling with invalid flags does not succeed. +TEST_F(CloseRangeTest, InvalidFlags) { + SKIP_IF(!IsRunningOnGvisor() && close_range(1, 0, 0) < 0 && errno == ENOSYS); + int num_files_in_range = 10; + + CreateFiles(num_files_in_range); + OpenFilesRdwr(); + + unsigned int flags = CLOSE_RANGE_CLOEXEC | CLOSE_RANGE_UNSHARE | 0xF; + EXPECT_THAT(close_range(fds_[0], fds_[num_files_in_range - 1], flags), + SyscallFailsWithErrno(EINVAL)); + + flags = 0xF0; + EXPECT_THAT(close_range(fds_[0], fds_[num_files_in_range - 1], flags), + SyscallFailsWithErrno(EINVAL)); + + flags = CLOSE_RANGE_CLOEXEC | 0xF00; + EXPECT_THAT(close_range(fds_[0], fds_[num_files_in_range - 1], flags), + SyscallFailsWithErrno(EINVAL)); +} + +} // namespace + +} // namespace testing +} // namespace gvisor