Implement close_range.

Fixes #5500

PiperOrigin-RevId: 431454836
This commit is contained in:
Konstantin Bogomolov
2022-02-28 09:37:03 -08:00
committed by gVisor bot
parent f375784d83
commit 5c95e1d39c
20 changed files with 819 additions and 37 deletions
+6
View File
@@ -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)
)
+32 -5
View File
@@ -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.
+40 -4
View File
@@ -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)
}
}
}
+129 -25
View File
@@ -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)
}
+45
View File
@@ -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{})
})
}
}
+15 -2
View File
@@ -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
+1 -1
View File
@@ -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.
+1
View File
@@ -7,6 +7,7 @@ go_library(
srcs = [
"capability.go",
"clone.go",
"close_range.go",
"epoll.go",
"futex.go",
"linux64_amd64.go",
+32
View File
@@ -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",
},
}
+1
View File
@@ -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),
}
+1
View File
@@ -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),
}
+2
View File
@@ -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:
+3
View File
@@ -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
+2
View File
@@ -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{},
+56
View File
@@ -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()
+56
View File
@@ -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()
+2
View File
@@ -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()
+5
View File
@@ -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",
)
+17
View File
@@ -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",
],
)
+373
View File
@@ -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 <asm-generic/errno-base.h>
#include <unistd.h>
#include <vector>
#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<std::string> file_names_;
std::vector<unsigned int> 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