mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
A new runsc flag controls the dispatch with the default being to use sentry implementations alone. PiperOrigin-RevId: 966266713
2460 lines
83 KiB
Go
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)
|
|
}
|