From b460bf947598c48770b9f83a7618be2dd9e12500 Mon Sep 17 00:00:00 2001 From: Ayush Ranjan Date: Fri, 17 Feb 2023 10:31:16 -0800 Subject: [PATCH] Directfs implementation. Directfs accesses host filesystem directly, without going through the gofer server. This change basically adds runsc fsgofer functionality into the gofer client. We use the gofer dentry impl abstraction to make direct host syscalls instead of making LISAFS RPCs. It works on a donated FD to the root of the mount. All other relevant mount options supported in gofer client are also supported here. Directfs is also a lisafs user. Directfs client makes a Mount RPC. The server is expected to donate a host FD to the mount point that the client can use to perform all necessary filesystem operations. Directfs client does not set up any flipcall channels, because it (mostly) doesn't make any RPCs. Note however that we can not avoid LISAFS RPCs in certain cases. Certain operations like bind(2) and connect(2) require host paths. But the container filesystem is not mounted inside the sandbox's mount namespace. The sandbox just has an FD to the filesystem root. Furthermore, procfs is also not mounted in the sandbox process for security reasons. Otherwise we could have used host paths like `/proc/self/fd/`. The only viable option is to use fchdir(2) to change working directory to the socket's parent directory and use a relative host path from there. But in certain situations, we don't even have access to a socket's parent directory (in case of a socket mount point). The parent lives in a different gofer mount in the sentry. There is no clean way of fetching that. In these extreme corner cases, directfs falls back to using LISAFS RPCs. These corner cases are: - chmod(2) on mount point socket. - utimensat(2) on mount point symlink. - connect(2) on mount point socket. PiperOrigin-RevId: 510465837 --- pkg/fsutil/BUILD | 20 + pkg/fsutil/chdir.go | 48 ++ pkg/fsutil/fsutil.go | 17 + .../fsutil/fsutil_amd64_unsafe.go | 5 +- .../fsutil/fsutil_arm64_unsafe.go | 5 +- .../fsutil/fsutil_unsafe.go | 17 +- pkg/sentry/fsimpl/gofer/BUILD | 3 + pkg/sentry/fsimpl/gofer/dentry_impl.go | 130 ++- pkg/sentry/fsimpl/gofer/directfs_dentry.go | 775 ++++++++++++++++++ pkg/sentry/fsimpl/gofer/filesystem.go | 40 +- pkg/sentry/fsimpl/gofer/gofer.go | 183 +++-- pkg/sentry/fsimpl/gofer/lisafs_dentry.go | 48 +- pkg/sentry/fsimpl/gofer/save_restore.go | 5 + pkg/sentry/fsimpl/gofer/socket.go | 11 + pkg/sentry/fsimpl/gofer/time.go | 7 +- runsc/fsgofer/BUILD | 5 +- runsc/fsgofer/lisafs.go | 24 +- 17 files changed, 1247 insertions(+), 96 deletions(-) create mode 100644 pkg/fsutil/BUILD create mode 100644 pkg/fsutil/chdir.go create mode 100644 pkg/fsutil/fsutil.go rename runsc/fsgofer/fsgofer_amd64_unsafe.go => pkg/fsutil/fsutil_amd64_unsafe.go (89%) rename runsc/fsgofer/fsgofer_arm64_unsafe.go => pkg/fsutil/fsutil_arm64_unsafe.go (89%) rename runsc/fsgofer/fsgofer_unsafe.go => pkg/fsutil/fsutil_unsafe.go (79%) create mode 100644 pkg/sentry/fsimpl/gofer/directfs_dentry.go 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)