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/<sockFD>`. 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
This commit is contained in:
Ayush Ranjan
2023-02-17 10:33:43 -08:00
committed by gVisor bot
parent 17d9b14478
commit b460bf9475
17 changed files with 1247 additions and 96 deletions
+20
View File
@@ -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",
],
)
+48
View File
@@ -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()
}
+17
View File
@@ -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
@@ -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
@@ -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
@@ -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]))
+3
View File
@@ -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",
+113 -17
View File
@@ -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")
}
File diff suppressed because it is too large Load Diff
+37 -3
View File
@@ -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 {
+131 -52
View File
@@ -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())
}
+47 -1
View File
@@ -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.
+5
View File
@@ -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() {
+11
View File
@@ -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
}
+6 -1
View File
@@ -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.
+1 -4
View File
@@ -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",
],
+15 -9
View File
@@ -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)