diff --git a/pkg/fsutil/BUILD b/pkg/fsutil/BUILD new file mode 100644 index 000000000..f3ab1ba90 --- /dev/null +++ b/pkg/fsutil/BUILD @@ -0,0 +1,20 @@ +load("//tools:defs.bzl", "go_library") + +licenses(["notice"]) + +go_library( + name = "fsutil", + srcs = [ + "chdir.go", + "fsutil.go", + "fsutil_amd64_unsafe.go", + "fsutil_arm64_unsafe.go", + "fsutil_unsafe.go", + ], + visibility = ["//visibility:public"], + deps = [ + "//pkg/sync", + "//pkg/syserr", + "@org_golang_x_sys//unix:go_default_library", + ], +) diff --git a/pkg/fsutil/chdir.go b/pkg/fsutil/chdir.go new file mode 100644 index 000000000..8d8c1fcd0 --- /dev/null +++ b/pkg/fsutil/chdir.go @@ -0,0 +1,48 @@ +// Copyright 2023 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 fsutil + +import ( + "fmt" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/sync" +) + +// chdirMu is the global mutex that synchronizes host chdir operations. +var chdirMu sync.Mutex + +// DoInDir performs fn after chdir-ing to dirFD and then reverts back to the +// original CWD. +func DoInDir(dirFD int, fn func() error) error { + chdirMu.Lock() + defer chdirMu.Unlock() + + oldCWD, err := unix.Openat(unix.AT_FDCWD, ".", unix.O_PATH, 0 /* mode */) + if err != nil { + return err + } + + defer func() { + if err := unix.Fchdir(oldCWD); err != nil { + panic(fmt.Errorf("restoring orginial CWD failed: %v", err)) + } + }() + + if err := unix.Fchdir(dirFD); err != nil { + return err + } + return fn() +} diff --git a/pkg/fsutil/fsutil.go b/pkg/fsutil/fsutil.go new file mode 100644 index 000000000..cf6fd351e --- /dev/null +++ b/pkg/fsutil/fsutil.go @@ -0,0 +1,17 @@ +// 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 fsutil contains filesystem utilities that can be shared between the +// sentry and other sandbox components. +package fsutil diff --git a/runsc/fsgofer/fsgofer_amd64_unsafe.go b/pkg/fsutil/fsutil_amd64_unsafe.go similarity index 89% rename from runsc/fsgofer/fsgofer_amd64_unsafe.go rename to pkg/fsutil/fsutil_amd64_unsafe.go index 884f7fc26..492ded677 100644 --- a/runsc/fsgofer/fsgofer_amd64_unsafe.go +++ b/pkg/fsutil/fsutil_amd64_unsafe.go @@ -15,7 +15,7 @@ //go:build amd64 // +build amd64 -package fsgofer +package fsutil import ( "unsafe" @@ -24,7 +24,8 @@ import ( "gvisor.dev/gvisor/pkg/syserr" ) -func statAt(dirFd int, name string) (unix.Stat_t, error) { +// StatAt is a convenience wrapper around newfstatat(2). +func StatAt(dirFd int, name string) (unix.Stat_t, error) { nameBytes, err := unix.BytePtrFromString(name) if err != nil { return unix.Stat_t{}, err diff --git a/runsc/fsgofer/fsgofer_arm64_unsafe.go b/pkg/fsutil/fsutil_arm64_unsafe.go similarity index 89% rename from runsc/fsgofer/fsgofer_arm64_unsafe.go rename to pkg/fsutil/fsutil_arm64_unsafe.go index 1207d9e8a..86f2e38b6 100644 --- a/runsc/fsgofer/fsgofer_arm64_unsafe.go +++ b/pkg/fsutil/fsutil_arm64_unsafe.go @@ -15,7 +15,7 @@ //go:build arm64 // +build arm64 -package fsgofer +package fsutil import ( "unsafe" @@ -24,7 +24,8 @@ import ( "gvisor.dev/gvisor/pkg/syserr" ) -func statAt(dirFd int, name string) (unix.Stat_t, error) { +// StatAt is a convenience wrapper around fstatat(2). +func StatAt(dirFd int, name string) (unix.Stat_t, error) { nameBytes, err := unix.BytePtrFromString(name) if err != nil { return unix.Stat_t{}, err diff --git a/runsc/fsgofer/fsgofer_unsafe.go b/pkg/fsutil/fsutil_unsafe.go similarity index 79% rename from runsc/fsgofer/fsgofer_unsafe.go rename to pkg/fsutil/fsutil_unsafe.go index 5af99b5b3..f82b2e751 100644 --- a/runsc/fsgofer/fsgofer_unsafe.go +++ b/pkg/fsutil/fsutil_unsafe.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package fsgofer +package fsutil import ( "unsafe" @@ -21,9 +21,12 @@ import ( "gvisor.dev/gvisor/pkg/syserr" ) -var unixDirentMaxSize = int(unsafe.Sizeof(unix.Dirent{})) +// UnixDirentMaxSize is the maximum size of unix.Dirent in bytes. +var UnixDirentMaxSize = int(unsafe.Sizeof(unix.Dirent{})) -func utimensat(dirFd int, name string, times [2]unix.Timespec, flags int) error { +// Utimensat is a convenience wrapper to make the utimensat(2) syscall. It +// additionally handles empty name. +func Utimensat(dirFd int, name string, times [2]unix.Timespec, flags int) error { // utimensat(2) doesn't accept empty name, instead name must be nil to make it // operate directly on 'dirFd' unlike other *at syscalls. var namePtr unsafe.Pointer @@ -51,7 +54,9 @@ func utimensat(dirFd int, name string, times [2]unix.Timespec, flags int) error return nil } -func renameat(oldDirFD int, oldName string, newDirFD int, newName string) error { +// RenameAt is a convenience wrapper to make the renameat(2) syscall. It +// additionally handles empty names. +func RenameAt(oldDirFD int, oldName string, newDirFD int, newName string) error { var oldNamePtr unsafe.Pointer if oldName != "" { nameBytes, err := unix.BytePtrFromString(oldName) @@ -83,7 +88,9 @@ func renameat(oldDirFD int, oldName string, newDirFD int, newName string) error return nil } -func parseDirents(buf []byte, handleDirent func(ino uint64, off int64, ftype uint8, name string, reclen uint16) bool) { +// ParseDirents parses dirents from buf. buf must have been populated by +// getdents64(2) syscall. It calls the handleDirent callback for each dirent. +func ParseDirents(buf []byte, handleDirent func(ino uint64, off int64, ftype uint8, name string, reclen uint16) bool) { for len(buf) > 0 { // Interpret the buf populated by unix.Getdents as unix.Dirent. dirent := *(*unix.Dirent)(unsafe.Pointer(&buf[0])) diff --git a/pkg/sentry/fsimpl/gofer/BUILD b/pkg/sentry/fsimpl/gofer/BUILD index 4d6b0fb64..102df1aea 100644 --- a/pkg/sentry/fsimpl/gofer/BUILD +++ b/pkg/sentry/fsimpl/gofer/BUILD @@ -55,6 +55,7 @@ go_library( srcs = [ "dentry_impl.go", "dentry_list.go", + "directfs_dentry.go", "directory.go", "filesystem.go", "fstree.go", @@ -76,11 +77,13 @@ go_library( deps = [ "//pkg/abi/linux", "//pkg/atomicbitops", + "//pkg/cleanup", "//pkg/context", "//pkg/errors/linuxerr", "//pkg/fd", "//pkg/fdnotifier", "//pkg/fspath", + "//pkg/fsutil", "//pkg/hostarch", "//pkg/lisafs", "//pkg/log", diff --git a/pkg/sentry/fsimpl/gofer/dentry_impl.go b/pkg/sentry/fsimpl/gofer/dentry_impl.go index 6853d48b5..b5728f359 100644 --- a/pkg/sentry/fsimpl/gofer/dentry_impl.go +++ b/pkg/sentry/fsimpl/gofer/dentry_impl.go @@ -18,6 +18,8 @@ import ( "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/fsutil" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" "gvisor.dev/gvisor/pkg/sentry/vfs" @@ -46,6 +48,8 @@ func (d *dentry) isReadHandleOk() bool { switch dt := d.impl.(type) { case *lisafsDentry: return dt.readFDLisa.Ok() + case *directfsDentry: + return d.readFD.RacyLoad() >= 0 case nil: // synthetic dentry return false default: @@ -58,6 +62,8 @@ func (d *dentry) isWriteHandleOk() bool { switch dt := d.impl.(type) { case *lisafsDentry: return dt.writeFDLisa.Ok() + case *directfsDentry: + return d.writeFD.RacyLoad() >= 0 case nil: // synthetic dentry return false default: @@ -73,6 +79,8 @@ func (d *dentry) readHandle() handle { fdLisa: dt.readFDLisa, fd: d.readFD.RacyLoad(), } + case *directfsDentry: + return handle{fd: d.readFD.RacyLoad()} case nil: // synthetic dentry return noHandle default: @@ -88,6 +96,8 @@ func (d *dentry) writeHandle() handle { fdLisa: dt.writeFDLisa, fd: d.writeFD.RacyLoad(), } + case *directfsDentry: + return handle{fd: d.writeFD.RacyLoad()} case nil: // synthetic dentry return noHandle default: @@ -95,7 +105,9 @@ func (d *dentry) writeHandle() handle { } } -// Precondition: !d.isSynthetic(). +// Preconditions: +// - !d.isSynthetic(). +// - fs.renameMu is locked. func (d *dentry) openHandle(ctx context.Context, read, write, trunc bool) (handle, error) { flags := uint32(unix.O_RDONLY) switch { @@ -113,14 +125,9 @@ func (d *dentry) openHandle(ctx context.Context, read, write, trunc bool) (handl } switch dt := d.impl.(type) { case *lisafsDentry: - openFD, hostFD, err := dt.controlFD.OpenAt(ctx, flags) - if err != nil { - return noHandle, err - } - return handle{ - fdLisa: dt.controlFD.Client().NewFD(openFD), - fd: int32(hostFD), - }, nil + return dt.openHandle(ctx, flags) + case *directfsDentry: + return dt.openHandle(ctx, flags) default: panic("unknown dentry implementation") } @@ -133,6 +140,8 @@ func (d *dentry) updateHandles(ctx context.Context, h handle, readable, writable switch dt := d.impl.(type) { case *lisafsDentry: dt.updateHandles(ctx, h, readable, writable) + case *directfsDentry: + // No update needed. default: panic("unknown dentry implementation") } @@ -148,18 +157,41 @@ func (d *dentry) updateHandles(ctx context.Context, h handle, readable, writable // // +checklocks:d.metadataMu func (d *dentry) updateMetadataLocked(ctx context.Context, h handle) error { + // Need checklocksforce below because checklocks has no way of knowing that + // d.impl.(*dentryImpl).dentry == d. It can't know that the right metadataMu + // is already locked. switch dt := d.impl.(type) { case *lisafsDentry: return dt.updateMetadataLocked(ctx, h) // +checklocksforce: acquired by precondition. + case *directfsDentry: + return dt.updateMetadataLocked(h) // +checklocksforce: acquired by precondition. default: panic("unknown dentry implementation") } } +// Preconditions: +// - !d.isSynthetic(). +// - fs.renameMu is locked. +func (d *dentry) prepareSetStat(ctx context.Context, stat *linux.Statx) error { + switch dt := d.impl.(type) { + case *lisafsDentry: + // Nothing to be done. + return nil + case *directfsDentry: + return dt.prepareSetStat(ctx, stat) + default: + panic("unknown dentry implementation") + } +} + +// Precondition: fs.renameMu is locked if d is a socket. func (d *dentry) chmod(ctx context.Context, mode uint16) error { switch dt := d.impl.(type) { case *lisafsDentry: return chmod(ctx, dt.controlFD, mode) + case *directfsDentry: + return dt.chmod(ctx, mode) default: panic("unknown dentry implementation") } @@ -168,10 +200,14 @@ func (d *dentry) chmod(ctx context.Context, mode uint16) error { // Preconditions: // - !d.isSynthetic(). // - d.handleMu is locked. +// - fs.renameMu is locked. func (d *dentry) setStatLocked(ctx context.Context, stat *linux.Statx) (uint32, error, error) { switch dt := d.impl.(type) { case *lisafsDentry: return dt.controlFD.SetStat(ctx, stat) + case *directfsDentry: + failureMask, failureErr := dt.setStatLocked(ctx, stat) + return failureMask, failureErr, nil default: panic("unknown dentry implementation") } @@ -181,6 +217,8 @@ func (d *dentry) destroyImpl(ctx context.Context) { switch dt := d.impl.(type) { case *lisafsDentry: dt.destroy(ctx) + case *directfsDentry: + dt.destroy(ctx) case nil: // synthetic dentry default: panic("unknown dentry implementation") @@ -194,6 +232,8 @@ func (d *dentry) getRemoteChild(ctx context.Context, name string) (*dentry, erro switch dt := d.impl.(type) { case *lisafsDentry: return dt.getRemoteChild(ctx, name) + case *directfsDentry: + return dt.getHostChild(name) default: panic("unknown dentry implementation") } @@ -204,7 +244,6 @@ func (d *dentry) getRemoteChild(ctx context.Context, name string) (*dentry, erro // - parent.opMu must be locked for reading. // - parent.isDir(). // - !rp.Done() && rp.Component() is not "." or "..". -// - dentry at name must not already exist in dentry tree. // // Postcondition: The returned dentry is already cached appropriately. // @@ -213,8 +252,10 @@ func (d *dentry) getRemoteChildAndWalkPathLocked(ctx context.Context, rp *vfs.Re switch dt := d.impl.(type) { case *lisafsDentry: return dt.getRemoteChildAndWalkPathLocked(ctx, rp, ds) - // TODO(b/258687694): For directfs, remember to use fs.getRemoteChildLocked - // so that dentry caching is done properly. + case *directfsDentry: + // We need to check for races because opMu is read locked which allows + // concurrent walks to occur. + return d.fs.getRemoteChildLocked(ctx, d, rp.Component(), true /* checkForRace */, ds) default: panic("unknown dentry implementation") } @@ -225,6 +266,9 @@ func (d *dentry) listXattrImpl(ctx context.Context, size uint64) ([]string, erro switch dt := d.impl.(type) { case *lisafsDentry: return dt.controlFD.ListXattr(ctx, size) + case *directfsDentry: + // Consistent with runsc/fsgofer. + return nil, linuxerr.EOPNOTSUPP default: panic("unknown dentry implementation") } @@ -235,6 +279,9 @@ func (d *dentry) getXattrImpl(ctx context.Context, opts *vfs.GetXattrOptions) (s switch dt := d.impl.(type) { case *lisafsDentry: return dt.controlFD.GetXattr(ctx, opts.Name, opts.Size) + case *directfsDentry: + // Consistent with runsc/fsgofer. + return "", linuxerr.EOPNOTSUPP default: panic("unknown dentry implementation") } @@ -245,6 +292,9 @@ func (d *dentry) setXattrImpl(ctx context.Context, opts *vfs.SetXattrOptions) er switch dt := d.impl.(type) { case *lisafsDentry: return dt.controlFD.SetXattr(ctx, opts.Name, opts.Value, opts.Flags) + case *directfsDentry: + // Consistent with runsc/fsgofer. + return linuxerr.EOPNOTSUPP default: panic("unknown dentry implementation") } @@ -255,6 +305,9 @@ func (d *dentry) removeXattrImpl(ctx context.Context, name string) error { switch dt := d.impl.(type) { case *lisafsDentry: return dt.controlFD.RemoveXattr(ctx, name) + case *directfsDentry: + // Consistent with runsc/fsgofer. + return linuxerr.EOPNOTSUPP default: panic("unknown dentry implementation") } @@ -265,6 +318,8 @@ func (d *dentry) mknod(ctx context.Context, name string, creds *auth.Credentials switch dt := d.impl.(type) { case *lisafsDentry: return dt.mknod(ctx, name, creds, opts) + case *directfsDentry: + return dt.mknod(ctx, name, creds, opts) default: panic("unknown dentry implementation") } @@ -275,6 +330,8 @@ func (d *dentry) link(ctx context.Context, target *dentry, name string) (*dentry switch dt := d.impl.(type) { case *lisafsDentry: return dt.link(ctx, target.impl.(*lisafsDentry), name) + case *directfsDentry: + return dt.link(target.impl.(*directfsDentry), name) default: panic("unknown dentry implementation") } @@ -285,6 +342,8 @@ func (d *dentry) mkdir(ctx context.Context, name string, mode linux.FileMode, ui switch dt := d.impl.(type) { case *lisafsDentry: return dt.mkdir(ctx, name, mode, uid, gid) + case *directfsDentry: + return dt.mkdir(name, mode, uid, gid) default: panic("unknown dentry implementation") } @@ -295,6 +354,8 @@ func (d *dentry) symlink(ctx context.Context, name, target string, creds *auth.C switch dt := d.impl.(type) { case *lisafsDentry: return dt.symlink(ctx, name, target, creds) + case *directfsDentry: + return dt.symlink(name, target, creds) default: panic("unknown dentry implementation") } @@ -305,6 +366,8 @@ func (d *dentry) openCreate(ctx context.Context, name string, accessFlags uint32 switch dt := d.impl.(type) { case *lisafsDentry: return dt.openCreate(ctx, name, accessFlags, mode, uid, gid) + case *directfsDentry: + return dt.openCreate(name, accessFlags, mode, uid, gid) default: panic("unknown dentry implementation") } @@ -318,6 +381,8 @@ func (d *dentry) getDirentsLocked(ctx context.Context, count int, recordDirent f switch dt := d.impl.(type) { case *lisafsDentry: return dt.getDirentsLocked(ctx, count, recordDirent) + case *directfsDentry: + return dt.getDirentsLocked(count, recordDirent) default: panic("unknown dentry implementation") } @@ -330,6 +395,9 @@ func (d *dentry) flush(ctx context.Context) error { switch dt := d.impl.(type) { case *lisafsDentry: return flush(ctx, dt.writeFDLisa) + case *directfsDentry: + // Nothing to do here. + return nil default: panic("unknown dentry implementation") } @@ -342,16 +410,22 @@ func (d *dentry) allocate(ctx context.Context, mode, offset, length uint64) erro switch dt := d.impl.(type) { case *lisafsDentry: return dt.writeFDLisa.Allocate(ctx, mode, offset, length) + case *directfsDentry: + return unix.Fallocate(int(d.writeFD.RacyLoad()), uint32(mode), int64(offset), int64(length)) default: panic("unknown dentry implementation") } } -// Precondition: !d.isSynthetic(). +// Preconditions: +// - !d.isSynthetic(). +// - fs.renameMu is locked. func (d *dentry) connect(ctx context.Context, sockType linux.SockType) (int, error) { switch dt := d.impl.(type) { case *lisafsDentry: return dt.controlFD.Connect(ctx, sockType) + case *directfsDentry: + return dt.connect(ctx, sockType) default: panic("unknown dentry implementation") } @@ -362,6 +436,8 @@ func (d *dentry) readlinkImpl(ctx context.Context) (string, error) { switch dt := d.impl.(type) { case *lisafsDentry: return dt.controlFD.ReadLinkAt(ctx) + case *directfsDentry: + return dt.readlink() default: panic("unknown dentry implementation") } @@ -372,6 +448,8 @@ func (d *dentry) unlink(ctx context.Context, name string, flags uint32) error { switch dt := d.impl.(type) { case *lisafsDentry: return dt.controlFD.UnlinkAt(ctx, name, flags) + case *directfsDentry: + return unix.Unlinkat(dt.controlFD, name, int(flags)) default: panic("unknown dentry implementation") } @@ -382,6 +460,8 @@ func (d *dentry) rename(ctx context.Context, oldName string, newParent *dentry, switch dt := d.impl.(type) { case *lisafsDentry: return dt.controlFD.RenameAt(ctx, oldName, newParent.impl.(*lisafsDentry).controlFD.ID(), newName) + case *directfsDentry: + return fsutil.RenameAt(dt.controlFD, oldName, newParent.impl.(*directfsDentry).controlFD, newName) default: panic("unknown dentry implementation") } @@ -392,20 +472,26 @@ func (d *dentry) statfs(ctx context.Context) (linux.Statfs, error) { switch dt := d.impl.(type) { case *lisafsDentry: return dt.statfs(ctx) + case *directfsDentry: + return dt.statfs() default: panic("unknown dentry implementation") } } func (fs *filesystem) restoreRoot(ctx context.Context, opts *vfs.CompleteRestoreOptions) error { + rootInode, rootHostFD, err := fs.initClientAndGetRoot(ctx) + if err != nil { + return err + } + // The root is always non-synthetic. switch dt := fs.root.impl.(type) { case *lisafsDentry: - rootInode, err := fs.initClient(ctx) - if err != nil { - return err - } return dt.restoreFile(ctx, &rootInode, opts) + case *directfsDentry: + dt.rootControlFDLisa = fs.client.NewFD(rootInode.ControlFD) + return dt.restoreFile(ctx, rootHostFD, opts) default: panic("unknown dentry implementation") } @@ -422,6 +508,14 @@ func (d *dentry) restoreFile(ctx context.Context, opts *vfs.CompleteRestoreOptio return err } return dt.restoreFile(ctx, &inode, opts) + case *directfsDentry: + childFD, err := tryOpen(func(flags int) (int, error) { + return unix.Openat(d.parent.impl.(*directfsDentry).controlFD, d.name, flags, 0) + }) + if err != nil { + return err + } + return dt.restoreFile(ctx, childFD, opts) default: panic("unknown dentry implementation") } @@ -442,6 +536,8 @@ func (r *revalidateState) doRevalidation(ctx context.Context, vfsObj *vfs.Virtua switch r.start.impl.(type) { case *lisafsDentry: return doRevalidationLisafs(ctx, vfsObj, r, ds) + case *directfsDentry: + return doRevalidationDirectfs(ctx, vfsObj, r, ds) default: panic("unknown dentry implementation") } diff --git a/pkg/sentry/fsimpl/gofer/directfs_dentry.go b/pkg/sentry/fsimpl/gofer/directfs_dentry.go new file mode 100644 index 000000000..1b802a778 --- /dev/null +++ b/pkg/sentry/fsimpl/gofer/directfs_dentry.go @@ -0,0 +1,775 @@ +// 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 gofer + +import ( + "fmt" + "math" + "path" + "path/filepath" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/atomicbitops" + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/fsutil" + "gvisor.dev/gvisor/pkg/lisafs" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/sentry/kernel/auth" + "gvisor.dev/gvisor/pkg/sentry/socket/unix/transport" + "gvisor.dev/gvisor/pkg/sentry/vfs" +) + +// LINT.IfChange + +const ( + hostOpenFlags = unix.O_NOFOLLOW | unix.O_CLOEXEC +) + +// tryOpen tries to open() with different modes in the following order: +// 1. RDONLY | NONBLOCK: for all files, directories, ro mounts, FIFOs. +// Use non-blocking to prevent getting stuck inside open(2) for +// FIFOs. This option has no effect on regular files. +// 2. PATH: for symlinks, sockets. +func tryOpen(open func(int) (int, error)) (int, error) { + flags := []int{ + unix.O_RDONLY | unix.O_NONBLOCK, + unix.O_PATH, + } + + var ( + hostFD int + err error + ) + for _, flag := range flags { + hostFD, err = open(flag | hostOpenFlags) + if err == nil { + return hostFD, nil + } + + if err == unix.ENOENT { + // File doesn't exist, no point in retrying. + break + } + } + return -1, err +} + +// getDirectfsRootDentry creates a new dentry representing the root dentry for +// this mountpoint. getDirectfsRootDentry takes ownership of rootHostFD and +// rootControlFD. +func (fs *filesystem) getDirectfsRootDentry(ctx context.Context, rootHostFD int, rootControlFD lisafs.ClientFD) (*dentry, error) { + d, err := fs.newDirectfsDentry(rootHostFD) + if err != nil { + log.Warningf("newDirectfsDentry failed for mount point dentry: %v", err) + rootControlFD.Close(ctx, false /* flush */) + return nil, err + } + d.impl.(*directfsDentry).rootControlFDLisa = rootControlFD + return d, nil +} + +// directfsDentry is a host dentry implementation. It represents a dentry +// backed by a host file descriptor. All operations are directly performed on +// the host. A gofer is only involved for some operations on the mount point +// dentry (when dentry.parent = nil). We are forced to fall back to the gofer +// due to the lack of procfs in the sandbox process. +// +// +stateify savable +type directfsDentry struct { + dentry + + // controlFD is the host FD to this file. controlFD is immutable. + controlFD int + + // rootControlFDLisa is a lisafs control FD on this dentry. This is only set + // when this dentry represents the root of the current mount. This is + // required in cases where we require dentry.parent to perform operations. + // But for the root dentry, the parent is not available. So we fallback to + // using lisafs RPCs. rootControlFDLisa is immutable. + rootControlFDLisa lisafs.ClientFD `state:"nosave"` +} + +// newDirectfsDentry creates a new dentry representing the given file. The dentry +// initially has no references, but is not cached; it is the caller's +// responsibility to set the dentry's reference count and/or call +// dentry.checkCachingLocked() as appropriate. +// newDirectDentry takes ownership of controlFD. +func (fs *filesystem) newDirectfsDentry(controlFD int) (*dentry, error) { + var stat unix.Stat_t + if err := unix.Fstat(controlFD, &stat); err != nil { + log.Warningf("failed to fstat(2) FD %d: %v", controlFD, err) + _ = unix.Close(controlFD) + return nil, err + } + inoKey := inoKeyFromStat(&stat) + d := &directfsDentry{ + dentry: dentry{ + fs: fs, + inoKey: inoKey, + ino: fs.inoFromKey(inoKey), + mode: atomicbitops.FromUint32(stat.Mode), + uid: atomicbitops.FromUint32(stat.Uid), + gid: atomicbitops.FromUint32(stat.Gid), + blockSize: atomicbitops.FromUint32(uint32(stat.Blksize)), + readFD: atomicbitops.FromInt32(-1), + writeFD: atomicbitops.FromInt32(-1), + mmapFD: atomicbitops.FromInt32(-1), + size: atomicbitops.FromUint64(uint64(stat.Size)), + atime: atomicbitops.FromInt64(dentryTimestampFromUnix(stat.Atim)), + mtime: atomicbitops.FromInt64(dentryTimestampFromUnix(stat.Mtim)), + ctime: atomicbitops.FromInt64(dentryTimestampFromUnix(stat.Ctim)), + nlink: atomicbitops.FromUint32(uint32(stat.Nlink)), + }, + controlFD: controlFD, + } + d.dentry.init(d) + fs.syncMu.Lock() + fs.syncableDentries.PushBack(&d.syncableListEntry) + fs.syncMu.Unlock() + return &d.dentry, nil +} + +// Precondition: fs.renameMu is locked. +func (d *directfsDentry) openHandle(ctx context.Context, flags uint32) (handle, error) { + if d.parent == nil { + // This is a mount point. We don't have parent. Fallback to using lisafs. + if !d.rootControlFDLisa.Ok() { + panic("directfsDentry.rootControlFDLisa is not set for mount point file") + } + openFD, hostFD, err := d.rootControlFDLisa.OpenAt(ctx, flags) + if err != nil { + return noHandle, err + } + d.fs.client.CloseFD(ctx, openFD, true /* flush */) + if hostFD < 0 { + log.Warningf("gofer did not donate an FD for mount point") + return noHandle, unix.EIO + } + return handle{fd: int32(hostFD)}, nil + } + + // The only way to re-open an FD with different flags is via procfs or + // openat(2) from the parent. Procfs does not exist here. So use parent. + flags |= hostOpenFlags + openFD, err := unix.Openat(d.parent.impl.(*directfsDentry).controlFD, d.name, int(flags), 0) + if err != nil { + return noHandle, err + } + return handle{fd: int32(openFD)}, nil +} + +// Precondition: d.metadataMu must be locked. +// +// +checklocks:d.metadataMu +func (d *directfsDentry) updateMetadataLocked(h handle) error { + handleMuRLocked := false + if h.fd < 0 { + // Use open FDs in preferenece to the control FD. Control FDs may be opened + // with O_PATH. This may be significantly more efficient in some + // implementations. Prefer a writable FD over a readable one since some + // filesystem implementations may update a writable FD's metadata after + // writes, without making metadata updates immediately visible to read-only + // FDs representing the same file. + d.handleMu.RLock() + switch { + case d.writeFD.RacyLoad() >= 0: + h.fd = d.writeFD.RacyLoad() + handleMuRLocked = true + case d.readFD.RacyLoad() >= 0: + h.fd = d.readFD.RacyLoad() + handleMuRLocked = true + default: + h.fd = int32(d.controlFD) + d.handleMu.RUnlock() + } + } + + var stat unix.Stat_t + err := unix.Fstat(int(h.fd), &stat) + if handleMuRLocked { + // handleMu must be released before updateMetadataFromStatLocked(). + d.handleMu.RUnlock() // +checklocksforce: complex case. + } + if err != nil { + return err + } + return d.updateMetadataFromStatLocked(&stat) +} + +// Precondition: fs.renameMu is locked if d is a socket. +func (d *directfsDentry) chmod(ctx context.Context, mode uint16) error { + if !d.isSocket() { + return unix.Fchmod(d.controlFD, uint32(mode)) + } + + // fchmod(2) on socket files created via bind(2) fails. We need to + // fchmodat(2) it from its parent. + if d.parent != nil { + // We have parent FD, just use that. Note that AT_SYMLINK_NOFOLLOW flag is + // currently not supported. So we don't use it. + return unix.Fchmodat(d.parent.impl.(*directfsDentry).controlFD, d.name, uint32(mode), 0 /* flags */) + } + + // This is a mount point socket. We don't have a parent FD. Fallback to using + // lisafs. + if !d.rootControlFDLisa.Ok() { + panic("directfsDentry.rootControlFDLisa is not set for mount point socket") + } + + return chmod(ctx, d.rootControlFDLisa, mode) +} + +// Preconditions: +// - d.handleMu is locked if d is a regular file. +// - fs.renameMu is locked if d is a symlink. +func (d *directfsDentry) utimensat(ctx context.Context, stat *linux.Statx) error { + if stat.Mask&(linux.STATX_ATIME|linux.STATX_MTIME) == 0 { + return nil + } + + utimes := [2]unix.Timespec{ + {Sec: 0, Nsec: unix.UTIME_OMIT}, + {Sec: 0, Nsec: unix.UTIME_OMIT}, + } + if stat.Mask&unix.STATX_ATIME != 0 { + utimes[0].Sec = stat.Atime.Sec + utimes[0].Nsec = int64(stat.Atime.Nsec) + } + if stat.Mask&unix.STATX_MTIME != 0 { + utimes[1].Sec = stat.Mtime.Sec + utimes[1].Nsec = int64(stat.Mtime.Nsec) + } + + if !d.isSymlink() { + hostFD := d.controlFD + if d.isRegularFile() { + // utimensat(2) requires a writable FD for regular files. See BUGS + // section. dentry.prepareSetStat() should have acquired a writable FD. + hostFD = int(d.writeFD.RacyLoad()) + } + // Non-symlinks can operate directly on the fd using an empty name. + return fsutil.Utimensat(hostFD, "", utimes, 0) + } + + // utimensat operates different that other syscalls. To operate on a + // symlink it *requires* AT_SYMLINK_NOFOLLOW with dirFD and a non-empty + // name. + if d.parent != nil { + return fsutil.Utimensat(d.parent.impl.(*directfsDentry).controlFD, d.name, utimes, unix.AT_SYMLINK_NOFOLLOW) + } + + // This is a mount point symlink. We don't have a parent FD. Fallback to + // using lisafs. + if !d.rootControlFDLisa.Ok() { + panic("directfsDentry.rootControlFDLisa is not set for mount point symlink") + } + + setStat := linux.Statx{ + Mask: stat.Mask & (linux.STATX_ATIME | linux.STATX_MTIME), + Atime: stat.Atime, + Mtime: stat.Mtime, + } + _, failureErr, err := d.rootControlFDLisa.SetStat(ctx, &setStat) + if err != nil { + return err + } + return failureErr +} + +// Precondition: fs.renameMu is locked. +func (d *directfsDentry) prepareSetStat(ctx context.Context, stat *linux.Statx) error { + if stat.Mask&unix.STATX_SIZE != 0 || + (stat.Mask&(unix.STATX_ATIME|unix.STATX_MTIME) != 0 && d.isRegularFile()) { + // Need to ensure a writable FD is available. See setStatLocked() to + // understand why. + return d.ensureSharedHandle(ctx, false /* read */, true /* write */, false /* trunc */) + } + return nil +} + +// Preconditions: +// - d.handleMu is locked. +// - fs.renameMu is locked. +func (d *directfsDentry) setStatLocked(ctx context.Context, stat *linux.Statx) (failureMask uint32, failureErr error) { + if stat.Mask&unix.STATX_MODE != 0 { + if err := d.chmod(ctx, stat.Mode&^unix.S_IFMT); err != nil { + failureMask |= unix.STATX_MODE + failureErr = err + } + } + + if stat.Mask&unix.STATX_SIZE != 0 { + // ftruncate(2) requires a writable FD. + if err := unix.Ftruncate(int(d.writeFD.RacyLoad()), int64(stat.Size)); err != nil { + failureMask |= unix.STATX_SIZE + failureErr = err + } + } + + if err := d.utimensat(ctx, stat); err != nil { + failureMask |= (stat.Mask & (unix.STATX_ATIME | unix.STATX_MTIME)) + failureErr = err + } + + if stat.Mask&(unix.STATX_UID|unix.STATX_GID) != 0 { + // "If the owner or group is specified as -1, then that ID is not changed" + // - chown(2) + uid := -1 + if stat.Mask&unix.STATX_UID != 0 { + uid = int(stat.UID) + } + gid := -1 + if stat.Mask&unix.STATX_GID != 0 { + gid = int(stat.GID) + } + if err := fchown(d.controlFD, uid, gid); err != nil { + failureMask |= stat.Mask & (unix.STATX_UID | unix.STATX_GID) + failureErr = err + } + } + return +} + +func fchown(fd, uid, gid int) error { + return unix.Fchownat(fd, "", uid, gid, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW) +} + +func (d *directfsDentry) destroy(ctx context.Context) { + if d.controlFD >= 0 { + _ = unix.Close(d.controlFD) + } + if d.rootControlFDLisa.Ok() { + d.rootControlFDLisa.Close(ctx, true /* flush */) + } +} + +func (d *directfsDentry) getHostChild(name string) (*dentry, error) { + childFD, err := tryOpen(func(flags int) (int, error) { + return unix.Openat(d.controlFD, name, flags, 0) + }) + if err != nil { + return nil, err + } + return d.fs.newDirectfsDentry(childFD) +} + +// getCreatedChild opens the newly created child, sets its uid/gid, constructs +// a disconnected dentry and returns it. +func (d *directfsDentry) getCreatedChild(name string, uid, gid int, isDir bool) (*dentry, error) { + unlinkFlags := 0 + extraOpenFlags := 0 + if isDir { + extraOpenFlags |= unix.O_DIRECTORY + unlinkFlags |= unix.AT_REMOVEDIR + } + deleteChild := func() { + // Best effort attempt to remove the newly created child on failure. + if err := unix.Unlinkat(d.controlFD, name, unlinkFlags); err != nil { + log.Warningf("error unlinking newly created child %q after failure: %v", filepath.Join(genericDebugPathname(&d.dentry), name), err) + } + } + + childFD, err := tryOpen(func(flags int) (int, error) { + return unix.Openat(d.controlFD, name, flags|extraOpenFlags, 0) + }) + if err != nil { + deleteChild() + return nil, err + } + + // "If the owner or group is specified as -1, then that ID is not changed" + // - chown(2). Only bother making the syscall if the owner is changing. + if uid != -1 || gid != -1 { + if err := fchown(childFD, uid, gid); err != nil { + deleteChild() + _ = unix.Close(childFD) + return nil, err + } + } + child, err := d.fs.newDirectfsDentry(childFD) + if err != nil { + // Ownership of childFD was passed to newDirectDentry(), so no need to + // clean that up. + deleteChild() + return nil, err + } + return child, nil +} + +func (d *directfsDentry) mknod(ctx context.Context, name string, creds *auth.Credentials, opts *vfs.MknodOptions) (*dentry, error) { + if _, ok := opts.Endpoint.(transport.HostBoundEndpoint); ok { + return d.bindAt(ctx, name, creds, opts) + } + + // From mknod(2) man page: + // "EPERM: [...] if the filesystem containing pathname does not support + // the type of node requested." + if opts.Mode.FileType() != linux.ModeRegular { + return nil, unix.EPERM + } + + if err := unix.Mknodat(d.controlFD, name, uint32(opts.Mode), 0); err != nil { + return nil, err + } + return d.getCreatedChild(name, int(creds.EffectiveKUID), int(creds.EffectiveKGID), false /* isDir */) +} + +type boundSocketFD struct { + sock int +} + +// Close closes the host and gofer-backed FDs associated to this bound socket. +func (fd *boundSocketFD) Close(ctx context.Context) { + _ = unix.Close(fd.sock) +} + +// NotificationFD is a host FD that can be used to notify when new clients +// connect to the socket. +func (fd *boundSocketFD) NotificationFD() int32 { + return int32(fd.sock) +} + +// Listen makes a Listen RPC. +func (fd *boundSocketFD) Listen(ctx context.Context, backlog int32) error { + return unix.Listen(int(fd.sock), int(backlog)) +} + +// Accept makes an Accept RPC. +func (fd *boundSocketFD) Accept(ctx context.Context) (int, error) { + flags := unix.O_NONBLOCK | unix.O_CLOEXEC + nfd, _, err := unix.Accept4(int(fd.sock), flags) + if err != nil { + return -1, err + } + return nfd, nil +} + +// Precondition: opts.Endpoint != nil and is transport.HostBoundEndpoint type. +func (d *directfsDentry) bindAt(ctx context.Context, name string, creds *auth.Credentials, opts *vfs.MknodOptions) (*dentry, error) { + if !d.fs.opts.directfs.hostUDSBind { + return nil, unix.EPERM + } + + // This mknod(2) is coming from unix bind(2), as opts.Endpoint is set. + sockType := opts.Endpoint.(transport.Endpoint).Type() + if !isSocketTypeSupported(sockType) { + return nil, unix.ENXIO + } + // Create the socket. + sockFD, err := unix.Socket(unix.AF_UNIX, int(sockType), 0) + if err != nil { + return nil, err + } + bsFD := &boundSocketFD{sockFD} + hbep := opts.Endpoint.(transport.HostBoundEndpoint) + if err := hbep.SetBoundSocketFD(bsFD); err != nil { + bsFD.Close(ctx) + return nil, err + } + + // fchmod(2) has to happen *before* the bind(2). sockFD's file mode will + // be used in creating the filesystem-object in bind(2). + if err := unix.Fchmod(sockFD, uint32(opts.Mode&^unix.S_IFMT)); err != nil { + hbep.ResetBoundSocketFD(ctx) + return nil, err + } + + // There are no filesystems mounted in the sandbox process's mount namespace. + // So we can't perform absolute path traversals. So fchdir(2) to this + // directory and bind at name (relative path traversal). + if err := fsutil.DoInDir(d.controlFD, func() error { + return unix.Bind(sockFD, &unix.SockaddrUnix{Name: name}) + }); err != nil { + hbep.ResetBoundSocketFD(ctx) + return nil, err + } + child, err := d.getCreatedChild(name, int(creds.EffectiveKUID), int(creds.EffectiveKGID), false /* isDir */) + if err != nil { + hbep.ResetBoundSocketFD(ctx) + return nil, err + } + // Set the endpoint on the newly created child dentry. + child.endpoint = opts.Endpoint + return child, nil +} + +func (d *directfsDentry) link(target *directfsDentry, name string) (*dentry, error) { + if err := unix.Linkat(target.controlFD, "", d.controlFD, name, unix.AT_EMPTY_PATH); err != nil { + return nil, err + } + // Note that we don't need to set uid/gid for the new child. This is a hard + // link. The original file already has the right owner. + return d.getCreatedChild(name, -1 /* uid */, -1 /* gid */, false /* isDir */) +} + +func (d *directfsDentry) mkdir(name string, mode linux.FileMode, uid auth.KUID, gid auth.KGID) (*dentry, error) { + if err := unix.Mkdirat(d.controlFD, name, uint32(mode)); err != nil { + return nil, err + } + return d.getCreatedChild(name, int(uid), int(gid), true /* isDir */) +} + +func (d *directfsDentry) symlink(name, target string, creds *auth.Credentials) (*dentry, error) { + if err := unix.Symlinkat(target, d.controlFD, name); err != nil { + return nil, err + } + return d.getCreatedChild(name, int(creds.EffectiveKUID), int(creds.EffectiveKGID), false /* isDir */) +} + +func (d *directfsDentry) openCreate(name string, accessFlags uint32, mode linux.FileMode, uid auth.KUID, gid auth.KGID) (*dentry, handle, error) { + createFlags := unix.O_CREAT | unix.O_EXCL | int(accessFlags) | hostOpenFlags + childHandleFD, err := unix.Openat(d.controlFD, name, createFlags, uint32(mode&^linux.FileTypeMask)) + if err != nil { + return nil, noHandle, err + } + + child, err := d.getCreatedChild(name, int(uid), int(gid), false /* isDir */) + if err != nil { + _ = unix.Close(childHandleFD) + return nil, noHandle, err + } + return child, handle{fd: int32(childHandleFD)}, nil +} + +func (d *directfsDentry) getDirentsLocked(count int, recordDirent func(name string, key inoKey, dType uint8)) error { + readFD := int(d.readFD.RacyLoad()) + if _, err := unix.Seek(readFD, 0, 0); err != nil { + return err + } + + var direntsBuf [8192]byte + for bytesRead := 0; bytesRead < count; { + bufEnd := len(direntsBuf) + if remaining := int(count) - bytesRead; remaining < bufEnd { + bufEnd = remaining + } + n, err := unix.Getdents(readFD, direntsBuf[:bufEnd]) + if err != nil { + if err == unix.EINVAL && bufEnd < fsutil.UnixDirentMaxSize { + // getdents64(2) returns EINVAL is returned when the result + // buffer is too small. If bufEnd is smaller than the max + // size of unix.Dirent, then just break here to return all + // dirents collected till now. + return nil + } + return err + } + if n <= 0 { + return nil + } + + fsutil.ParseDirents(direntsBuf[:n], func(ino uint64, off int64, ftype uint8, name string, reclen uint16) bool { + // We also want the device ID, which annoyingly incurs an additional + // syscall per dirent. + // TODO(gvisor.dev/issue/6665): Get rid of per-dirent stat. + stat, err := fsutil.StatAt(d.controlFD, name) + if err != nil { + log.Warningf("Getdent64: skipping file %q with failed stat, err: %v", path.Join(genericDebugPathname(&d.dentry), name), err) + return true + } + bytesRead += int(reclen) + recordDirent(name, inoKeyFromStat(&stat), ftype) + return true + }) + } + return nil +} + +// Precondition: fs.renameMu is locked. +func (d *directfsDentry) connect(ctx context.Context, sockType linux.SockType) (int, error) { + if !d.fs.opts.directfs.hostUDSConnect { + return -1, unix.EPERM + } + + if d.parent == nil { + // This is a mount point socket. Fall back to lisafs for connect since we + // don't have parent. + if !d.rootControlFDLisa.Ok() { + panic("directfsDentry.rootControlFDLisa is not set for mount point socket") + } + return d.rootControlFDLisa.Connect(ctx, sockType) + } + + if !isSocketTypeSupported(sockType) { + log.Warningf("unsupported socket type %d", sockType) + return -1, unix.ENXIO + } + + sock, err := unix.Socket(unix.AF_UNIX, int(sockType), 0) + if err != nil { + log.Warningf("socket(2) failed: %v", err) + return -1, err + } + + // There are no filesystems mounted in the sandbox process's mount namespace. + // So we can't perform absolute path traversals. So fchdir(2) to parent + // and connect to this socket at name (relative path traversal). + if err := fsutil.DoInDir(d.parent.impl.(*directfsDentry).controlFD, func() error { + return unix.Connect(sock, &unix.SockaddrUnix{Name: d.name}) + }); err != nil { + unix.Close(sock) + log.Warningf("connect(2) failed: %v", err) + return -1, err + } + return sock, nil +} + +func (d *directfsDentry) readlink() (string, error) { + // This is similar to what os.Readlink does. + for linkLen := 128; linkLen < math.MaxUint16; linkLen *= 2 { + b := make([]byte, linkLen) + n, err := unix.Readlinkat(d.controlFD, "", b) + + if err != nil { + return "", err + } + if n < int(linkLen) { + return string(b[:n]), nil + } + } + return "", unix.ENOMEM +} + +func (d *directfsDentry) statfs() (linux.Statfs, error) { + var statFS unix.Statfs_t + if err := unix.Fstatfs(d.controlFD, &statFS); err != nil { + return linux.Statfs{}, err + } + return linux.Statfs{ + BlockSize: statFS.Bsize, + FragmentSize: statFS.Bsize, + Blocks: statFS.Blocks, + BlocksFree: statFS.Bfree, + BlocksAvailable: statFS.Bavail, + Files: statFS.Files, + FilesFree: statFS.Ffree, + NameLength: uint64(statFS.Namelen), + }, nil +} + +func (d *directfsDentry) restoreFile(ctx context.Context, controlFD int, opts *vfs.CompleteRestoreOptions) error { + if controlFD < 0 { + log.Warningf("directfsDentry.restoreFile called with invalid controlFD") + return unix.EINVAL + } + var stat unix.Stat_t + if err := unix.Fstat(controlFD, &stat); err != nil { + _ = unix.Close(controlFD) + return err + } + + d.controlFD = controlFD + // We do not preserve inoKey across checkpoint/restore, so: + // + // - We must assume that the host filesystem did not change in a way that + // would invalidate dentries, since we can't revalidate dentries by + // checking inoKey. + // + // - We need to associate the new inoKey with the existing d.ino. + d.inoKey = inoKeyFromStat(&stat) + d.fs.inoMu.Lock() + d.fs.inoByKey[d.inoKey] = d.ino + d.fs.inoMu.Unlock() + + // Check metadata stability before updating metadata. + d.metadataMu.Lock() + defer d.metadataMu.Unlock() + if d.isRegularFile() { + if opts.ValidateFileSizes { + if d.size.RacyLoad() != uint64(stat.Size) { + return vfs.ErrCorruption{fmt.Errorf("gofer.dentry(%q).restoreFile: file size validation failed: size changed from %d to %d", genericDebugPathname(&d.dentry), d.size.Load(), stat.Size)} + } + } + if opts.ValidateFileModificationTimestamps { + if want := dentryTimestampFromUnix(stat.Mtim); d.mtime.RacyLoad() != want { + return vfs.ErrCorruption{fmt.Errorf("gofer.dentry(%q).restoreFile: mtime validation failed: mtime changed from %+v to %+v", genericDebugPathname(&d.dentry), linux.NsecToStatxTimestamp(d.mtime.RacyLoad()), linux.NsecToStatxTimestamp(want))} + } + } + } + if !d.cachedMetadataAuthoritative() { + d.updateMetadataFromStatLocked(&stat) + } + + if rw, ok := d.fs.savedDentryRW[&d.dentry]; ok { + if err := d.ensureSharedHandle(ctx, rw.read, rw.write, false /* trunc */); err != nil { + return err + } + } + + return nil +} + +// doRevalidationDirectfs stats all dentries in `state`. It will update or +// invalidate dentries in the cache based on the result. +// +// Preconditions: +// - fs.renameMu must be locked. +// - InteropModeShared is in effect. +func doRevalidationDirectfs(ctx context.Context, vfsObj *vfs.VirtualFilesystem, state *revalidateState, ds **[]*dentry) error { + // Explicitly declare start dentry, instead of using the function receiver. + // The function receiver has to be named `d` (to be consistent with other + // receivers). But `d` variable is also used below in various places. This + // helps with readability and makes code less error prone. + start := state.start.impl.(*directfsDentry) + if state.refreshStart { + start.updateMetadata(ctx) + } + + parent := start + for _, d := range state.dentries { + childFD, err := unix.Openat(parent.controlFD, d.name, unix.O_PATH|hostOpenFlags, 0) + if err != nil && err != unix.ENOENT { + return err + } + + var stat unix.Stat_t + // Lock metadata *before* getting attributes for d. + d.metadataMu.Lock() + found := err == nil + if found { + err = unix.Fstat(childFD, &stat) + _ = unix.Close(childFD) + if err != nil { + d.metadataMu.Unlock() + return err + } + } + + // Note that synthetic dentries will always fail this comparison check. + if !found || d.inoKey != inoKeyFromStat(&stat) { + d.metadataMu.Unlock() + if !found && d.isSynthetic() { + // We have a synthetic file, and no remote file has arisen to replace + // it. + return nil + } + // The file at this path has changed or no longer exists. Mark the + // dentry invalidated. + d.invalidate(ctx, vfsObj, ds) + return nil + } + + // The file at this path hasn't changed. Just update cached metadata. + d.impl.(*directfsDentry).updateMetadataFromStatLocked(&stat) // +checklocksforce: d.metadataMu is locked above. + d.metadataMu.Unlock() + + // Advance parent. + parent = d.impl.(*directfsDentry) + } + return nil +} + +// LINT.ThenChange(../../../../runsc/fsgofer/lisafs.go) diff --git a/pkg/sentry/fsimpl/gofer/filesystem.go b/pkg/sentry/fsimpl/gofer/filesystem.go index 0400b33f3..d32cdb37a 100644 --- a/pkg/sentry/fsimpl/gofer/filesystem.go +++ b/pkg/sentry/fsimpl/gofer/filesystem.go @@ -252,6 +252,8 @@ func (fs *filesystem) getChildLocked(ctx context.Context, parent *dentry, name s // - If checkForRace is false, then parent.opMu must be held for writing. // - Otherwise, parent.opMu must be held for reading. // +// Postcondition: The returned dentry is already cached appropriately. +// // +checklocksread:parent.opMu func (fs *filesystem) getRemoteChildLocked(ctx context.Context, parent *dentry, name string, checkForRace bool, ds **[]*dentry) (*dentry, error) { child, err := parent.getRemoteChild(ctx, name) @@ -1038,6 +1040,16 @@ func (d *dentry) open(ctx context.Context, rp *vfs.ResolvingPath, opts *vfs.Open return nil, err } + if !d.isSynthetic() { + // renameMu is locked here because it is required by d.openHandle(), which + // is called by d.ensureSharedHandle() and d.openSpecialFile() below. It is + // also required by d.connect() which is called by + // d.openSocketByConnecting(). Note that opening non-synthetic pipes may + // block, renameMu is unlocked separately in d.openSpecialFile() for pipes. + d.fs.renameMu.RLock() + defer d.fs.renameMu.RUnlock() + } + trunc := opts.Flags&linux.O_TRUNC != 0 && d.fileType() == linux.S_IFREG if trunc { // Lock metadataMu *while* we open a regular file with O_TRUNC because @@ -1125,6 +1137,7 @@ func (d *dentry) open(ctx context.Context, rp *vfs.ResolvingPath, opts *vfs.Open return vfd, err } +// Precondition: fs.renameMu is locked. func (d *dentry) openSocketByConnecting(ctx context.Context, opts *vfs.OpenOptions) (*vfs.FileDescription, error) { if opts.Flags&linux.O_DIRECT != 0 { return nil, linuxerr.EINVAL @@ -1146,6 +1159,10 @@ func (d *dentry) openSocketByConnecting(ctx context.Context, opts *vfs.OpenOptio return fd, nil } +// Preconditions: +// - !d.isSynthetic(). +// - fs.renameMu is locked. It may be released temporarily while pipe blocks. +// - If d is a pipe, no other locks (other than fs.renameMu) should be held. func (d *dentry) openSpecialFile(ctx context.Context, mnt *vfs.Mount, opts *vfs.OpenOptions) (*vfs.FileDescription, error) { ats := vfs.AccessTypesForOpenFlags(opts) if opts.Flags&linux.O_DIRECT != 0 && !d.isRegularFile() { @@ -1166,8 +1183,12 @@ retry: if isBlockingOpenOfNamedPipe && ats == vfs.MayWrite && linuxerr.Equals(linuxerr.ENXIO, err) { // An attempt to open a named pipe with O_WRONLY|O_NONBLOCK fails // with ENXIO if opening the same named pipe with O_WRONLY would - // block because there are no readers of the pipe. - if err := sleepBetweenNamedPipeOpenChecks(ctx); err != nil { + // block because there are no readers of the pipe. Release renameMu + // while blocking. + d.fs.renameMu.RUnlock() + err := sleepBetweenNamedPipeOpenChecks(ctx) + d.fs.renameMu.RLock() + if err != nil { return nil, err } goto retry @@ -1175,7 +1196,11 @@ retry: return nil, err } if isBlockingOpenOfNamedPipe && ats == vfs.MayRead && h.fd >= 0 { - if err := blockUntilNonblockingPipeHasWriter(ctx, h.fd); err != nil { + // Release renameMu while blocking. + d.fs.renameMu.RUnlock() + err := blockUntilNonblockingPipeHasWriter(ctx, h.fd) + d.fs.renameMu.RLock() + if err != nil { h.close(ctx) return nil, err } @@ -1726,6 +1751,15 @@ func (fs *filesystem) MountOptions() string { if fs.opts.overlayfsStaleRead { optsKV = append(optsKV, mopt{moptOverlayfsStaleRead, nil}) } + if fs.opts.directfs.enabled { + optsKV = append(optsKV, mopt{moptDirectfs, nil}) + if fs.opts.directfs.hostUDSBind { + optsKV = append(optsKV, mopt{moptHostUDSBind, nil}) + } + if fs.opts.directfs.hostUDSConnect { + optsKV = append(optsKV, mopt{moptHostUDSConnect, nil}) + } + } opts := make([]string, 0, len(optsKV)) for _, opt := range optsKV { diff --git a/pkg/sentry/fsimpl/gofer/gofer.go b/pkg/sentry/fsimpl/gofer/gofer.go index c4c568df5..2aa95b0a3 100644 --- a/pkg/sentry/fsimpl/gofer/gofer.go +++ b/pkg/sentry/fsimpl/gofer/gofer.go @@ -48,6 +48,7 @@ import ( "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/atomicbitops" + "gvisor.dev/gvisor/pkg/cleanup" "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/hostarch" @@ -82,6 +83,11 @@ const ( moptForcePageCache = "force_page_cache" moptLimitHostFDTranslation = "limit_host_fd_translation" moptOverlayfsStaleRead = "overlayfs_stale_read" + + // Directfs options. + moptDirectfs = "directfs" + moptHostUDSConnect = "host_uds_connect" + moptHostUDSBind = "host_uds_bind" ) // Valid values for the "cache" mount option. @@ -271,6 +277,24 @@ type filesystemOptions struct { // may regress performance due to excessive Open RPCs. This option is not // supported with overlayfsStaleRead for now. regularFilesUseSpecialFileFD bool + + // directfs holds options for directfs mode. + directfs directfsOpts +} + +// +stateify savable +type directfsOpts struct { + // If directfs is enabled, the gofer client does not make RPCs to the gofer + // process. Instead, it makes host syscalls to perform file operations. + enabled bool + + // hostUDSBind dictates whether this mount can create host unix domain + // sockets. + hostUDSBind bool + + // hostUDSConnect dictates whether this mount can connect to host unix domain + // sockets. + hostUDSConnect bool } // InteropMode controls the client's interaction with other remote filesystem @@ -457,6 +481,18 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt delete(mopts, moptOverlayfsStaleRead) fsopts.overlayfsStaleRead = true } + if _, ok := mopts[moptDirectfs]; ok { + delete(mopts, moptDirectfs) + fsopts.directfs.enabled = true + } + if _, ok := mopts[moptHostUDSBind]; ok { + delete(mopts, moptHostUDSBind) + fsopts.directfs.hostUDSBind = true + } + if _, ok := mopts[moptHostUDSConnect]; ok { + delete(mopts, moptHostUDSConnect) + fsopts.directfs.hostUDSConnect = true + } // fsopts.regularFilesUseSpecialFileFD can only be enabled by specifying // "cache=none". @@ -507,81 +543,82 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt fs.vfsfs.Init(vfsObj, &fstype, fs) - // TODO(b/258687694): Handle directfs. - if err := fs.initClientAndRoot(ctx); err != nil { + rootInode, rootHostFD, err := fs.initClientAndGetRoot(ctx) + if err != nil { fs.vfsfs.DecRef(ctx) return nil, nil, err } - - return &fs.vfsfs, &fs.root.vfsd, nil -} - -func (fs *filesystem) initClientAndRoot(ctx context.Context) error { - rootInode, err := fs.initClient(ctx) - if err != nil { - return err + if fs.opts.directfs.enabled { + fs.root, err = fs.getDirectfsRootDentry(ctx, rootHostFD, fs.client.NewFD(rootInode.ControlFD)) + } else { + fs.root, err = fs.newLisafsDentry(ctx, &rootInode) } - fs.root, err = fs.newLisafsDentry(ctx, &rootInode) if err != nil { - return err + fs.vfsfs.DecRef(ctx) + return nil, nil, err } - // Set the root's reference count to 2. One reference is returned to the // caller, and the other is held by fs to prevent the root from being "cached" // and subsequently evicted. fs.root.refs = atomicbitops.FromInt64(2) - return nil + return &fs.vfsfs, &fs.root.vfsd, nil } -func (fs *filesystem) initClient(ctx context.Context) (lisafs.Inode, error) { +// initClientAndGetRoot initializes fs.client and returns the root inode for +// this mount point. It handles the attach point (fs.opts.aname) resolution. +func (fs *filesystem) initClientAndGetRoot(ctx context.Context) (lisafs.Inode, int, error) { sock, err := unet.NewSocket(fs.opts.fd) if err != nil { - return lisafs.Inode{}, err + return lisafs.Inode{}, -1, err } - var rootInode lisafs.Inode ctx.UninterruptibleSleepStart(false) - fs.client, rootInode, _, err = lisafs.NewClient(sock) - ctx.UninterruptibleSleepFinish(false) + defer ctx.UninterruptibleSleepFinish(false) + + var ( + rootInode lisafs.Inode + rootHostFD int + ) + fs.client, rootInode, rootHostFD, err = lisafs.NewClient(sock) if err != nil { - return lisafs.Inode{}, err - } - ctx.UninterruptibleSleepStart(false) - err = fs.client.StartChannels() - ctx.UninterruptibleSleepFinish(false) - if err != nil { - return lisafs.Inode{}, err - } - if fs.opts.aname == "/" { - return rootInode, nil + return lisafs.Inode{}, -1, err } - // Walk to the attach point from root inode. aname is always absolute. - rootFD := fs.client.NewFD(rootInode.ControlFD) - status, inodes, err := rootFD.WalkMultiple(ctx, strings.Split(fs.opts.aname, "/")[1:]) - rootFD.Close(ctx, false /* flush */) - if err != nil { - return lisafs.Inode{}, err - } + cu := cleanup.Make(func() { + if rootHostFD >= 0 { + _ = unix.Close(rootHostFD) + } + rootControlFD := fs.client.NewFD(rootInode.ControlFD) + rootControlFD.Close(ctx, false /* flush */) + }) + defer cu.Clean() - // Close all intermediate FDs to the attach point. - numInodes := len(inodes) - for i := 0; i < numInodes-1; i++ { - curFD := fs.client.NewFD(inodes[i].ControlFD) - curFD.Close(ctx, false /* flush */) - } - - switch status { - case lisafs.WalkSuccess: - return inodes[numInodes-1], nil - default: - if numInodes > 0 { - last := fs.client.NewFD(inodes[numInodes-1].ControlFD) - last.Close(ctx, false /* flush */) + if fs.opts.directfs.enabled { + if fs.opts.aname != "/" { + log.Warningf("directfs does not support aname filesystem option: aname=%q", fs.opts.aname) + return lisafs.Inode{}, -1, unix.EINVAL + } + if rootHostFD < 0 { + log.Warningf("Mount RPC did not return host FD to mount point with directfs enabled") + return lisafs.Inode{}, -1, unix.EINVAL + } + } else { + if rootHostFD >= 0 { + log.Warningf("Mount RPC returned a host FD to mount point without directfs, we didn't ask for it") + _ = unix.Close(rootHostFD) + rootHostFD = -1 + } + // Use flipcall channels with lisafs because it makes a lot of RPCs. + if err := fs.client.StartChannels(); err != nil { + return lisafs.Inode{}, -1, err + } + rootInode, err = fs.handleAnameLisafs(ctx, rootInode) + if err != nil { + return lisafs.Inode{}, -1, err } - log.Warningf("initClient failed because walk to attach point %q failed: lisafs.WalkStatus = %v", fs.opts.aname, status) - return lisafs.Inode{}, unix.ENOENT } + cu.Release() + return rootInode, rootHostFD, nil } func getFDFromMountOptionsMap(ctx context.Context, mopts map[string]string) (int, error) { @@ -731,6 +768,14 @@ func inoKeyFromStatx(stat *linux.Statx) inoKey { } } +func inoKeyFromStat(stat *unix.Stat_t) inoKey { + return inoKey{ + ino: stat.Ino, + devMinor: unix.Minor(stat.Dev), + devMajor: unix.Major(stat.Dev), + } +} + // dentry implements vfs.DentryImpl. // // +stateify savable @@ -1049,6 +1094,32 @@ func (d *lisafsDentry) updateMetadataFromStatxLocked(stat *linux.Statx) { } } +// updateMetadataFromStatLocked is similar to updateMetadataFromStatxLocked, +// except that it takes a unix.Stat_t argument. +// Precondition: d.metadataMu must be locked. +// +checklocks:d.metadataMu +func (d *directfsDentry) updateMetadataFromStatLocked(stat *unix.Stat_t) error { + if got, want := stat.Mode&unix.S_IFMT, d.fileType(); got != want { + panic(fmt.Sprintf("direct.dentry file type changed from %#o to %#o", want, got)) + } + d.mode.Store(stat.Mode) + d.uid.Store(stat.Uid) + d.gid.Store(stat.Gid) + d.blockSize.Store(uint32(stat.Blksize)) + // Don't override newer client-defined timestamps with old host-defined + // ones. + if d.atimeDirty.Load() == 0 { + d.atime.Store(dentryTimestampFromUnix(stat.Atim)) + } + if d.mtimeDirty.Load() == 0 { + d.mtime.Store(dentryTimestampFromUnix(stat.Mtim)) + } + d.ctime.Store(dentryTimestampFromUnix(stat.Ctim)) + d.nlink.Store(uint32(stat.Nlink)) + d.updateSizeLocked(uint64(stat.Size)) + return nil +} + // Preconditions: !d.isSynthetic(). // Preconditions: d.metadataMu is locked. // +checklocks:d.metadataMu @@ -1115,6 +1186,7 @@ func (d *dentry) statTo(stat *linux.Statx) { stat.DevMinor = d.fs.devMinor } +// Precondition: fs.renameMu is locked. func (d *dentry) setStat(ctx context.Context, creds *auth.Credentials, opts *vfs.SetStatOptions, mnt *vfs.Mount) error { stat := &opts.Stat if stat.Mask == 0 { @@ -1195,6 +1267,9 @@ func (d *dentry) setStat(ctx context.Context, creds *auth.Credentials, opts *vfs var failureErr error if !d.isSynthetic() { if stat.Mask != 0 { + if err := d.prepareSetStat(ctx, stat); err != nil { + return err + } d.handleMu.RLock() if stat.Mask&linux.STATX_SIZE != 0 { // d.dataMu must be held around the update to both the remote @@ -1832,6 +1907,7 @@ func (d *dentry) removeXattr(ctx context.Context, creds *auth.Credentials, name // Preconditions: // - !d.isSynthetic(). // - d.isRegularFile() || d.isDir(). +// - fs.renameMu is locked. func (d *dentry) ensureSharedHandle(ctx context.Context, read, write, trunc bool) error { // O_TRUNC unconditionally requires us to obtain a new handle (opened with // O_TRUNC). @@ -2093,6 +2169,9 @@ func (fd *fileDescription) Stat(ctx context.Context, opts vfs.StatOptions) (linu // SetStat implements vfs.FileDescriptionImpl.SetStat. func (fd *fileDescription) SetStat(ctx context.Context, opts vfs.SetStatOptions) error { + fs := fd.filesystem() + fs.renameMu.RLock() + defer fs.renameMu.RUnlock() return fd.dentry().setStat(ctx, auth.CredentialsFromContext(ctx), &opts, fd.vfsfd.Mount()) } diff --git a/pkg/sentry/fsimpl/gofer/lisafs_dentry.go b/pkg/sentry/fsimpl/gofer/lisafs_dentry.go index fec896d0e..e2a11f938 100644 --- a/pkg/sentry/fsimpl/gofer/lisafs_dentry.go +++ b/pkg/sentry/fsimpl/gofer/lisafs_dentry.go @@ -16,6 +16,7 @@ package gofer import ( "fmt" + "strings" "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/atomicbitops" @@ -29,6 +30,39 @@ import ( "gvisor.dev/gvisor/pkg/sentry/vfs" ) +func (fs *filesystem) handleAnameLisafs(ctx context.Context, rootInode lisafs.Inode) (lisafs.Inode, error) { + if fs.opts.aname == "/" { + return rootInode, nil + } + + // Walk to the attach point from root inode. aname is always absolute. + rootFD := fs.client.NewFD(rootInode.ControlFD) + status, inodes, err := rootFD.WalkMultiple(ctx, strings.Split(fs.opts.aname, "/")[1:]) + if err != nil { + return lisafs.Inode{}, err + } + + // Close all intermediate FDs to the attach point. + rootFD.Close(ctx, false /* flush */) + numInodes := len(inodes) + for i := 0; i < numInodes-1; i++ { + curFD := fs.client.NewFD(inodes[i].ControlFD) + curFD.Close(ctx, false /* flush */) + } + + switch status { + case lisafs.WalkSuccess: + return inodes[numInodes-1], nil + default: + if numInodes > 0 { + last := fs.client.NewFD(inodes[numInodes-1].ControlFD) + last.Close(ctx, false /* flush */) + } + log.Warningf("initClient failed because walk to attach point %q failed: lisafs.WalkStatus = %v", fs.opts.aname, status) + return lisafs.Inode{}, linuxerr.ENOENT + } +} + // lisafsDentry is a gofer dentry implementation. It represents a dentry backed // by a lisafs connection. // @@ -139,6 +173,17 @@ func (fs *filesystem) newLisafsDentry(ctx context.Context, ino *lisafs.Inode) (* return &d.dentry, nil } +func (d *lisafsDentry) openHandle(ctx context.Context, flags uint32) (handle, error) { + openFD, hostFD, err := d.controlFD.OpenAt(ctx, flags) + if err != nil { + return noHandle, err + } + return handle{ + fdLisa: d.controlFD.Client().NewFD(openFD), + fd: int32(hostFD), + }, nil +} + func (d *lisafsDentry) updateHandles(ctx context.Context, h handle, readable, writable bool) { // Switch to new LISAFS FDs. Note that the read, write and mmap host FDs are // updated separately. @@ -242,7 +287,8 @@ func (d *lisafsDentry) getRemoteChild(ctx context.Context, name string) (*dentry // - parent.opMu must be locked. // - parent.isDir(). // - !rp.Done(). -// - dentry at name must not already exist in dentry tree. +// +// Postcondition: The returned dentry is already cached appropriately. func (d *lisafsDentry) getRemoteChildAndWalkPathLocked(ctx context.Context, rp *vfs.ResolvingPath, ds **[]*dentry) (*dentry, error) { // Walk as much of the path as possible in 1 RPC. // Note that pit is a copy of the iterator that does not affect rp. diff --git a/pkg/sentry/fsimpl/gofer/save_restore.go b/pkg/sentry/fsimpl/gofer/save_restore.go index f31dcd6c3..bf91ad442 100644 --- a/pkg/sentry/fsimpl/gofer/save_restore.go +++ b/pkg/sentry/fsimpl/gofer/save_restore.go @@ -146,6 +146,11 @@ func (d *dentry) afterLoad() { } } +// afterLoad is invoked by stateify. +func (d *directfsDentry) afterLoad() { + d.controlFD = -1 +} + // afterLoad is invoked by stateify. func (d *dentryPlatformFile) afterLoad() { if d.hostFileMapper.IsInited() { diff --git a/pkg/sentry/fsimpl/gofer/socket.go b/pkg/sentry/fsimpl/gofer/socket.go index 4b7012b30..34c34f6a6 100644 --- a/pkg/sentry/fsimpl/gofer/socket.go +++ b/pkg/sentry/fsimpl/gofer/socket.go @@ -28,6 +28,15 @@ func (d *dentry) isSocket() bool { return d.fileType() == linux.S_IFSOCK } +func isSocketTypeSupported(sockType linux.SockType) bool { + switch sockType { + case unix.SOCK_STREAM, unix.SOCK_DGRAM, unix.SOCK_SEQPACKET: + return true + default: + return false + } +} + // endpoint is a Gofer-backed transport.BoundEndpoint. // // An endpoint's lifetime is the time between when filesystem.BoundEndpointAt() @@ -94,7 +103,9 @@ func (e *endpoint) UnidirectionalConnect(ctx context.Context) (transport.Connect } func (e *endpoint) newConnectedEndpoint(ctx context.Context, sockType linux.SockType, queue *waiter.Queue) (*transport.SCMConnectedEndpoint, *syserr.Error) { + e.dentry.fs.renameMu.RLock() hostSockFD, err := e.dentry.connect(ctx, sockType) + e.dentry.fs.renameMu.RUnlock() if err != nil { return nil, syserr.ErrConnectionRefused } diff --git a/pkg/sentry/fsimpl/gofer/time.go b/pkg/sentry/fsimpl/gofer/time.go index 704764170..e9c791110 100644 --- a/pkg/sentry/fsimpl/gofer/time.go +++ b/pkg/sentry/fsimpl/gofer/time.go @@ -15,12 +15,17 @@ package gofer import ( + "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/sentry/vfs" ) func dentryTimestamp(t linux.StatxTimestamp) int64 { - return t.Sec*1e9 + int64(t.Nsec) + return t.ToNsec() +} + +func dentryTimestampFromUnix(t unix.Timespec) int64 { + return dentryTimestamp(linux.StatxTimestamp{Sec: t.Sec, Nsec: uint32(t.Nsec)}) } // Preconditions: d.cachedMetadataAuthoritative() == true. diff --git a/runsc/fsgofer/BUILD b/runsc/fsgofer/BUILD index 6abad2ff2..c29aa0efa 100644 --- a/runsc/fsgofer/BUILD +++ b/runsc/fsgofer/BUILD @@ -5,9 +5,6 @@ package(licenses = ["notice"]) go_library( name = "fsgofer", srcs = [ - "fsgofer_amd64_unsafe.go", - "fsgofer_arm64_unsafe.go", - "fsgofer_unsafe.go", "lisafs.go", ], visibility = ["//runsc:__subpackages__"], @@ -16,10 +13,10 @@ go_library( "//pkg/atomicbitops", "//pkg/cleanup", "//pkg/fd", + "//pkg/fsutil", "//pkg/lisafs", "//pkg/log", "//pkg/marshal/primitive", - "//pkg/syserr", "//runsc/config", "@org_golang_x_sys//unix:go_default_library", ], diff --git a/runsc/fsgofer/lisafs.go b/runsc/fsgofer/lisafs.go index 33091fd49..c78e34013 100644 --- a/runsc/fsgofer/lisafs.go +++ b/runsc/fsgofer/lisafs.go @@ -30,12 +30,15 @@ import ( "gvisor.dev/gvisor/pkg/atomicbitops" "gvisor.dev/gvisor/pkg/cleanup" rwfd "gvisor.dev/gvisor/pkg/fd" + "gvisor.dev/gvisor/pkg/fsutil" "gvisor.dev/gvisor/pkg/lisafs" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/marshal/primitive" "gvisor.dev/gvisor/runsc/config" ) +// LINT.IfChange + const ( openFlags = unix.O_NOFOLLOW | unix.O_CLOEXEC @@ -332,7 +335,7 @@ func (fd *controlFDLisa) SetStat(stat lisafs.SetStatReq) (failureMask uint32, fa symlinkPath := fd.Node().FilePath() parent, err := unix.Open(path.Dir(symlinkPath), openFlags|unix.O_PATH, 0) if err == nil { - err = utimensat(parent, path.Base(symlinkPath), utimes, unix.AT_SYMLINK_NOFOLLOW) + err = fsutil.Utimensat(parent, path.Base(symlinkPath), utimes, unix.AT_SYMLINK_NOFOLLOW) unix.Close(parent) } if err != nil { @@ -352,7 +355,7 @@ func (fd *controlFDLisa) SetStat(stat lisafs.SetStatReq) (failureMask uint32, fa } // Directories and regular files can operate directly on the fd // using empty name. - err := utimensat(hostFD, "", utimes, 0) + err := fsutil.Utimensat(hostFD, "", utimes, 0) if err != nil { log.Warningf("SetStat utimens failed %q, err: %v", fd.Node().FilePath(), err) failureMask |= (stat.Mask & (unix.STATX_ATIME | unix.STATX_MTIME)) @@ -749,7 +752,7 @@ func (fd *controlFDLisa) Connect(sockType uint32) (int, error) { // hostPath in our sockaddr. We'd need to redirect through a shorter path // in order to actually connect to this socket. hostPath := fd.Node().FilePath() - if len(hostPath) >= unixPathMax { + if len(hostPath) >= linux.UnixPathMax { return -1, unix.EINVAL } @@ -784,7 +787,7 @@ func (fd *controlFDLisa) BindAt(name string, sockType uint32, mode linux.FileMod // mappings, the app path may have fit in the sockaddr, but we can't fit // hostPath in our sockaddr. We'd need to redirect through a shorter path // in order to actually connect to this socket. - if len(socketPath) >= unixPathMax { + if len(socketPath) >= linux.UnixPathMax { log.Warningf("BindAt called with name too long: %q (len=%d)", socketPath, len(socketPath)) return nil, linux.Statx{}, nil, -1, unix.EINVAL } @@ -861,7 +864,7 @@ func (fd *controlFDLisa) Unlink(name string, flags uint32) error { // RenameAt implements lisafs.ControlFDImpl.RenameAt. func (fd *controlFDLisa) RenameAt(oldName string, newDir lisafs.ControlFDImpl, newName string) error { - return renameat(fd.hostFD, oldName, newDir.(*controlFDLisa).hostFD, newName) + return fsutil.RenameAt(fd.hostFD, oldName, newDir.(*controlFDLisa).hostFD, newName) } // Renamed implements lisafs.ControlFDImpl.Renamed. @@ -977,7 +980,7 @@ func (fd *openFDLisa) Getdent64(count uint32, seek0 bool, recordDirent func(lisa } n, err := unix.Getdents(fd.hostFD, direntsBuf[:bufEnd]) if err != nil { - if err == unix.EINVAL && bufEnd < unixDirentMaxSize { + if err == unix.EINVAL && bufEnd < fsutil.UnixDirentMaxSize { // getdents64(2) returns EINVAL is returned when the result // buffer is too small. If bufEnd is smaller than the max // size of unix.Dirent, then just break here to return all @@ -990,7 +993,7 @@ func (fd *openFDLisa) Getdent64(count uint32, seek0 bool, recordDirent func(lisa break } - parseDirents(direntsBuf[:n], func(ino uint64, off int64, ftype uint8, name string, reclen uint16) bool { + fsutil.ParseDirents(direntsBuf[:n], func(ino uint64, off int64, ftype uint8, name string, reclen uint16) bool { dirent := lisafs.Dirent64{ Ino: primitive.Uint64(ino), Off: primitive.Uint64(off), @@ -999,8 +1002,9 @@ func (fd *openFDLisa) Getdent64(count uint32, seek0 bool, recordDirent func(lisa } // The client also wants the device ID, which annoyingly incurs an - // additional syscall per dirent. Live with it. - stat, err := statAt(fd.hostFD, name) + // additional syscall per dirent. + // TODO(gvisor.dev/issue/6665): Get rid of per-dirent stat. + stat, err := fsutil.StatAt(fd.hostFD, name) if err != nil { log.Warningf("Getdent64: skipping file %q with failed stat, err: %v", path.Join(fd.ControlFD().FD().Node().FilePath(), name), err) return true @@ -1177,3 +1181,5 @@ func extractErrno(err error) unix.Errno { log.Debugf("Unknown error: %v, defaulting to EIO", err) return unix.EIO } + +// LINT.ThenChange(../../pkg/sentry/fsimpl/gofer/directfs_dentry.go)