Files
Shailend ChandandgVisor bot 0a789f7147 gofer: dispatch device special file opens to the sentry
A new runsc flag controls the dispatch with the default being to
use sentry implementations alone.

PiperOrigin-RevId: 966266713
2026-08-17 17:17:02 -07:00

2460 lines
83 KiB
Go

// Copyright 2019 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 provides a filesystem implementation that is backed by a 9p
// server, interchangeably referred to as "gofers" throughout this package.
//
// Lock order:
//
// regularFileFD/directoryFD.mu
// filesystem.renameMu
// dentry.cachingMu
// dentryCache.mu
// dentry.opMu
// dentry.childrenMu
// filesystem.syncMu
// dentry.metadataMu
// *** "memmap.Mappable/MappingIdentity locks" below this point
// dentry.mapsMu
// *** "memmap.Mappable locks taken by Translate" below this point
// dentry.handleMu
// dentry.dataMu
// filesystem.ancestryMu
// filesystem.inoMu
// filesystem.inodeMu
// specialFileFD.mu
// specialFileFD.bufMu
//
// Locking dentry.opMu and dentry.metadataMu in multiple dentries requires that
// either ancestor dentries are locked before descendant dentries, or that
// filesystem.renameMu is locked for writing.
package gofer
import (
"fmt"
"path"
"strconv"
"strings"
"sync/atomic"
"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"
"gvisor.dev/gvisor/pkg/lisafs"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/refs"
"gvisor.dev/gvisor/pkg/sentry/checkpoint"
fslock "gvisor.dev/gvisor/pkg/sentry/fsimpl/lock"
"gvisor.dev/gvisor/pkg/sentry/fsutil"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/kernel/pipe"
"gvisor.dev/gvisor/pkg/sentry/ktime"
"gvisor.dev/gvisor/pkg/sentry/memmap"
"gvisor.dev/gvisor/pkg/sentry/pgalloc"
"gvisor.dev/gvisor/pkg/sentry/socket/unix/transport"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/unet"
)
// Name is the default filesystem name.
const Name = "9p"
// Mount option names for goferfs.
const (
moptTransport = "trans"
moptReadFD = "rfdno"
moptWriteFD = "wfdno"
moptAname = "aname"
moptDfltUID = "dfltuid"
moptDfltGID = "dfltgid"
moptCache = "cache"
moptDcache = "dcache"
moptForcePageCache = "force_page_cache"
moptLimitHostFDTranslation = "limit_host_fd_translation"
moptOverlayfsStaleRead = "overlayfs_stale_read"
moptDisableFileHandleSharing = "disable_file_handle_sharing"
moptDisableFifoOpen = "disable_fifo_open"
moptCharDevicePolicy = "char_device_policy"
// Directfs options.
moptDirectfs = "directfs"
)
// Valid values for the "char_device_policy" mount option.
const (
charDevPolicyEmulatedOnly = "emulated-only"
charDevPolicyPreferEmulated = "prefer-emulated"
charDevPolicyPassthrough = "passthrough"
)
// Valid values for the "cache" mount option.
const (
cacheFSCache = "fscache"
cacheFSCacheWritethrough = "fscache_writethrough"
cacheRemoteRevalidating = "remote_revalidating"
)
// SupportedMountOptions is the set of mount options that can be set externally.
var SupportedMountOptions = []string{moptOverlayfsStaleRead, moptDisableFileHandleSharing, moptDcache}
const (
defaultMaxCachedDentries = 1000
maxCachedNegativeChildren = 1000
)
// stringFixedCache is a fixed sized cache, once initialized,
// its size never changes.
//
// +stateify savable
type stringFixedCache struct {
// namesList stores negative names with fifo list.
// name stored in namesList only means it used to be negative
// at the moment you pushed it to the list.
namesList stringList
size uint64
}
func (cache *stringFixedCache) isInited() bool {
return cache.size != 0
}
func (cache *stringFixedCache) init(size uint64) {
elements := make([]stringListElem, size)
for i := uint64(0); i < size; i++ {
cache.namesList.PushFront(&elements[i])
}
cache.size = size
}
// Update will push name to the front of the list,
// and pop the tail value.
func (cache *stringFixedCache) add(name string) string {
tail := cache.namesList.Back()
victimName := tail.str
tail.str = name
cache.namesList.Remove(tail)
cache.namesList.PushFront(tail)
return victimName
}
// +stateify savable
type dentryCache struct {
// maxCachedDentries is the maximum number of cacheable dentries.
// maxCachedDentries is immutable.
maxCachedDentries uint64
// mu protects the below fields.
mu sync.Mutex `state:"nosave"`
// dentries contains all dentries with 0 references. Due to race conditions,
// it may also contain dentries with non-zero references.
dentries dentryList
// dentriesLen is the number of dentries in dentries.
dentriesLen uint64
}
// SetDentryCacheSize sets the size of the global gofer dentry cache.
func SetDentryCacheSize(size int) {
if size < 0 {
return
}
if globalDentryCache != nil {
log.Warningf("Global dentry cache has already been initialized. Ignoring subsequent attempt.")
return
}
globalDentryCache = &dentryCache{maxCachedDentries: uint64(size)}
}
// globalDentryCache is a global cache of dentries across all gofer clients.
var globalDentryCache *dentryCache
// Valid values for "trans" mount option.
const transportModeFD = "fd"
// FilesystemType implements vfs.FilesystemType.
//
// +stateify savable
type FilesystemType struct{}
// filesystem implements vfs.FilesystemImpl.
//
// +stateify savable
type filesystem struct {
vfsfs vfs.Filesystem
// mf is used to allocate memory that caches regular file contents. mf is
// immutable.
mf *pgalloc.MemoryFile `state:"nosave"`
// Immutable options.
opts filesystemOptions
iopts InternalFilesystemOptions
// client is the LISAFS client used for communicating with the server. client
// is immutable.
client *lisafs.Client `state:"nosave"`
// clock is a realtime clock used to set timestamps in file operations.
clock ktime.Clock
// devMinor is the filesystem's minor device number. devMinor is immutable.
devMinor uint32
// root is the root dentry. root is immutable.
root *dentry
// renameMu serves two purposes:
//
// - It synchronizes path resolution with renaming initiated by this
// client.
//
// - It is held by path resolution to ensure that reachable dentries remain
// valid. A dentry is reachable by path resolution if it has a non-zero
// reference count (such that it is usable as vfs.ResolvingPath.Start() or
// is reachable from its children), or if it is a child dentry (such that
// it is reachable from its parent).
renameMu sync.RWMutex `state:"nosave"`
// ancestryMu additionally protects dentry.parent and dentry.name as
// required by genericfstree.
ancestryMu sync.RWMutex `state:"nosave"`
dentryCache *dentryCache
// syncableDentries contains all non-synthetic dentries. specialFileFDs
// contains all open specialFileFDs. These fields are protected by syncMu.
syncMu sync.Mutex `state:"nosave"`
syncableDentries dentryList
specialFileFDs specialFDList
// inoByKey maps previously-observed device ID and host inode numbers to
// internal inode numbers assigned to those files. inoByKey is not preserved
// across checkpoint/restore because inode numbers may be reused between
// different gofer processes, so inode numbers may be repeated for different
// files across checkpoint/restore. inoByKey is protected by inoMu.
inoMu sync.Mutex `state:"nosave"`
inoByKey map[inoKey]uint64 `state:"nosave"`
// inodeByKey maps inoKey to non-directory inodes. inodeByKey is protected by
// inodeMu. inodeByKey is similar to inoByKey, except that is not an
// ever-growing map. This is to allow for the sharing of inodes across
// dentries. As non-directory dentries are destroyed, their associated inode
// entry in this map is cleared.
inodeMu sync.Mutex `state:"nosave"`
inodeByKey map[inoKey]*inode `state:"nosave"`
// lastIno is the last inode number assigned to a file. lastIno is accessed
// using atomic memory operations.
lastIno atomicbitops.Uint64
// savedDentryRW records open read/write handles during save/restore.
savedDentryRW map[*dentry]savedDentryRW
// savedDeletedOpenDentries records deleted dentries that are saved during
// save/restore. These are still accessible via open application FDs.
savedDeletedOpenDentries map[*dentry]struct{}
// released is nonzero once filesystem.Release has been called.
released atomicbitops.Int32
}
// getOrCreateInode returns an inode for the given inoKey, to avoid creating
// multiple inodes objects for the same inoKey. This is useful for sharing
// inodes across dentries.
//
// If a miss, `createInode` is called and the result is cached. If a hit,
// `onCacheHit` is called. This path locks fs.inodeMu.
//
// The caller is responsible for decrementing the inode's reference count when done.
func (fs *filesystem) getOrCreateInode(inoKey inoKey, dontCache bool, onCacheHit func(), createInode func() *inode) *inode {
if dontCache {
// Create a new inode and return it bypassing the cache. This is used for
// creating inodes that are not meant to be shared across dentries.
return createInode()
}
fs.inodeMu.Lock()
defer fs.inodeMu.Unlock()
if cachedInode, ok := fs.inodeByKey[inoKey]; ok {
onCacheHit()
cachedInode.incRef()
return cachedInode
}
inode := createInode()
fs.inodeByKey[inoKey] = inode
return inode
}
// +stateify savable
type filesystemOptions struct {
fd int
aname string
interop InteropMode // derived from the "cache" mount option
dfltuid auth.KUID
dfltgid auth.KGID
// dcache is the maximum number of dentries that can be cached. This is
// effective only if globalDentryCache is not being used.
dcache uint64
// If forcePageCache is true, host FDs may not be used for application
// memory mappings even if available; instead, the client must perform its
// own caching of regular file pages. This is primarily useful for testing.
forcePageCache bool
// If limitHostFDTranslation is true, apply maxFillRange() constraints to
// host FD mappings returned by dentry.(memmap.Mappable).Translate(). This
// makes memory accounting behavior more consistent between cases where
// host FDs are / are not available, but may increase the frequency of
// sentry-handled page faults on files for which a host FD is available.
limitHostFDTranslation bool
// If overlayfsStaleRead is true, O_RDONLY host FDs provided by the remote
// filesystem may not be coherent with writable host FDs opened later, so
// all uses of the former must be replaced by uses of the latter. This is
// usually only the case when the remote filesystem is a Linux overlayfs
// mount. (Prior to Linux 4.18, patch series centered on commit
// d1d04ef8572b "ovl: stack file ops", both I/O and memory mappings were
// incoherent between pre-copy-up and post-copy-up FDs; after that patch
// series, only memory mappings are incoherent.)
overlayfsStaleRead bool
// If regularFilesUseSpecialFileFD is true, application FDs representing
// regular files will use distinct file handles for each FD, in the same
// way that application FDs representing "special files" such as sockets
// do. Note that this disables client caching for regular files. This option
// may regress performance due to excessive Open RPCs. This option is not
// supported with overlayfsStaleRead for now.
regularFilesUseSpecialFileFD bool
// If disableFifoOpen is true, application attempts to open(2) a host FIFO
// are disallowed.
disableFifoOpen bool
// charDevicePolicy controls whether opens of character device files are
// dispatched to the sentry's device registry, passed through to the host
// via the gofer, or a combination of both.
charDevicePolicy charDevicePolicy
// directfs holds options for directfs mode.
directfs directfsOpts
}
// charDevicePolicy tells how opens of character device files are handled.
// It is set by the "char_device_policy" mount option.
//
// +stateify savable
type charDevicePolicy int
const (
// charDevEmulatedOnly dispatches all character device opens to the
// sentry's device registry. Opens of devices the sentry does not
// implement fail with ENXIO. This is the default.
charDevEmulatedOnly charDevicePolicy = iota
// charDevPreferEmulated dispatches opens of sentry-implemented devices
// to the sentry's device registry, and opens the rest through the gofer,
// exposing the corresponding host devices.
charDevPreferEmulated
// charDevPassthrough opens all character devices through the gofer,
// exposing the corresponding host devices.
charDevPassthrough
)
// +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
}
// InteropMode controls the client's interaction with other remote filesystem
// users.
//
// +stateify savable
type InteropMode uint32
const (
// InteropModeExclusive is appropriate when the filesystem client is the
// only user of the remote filesystem.
//
// - The client may cache arbitrary filesystem state (file data, metadata,
// filesystem structure, etc.).
//
// - Client changes to filesystem state may be sent to the remote
// filesystem asynchronously, except when server permission checks are
// necessary.
//
// - File timestamps are based on client clocks. This ensures that users of
// the client observe timestamps that are coherent with their own clocks
// and consistent with Linux's semantics (in particular, it is not always
// possible for clients to set arbitrary atimes and mtimes depending on the
// remote filesystem implementation, and never possible for clients to set
// arbitrary ctimes.)
InteropModeExclusive InteropMode = iota
// InteropModeWritethrough is appropriate when there are read-only users of
// the remote filesystem that expect to observe changes made by the
// filesystem client.
//
// - The client may cache arbitrary filesystem state.
//
// - Client changes to filesystem state must be sent to the remote
// filesystem synchronously.
//
// - File timestamps are based on client clocks. As a corollary, access
// timestamp changes from other remote filesystem users will not be visible
// to the client.
InteropModeWritethrough
// InteropModeShared is appropriate when there are users of the remote
// filesystem that may mutate its state other than the client.
//
// - The client must verify ("revalidate") cached filesystem state before
// using it.
//
// - Client changes to filesystem state must be sent to the remote
// filesystem synchronously.
//
// - File timestamps are based on server clocks. This is necessary to
// ensure that timestamp changes are synchronized between remote filesystem
// users.
//
// Note that the correctness of InteropModeShared depends on the server
// correctly implementing 9P fids (i.e. each fid immutably represents a
// single filesystem object), even in the presence of remote filesystem
// mutations from other users. If this is violated, the behavior of the
// client is undefined.
InteropModeShared
)
// InternalFilesystemOptions may be passed as
// vfs.GetFilesystemOptions.InternalData to FilesystemType.GetFilesystem.
//
// +stateify savable
type InternalFilesystemOptions struct {
// If UniqueID is non-empty, it is used to reassociate the filesystem with
// a new server FD during restoration from checkpoint.
UniqueID checkpoint.ResourceID
// If LeakConnection is true, do not close the connection to the server
// when the Filesystem is released. This is necessary for deployments in
// which servers can handle only a single client and report failure if that
// client disconnects.
LeakConnection bool
// If OpenSocketsByConnecting is true, silently translate attempts to open
// files identifying as sockets to connect RPCs.
//
// TODO(b/354724938): Remove this option once there are no callers who
// rely on this behavior.
OpenSocketsByConnecting bool
}
// _V9FS_DEFUID and _V9FS_DEFGID (from Linux's fs/9p/v9fs.h) are the default
// UIDs and GIDs used for files that do not provide a specific owner or group
// respectively.
const (
// uint32(-2) doesn't work in Go.
_V9FS_DEFUID = auth.KUID(4294967294)
_V9FS_DEFGID = auth.KGID(4294967294)
)
// Name implements vfs.FilesystemType.Name.
func (FilesystemType) Name() string {
return Name
}
// Release implements vfs.FilesystemType.Release.
func (FilesystemType) Release(ctx context.Context) {}
// GetFilesystem implements vfs.FilesystemType.GetFilesystem.
func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.VirtualFilesystem, creds *auth.Credentials, source string, opts vfs.GetFilesystemOptions) (*vfs.Filesystem, *vfs.Dentry, error) {
mf := pgalloc.MemoryFileFromContext(ctx)
if mf == nil {
ctx.Warningf("gofer.FilesystemType.GetFilesystem: CtxMemoryFile is nil")
return nil, nil, linuxerr.EINVAL
}
mopts := vfs.GenericParseMountOptions(opts.Data)
var fsopts filesystemOptions
fd, err := getFDFromMountOptionsMap(ctx, mopts)
if err != nil {
return nil, nil, err
}
fsopts.fd = fd
// Get the attach name.
fsopts.aname = "/"
if aname, ok := mopts[moptAname]; ok {
delete(mopts, moptAname)
if !path.IsAbs(aname) {
ctx.Warningf("gofer.FilesystemType.GetFilesystem: aname is not absolute: %s=%s", moptAname, aname)
return nil, nil, linuxerr.EINVAL
}
fsopts.aname = path.Clean(aname)
}
// Parse the cache policy. For historical reasons, this defaults to the
// least generally-applicable option, InteropModeExclusive.
fsopts.interop = InteropModeExclusive
if cache, ok := mopts[moptCache]; ok {
delete(mopts, moptCache)
switch cache {
case cacheFSCache:
fsopts.interop = InteropModeExclusive
case cacheFSCacheWritethrough:
fsopts.interop = InteropModeWritethrough
case cacheRemoteRevalidating:
fsopts.interop = InteropModeShared
default:
ctx.Warningf("gofer.FilesystemType.GetFilesystem: invalid cache policy: %s=%s", moptCache, cache)
return nil, nil, linuxerr.EINVAL
}
}
// Parse the dentry cache size.
fsopts.dcache = defaultMaxCachedDentries
if dcacheStr, ok := mopts[moptDcache]; ok {
delete(mopts, moptDcache)
dcache, err := strconv.ParseInt(dcacheStr, 10, 64)
if err != nil {
ctx.Warningf("gofer.FilesystemType.GetFilesystem: invalid dcache: %s=%s", moptDcache, dcacheStr)
return nil, nil, linuxerr.EINVAL
}
if dcache >= 0 {
fsopts.dcache = uint64(dcache)
}
}
// Parse the default UID and GID.
fsopts.dfltuid = _V9FS_DEFUID
if dfltuidstr, ok := mopts[moptDfltUID]; ok {
delete(mopts, moptDfltUID)
dfltuid, err := strconv.ParseUint(dfltuidstr, 10, 32)
if err != nil {
ctx.Warningf("gofer.FilesystemType.GetFilesystem: invalid default UID: %s=%s", moptDfltUID, dfltuidstr)
return nil, nil, linuxerr.EINVAL
}
// In Linux, dfltuid is interpreted as a UID and is converted to a KUID
// in the caller's user namespace, but goferfs isn't
// application-mountable.
fsopts.dfltuid = auth.KUID(dfltuid)
}
fsopts.dfltgid = _V9FS_DEFGID
if dfltgidstr, ok := mopts[moptDfltGID]; ok {
delete(mopts, moptDfltGID)
dfltgid, err := strconv.ParseUint(dfltgidstr, 10, 32)
if err != nil {
ctx.Warningf("gofer.FilesystemType.GetFilesystem: invalid default UID: %s=%s", moptDfltGID, dfltgidstr)
return nil, nil, linuxerr.EINVAL
}
fsopts.dfltgid = auth.KGID(dfltgid)
}
// Handle simple flags.
if _, ok := mopts[moptDisableFileHandleSharing]; ok {
delete(mopts, moptDisableFileHandleSharing)
fsopts.regularFilesUseSpecialFileFD = true
}
if _, ok := mopts[moptDisableFifoOpen]; ok {
delete(mopts, moptDisableFifoOpen)
fsopts.disableFifoOpen = true
}
if policy, ok := mopts[moptCharDevicePolicy]; ok {
delete(mopts, moptCharDevicePolicy)
switch policy {
case charDevPolicyEmulatedOnly:
fsopts.charDevicePolicy = charDevEmulatedOnly
case charDevPolicyPreferEmulated:
fsopts.charDevicePolicy = charDevPreferEmulated
case charDevPolicyPassthrough:
fsopts.charDevicePolicy = charDevPassthrough
default:
ctx.Warningf("gofer.FilesystemType.GetFilesystem: invalid character device policy: %s=%s", moptCharDevicePolicy, policy)
return nil, nil, linuxerr.EINVAL
}
}
if _, ok := mopts[moptForcePageCache]; ok {
delete(mopts, moptForcePageCache)
fsopts.forcePageCache = true
}
if _, ok := mopts[moptLimitHostFDTranslation]; ok {
delete(mopts, moptLimitHostFDTranslation)
fsopts.limitHostFDTranslation = true
}
if _, ok := mopts[moptOverlayfsStaleRead]; ok {
delete(mopts, moptOverlayfsStaleRead)
fsopts.overlayfsStaleRead = true
}
if _, ok := mopts[moptDirectfs]; ok {
delete(mopts, moptDirectfs)
fsopts.directfs.enabled = true
}
// fsopts.regularFilesUseSpecialFileFD can only be enabled by specifying
// "cache=none".
// Check for unparsed options.
if len(mopts) != 0 {
ctx.Warningf("gofer.FilesystemType.GetFilesystem: unknown options: %v", mopts)
return nil, nil, linuxerr.EINVAL
}
// Validation.
if fsopts.regularFilesUseSpecialFileFD && fsopts.overlayfsStaleRead {
// These options are not supported together. To support this, when a dentry
// is opened writably for the first time, we need to iterate over all the
// specialFileFDs of that dentry that represent a regular file and call
// fd.hostFileMapper.RegenerateMappings(writable_fd).
ctx.Warningf("gofer.FilesystemType.GetFilesystem: regularFilesUseSpecialFileFD and overlayfsStaleRead options are not supported together.")
return nil, nil, linuxerr.EINVAL
}
// Handle internal options.
iopts, ok := opts.InternalData.(InternalFilesystemOptions)
if opts.InternalData != nil && !ok {
ctx.Warningf("gofer.FilesystemType.GetFilesystem: GetFilesystemOptions.InternalData has type %T, wanted gofer.InternalFilesystemOptions", opts.InternalData)
return nil, nil, linuxerr.EINVAL
}
// If !ok, iopts being the zero value is correct.
// Construct the filesystem object.
devMinor, err := vfsObj.GetAnonBlockDevMinor()
if err != nil {
return nil, nil, err
}
fs := &filesystem{
mf: mf,
opts: fsopts,
iopts: iopts,
clock: ktime.RealtimeClockFromContext(ctx),
devMinor: devMinor,
inoByKey: make(map[inoKey]uint64),
inodeByKey: make(map[inoKey]*inode),
}
// Did the user configure a global dentry cache?
if globalDentryCache != nil {
fs.dentryCache = globalDentryCache
} else {
fs.dentryCache = &dentryCache{maxCachedDentries: fsopts.dcache}
}
fs.vfsfs.Init(vfsObj, &fstype, fs)
rootInode, rootHostFD, err := fs.initClientAndGetRoot(ctx)
if err != nil {
fs.vfsfs.DecRef(ctx)
return nil, nil, err
}
// Create the root dentry
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)
}
if err != nil {
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 &fs.vfsfs, &fs.root.vfsd, nil
}
// 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{}, -1, err
}
ctx.UninterruptibleSleepStart()
defer ctx.UninterruptibleSleepFinish()
var (
rootInode lisafs.Inode
rootHostFD int
)
fs.client, rootInode, rootHostFD, err = lisafs.NewClient(sock)
if err != nil {
return lisafs.Inode{}, -1, 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()
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
}
}
cu.Release()
return rootInode, rootHostFD, nil
}
func getFDFromMountOptionsMap(ctx context.Context, mopts map[string]string) (int, error) {
// Check that the transport is "fd".
trans, ok := mopts[moptTransport]
if !ok || trans != transportModeFD {
ctx.Warningf("gofer.getFDFromMountOptionsMap: transport must be specified as '%s=%s'", moptTransport, transportModeFD)
return -1, linuxerr.EINVAL
}
delete(mopts, moptTransport)
// Check that read and write FDs are provided and identical.
rfdstr, ok := mopts[moptReadFD]
if !ok {
ctx.Warningf("gofer.getFDFromMountOptionsMap: read FD must be specified as '%s=<file descriptor>'", moptReadFD)
return -1, linuxerr.EINVAL
}
delete(mopts, moptReadFD)
rfd, err := strconv.Atoi(rfdstr)
if err != nil {
ctx.Warningf("gofer.getFDFromMountOptionsMap: invalid read FD: %s=%s", moptReadFD, rfdstr)
return -1, linuxerr.EINVAL
}
wfdstr, ok := mopts[moptWriteFD]
if !ok {
ctx.Warningf("gofer.getFDFromMountOptionsMap: write FD must be specified as '%s=<file descriptor>'", moptWriteFD)
return -1, linuxerr.EINVAL
}
delete(mopts, moptWriteFD)
wfd, err := strconv.Atoi(wfdstr)
if err != nil {
ctx.Warningf("gofer.getFDFromMountOptionsMap: invalid write FD: %s=%s", moptWriteFD, wfdstr)
return -1, linuxerr.EINVAL
}
if rfd != wfd {
ctx.Warningf("gofer.getFDFromMountOptionsMap: read FD (%d) and write FD (%d) must be equal", rfd, wfd)
return -1, linuxerr.EINVAL
}
return rfd, nil
}
// Release implements vfs.FilesystemImpl.Release.
func (fs *filesystem) Release(ctx context.Context) {
fs.released.Store(1)
mf := fs.mf
fs.syncMu.Lock()
for elem := fs.syncableDentries.Front(); elem != nil; elem = elem.Next() {
d := elem.d
d.inode.handleMu.Lock()
d.inode.dataMu.Lock()
if d.inode.isWriteHandleOk() {
// Write dirty cached data to the remote file.
h := d.inode.writeHandle()
if err := fsutil.SyncDirtyAll(ctx, &d.inode.cache, &d.inode.dirty, d.inode.size.Load(), mf, h.writeFromBlocksAt); err != nil {
log.Warningf("gofer.filesystem.Release: failed to flush dentry: %v", err)
}
// TODO(jamieliu): Do we need to flushf/fsync d?
}
// Discard cached pages.
d.inode.cache.DropAll(mf)
d.inode.dirty.RemoveAllAndAccount()
d.inode.dataMu.Unlock()
// Close host FDs if they exist.
d.inode.closeHostFDs()
d.inode.handleMu.Unlock()
}
// There can't be any specialFileFDs still using fs, since each such
// FileDescription would hold a reference on a Mount holding a reference on
// fs.
fs.syncMu.Unlock()
// If leak checking is enabled, release all outstanding references in the
// filesystem. We deliberately avoid doing this outside of leak checking; we
// have released all external resources above rather than relying on dentry
// destructors. fs.root may be nil if creating the client or initializing the
// root dentry failed in GetFilesystem.
if refs.GetLeakMode() != refs.NoLeakChecking && fs.root != nil {
fs.renameMu.Lock()
fs.root.releaseExtraRefsRecursiveLocked(ctx)
fs.evictAllCachedDentriesLocked(ctx)
fs.renameMu.Unlock()
// An extra reference was held by the filesystem on the root to prevent it from
// being cached/evicted.
fs.root.DecRef(ctx)
}
if !fs.iopts.LeakConnection {
// Close the connection to the server. This implicitly closes all FDs.
if fs.client != nil {
fs.client.Close()
}
}
fs.vfsfs.VirtualFilesystem().PutAnonBlockDevMinor(fs.devMinor)
}
// releaseExtraRefsRecursiveLocked traverses the tree with root d and
// decrements the reference count on every synthetic dentry and dentry with
// endpoint != nil. Such dentries have one reference for existence that should
// be dropped during filesystem.Release.
//
// Precondition: d.inode.fs.renameMu is locked for writing.
func (d *dentry) releaseExtraRefsRecursiveLocked(ctx context.Context) {
if d.inode.isSynthetic() || d.inode.endpoint != nil {
d.decRefNoCaching()
d.checkCachingLocked(ctx, true /* renameMuWriteLocked */)
}
if d.isDir() {
var children []*dentry
d.childrenMu.Lock()
for _, child := range d.children {
children = append(children, child)
}
d.childrenMu.Unlock()
for _, child := range children {
if child != nil {
child.releaseExtraRefsRecursiveLocked(ctx)
}
}
}
}
// inoKey is the key used to identify the inode backing the dentry.
// +stateify savable
type inoKey struct {
ino uint64
devMinor uint32
devMajor uint32
}
func inoKeyFromStatx(stat *lisafs.Statx) inoKey {
return inoKey{
ino: stat.Ino,
devMinor: stat.DevMinor,
devMajor: stat.DevMajor,
}
}
func inoKeyFromUnixStatx(stat *unix.Statx_t) inoKey {
return inoKey{
ino: stat.Ino,
devMinor: stat.Dev_minor,
devMajor: stat.Dev_major,
}
}
func inoKeyFromStat(stat *unix.Stat_t) inoKey {
return inoKey{
ino: stat.Ino,
devMinor: unix.Minor(stat.Dev),
devMajor: unix.Major(stat.Dev),
}
}
// inode represents a filesystem object.
//
// +stateify savable
type inode struct {
// fs is the owning filesystem. fs is immutable.
fs *filesystem
// inoKey is used to identify this inode.
inoKey inoKey
// rdevMajor and rdevMinor are the device numbers reported by the
// filesystem for device special files. They are only used to dispatch
// opens of device special files to the sentry's device registry. Immutable.
rdevMajor uint32
rdevMinor uint32
// Cached metadata; protected by metadataMu.
// To access:
// - In situations where consistency is not required (like stat), these
// can be accessed using atomic operations only (without locking).
// - Lock metadataMu and can access without atomic operations.
// To mutate:
// - Lock metadataMu and use atomic operations to update because we might
// have atomic readers that don't hold the lock.
metadataMu sync.Mutex `state:"nosave"`
ino uint64 // immutable
mode atomicbitops.Uint32 // type is immutable, perms are mutable
uid atomicbitops.Uint32 // auth.KUID, but stored as raw uint32 for sync/atomic
gid atomicbitops.Uint32 // auth.KGID, but ...
blockSize atomicbitops.Uint32 // 0 if unknown
// Timestamps, all nsecs from the Unix epoch.
atime atomicbitops.Int64
mtime atomicbitops.Int64
ctime atomicbitops.Int64
btime atomicbitops.Int64
// btimeValid is true if btime contains a valid value reported by the
// underlying filesystem. statx(STATX_BTIME) on filesystems that don't
// track creation time (e.g. NFSv4) won't set STATX_BTIME in the result
// mask, so we cache that information here to avoid exposing a zero btime.
btimeValid atomicbitops.Bool
// File size, which differs from other metadata in two ways:
//
// - We make a best-effort attempt to keep it up to date even if
// !dentry.cachedMetadataAuthoritative() for the sake of O_APPEND writes.
//
// - size is protected by both metadataMu and dataMu (i.e. both must be
// locked to mutate it; locking either is sufficient to access it).
size atomicbitops.Uint64
// If this inode does not represent a synthetic file, deleted is 0, and
// atimeDirty/mtimeDirty are non-zero, atime/mtime may have diverged from the
// remote file's timestamps, which should be updated when this inode is
// evicted.
atimeDirty atomicbitops.Uint32
mtimeDirty atomicbitops.Uint32
// xattrCache caches xattrs when cachedMetadataAuthoritative() is true. It is
// protected by metadataMu.
xattrCache xattrCache `state:"nosave"`
// nlink counts the number of hard links to this inode. It's updated and
// accessed using atomic operations. It's not protected by metadataMu like the
// other metadata fields.
nlink atomicbitops.Uint32
mapsMu sync.Mutex `state:"nosave"`
// If this inode represents a regular file, mappings tracks mappings of
// the file into memmap.MappingSpaces. mappings is protected by mapsMu.
mappings memmap.MappingSet
// - If this i represents a regular file or directory, readFD (if not
// -1) is a host FD used for reads by all regularFileFDs/directoryFDs
// representing this inode.
//
// - If this inode represents a regular file, writeFD (if not -1) is a host
// FD used for writes by all regularFileFDs representing this inode.
//
// - If this inode represents a regular file, mmapFD is the host FD used
// for memory mappings. If mmapFD is -1, no such FD is available, and the
// internal page cache implementation is used for memory mappings instead.
//
// These fields are protected by handleMu. readFD, writeFD, and mmapFD are
// additionally written using atomic memory operations, allowing them to be
// read (albeit racily) with atomic.LoadInt32() without locking handleMu.
//
// readFD and writeFD may or may not be the same file descriptor. Once either
// transitions from closed (-1) to open, it may be mutated with handleMu
// locked, but cannot be closed until the inode is destroyed.
//
// readFD and writeFD may or may not be the same file descriptor. mmapFD is
// always either -1 or equal to readFD; if the file has been opened for
// writing, it is additionally either -1 or equal to writeFD.
handleMu sync.RWMutex `state:"nosave"`
readFD atomicbitops.Int32 `state:"nosave"`
writeFD atomicbitops.Int32 `state:"nosave"`
mmapFD atomicbitops.Int32 `state:"nosave"`
dataMu sync.RWMutex `state:"nosave"`
// If this inode represents a regular file that is client-cached, cache
// maps offsets into the cached file to offsets into
// filesystem.mfp.MemoryFile() that store the file's data. cache is
// protected by dataMu.
cache fsutil.FileRangeSet
// If this inode represents a regular file that is client-cached, dirty
// tracks dirty segments in cache. dirty is protected by dataMu.
dirty fsutil.DirtySet
// If this inode represents a deleted regular file, savedDeletedData is used
// to store file data for save/restore.
savedDeletedData []byte
// mmapFile implements memmap.File for mmapFD.
//
// Note that mmapFile.FD() does not necessarily match mmapFD. The latter
// can transition from a non-negative FD to -1 when a write-only FD is
// obtained for an inode previously opened read-only, but the former
// cannot. Thus, mmapFD == -1 indicates that *new* mappings of the inode
// must use the sentry page cache rather than mapping a host FD, while
// mmapFile.FD() == (read-only FD) is needed to ensure that *old*
// translations to mmapFile continue to work correctly until invalidated.
mmapFile fsutil.MmapCachedFile
// If this inode represents a symbolic link, InteropModeShared is not in
// effect, and haveTarget is true, target is the symlink target. haveTarget
// and target are protected by dataMu.
haveTarget bool
target string
// If this inode represents a socket file, endpoint is the transport
// endpoint bound to this file.
//
// endpoint often originates from vfs.MknodOptions.Endpoint, in which case
// it can't be recovered if the inode is evicted from the inode cache.
// Consequently, an extra reference is held on dentries for which endpoint
// is non-nil to prevent eviction.
endpoint transport.BoundEndpoint
// If this inode represents a synthetic named pipe, pipe is the pipe
// endpoint bound to this file.
pipe *pipe.VFSPipe
locks vfs.FileLocks
// Inotify watches for this inode.
//
// Note that inotify may behave unexpectedly in the presence of hard links,
// because dentries corresponding to the same file have separate inotify
// watches when they should share the same set. This is the case because it is
// impossible for us to know for sure whether two dentries correspond to the
// same underlying file (see the gofer filesystem section fo vfs/inotify.md for
// a more in-depth discussion on this matter).
watches vfs.Watches
// refs is the reference count of the inode. A dentry holds a reference on the inode
// it points to. rsfs is protected by fs.inodeMu.
refs inodeRefs
// impl is the specific inode implementation for non-synthetic dentries.
// impl is immutable.
//
// If impl is nil, this inode represents a synthetic file, i.e. a
// file that does not exist on the host filesystem. As of this writing, the
// only files that can be synthetic are sockets, pipes, and directories.
impl any // immutable
}
func (i *inode) init(impl any) {
i.refs.InitRefs()
i.mmapFile.SetFD(-1)
// Nested impl-inheritance pattern. In memory it looks like:
// [[ inode ] inodeImpl ]
i.impl = impl
}
// incRef increments the reference count on the inode. The ref count is used to determine
// whether the inode can be destroyed and removed from the inode cache.
func (i *inode) incRef() {
i.refs.IncRef()
}
func (i *inode) isSynthetic() bool {
return i.impl == nil
}
func (i *inode) cachedMetadataAuthoritative() bool {
return i.fs.opts.interop != InteropModeShared || i.isSynthetic()
}
func (i *inode) destroy(ctx context.Context, d *dentry) {
i.handleMu.Lock()
defer i.handleMu.Unlock()
i.dataMu.Lock()
mf := i.fs.mf
if i.isWriteHandleOk() {
// Write dirty pages back to the remote filesystem.
h := i.writeHandle()
if err := fsutil.SyncDirtyAll(ctx, &i.cache, &i.dirty, i.size.Load(), mf, h.writeFromBlocksAt); err != nil {
log.Warningf("gofer.dentry.destroyLocked: failed to write dirty data back: %v", err)
}
}
// Discard cached data.
if !i.cache.IsEmpty() {
mf.MarkAllUnevictable(i)
i.cache.DropAll(mf)
i.dirty.RemoveAllAndAccount()
}
i.dataMu.Unlock()
// Close any resources held by the implementation.
i.destroyImpl(ctx, d)
// Close FDs, other than what is owned by i.mmapFile.
// Can use RacyLoad() because handleMu is locked.
realMmapFD := i.mmapFile.FD()
readFD := int(i.readFD.RacyLoad())
if readFD >= 0 && readFD != realMmapFD {
_ = unix.Close(int(readFD))
}
if writeFD := int(i.writeFD.RacyLoad()); writeFD >= 0 && readFD != writeFD && writeFD != realMmapFD {
_ = unix.Close(writeFD)
}
i.readFD.Store(-1)
i.writeFD.Store(-1)
i.mmapFD.Store(-1)
i.mmapFile.MappableRelease() // eventually closes realMmapFD
}
// dentry implements vfs.DentryImpl.
//
// +stateify savable
type dentry struct {
vfsd vfs.Dentry
// refs is the reference count. Each dentry holds a reference on its
// parent, even if disowned. An additional reference is held on all
// synthetic dentries, and all dentries for which endpoint is non-nil,
// until they are unlinked or invalidated. (Only a single additional
// reference is held on synthetic dentries that also have endpoint != nil.)
// When refs reaches 0, the dentry may be added to the cache or destroyed.
// If refs == -1, the dentry has already been destroyed. refs is accessed
// using atomic memory operations.
refs atomicbitops.Int64
// parent is this dentry's parent directory. Each dentry holds a reference
// on its parent. If this dentry is a filesystem root, parent is nil.
// parent is protected by filesystem.renameMu.
parent atomic.Pointer[dentry] `state:".(*dentry)"`
// name is the name of this dentry in its parent. If this dentry is a
// filesystem root, name is the empty string. name is protected by
// filesystem.renameMu.
name string
// inode is the inode represented by this dentry. Multiple Dentries may
// share a single non-directory inode (with hard links). inode is
// immutable.
inode *inode
// If deleted is non-zero, the file represented by this dentry has been
// deleted. It is accessed using atomic memory operations.
deleted atomicbitops.Uint32
// cachingMu is used to synchronize concurrent dentry caching attempts on
// this dentry.
cachingMu sync.Mutex `state:"nosave"`
// If cached is true, this dentry is part of filesystem.dentryCache. cached
// is protected by cachingMu.
cached bool
// cacheEntry links dentry into filesystem.dentryCache.dentries. It is
// protected by filesystem.dentryCache.mu.
cacheEntry dentryListElem
// syncableListEntry links dentry into filesystem.syncableDentries. It is
// protected by filesystem.syncMu.
syncableListEntry dentryListElem
// opMu synchronizes operations on this dentry. Operations that mutate
// the dentry tree must hold this lock for writing. Operations that
// only read the tree must hold for reading.
opMu sync.RWMutex `state:"nosave"`
// childrenMu protects the cached children data for this dentry.
childrenMu sync.Mutex `state:"nosave"`
// If this dentry represents a directory, children contains:
//
// - Mappings of child filenames to dentries representing those children.
//
// - Mappings of child filenames that are known not to exist to nil
// dentries (only if InteropModeShared is not in effect and the directory
// is not synthetic).
//
// +checklocks:childrenMu
children map[string]*dentry
// If this dentry represents a directory, negativeChildrenCache cache
// names of negative children. negativeChildrenCache is not saved since
// dentry.prepareSaveRecursive() drops all negative children.
//
// +checklocks:childrenMu
negativeChildrenCache stringFixedCache `state:"nosave"`
// If this dentry represents a directory, negativeChildren is the number of
// negative children cached in dentry.children. negativeChildren is not
// saved since dentry.prepareSaveRecursive() drops all negative children.
//
// +checklocks:childrenMu
negativeChildren int `state:"nosave"`
// If this dentry represents a directory, syntheticChildren is the number
// of child dentries for which dentry.isSynthetic() == true.
//
// +checklocks:childrenMu
syntheticChildren int
// If this dentry represents a directory,
// dentry.inode.cachedMetadataAuthoritative() == true, and dirents is not
// nil, then dirents is a cache of all entries in the directory, in the
// order they were returned by the server. childrenSet just stores the
// `Name` field of all dirents in a set for fast query. dirents and
// childrenSet share the same lifecycle.
//
// +checklocks:childrenMu
dirents []vfs.Dirent `state:"nosave"`
// +checklocks:childrenMu
childrenSet map[string]struct{} `state:"nosave"`
// forMountpoint marks directories that were created for mount points during
// container startup. This is used during restore, in case these mount points
// need to be recreated.
forMountpoint bool
}
// +stateify savable
type stringListElem struct {
// str is the string that this elem represents.
str string
stringEntry
}
// +stateify savable
type dentryListElem struct {
// d is the dentry that this elem represents.
d *dentry
dentryEntry
}
func (fs *filesystem) inoFromKey(key inoKey) uint64 {
fs.inoMu.Lock()
defer fs.inoMu.Unlock()
if ino, ok := fs.inoByKey[key]; ok {
return ino
}
ino := fs.nextIno()
fs.inoByKey[key] = ino
return ino
}
func (fs *filesystem) nextIno() uint64 {
return fs.lastIno.Add(1)
}
// init must be called before first use of d.
func (d *dentry) init() {
d.cacheEntry.d = d
d.syncableListEntry.d = d
d.vfsd.Init(d)
refs.Register(d)
}
// Preconditions: !d.inode.isSynthetic().
// Preconditions: d.inode.metadataMu is locked.
// +checklocks:d.inode.metadataMu
func (d *dentry) refreshSizeLocked(ctx context.Context) error {
d.inode.handleMu.RLock()
// Can use RacyLoad() because handleMu is locked.
if d.inode.writeFD.RacyLoad() < 0 {
d.inode.handleMu.RUnlock()
// Use a suitable FD if we don't have a writable host FD.
return d.inode.updateMetadataLocked(ctx, noHandle)
}
// Using statx(2) with a minimal mask is faster than fstat(2).
var stat unix.Statx_t
// Can use RacyLoad() because handleMu is locked.
err := unix.Statx(int(d.inode.writeFD.RacyLoad()), "", unix.AT_EMPTY_PATH, unix.STATX_SIZE, &stat)
d.inode.handleMu.RUnlock() // must be released before updateSizeLocked()
if err != nil {
return err
}
d.inode.updateSizeLocked(stat.Size)
return nil
}
// Preconditions: !i.isSynthetic().
func (i *inode) updateMetadata(ctx context.Context) error {
// d.inode.metadataMu must be locked *before* we stat so that we do not end up
// updating stale attributes in d.updateMetadataFromStatxLocked().
i.metadataMu.Lock()
defer i.metadataMu.Unlock()
return i.updateMetadataLocked(ctx, noHandle)
}
func (i *inode) fileType() uint32 {
return i.mode.Load() & linux.S_IFMT
}
func (d *dentry) statTo(stat *linux.Statx) {
stat.Mask = linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_NLINK | linux.STATX_UID | linux.STATX_GID | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME | linux.STATX_INO | linux.STATX_SIZE | linux.STATX_BLOCKS
stat.Blksize = d.inode.blockSize.Load()
stat.Nlink = d.inode.nlink.Load()
if stat.Nlink == 0 {
// The remote filesystem doesn't support link count; just make
// something up. This is consistent with Linux, where
// fs/inode.c:inode_init_always() initializes link count to 1, and
// fs/9p/vfs_inode_dotl.c:v9fs_stat2inode_dotl() doesn't touch it if
// it's not provided by the remote filesystem.
stat.Nlink = 1
}
stat.UID = d.inode.uid.Load()
stat.GID = d.inode.gid.Load()
stat.Mode = uint16(d.inode.mode.Load())
stat.Ino = uint64(d.inode.ino)
stat.Size = d.inode.size.Load()
// This is consistent with regularFileFD.Seek(), which treats regular files
// as having no holes.
stat.Blocks = (stat.Size + 511) / 512
stat.Atime = linux.NsecToStatxTimestamp(d.inode.atime.Load())
if d.inode.btimeValid.Load() {
stat.Mask |= linux.STATX_BTIME
stat.Btime = linux.NsecToStatxTimestamp(d.inode.btime.Load())
}
stat.Ctime = linux.NsecToStatxTimestamp(d.inode.ctime.Load())
stat.Mtime = linux.NsecToStatxTimestamp(d.inode.mtime.Load())
stat.DevMajor = linux.UNNAMED_MAJOR
stat.DevMinor = d.inode.fs.devMinor
stat.RdevMajor = d.inode.rdevMajor
stat.RdevMinor = d.inode.rdevMinor
}
// 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 {
return nil
}
if stat.Mask&^(linux.STATX_MODE|linux.STATX_UID|linux.STATX_GID|linux.STATX_ATIME|linux.STATX_MTIME|linux.STATX_SIZE) != 0 {
return linuxerr.EPERM
}
mode := linux.FileMode(d.inode.mode.Load())
if err := vfs.CheckSetStat(ctx, creds, opts, mode, nil, auth.KUID(d.inode.uid.Load()), auth.KGID(d.inode.gid.Load())); err != nil {
return err
}
if err := mnt.CheckBeginWrite(); err != nil {
return err
}
defer mnt.EndWrite()
if stat.Mask&linux.STATX_SIZE != 0 {
// Reject attempts to truncate files other than regular files, since
// filesystem implementations may return the wrong errno.
switch mode.FileType() {
case linux.S_IFREG:
// ok
case linux.S_IFDIR:
return linuxerr.EISDIR
default:
return linuxerr.EINVAL
}
}
var now int64
if d.inode.cachedMetadataAuthoritative() {
// Truncate updates mtime.
if stat.Mask&(linux.STATX_SIZE|linux.STATX_MTIME) == linux.STATX_SIZE {
stat.Mask |= linux.STATX_MTIME
stat.Mtime = linux.StatxTimestamp{
Nsec: linux.UTIME_NOW,
}
}
// Use client clocks for timestamps.
now = d.inode.fs.clock.Now().Nanoseconds()
if stat.Mask&linux.STATX_ATIME != 0 && stat.Atime.Nsec == linux.UTIME_NOW {
stat.Atime = linux.NsecToStatxTimestamp(now)
}
if stat.Mask&linux.STATX_MTIME != 0 && stat.Mtime.Nsec == linux.UTIME_NOW {
stat.Mtime = linux.NsecToStatxTimestamp(now)
}
}
d.inode.metadataMu.Lock()
defer d.inode.metadataMu.Unlock()
// To avoid making the SetStat RPC if UID/GID is not actually changing.
if stat.Mask&linux.STATX_UID != 0 {
if stat.UID == d.inode.uid.RacyLoad() {
stat.Mask &^= linux.STATX_UID
}
}
if stat.Mask&linux.STATX_GID != 0 {
if stat.GID == d.inode.gid.RacyLoad() {
stat.Mask &^= linux.STATX_GID
}
}
if opts.ClearPrivs {
if stat.Mask&linux.STATX_MODE != 0 {
stat.Mode = uint16(vfs.ClearSUIDAndSGID(uint32(stat.Mode)))
} else {
oldMode := d.inode.mode.Load()
if updatedMode := vfs.ClearSUIDAndSGID(oldMode); updatedMode != oldMode {
stat.Mode = uint16(updatedMode)
stat.Mask |= linux.STATX_MODE
}
}
}
// failureMask indicates which attributes could not be set on the remote
// filesystem. p9 returns an error if any of the attributes could not be set
// but that leads to inconsistency as the server could have set a few
// attributes successfully but a later failure will cause the successful ones
// to not be updated in the dentry cache.
var failureMask uint32
var failureErr error
if !d.inode.isSynthetic() {
if stat.Mask != 0 {
if err := d.prepareSetStat(ctx, stat); err != nil {
return err
}
d.inode.handleMu.RLock()
if stat.Mask&linux.STATX_SIZE != 0 {
// d.inode.dataMu must be held around the update to both the remote
// file's size and d.inode.size to serialize with writeback (which
// might otherwise write data back up to the old d.inode.size after
// the remote file has been truncated).
d.inode.dataMu.Lock()
}
var err error
failureMask, failureErr, err = d.setStatLocked(ctx, stat)
d.inode.handleMu.RUnlock()
if err != nil {
if stat.Mask&linux.STATX_SIZE != 0 {
d.inode.dataMu.Unlock() // +checklocksforce: locked conditionally above
}
return err
}
if stat.Mask&linux.STATX_SIZE != 0 {
if failureMask&linux.STATX_SIZE == 0 {
// d.inode.size should be kept up to date, and privatized
// copy-on-write mappings of truncated pages need to be
// invalidated, even if InteropModeShared is in effect.
d.inode.updateSizeAndUnlockDataMuLocked(stat.Size) // +checklocksforce: locked conditionally above
} else {
d.inode.dataMu.Unlock() // +checklocksforce: locked conditionally above
}
}
}
if d.inode.fs.opts.interop == InteropModeShared {
// There's no point to updating d's metadata in this case since
// it'll be overwritten by revalidation before the next time it's
// used anyway. (InteropModeShared inhibits client caching of
// regular file data, so there's no cache to truncate either.)
return nil
}
}
if stat.Mask&linux.STATX_MODE != 0 && failureMask&linux.STATX_MODE == 0 {
d.inode.mode.Store(d.inode.fileType() | uint32(stat.Mode))
}
if stat.Mask&linux.STATX_UID != 0 && failureMask&linux.STATX_UID == 0 {
d.inode.uid.Store(stat.UID)
}
if stat.Mask&linux.STATX_GID != 0 && failureMask&linux.STATX_GID == 0 {
d.inode.gid.Store(stat.GID)
}
// Note that stat.Atime.Nsec and stat.Mtime.Nsec can't be UTIME_NOW because
// if d.inode.cachedMetadataAuthoritative() then we converted stat.Atime and
// stat.Mtime to client-local timestamps above, and if
// !d.inode.cachedMetadataAuthoritative() then we returned after calling
// d.file.setAttr(). For the same reason, now must have been initialized.
if stat.Mask&linux.STATX_ATIME != 0 && failureMask&linux.STATX_ATIME == 0 {
d.inode.atime.Store(stat.Atime.ToNsec())
d.inode.atimeDirty.Store(0)
}
if stat.Mask&linux.STATX_MTIME != 0 && failureMask&linux.STATX_MTIME == 0 {
d.inode.mtime.Store(stat.Mtime.ToNsec())
d.inode.mtimeDirty.Store(0)
}
d.inode.ctime.Store(now)
if failureMask != 0 {
// Setting some attribute failed on the remote filesystem.
return failureErr
}
return nil
}
// doAllocate performs an allocate operation on d. Note that d.inode.metadataMu will
// be held when allocate is called.
func (d *dentry) doAllocate(ctx context.Context, offset, length uint64, allocate func() error) error {
d.inode.metadataMu.Lock()
defer d.inode.metadataMu.Unlock()
// Allocating a smaller size is a noop.
size := offset + length
if d.inode.cachedMetadataAuthoritative() && size <= d.inode.size.RacyLoad() {
return nil
}
err := allocate()
if err != nil {
return err
}
d.inode.updateSizeLocked(size)
if d.inode.cachedMetadataAuthoritative() {
d.touchCMtimeLocked()
}
return nil
}
// Preconditions: d.inode.metadataMu must be locked.
func (i *inode) updateSizeLocked(newSize uint64) {
i.dataMu.Lock()
i.updateSizeAndUnlockDataMuLocked(newSize)
}
// Preconditions: i.metadataMu and i.dataMu must be locked.
//
// Postconditions: i.dataMu is unlocked.
// +checklocksrelease:i.dataMu
func (i *inode) updateSizeAndUnlockDataMuLocked(newSize uint64) {
oldSize := i.size.RacyLoad()
i.size.Store(newSize)
// i.dataMu must be unlocked to lock i.mapsMu and invalidate mappings
// below. This allows concurrent calls to Read/Translate/etc. These
// functions synchronize with truncation by refusing to use cache
// contents beyond the new i.size. (We are still holding i.metadataMu,
// so we can't race with Write or another truncate.)
i.dataMu.Unlock()
if newSize < oldSize {
oldpgend, _ := hostarch.PageRoundUp(oldSize)
newpgend, _ := hostarch.PageRoundUp(newSize)
if oldpgend != newpgend {
i.mapsMu.Lock()
i.mappings.Invalidate(memmap.MappableRange{Start: newpgend, End: oldpgend}, memmap.InvalidateOpts{
// Compare Linux's mm/truncate.c:truncate_setsize() =>
// truncate_pagecache() =>
// mm/memory.c:unmap_mapping_range(evencows=1).
InvalidatePrivate: true,
})
i.mapsMu.Unlock()
}
// We are now guaranteed that there are no translations of
// truncated pages, and can remove them from the cache. Since
// truncated pages have been removed from the remote file, they
// should be dropped without being written back.
i.dataMu.Lock()
i.cache.Truncate(newSize, i.fs.mf)
i.dirty.KeepClean(memmap.MappableRange{Start: newSize, End: oldpgend})
i.dataMu.Unlock()
}
}
func (d *dentry) checkPermissions(creds *auth.Credentials, ats vfs.AccessTypes) error {
return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(d.inode.mode.Load()), nil, auth.KUID(d.inode.uid.Load()), auth.KGID(d.inode.gid.Load()))
}
// Preconditions: d.inode.metadataMu must be locked.
func (d *dentry) checkXattrPermissions(creds *auth.Credentials, name string, ats vfs.AccessTypes) error {
// Deny access to the "system" namespaces since applications may expect these
// to affect kernel behavior in unimplemented ways (b/148380782).
if strings.HasPrefix(name, linux.XATTR_SYSTEM_PREFIX) {
return linuxerr.EOPNOTSUPP
}
// Disallow the "trusted" namespace because neither the gofer nor the sentry
// processes have the capability to set these. It requires CAP_SYS_ADMIN in
// the host's root user namespace.
if strings.HasPrefix(name, linux.XATTR_TRUSTED_PREFIX) {
return linuxerr.EOPNOTSUPP
}
// Do not allow writes to the "security" namespace on the host filesystem.
if ats.MayWrite() && strings.HasPrefix(name, linux.XATTR_SECURITY_PREFIX) {
return linuxerr.EOPNOTSUPP
}
// Allow all other extended attributes to be passed through to remote.
mode := linux.FileMode(d.inode.mode.RacyLoad())
kuid := auth.KUID(d.inode.uid.RacyLoad())
kgid := auth.KGID(d.inode.gid.RacyLoad())
if err := vfs.GenericCheckPermissions(creds, ats, mode, nil, kuid, kgid); err != nil {
return err
}
return vfs.CheckXattrPermissions(creds, ats, mode, kuid, name)
}
func (d *dentry) mayDelete(creds *auth.Credentials, child *dentry) error {
return vfs.CheckDeleteSticky(
creds,
linux.FileMode(d.inode.mode.Load()),
auth.KUID(d.inode.uid.Load()),
auth.KUID(child.inode.uid.Load()),
auth.KGID(child.inode.gid.Load()),
)
}
func dentryUID(uid lisafs.UID) uint32 {
if !uid.Ok() {
return uint32(auth.OverflowUID)
}
return uint32(uid)
}
func dentryGID(gid lisafs.GID) uint32 {
if !gid.Ok() {
return uint32(auth.OverflowGID)
}
return uint32(gid)
}
// IncRef implements vfs.DentryImpl.IncRef.
func (d *dentry) IncRef() {
// d.refs may be 0 if d.inode.fs.renameMu is locked, which serializes against
// d.checkCachingLocked().
r := d.refs.Add(1)
if d.LogRefs() {
refs.LogIncRef(d, r)
}
}
// TryIncRef implements vfs.DentryImpl.TryIncRef.
func (d *dentry) TryIncRef() bool {
for {
r := d.refs.Load()
if r <= 0 {
return false
}
if d.refs.CompareAndSwap(r, r+1) {
if d.LogRefs() {
refs.LogTryIncRef(d, r+1)
}
return true
}
}
}
// DecRef implements vfs.DentryImpl.DecRef.
func (d *dentry) DecRef(ctx context.Context) {
if d.decRefNoCaching() == 0 {
d.checkCachingLocked(ctx, false /* renameMuWriteLocked */)
}
}
// decRefNoCaching decrements d's reference count without calling
// d.checkCachingLocked, even if d's reference count reaches 0; callers are
// responsible for ensuring that d.checkCachingLocked will be called later.
func (d *dentry) decRefNoCaching() int64 {
r := d.refs.Add(-1)
if d.LogRefs() {
refs.LogDecRef(d, r)
}
if r < 0 {
panic("gofer.dentry.decRefNoCaching() called without holding a reference")
}
return r
}
// RefType implements refs.CheckedObject.Type.
func (d *dentry) RefType() string {
return "gofer.dentry"
}
// LeakMessage implements refs.CheckedObject.LeakMessage.
func (d *dentry) LeakMessage() string {
return fmt.Sprintf("[gofer.dentry %p] reference count of %d instead of -1", d, d.refs.Load())
}
// LogRefs implements refs.CheckedObject.LogRefs.
//
// This should only be set to true for debugging purposes, as it can generate an
// extremely large amount of output and drastically degrade performance.
func (d *dentry) LogRefs() bool {
return false
}
// InotifyWithParent implements vfs.DentryImpl.InotifyWithParent.
func (d *dentry) InotifyWithParent(ctx context.Context, events, cookie uint32, et vfs.EventType) {
if d.isDir() {
events |= linux.IN_ISDIR
}
d.inode.fs.ancestryMu.RLock()
// The ordering below is important, Linux always notifies the parent first.
if parent := d.parent.Load(); parent != nil {
parent.inode.watches.Notify(ctx, d.name, events, cookie, et, d.isDeleted())
}
d.inode.watches.Notify(ctx, "", events, cookie, et, d.isDeleted())
d.inode.fs.ancestryMu.RUnlock()
}
// Watches implements vfs.DentryImpl.Watches.
func (d *dentry) Watches() *vfs.Watches {
return &d.inode.watches
}
// OnZeroWatches implements vfs.DentryImpl.OnZeroWatches.
//
// If no watches are left on this dentry and it has no references, cache it.
func (d *dentry) OnZeroWatches(ctx context.Context) {
d.checkCachingLocked(ctx, false /* renameMuWriteLocked */)
}
// checkCachingLocked should be called after d's reference count becomes 0 or
// it becomes disowned.
//
// For performance, checkCachingLocked can also be called after d's reference
// count becomes non-zero, so that d can be removed from the LRU cache. This
// may help in reducing the size of the cache and hence reduce evictions. Note
// that this is not necessary for correctness.
//
// It may be called on a destroyed dentry. For example,
// renameMu[R]UnlockAndCheckCaching may call checkCachingLocked multiple times
// for the same dentry when the dentry is visited more than once in the same
// operation. One of the calls may destroy the dentry, so subsequent calls will
// do nothing.
//
// Preconditions: d.inode.fs.renameMu must be locked for writing if
// renameMuWriteLocked is true; it may be temporarily unlocked.
func (d *dentry) checkCachingLocked(ctx context.Context, renameMuWriteLocked bool) {
d.cachingMu.Lock()
refs := d.refs.Load()
if refs == -1 {
// Dentry has already been destroyed.
d.cachingMu.Unlock()
return
}
if refs > 0 {
// fs.dentryCache.dentries is permitted to contain dentries with non-zero
// refs, which are skipped by fs.evictCachedDentryLocked() upon reaching
// the end of the LRU. But it is still beneficial to remove d from the
// cache as we are already holding d.cachingMu. Keeping a cleaner cache
// also reduces the number of evictions (which is expensive as it acquires
// fs.renameMu).
d.removeFromCacheLocked()
d.cachingMu.Unlock()
return
}
// Deleted and invalidated dentries with zero references are no longer
// reachable by path resolution and should be dropped immediately.
if d.vfsd.IsDead() {
d.removeFromCacheLocked()
d.cachingMu.Unlock()
if !renameMuWriteLocked {
// Need to lock d.inode.fs.renameMu for writing as needed by d.destroyLocked().
d.inode.fs.renameMu.Lock()
defer d.inode.fs.renameMu.Unlock()
// Now that renameMu is locked for writing, no more refs can be taken on
// d because path resolution requires renameMu for reading at least.
if d.refs.Load() != 0 {
// Destroy d only if its ref is still 0. If not, either someone took a
// ref on it or it got destroyed before fs.renameMu could be acquired.
return
}
}
if d.isDeleted() {
d.inode.watches.HandleDeletion(ctx)
}
d.destroyLocked(ctx) // +checklocksforce: renameMu must be acquired at this point.
return
}
if d.vfsd.IsEvictable() {
d.cachingMu.Unlock()
// Attempt to evict.
if renameMuWriteLocked {
d.evictLocked(ctx) // +checklocksforce: renameMu is locked in this case.
return
}
d.evict(ctx)
return
}
// If d still has inotify watches and it is not deleted or invalidated, it
// can't be evicted. Otherwise, we will lose its watches, even if a new
// dentry is created for the same file in the future. Note that the size of
// d.inode.watches cannot concurrently transition from zero to non-zero, because
// adding a watch requires holding a reference on d.
if d.inode.watches.Size() > 0 {
// As in the refs > 0 case, removing d is beneficial.
d.removeFromCacheLocked()
d.cachingMu.Unlock()
return
}
if d.inode.fs.released.Load() != 0 {
d.cachingMu.Unlock()
if !renameMuWriteLocked {
// Need to lock d.inode.fs.renameMu to access d.parent. Lock it for writing as
// needed by d.destroyLocked() later.
d.inode.fs.renameMu.Lock()
defer d.inode.fs.renameMu.Unlock()
}
if parent := d.parent.Load(); parent != nil {
parent.childrenMu.Lock()
delete(parent.children, d.name)
parent.childrenMu.Unlock()
}
d.destroyLocked(ctx) // +checklocksforce: see above.
return
}
d.inode.fs.dentryCache.mu.Lock()
// If d is already cached, just move it to the front of the LRU.
if d.cached {
d.inode.fs.dentryCache.dentries.Remove(&d.cacheEntry)
d.inode.fs.dentryCache.dentries.PushFront(&d.cacheEntry)
d.inode.fs.dentryCache.mu.Unlock()
d.cachingMu.Unlock()
return
}
// Cache the dentry, then evict the least recently used cached dentry if
// the cache becomes over-full.
d.inode.fs.dentryCache.dentries.PushFront(&d.cacheEntry)
d.inode.fs.dentryCache.dentriesLen++
d.cached = true
shouldEvict := d.inode.fs.dentryCache.dentriesLen > d.inode.fs.dentryCache.maxCachedDentries
d.inode.fs.dentryCache.mu.Unlock()
d.cachingMu.Unlock()
if shouldEvict {
if !renameMuWriteLocked {
// Need to lock d.inode.fs.renameMu for writing as needed by
// d.evictCachedDentryLocked().
d.inode.fs.renameMu.Lock()
defer d.inode.fs.renameMu.Unlock()
}
d.inode.fs.evictCachedDentryLocked(ctx) // +checklocksforce: see above.
}
}
// Preconditions: d.cachingMu must be locked.
func (d *dentry) removeFromCacheLocked() {
if d.cached {
d.inode.fs.dentryCache.mu.Lock()
d.inode.fs.dentryCache.dentries.Remove(&d.cacheEntry)
d.inode.fs.dentryCache.dentriesLen--
d.inode.fs.dentryCache.mu.Unlock()
d.cached = false
}
}
// Precondition: fs.renameMu must be locked for writing; it may be temporarily
// unlocked.
// +checklocks:fs.renameMu
func (fs *filesystem) evictAllCachedDentriesLocked(ctx context.Context) {
for fs.dentryCache.dentriesLen != 0 {
fs.evictCachedDentryLocked(ctx)
}
}
// Preconditions:
// - fs.renameMu must be locked for writing; it may be temporarily unlocked.
//
// +checklocks:fs.renameMu
func (fs *filesystem) evictCachedDentryLocked(ctx context.Context) {
fs.dentryCache.mu.Lock()
victim := fs.dentryCache.dentries.Back()
fs.dentryCache.mu.Unlock()
if victim == nil {
// fs.dentryCache.dentries may have become empty between when it was
// checked and when we locked fs.dentryCache.mu.
return
}
if victim.d.inode.fs == fs {
victim.d.evictLocked(ctx) // +checklocksforce: owned as precondition, victim.fs == fs
return
}
// The dentry cache is shared between all gofer filesystems and the victim is
// from another filesystem. Have that filesystem do the work. We unlock
// fs.renameMu to prevent deadlock: two filesystems could otherwise wait on
// each others' renameMu.
fs.renameMu.Unlock()
defer fs.renameMu.Lock()
victim.d.evict(ctx)
}
// Preconditions:
// - d.inode.fs.renameMu must not be locked for writing.
func (d *dentry) evict(ctx context.Context) {
d.inode.fs.renameMu.Lock()
defer d.inode.fs.renameMu.Unlock()
d.evictLocked(ctx)
}
// Preconditions:
// - d.inode.fs.renameMu must be locked for writing; it may be temporarily unlocked.
//
// +checklocks:d.inode.fs.renameMu
func (d *dentry) evictLocked(ctx context.Context) {
d.cachingMu.Lock()
d.removeFromCacheLocked()
// d.refs or d.inode.watches.Size() may have become non-zero from an earlier path
// resolution since it was inserted into fs.dentryCache.dentries.
if d.refs.Load() != 0 || d.inode.watches.Size() != 0 {
d.cachingMu.Unlock()
return
}
if parent := d.parent.Load(); parent != nil {
parent.opMu.Lock()
if !d.vfsd.IsDead() {
// Note that d can't be a mount point (in any mount namespace), since VFS
// holds references on mount points.
rcs := d.inode.fs.vfsfs.VirtualFilesystem().InvalidateDentry(ctx, &d.vfsd)
for _, rc := range rcs {
rc.DecRef(ctx)
}
parent.childrenMu.Lock()
delete(parent.children, d.name)
parent.childrenMu.Unlock()
// We're only deleting the dentry, not the file it
// represents, so we don't need to update
// victim parent.dirents etc.
}
parent.opMu.Unlock()
}
// Safe to unlock cachingMu now that d.vfsd.IsDead(). Henceforth any
// concurrent caching attempts on d will attempt to destroy it and so will
// try to acquire fs.renameMu (which we have already acquiredd). Hence,
// fs.renameMu will synchronize the destroy attempts.
d.cachingMu.Unlock()
d.destroyLocked(ctx) // +checklocksforce: owned as precondition.
}
// destroyDisconnected destroys an uncached, unparented dentry. There are no
// locking preconditions.
func (d *dentry) destroyDisconnected(ctx context.Context, destroyInode bool) {
if destroyInode {
d.inode.destroy(ctx, d)
}
if !d.inode.isSynthetic() {
// Note that it's possible that d.inode.atimeDirty or d.inode.mtimeDirty are true,
// i.e. client and server timestamps may differ (because e.g. a client
// write was serviced by the page cache, and only written back to the
// remote file later). Ideally, we'd write client timestamps back to
// the remote filesystem so that timestamps for a new dentry
// instantiated for the same file would remain coherent. Unfortunately,
// this turns out to be too expensive in many cases, so for now we
// don't do this.
// Remove d from the set of syncable dentries.
d.inode.fs.syncMu.Lock()
d.inode.fs.syncableDentries.Remove(&d.syncableListEntry)
d.inode.fs.syncMu.Unlock()
}
// Drop references and stop tracking this child.
d.refs.Store(-1)
refs.Unregister(d)
}
// destroyLocked destroys the dentry.
//
// Preconditions:
// - d.inode.fs.renameMu must be locked for writing; it may be temporarily unlocked.
// - d.refs == 0.
// - d.parent.children[d.name] != d, i.e. d is not reachable by path traversal
// from its former parent dentry.
//
// +checklocks:d.inode.fs.renameMu
func (d *dentry) destroyLocked(ctx context.Context) {
switch d.refs.Load() {
case 0:
// Mark the dentry destroyed.
d.refs.Store(-1)
case -1:
panic("dentry.destroyLocked() called on already destroyed dentry")
default:
panic("dentry.destroyLocked() called with references on the dentry")
}
destroyInode := false
// Remove the inode from the inode map.
d.inode.fs.inodeMu.Lock()
d.inode.refs.DecRef(func() {
destroyInode = true
if !d.isDir() {
// Only non-directory inodes are cached in inodeByKey.
delete(d.inode.fs.inodeByKey, d.inode.inoKey)
}
})
d.inode.fs.inodeMu.Unlock()
// Allow the following to proceed without renameMu locked to improve
// scalability.
d.inode.fs.renameMu.Unlock()
// No locks need to be held during destoryDisconnected.
d.destroyDisconnected(ctx, destroyInode)
d.inode.fs.renameMu.Lock()
// Drop the reference held by d on its parent without recursively locking
// d.inode.fs.renameMu.
if parent := d.parent.Load(); parent != nil && parent.decRefNoCaching() == 0 {
parent.checkCachingLocked(ctx, true /* renameMuWriteLocked */)
}
}
func (d *dentry) isDeleted() bool {
return d.deleted.Load() != 0
}
func (d *dentry) setDeleted() {
d.deleted.Store(1)
}
func (d *dentry) listXattr(ctx context.Context, size uint64) ([]string, error) {
if d.inode.isSynthetic() {
return nil, nil
}
d.inode.metadataMu.Lock()
defer d.inode.metadataMu.Unlock()
if d.inode.cachedMetadataAuthoritative() {
if names, cached := d.inode.xattrCache.getList(); cached {
return names, nil
}
}
names, err := d.listXattrImpl(ctx, size)
if err != nil {
return nil, err
}
if d.inode.cachedMetadataAuthoritative() {
d.inode.xattrCache.setList(names)
}
return names, nil
}
func (d *dentry) getXattr(ctx context.Context, creds *auth.Credentials, opts *vfs.GetXattrOptions) (string, error) {
if d.inode.isSynthetic() {
return "", linuxerr.ENODATA
}
d.inode.metadataMu.Lock()
defer d.inode.metadataMu.Unlock()
if err := d.checkXattrPermissions(creds, opts.Name, vfs.MayRead); err != nil {
return "", err
}
if d.inode.cachedMetadataAuthoritative() {
if val, negative, ok := d.inode.xattrCache.get(opts.Name); ok {
if negative {
return "", linuxerr.ENODATA
}
return val, nil
}
}
val, err := d.getXattrImpl(ctx, opts)
if err != nil {
if d.inode.cachedMetadataAuthoritative() && linuxerr.Equals(linuxerr.ENODATA, err) {
d.inode.xattrCache.addNegative(opts.Name)
}
return "", err
}
if d.inode.cachedMetadataAuthoritative() {
d.inode.xattrCache.add(opts.Name, val)
}
return val, nil
}
func (d *dentry) setXattr(ctx context.Context, creds *auth.Credentials, opts *vfs.SetXattrOptions) error {
if d.inode.isSynthetic() {
return linuxerr.EPERM
}
d.inode.metadataMu.Lock()
defer d.inode.metadataMu.Unlock()
if err := d.checkXattrPermissions(creds, opts.Name, vfs.MayWrite); err != nil {
return err
}
if err := d.setXattrImpl(ctx, opts); err != nil {
return err
}
if d.inode.cachedMetadataAuthoritative() {
d.inode.xattrCache.add(opts.Name, opts.Value)
}
return nil
}
func (d *dentry) removeXattr(ctx context.Context, creds *auth.Credentials, name string) error {
if d.inode.isSynthetic() {
return linuxerr.EPERM
}
d.inode.metadataMu.Lock()
defer d.inode.metadataMu.Unlock()
if err := d.checkXattrPermissions(creds, name, vfs.MayWrite); err != nil {
return err
}
if err := d.removeXattrImpl(ctx, name); err != nil {
return err
}
if d.inode.cachedMetadataAuthoritative() {
d.inode.xattrCache.addNegative(name)
}
return nil
}
// Preconditions:
// - !d.inode.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).
d.inode.handleMu.Lock()
needNewHandle := (read && !d.inode.isReadHandleOk()) || (write && !d.inode.isWriteHandleOk()) || trunc
if !needNewHandle {
d.inode.handleMu.Unlock()
// Current handles are sufficient.
return nil
}
var fdsToCloseArr [2]int32
fdsToClose := fdsToCloseArr[:0]
invalidateTranslations := false
// Get a new handle. If this file has been opened for both reading and
// writing, try to get a single handle that is usable for both:
//
// - Writable memory mappings of a host FD require that the host FD is
// opened for both reading and writing.
// - NOTE(b/141991141): Some filesystems may not ensure coherence
// between multiple handles for the same file.
openReadable := d.inode.isReadHandleOk() || read
openWritable := d.inode.isWriteHandleOk() || write
h, err := d.openHandle(ctx, openReadable, openWritable, trunc)
if linuxerr.Equals(linuxerr.EACCES, err) && (openReadable != read || openWritable != write) {
// It may not be possible to use a single handle for both
// reading and writing, since permissions on the file may have
// changed to e.g. disallow reading after previously being
// opened for reading. In this case, we have no choice but to
// use separate handles for reading and writing.
ctx.Debugf("gofer.dentry.ensureSharedHandle: bifurcating read/write handles for dentry %p", d)
openReadable = read
openWritable = write
h, err = d.openHandle(ctx, openReadable, openWritable, trunc)
}
if err != nil {
d.inode.handleMu.Unlock()
return err
}
// Update d.inode.readFD and d.inode.writeFD
if h.fd >= 0 {
if openReadable && openWritable && (d.inode.readFD.RacyLoad() < 0 || d.inode.writeFD.RacyLoad() < 0 || d.inode.readFD.RacyLoad() != d.inode.writeFD.RacyLoad()) {
// Replace existing FDs with this one.
if d.inode.readFD.RacyLoad() >= 0 {
// We already have a readable FD that may be in use by
// concurrent callers of d.inode.mmapFile.FD(). Use dup3 to
// make the old file descriptor refer to the new file
// description, then close the new file descriptor (which is no
// longer needed).
if err := unix.Dup3(int(h.fd), int(d.inode.readFD.RacyLoad()), unix.O_CLOEXEC); err != nil {
oldFD := d.inode.readFD.RacyLoad()
d.inode.handleMu.Unlock()
ctx.Warningf("gofer.dentry.ensureSharedHandle: failed to dup fd %d to fd %d: %v", h.fd, oldFD, err)
h.close(ctx)
return err
}
// If overlayfsStaleRead is in effect, then the new FD may not
// be coherent with the existing one, so we have no choice but
// to switch to mappings of the new FD in both the application
// and sentry. Otherwise, racing callers of
// d.inode.mmapFile.FD() may use the old or new file
// description, but this doesn't matter since they refer to the
// same file, and any racing mappings must be read-only. Thus
// we can avoid invalidating existing translations, which is
// expensive.
if d.inode.fs.opts.overlayfsStaleRead {
if err := d.inode.mmapFile.RegenerateMappings(); err != nil {
d.inode.handleMu.Unlock()
ctx.Warningf("gofer.dentry.ensureSharedHandle: failed to replace sentry mappings of old FD with mappings of new FD: %v", err)
h.close(ctx)
return err
}
invalidateTranslations = true
}
fdsToClose = append(fdsToClose, h.fd)
h.fd = d.inode.readFD.RacyLoad()
} else {
d.inode.readFD.Store(h.fd)
}
if oldWriteFD := d.inode.writeFD.RacyLoad(); oldWriteFD != h.fd {
if oldWriteFD >= 0 {
fdsToClose = append(fdsToClose, oldWriteFD)
}
d.inode.writeFD.Store(h.fd)
}
// d.inode.mmapFD may be negative, or equal to the old value of
// d.inode.readFD. (It cannot be equal to the old value of
// d.inode.writeFD unless that old value is negative, since
// d.inode.mmapFD can't be set to a write-only FD.) In the latter
// case, we already dup3'd h.fd over d.inode.readFD. So we only
// need to handle the former case here.
if d.inode.mmapFD.RacyLoad() < 0 {
d.inode.mmapFD.Store(h.fd)
d.inode.mmapFile.SetFD(int(h.fd))
}
} else if openReadable && d.inode.readFD.RacyLoad() < 0 {
readHandleWasOk := d.inode.isReadHandleOk()
d.inode.readFD.Store(h.fd)
// If the file has not been opened for writing, the new FD may
// be used for read-only memory mappings. If the file was
// previously opened for reading (without an FD), then existing
// translations of the file may use the internal page cache;
// invalidate those mappings.
if !d.inode.isWriteHandleOk() {
invalidateTranslations = readHandleWasOk
d.inode.mmapFD.Store(h.fd)
d.inode.mmapFile.SetFD(int(h.fd))
}
} else if openWritable && d.inode.writeFD.RacyLoad() < 0 {
d.inode.writeFD.Store(h.fd)
if d.inode.readFD.RacyLoad() >= 0 {
// We have an existing read-only FD, but the file has just
// been opened for writing, so we need to start supporting
// writable memory mappings. However, the new FD is not
// readable, so we have no FD that can be used to create
// writable memory mappings. Switch to using the internal
// page cache.
invalidateTranslations = true
d.inode.mmapFD.Store(-1)
}
} else {
// The new FD is not useful.
fdsToClose = append(fdsToClose, h.fd)
}
} else if openWritable && d.inode.writeFD.RacyLoad() < 0 && d.inode.mmapFD.RacyLoad() >= 0 {
// We have an existing read-only FD, but the file has just been
// opened for writing, so we need to start supporting writable
// memory mappings. However, we have no writable host FD. Switch to
// using the internal page cache.
invalidateTranslations = true
d.inode.mmapFD.Store(-1)
}
d.inode.updateHandles(ctx, h, openReadable, openWritable)
d.inode.handleMu.Unlock()
if invalidateTranslations {
// Invalidate application mappings that may be using an old FD; they
// will be replaced with mappings using the new FD after future calls
// to d.Translate(). This requires holding d.inode.mapsMu, which precedes
// d.inode.handleMu in the lock order.
d.inode.mapsMu.Lock()
d.inode.mappings.InvalidateAll(memmap.InvalidateOpts{})
d.inode.mapsMu.Unlock()
}
for _, fd := range fdsToClose {
unix.Close(int(fd))
}
return nil
}
func (d *dentry) syncRemoteFile(ctx context.Context, dataOnly bool) error {
d.inode.handleMu.RLock()
defer d.inode.handleMu.RUnlock()
return d.syncRemoteFileLocked(ctx, dataOnly)
}
// Preconditions: d.inode.handleMu must be locked.
func (d *dentry) syncRemoteFileLocked(ctx context.Context, dataOnly bool) error {
// Prefer syncing write handles over read handles, since some remote
// filesystem implementations may not sync changes made through write
// handles otherwise.
wh := d.inode.writeHandle()
if dataOnly {
wh.syncData(ctx)
} else {
wh.sync(ctx)
}
rh := d.inode.readHandle()
if dataOnly {
rh.syncData(ctx)
} else {
rh.sync(ctx)
}
return nil
}
func (d *dentry) syncCachedFile(ctx context.Context, forFilesystemSync bool, dataOnly bool) error {
d.inode.handleMu.RLock()
defer d.inode.handleMu.RUnlock()
if d.inode.isWriteHandleOk() {
// Write back dirty pages to the remote file.
d.inode.dataMu.Lock()
h := d.inode.writeHandle()
err := fsutil.SyncDirtyAll(ctx, &d.inode.cache, &d.inode.dirty, d.inode.size.Load(), d.inode.fs.mf, h.writeFromBlocksAt)
d.inode.dataMu.Unlock()
if err != nil {
return err
}
}
if err := d.syncRemoteFileLocked(ctx, dataOnly); err != nil {
if !forFilesystemSync {
return err
}
// Only return err if we can reasonably have expected sync to succeed
// (d is a regular file and was opened for writing).
if d.inode.isRegularFile() && d.inode.isWriteHandleOk() {
return err
}
ctx.Debugf("gofer.dentry.syncCachedFile: syncing non-writable or non-regular-file dentry failed: %v", err)
}
return nil
}
// incLinks increments link count.
func (d *dentry) incLinks() {
if d.inode.nlink.Load() == 0 {
// The remote filesystem doesn't support link count.
return
}
d.inode.nlink.Add(1)
}
// decLinks decrements link count.
func (d *dentry) decLinks() {
if d.inode.nlink.Load() == 0 {
// The remote filesystem doesn't support link count.
return
}
d.inode.nlink.Add(^uint32(0))
}
// fileDescription is embedded by gofer implementations of
// vfs.FileDescriptionImpl.
//
// +stateify savable
type fileDescription struct {
vfsfd vfs.FileDescription
vfs.FileDescriptionDefaultImpl
vfs.LockFD
lockLogging sync.Once `state:"nosave"`
}
func (fd *fileDescription) filesystem() *filesystem {
return fd.vfsfd.Mount().Filesystem().Impl().(*filesystem)
}
func (fd *fileDescription) dentry() *dentry {
return fd.vfsfd.Dentry().Impl().(*dentry)
}
// Stat implements vfs.FileDescriptionImpl.Stat.
func (fd *fileDescription) Stat(ctx context.Context, opts vfs.StatOptions) (linux.Statx, error) {
d := fd.dentry()
const validMask = uint32(linux.STATX_MODE | linux.STATX_UID | linux.STATX_GID | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME | linux.STATX_SIZE | linux.STATX_BLOCKS | linux.STATX_BTIME)
if !d.inode.cachedMetadataAuthoritative() && opts.Mask&validMask != 0 && opts.Sync != linux.AT_STATX_DONT_SYNC {
// Use specialFileFD.handle.fileLisa for the Stat if available, for the
// same reason that we try to use open FD in updateMetadataLocked().
var err error
if sffd, ok := fd.vfsfd.Impl().(*specialFileFD); ok {
err = sffd.updateMetadata(ctx)
} else {
err = d.inode.updateMetadata(ctx)
}
if err != nil {
return linux.Statx{}, err
}
}
var stat linux.Statx
d.statTo(&stat)
return stat, nil
}
// 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())
}
// ListXattr implements vfs.FileDescriptionImpl.ListXattr.
func (fd *fileDescription) ListXattr(ctx context.Context, size uint64) ([]string, error) {
return fd.dentry().listXattr(ctx, size)
}
// GetXattr implements vfs.FileDescriptionImpl.GetXattr.
func (fd *fileDescription) GetXattr(ctx context.Context, opts vfs.GetXattrOptions) (string, error) {
return fd.dentry().getXattr(ctx, auth.CredentialsFromContext(ctx), &opts)
}
// SetXattr implements vfs.FileDescriptionImpl.SetXattr.
func (fd *fileDescription) SetXattr(ctx context.Context, opts vfs.SetXattrOptions) error {
return fd.dentry().setXattr(ctx, auth.CredentialsFromContext(ctx), &opts)
}
// RemoveXattr implements vfs.FileDescriptionImpl.RemoveXattr.
func (fd *fileDescription) RemoveXattr(ctx context.Context, name string) error {
return fd.dentry().removeXattr(ctx, auth.CredentialsFromContext(ctx), name)
}
// LockBSD implements vfs.FileDescriptionImpl.LockBSD.
func (fd *fileDescription) LockBSD(ctx context.Context, uid fslock.UniqueID, ownerPID int32, t fslock.LockType, block bool) error {
fd.lockLogging.Do(func() {
log.Infof("File lock using gofer file handled internally.")
})
return fd.LockFD.LockBSD(ctx, uid, ownerPID, t, block)
}
// LockPOSIX implements vfs.FileDescriptionImpl.LockPOSIX.
func (fd *fileDescription) LockPOSIX(ctx context.Context, uid fslock.UniqueID, ownerPID int32, t fslock.LockType, r fslock.LockRange, block bool) error {
fd.lockLogging.Do(func() {
log.Infof("Range lock using gofer file handled internally.")
})
return fd.Locks().LockPOSIX(ctx, uid, ownerPID, t, r, block)
}
// UnlockPOSIX implements vfs.FileDescriptionImpl.UnlockPOSIX.
func (fd *fileDescription) UnlockPOSIX(ctx context.Context, uid fslock.UniqueID, r fslock.LockRange) error {
return fd.Locks().UnlockPOSIX(ctx, uid, r)
}
// resolvingPath is just a wrapper around *vfs.ResolvingPath. It additionally
// holds some information around the intent behind resolving the path.
type resolvingPath struct {
*vfs.ResolvingPath
// excludeLast indicates whether the intent is to resolve until the last path
// component. If true, the last path component should remain unresolved.
excludeLast bool
}
func resolvingPathFull(rp *vfs.ResolvingPath) resolvingPath {
return resolvingPath{ResolvingPath: rp, excludeLast: false}
}
func resolvingPathParent(rp *vfs.ResolvingPath) resolvingPath {
return resolvingPath{ResolvingPath: rp, excludeLast: true}
}
func (rp *resolvingPath) done() bool {
if rp.excludeLast {
return rp.Final()
}
return rp.Done()
}
func (rp *resolvingPath) copy() resolvingPath {
return resolvingPath{
ResolvingPath: rp.ResolvingPath.Copy(),
excludeLast: rp.excludeLast,
}
}
// Precondition: !rp.done() && rp.Component() is not "." or "..".
func (rp *resolvingPath) getComponents(emit func(string) bool) {
rp.GetComponents(rp.excludeLast, emit)
}