Simplify FD handling for container start/exec

VFS1 and VFS2 host FDs have different dupping behavior,
making error prone to code for both. Change the contract
so that FDs are released as they are used, so the caller
can simple defer a block that closes all remaining files.
This also addresses handling of partial failures.

With this fix, more VFS2 tests can be enabled.

Updates #1487

PiperOrigin-RevId: 330112266
This commit is contained in:
Fabricio Voznika
2020-09-04 11:42:02 -07:00
committed by gVisor bot
parent c564293b65
commit 2202812e07
11 changed files with 120 additions and 112 deletions
+32 -10
View File
@@ -49,7 +49,7 @@ func fixCount(n int, err error) (int, error) {
// Read implements io.Reader.
func (r *ReadWriter) Read(b []byte) (int, error) {
c, err := fixCount(syscall.Read(int(atomic.LoadInt64(&r.fd)), b))
c, err := fixCount(syscall.Read(r.FD(), b))
if c == 0 && len(b) > 0 && err == nil {
return 0, io.EOF
}
@@ -62,7 +62,7 @@ func (r *ReadWriter) Read(b []byte) (int, error) {
func (r *ReadWriter) ReadAt(b []byte, off int64) (c int, err error) {
for len(b) > 0 {
var m int
m, err = fixCount(syscall.Pread(int(atomic.LoadInt64(&r.fd)), b, off))
m, err = fixCount(syscall.Pread(r.FD(), b, off))
if m == 0 && err == nil {
return c, io.EOF
}
@@ -82,7 +82,7 @@ func (r *ReadWriter) Write(b []byte) (int, error) {
var n, remaining int
for remaining = len(b); remaining > 0; {
woff := len(b) - remaining
n, err = syscall.Write(int(atomic.LoadInt64(&r.fd)), b[woff:])
n, err = syscall.Write(r.FD(), b[woff:])
if n > 0 {
// syscall.Write wrote some bytes. This is the common case.
@@ -110,7 +110,7 @@ func (r *ReadWriter) Write(b []byte) (int, error) {
func (r *ReadWriter) WriteAt(b []byte, off int64) (c int, err error) {
for len(b) > 0 {
var m int
m, err = fixCount(syscall.Pwrite(int(atomic.LoadInt64(&r.fd)), b, off))
m, err = fixCount(syscall.Pwrite(r.FD(), b, off))
if err != nil {
break
}
@@ -121,6 +121,16 @@ func (r *ReadWriter) WriteAt(b []byte, off int64) (c int, err error) {
return
}
// FD returns the owned file descriptor. Ownership remains unchanged.
func (r *ReadWriter) FD() int {
return int(atomic.LoadInt64(&r.fd))
}
// String implements Stringer.String().
func (r *ReadWriter) String() string {
return fmt.Sprintf("FD: %d", r.FD())
}
// FD owns a host file descriptor.
//
// It is similar to os.File, with a few important distinctions:
@@ -167,6 +177,23 @@ func NewFromFile(file *os.File) (*FD, error) {
return New(fd), nil
}
// NewFromFiles creates new FDs for each file in the slice.
func NewFromFiles(files []*os.File) ([]*FD, error) {
rv := make([]*FD, 0, len(files))
for _, f := range files {
new, err := NewFromFile(f)
if err != nil {
// Cleanup on error.
for _, fd := range rv {
fd.Close()
}
return nil, err
}
rv = append(rv, new)
}
return rv, nil
}
// Open is equivalent to open(2).
func Open(path string, openmode int, perm uint32) (*FD, error) {
f, err := syscall.Open(path, openmode|syscall.O_LARGEFILE, perm)
@@ -204,11 +231,6 @@ func (f *FD) Release() int {
return int(atomic.SwapInt64(&f.fd, -1))
}
// FD returns the file descriptor owned by FD. FD retains ownership.
func (f *FD) FD() int {
return int(atomic.LoadInt64(&f.fd))
}
// File converts the FD to an os.File.
//
// FD does not transfer ownership of the file descriptor (it will be
@@ -219,7 +241,7 @@ func (f *FD) FD() int {
// This operation is somewhat expensive, so care should be taken to minimize
// its use.
func (f *FD) File() (*os.File, error) {
fd, err := syscall.Dup(int(atomic.LoadInt64(&f.fd)))
fd, err := syscall.Dup(f.FD())
if err != nil {
return nil, err
}
-1
View File
@@ -35,7 +35,6 @@ go_library(
"//pkg/sync",
"//pkg/tcpip/link/sniffer",
"//pkg/urpc",
"@org_golang_x_sys//unix:go_default_library",
],
)
+9 -19
View File
@@ -23,8 +23,8 @@ import (
"text/tabwriter"
"time"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/sentry/fdimport"
"gvisor.dev/gvisor/pkg/sentry/fs"
"gvisor.dev/gvisor/pkg/sentry/fs/host"
@@ -203,27 +203,17 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadI
}
initArgs.Filename = resolved
fds := make([]int, len(args.FilePayload.Files))
for i, file := range args.FilePayload.Files {
if kernel.VFS2Enabled {
// Need to dup to remove ownership from os.File.
dup, err := unix.Dup(int(file.Fd()))
if err != nil {
return nil, 0, nil, nil, fmt.Errorf("duplicating payload files: %w", err)
}
fds[i] = dup
} else {
// VFS1 dups the file on import.
fds[i] = int(file.Fd())
}
fds, err := fd.NewFromFiles(args.Files)
if err != nil {
return nil, 0, nil, nil, fmt.Errorf("duplicating payload files: %w", err)
}
defer func() {
for _, fd := range fds {
_ = fd.Close()
}
}()
ttyFile, ttyFileVFS2, err := fdimport.Import(ctx, fdTable, args.StdioIsPty, fds)
if err != nil {
if kernel.VFS2Enabled {
for _, fd := range fds {
unix.Close(fd)
}
}
return nil, 0, nil, nil, err
}
+1
View File
@@ -10,6 +10,7 @@ go_library(
visibility = ["//pkg/sentry:internal"],
deps = [
"//pkg/context",
"//pkg/fd",
"//pkg/sentry/fs",
"//pkg/sentry/fs/host",
"//pkg/sentry/fsimpl/host",
+14 -8
View File
@@ -18,6 +18,7 @@ import (
"fmt"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/sentry/fs"
"gvisor.dev/gvisor/pkg/sentry/fs/host"
hostvfs2 "gvisor.dev/gvisor/pkg/sentry/fsimpl/host"
@@ -27,8 +28,9 @@ import (
// Import imports a slice of FDs into the given FDTable. If console is true,
// sets up TTY for the first 3 FDs in the slice representing stdin, stdout,
// stderr. Upon success, Import takes ownership of all FDs.
func Import(ctx context.Context, fdTable *kernel.FDTable, console bool, fds []int) (*host.TTYFileOperations, *hostvfs2.TTYFileDescription, error) {
// stderr. Used FDs are either closed or released. It's safe for the caller to
// close any remaining files upon return.
func Import(ctx context.Context, fdTable *kernel.FDTable, console bool, fds []*fd.FD) (*host.TTYFileOperations, *hostvfs2.TTYFileDescription, error) {
if kernel.VFS2Enabled {
ttyFile, err := importVFS2(ctx, fdTable, console, fds)
return nil, ttyFile, err
@@ -37,7 +39,7 @@ func Import(ctx context.Context, fdTable *kernel.FDTable, console bool, fds []in
return ttyFile, nil, err
}
func importFS(ctx context.Context, fdTable *kernel.FDTable, console bool, fds []int) (*host.TTYFileOperations, error) {
func importFS(ctx context.Context, fdTable *kernel.FDTable, console bool, fds []*fd.FD) (*host.TTYFileOperations, error) {
var ttyFile *fs.File
for appFD, hostFD := range fds {
var appFile *fs.File
@@ -46,11 +48,12 @@ func importFS(ctx context.Context, fdTable *kernel.FDTable, console bool, fds []
// Import the file as a host TTY file.
if ttyFile == nil {
var err error
appFile, err = host.ImportFile(ctx, hostFD, true /* isTTY */)
appFile, err = host.ImportFile(ctx, hostFD.FD(), true /* isTTY */)
if err != nil {
return nil, err
}
defer appFile.DecRef(ctx)
_ = hostFD.Close() // FD is dup'd i ImportFile.
// Remember this in the TTY file, as we will
// use it for the other stdio FDs.
@@ -65,11 +68,12 @@ func importFS(ctx context.Context, fdTable *kernel.FDTable, console bool, fds []
} else {
// Import the file as a regular host file.
var err error
appFile, err = host.ImportFile(ctx, hostFD, false /* isTTY */)
appFile, err = host.ImportFile(ctx, hostFD.FD(), false /* isTTY */)
if err != nil {
return nil, err
}
defer appFile.DecRef(ctx)
_ = hostFD.Close() // FD is dup'd i ImportFile.
}
// Add the file to the FD map.
@@ -84,7 +88,7 @@ func importFS(ctx context.Context, fdTable *kernel.FDTable, console bool, fds []
return ttyFile.FileOperations.(*host.TTYFileOperations), nil
}
func importVFS2(ctx context.Context, fdTable *kernel.FDTable, console bool, stdioFDs []int) (*hostvfs2.TTYFileDescription, error) {
func importVFS2(ctx context.Context, fdTable *kernel.FDTable, console bool, stdioFDs []*fd.FD) (*hostvfs2.TTYFileDescription, error) {
k := kernel.KernelFromContext(ctx)
if k == nil {
return nil, fmt.Errorf("cannot find kernel from context")
@@ -98,11 +102,12 @@ func importVFS2(ctx context.Context, fdTable *kernel.FDTable, console bool, stdi
// Import the file as a host TTY file.
if ttyFile == nil {
var err error
appFile, err = hostvfs2.ImportFD(ctx, k.HostMount(), hostFD, true /* isTTY */)
appFile, err = hostvfs2.ImportFD(ctx, k.HostMount(), hostFD.FD(), true /* isTTY */)
if err != nil {
return nil, err
}
defer appFile.DecRef(ctx)
hostFD.Release() // FD is transfered to host FD.
// Remember this in the TTY file, as we will use it for the other stdio
// FDs.
@@ -115,11 +120,12 @@ func importVFS2(ctx context.Context, fdTable *kernel.FDTable, console bool, stdi
}
} else {
var err error
appFile, err = hostvfs2.ImportFD(ctx, k.HostMount(), hostFD, false /* isTTY */)
appFile, err = hostvfs2.ImportFD(ctx, k.HostMount(), hostFD.FD(), false /* isTTY */)
if err != nil {
return nil, err
}
defer appFile.DecRef(ctx)
hostFD.Release() // FD is transfered to host FD.
}
if err := fdTable.NewFDAtVFS2(ctx, int32(appFD), appFile, kernel.FDFlags{}); err != nil {
+2
View File
@@ -30,6 +30,7 @@ go_library(
"//pkg/control/server",
"//pkg/cpuid",
"//pkg/eventchannel",
"//pkg/fd",
"//pkg/fspath",
"//pkg/log",
"//pkg/memutil",
@@ -123,6 +124,7 @@ go_test(
library = ":boot",
deps = [
"//pkg/control/server",
"//pkg/fd",
"//pkg/fspath",
"//pkg/log",
"//pkg/p9",
+10 -2
View File
@@ -22,6 +22,7 @@ import (
specs "github.com/opencontainers/runtime-spec/specs-go"
"gvisor.dev/gvisor/pkg/control/server"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/control"
"gvisor.dev/gvisor/pkg/sentry/fs"
@@ -257,13 +258,20 @@ func (cm *containerManager) Start(args *StartArgs, _ *struct{}) error {
// All validation passed, logs the spec for debugging.
specutils.LogSpec(args.Spec)
err := cm.l.startContainer(args.Spec, args.Conf, args.CID, args.FilePayload.Files)
fds, err := fd.NewFromFiles(args.FilePayload.Files)
if err != nil {
return err
}
defer func() {
for _, fd := range fds {
_ = fd.Close()
}
}()
if err := cm.l.startContainer(args.Spec, args.Conf, args.CID, fds); err != nil {
log.Debugf("containerManager.Start failed %q: %+v: %v", args.CID, args, err)
return err
}
log.Debugf("Container %q started", args.CID)
return nil
}
+4 -3
View File
@@ -34,6 +34,7 @@ import (
specs "github.com/opencontainers/runtime-spec/specs-go"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/fs"
"gvisor.dev/gvisor/pkg/sentry/fs/gofer"
@@ -320,14 +321,14 @@ func adjustDirentCache(k *kernel.Kernel) error {
}
type fdDispenser struct {
fds []int
fds []*fd.FD
}
func (f *fdDispenser) remove() int {
if f.empty() {
panic("fdDispenser out of fds")
}
rv := f.fds[0]
rv := f.fds[0].Release()
f.fds = f.fds[1:]
return rv
}
@@ -564,7 +565,7 @@ type containerMounter struct {
hints *podMountHints
}
func newContainerMounter(spec *specs.Spec, goferFDs []int, k *kernel.Kernel, hints *podMountHints) *containerMounter {
func newContainerMounter(spec *specs.Spec, goferFDs []*fd.FD, k *kernel.Kernel, hints *podMountHints) *containerMounter {
return &containerMounter{
root: spec.Root,
mounts: compileMounts(spec),
+37 -62
View File
@@ -29,6 +29,7 @@ import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/cpuid"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/memutil"
"gvisor.dev/gvisor/pkg/rand"
@@ -89,10 +90,10 @@ type containerInfo struct {
procArgs kernel.CreateProcessArgs
// stdioFDs contains stdin, stdout, and stderr.
stdioFDs []int
stdioFDs []*fd.FD
// goferFDs are the FDs that attach the sandbox to the gofers.
goferFDs []int
goferFDs []*fd.FD
}
// Loader keeps state needed to start the kernel and run the container..
@@ -356,12 +357,17 @@ func New(args Args) (*Loader, error) {
k.SetHostMount(hostMount)
}
info := containerInfo{
conf: args.Conf,
spec: args.Spec,
procArgs: procArgs,
}
// Make host FDs stable between invocations. Host FDs must map to the exact
// same number when the sandbox is restored. Otherwise the wrong FD will be
// used.
var stdioFDs []int
newfd := startingStdioFD
for _, fd := range args.StdioFDs {
for _, stdioFD := range args.StdioFDs {
// Check that newfd is unused to avoid clobbering over it.
if _, err := unix.FcntlInt(uintptr(newfd), unix.F_GETFD, 0); !errors.Is(err, unix.EBADF) {
if err != nil {
@@ -370,14 +376,17 @@ func New(args Args) (*Loader, error) {
return nil, fmt.Errorf("unable to remap stdios, FD %d is already in use", newfd)
}
err := unix.Dup3(fd, newfd, unix.O_CLOEXEC)
err := unix.Dup3(stdioFD, newfd, unix.O_CLOEXEC)
if err != nil {
return nil, fmt.Errorf("dup3 of stdioFDs failed: %v", err)
return nil, fmt.Errorf("dup3 of stdios failed: %w", err)
}
stdioFDs = append(stdioFDs, newfd)
_ = unix.Close(fd)
info.stdioFDs = append(info.stdioFDs, fd.New(newfd))
_ = unix.Close(stdioFD)
newfd++
}
for _, goferFD := range args.GoferFDs {
info.goferFDs = append(info.goferFDs, fd.New(goferFD))
}
eid := execID{cid: args.ID}
l := &Loader{
@@ -386,13 +395,7 @@ func New(args Args) (*Loader, error) {
sandboxID: args.ID,
processes: map[execID]*execProcess{eid: {}},
mountHints: mountHints,
root: containerInfo{
conf: args.Conf,
stdioFDs: stdioFDs,
goferFDs: args.GoferFDs,
spec: args.Spec,
procArgs: procArgs,
},
root: info,
}
// We don't care about child signals; some platforms can generate a
@@ -466,9 +469,14 @@ func (l *Loader) Destroy() {
}
l.watchdog.Stop()
for i, fd := range l.root.stdioFDs {
_ = unix.Close(fd)
l.root.stdioFDs[i] = -1
// In the success case, stdioFDs and goferFDs will only contain
// released/closed FDs that ownership has been passed over to host FDs and
// gofer sessions. Close them here in case on failure.
for _, fd := range l.root.stdioFDs {
_ = fd.Close()
}
for _, fd := range l.root.goferFDs {
_ = fd.Close()
}
}
@@ -598,17 +606,6 @@ func (l *Loader) run() error {
}
})
// l.stdioFDs are derived from dup() in boot.New() and they are now dup()ed again
// either in createFDTable() during initial start or in descriptor.initAfterLoad()
// during restore, we can release l.stdioFDs now. VFS2 takes ownership of the
// passed FDs, so only close for VFS1.
if !kernel.VFS2Enabled {
for i, fd := range l.root.stdioFDs {
_ = unix.Close(fd)
l.root.stdioFDs[i] = -1
}
}
log.Infof("Process should have started...")
l.watchdog.Start()
return l.k.Start()
@@ -628,9 +625,9 @@ func (l *Loader) createContainer(cid string) error {
}
// startContainer starts a child container. It returns the thread group ID of
// the newly created process. Caller owns 'files' and may close them after
// this method returns.
func (l *Loader) startContainer(spec *specs.Spec, conf *config.Config, cid string, files []*os.File) error {
// the newly created process. Used FDs are either closed or released. It's safe
// for the caller to close any remaining files upon return.
func (l *Loader) startContainer(spec *specs.Spec, conf *config.Config, cid string, files []*fd.FD) error {
// Create capabilities.
caps, err := specutils.Capabilities(conf.EnableRaw, spec.Process.Capabilities)
if err != nil {
@@ -681,37 +678,15 @@ func (l *Loader) startContainer(spec *specs.Spec, conf *config.Config, cid strin
}
info := &containerInfo{
conf: conf,
spec: spec,
conf: conf,
spec: spec,
stdioFDs: files[:3],
goferFDs: files[3:],
}
info.procArgs, err = createProcessArgs(cid, spec, creds, l.k, pidns)
if err != nil {
return fmt.Errorf("creating new process: %v", err)
}
// VFS1 dups stdioFDs, so we don't need to dup them here. VFS2 takes
// ownership of the passed FDs, and we need to dup them here.
for _, f := range files[:3] {
if !kernel.VFS2Enabled {
info.stdioFDs = append(info.stdioFDs, int(f.Fd()))
} else {
fd, err := unix.Dup(int(f.Fd()))
if err != nil {
return fmt.Errorf("failed to dup file: %v", err)
}
info.stdioFDs = append(info.stdioFDs, fd)
}
}
// Can't take ownership away from os.File. dup them to get a new FDs.
for _, f := range files[3:] {
fd, err := unix.Dup(int(f.Fd()))
if err != nil {
return fmt.Errorf("failed to dup file: %v", err)
}
info.goferFDs = append(info.goferFDs, fd)
}
tg, err := l.createContainerProcess(false, cid, info, ep)
if err != nil {
return err
@@ -795,13 +770,13 @@ func (l *Loader) createContainerProcess(root bool, cid string, info *containerIn
// startGoferMonitor runs a goroutine to monitor gofer's health. It polls on
// the gofer FDs looking for disconnects, and destroys the container if a
// disconnect occurs in any of the gofer FDs.
func (l *Loader) startGoferMonitor(cid string, goferFDs []int) {
func (l *Loader) startGoferMonitor(cid string, goferFDs []*fd.FD) {
go func() {
log.Debugf("Monitoring gofer health for container %q", cid)
var events []unix.PollFd
for _, fd := range goferFDs {
for _, goferFD := range goferFDs {
events = append(events, unix.PollFd{
Fd: int32(fd),
Fd: int32(goferFD.FD()),
Events: unix.POLLHUP | unix.POLLRDHUP,
})
}
@@ -1281,7 +1256,7 @@ func (l *Loader) ttyFromIDLocked(key execID) (*host.TTYFileOperations, *hostvfs2
return ep.tty, ep.ttyVFS2, nil
}
func createFDTable(ctx context.Context, console bool, stdioFDs []int) (*kernel.FDTable, *host.TTYFileOperations, *hostvfs2.TTYFileDescription, error) {
func createFDTable(ctx context.Context, console bool, stdioFDs []*fd.FD) (*kernel.FDTable, *host.TTYFileOperations, *hostvfs2.TTYFileDescription, error) {
if len(stdioFDs) != 3 {
return nil, nil, nil, fmt.Errorf("stdioFDs should contain exactly 3 FDs (stdin, stdout, and stderr), but %d FDs received", len(stdioFDs))
}
+7 -2
View File
@@ -26,6 +26,7 @@ import (
specs "github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/control/server"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/fspath"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/p9"
@@ -444,7 +445,7 @@ func TestCreateMountNamespace(t *testing.T) {
}
defer cleanup()
mntr := newContainerMounter(&tc.spec, []int{sandEnd}, nil, &podMountHints{})
mntr := newContainerMounter(&tc.spec, []*fd.FD{fd.New(sandEnd)}, nil, &podMountHints{})
mns, err := mntr.createMountNamespace(ctx, conf)
if err != nil {
t.Fatalf("failed to create mount namespace: %v", err)
@@ -702,7 +703,11 @@ func TestRestoreEnvironment(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conf := testConfig()
mntr := newContainerMounter(tc.spec, tc.ioFDs, nil, &podMountHints{})
var ioFDs []*fd.FD
for _, ioFD := range tc.ioFDs {
ioFDs = append(ioFDs, fd.New(ioFD))
}
mntr := newContainerMounter(tc.spec, ioFDs, nil, &podMountHints{})
actualRenv, err := mntr.createRestoreEnvironment(conf)
if !tc.errorExpected && err != nil {
t.Fatalf("could not create restore environment for test:%s", tc.name)
+4 -5
View File
@@ -169,7 +169,7 @@ func TestMultiContainerSanity(t *testing.T) {
// TestMultiPIDNS checks that it is possible to run 2 dead-simple
// containers in the same sandbox with different pidns.
func TestMultiPIDNS(t *testing.T) {
for name, conf := range configs(t, all...) {
for name, conf := range configsWithVFS2(t, all...) {
t.Run(name, func(t *testing.T) {
rootDir, cleanup, err := testutil.SetupRootDir()
if err != nil {
@@ -214,7 +214,7 @@ func TestMultiPIDNS(t *testing.T) {
// TestMultiPIDNSPath checks the pidns path.
func TestMultiPIDNSPath(t *testing.T) {
for name, conf := range configs(t, all...) {
for name, conf := range configsWithVFS2(t, all...) {
t.Run(name, func(t *testing.T) {
rootDir, cleanup, err := testutil.SetupRootDir()
if err != nil {
@@ -580,7 +580,7 @@ func TestMultiContainerDestroy(t *testing.T) {
t.Fatal("error finding test_app:", err)
}
for name, conf := range configs(t, all...) {
for name, conf := range configsWithVFS2(t, all...) {
t.Run(name, func(t *testing.T) {
rootDir, cleanup, err := testutil.SetupRootDir()
if err != nil {
@@ -1252,8 +1252,7 @@ func TestMultiContainerSharedMountReadonly(t *testing.T) {
// Test that shared pod mounts continue to work after container is restarted.
func TestMultiContainerSharedMountRestart(t *testing.T) {
//TODO(gvisor.dev/issue/1487): This is failing with VFS2.
for name, conf := range configs(t, all...) {
for name, conf := range configsWithVFS2(t, all...) {
t.Run(name, func(t *testing.T) {
rootDir, cleanup, err := testutil.SetupRootDir()
if err != nil {