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

755 lines
26 KiB
Go

// Copyright 2022 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gofer
import (
"fmt"
"strings"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/atomicbitops"
"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/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/socket/unix/transport"
"gvisor.dev/gvisor/pkg/sentry/vfs"
)
func (fs *filesystem) handleAnameLisafs(ctx context.Context, rootInode lisafs.Inode) (lisafs.Inode, error) {
if fs.opts.aname == "/" {
return rootInode, nil
}
// Walk to the attach point from root inode. aname is always absolute.
rootFD := fs.client.NewFD(rootInode.ControlFD)
status, inodes, err := rootFD.WalkMultiple(ctx, strings.Split(fs.opts.aname, "/")[1:])
if err != nil {
return lisafs.Inode{}, err
}
// Close all intermediate FDs to the attach point.
rootFD.Close(ctx, false /* flush */)
numInodes := len(inodes)
for i := 0; i < numInodes-1; i++ {
curFD := fs.client.NewFD(inodes[i].ControlFD)
curFD.Close(ctx, false /* flush */)
}
switch status {
case lisafs.WalkSuccess:
return inodes[numInodes-1], nil
default:
if numInodes > 0 {
last := fs.client.NewFD(inodes[numInodes-1].ControlFD)
last.Close(ctx, false /* flush */)
}
log.Warningf("initClient failed because walk to attach point %q failed: lisafs.WalkStatus = %v", fs.opts.aname, status)
return lisafs.Inode{}, linuxerr.ENOENT
}
}
// lisafsInode is a gofer inode implementation. It represents a inode backed
// by a lisafs connection.
//
// +stateify savable
type lisafsInode struct {
inode
// controlFD is used by lisafs to perform path based operations on this
// dentry. controlFD is immutable.
//
// if !controlFD.Ok(), this dentry represents a synthetic file, i.e. a
// file that does not exist on the remote filesystem. As of this writing, the
// only files that can be synthetic are sockets, pipes, and directories.
controlFD lisafs.ClientFD `state:"nosave"`
// If this dentry represents a regular file or directory, readFDLisa is a
// LISAFS FD used for reads by all regularFileFDs/directoryFDs representing
// this dentry. readFDLisa is protected by dentry.handleMu.
readFDLisa lisafs.ClientFD `state:"nosave"`
// If this dentry represents a regular file, writeFDLisa is the LISAFS FD
// used for writes by all regularFileFDs representing this dentry.
// readFDLisa and writeFDLisa may or may not represent the same LISAFS FD.
// Once either transitions from closed (Ok() == false) to open
// (Ok() == true), it may be mutated with dentry.handleMu locked, but cannot
// be closed until the dentry is destroyei. writeFDLisa is protected by
// dentry.handleMu.
writeFDLisa lisafs.ClientFD `state:"nosave"`
}
// newLisafsDentry serves two purposes:
// 1. newLisafsDentry creates a new dentry representing the given file. The dentry
// initially has no references, but is not cached; it is the caller's
// responsibility to set the dentry's reference count and/or call.
// dentry.checkCachingLocked() as appropriate.
// 2. newLisafsDentry checks if there is a corresponding inode in the cache.
// If not, it creates a new inode representing the given file and takes
// ownership of controlFD.
//
// newLisafsDentry takes ownership of ino.
func (fs *filesystem) newLisafsDentry(ctx context.Context, ino *lisafs.Inode) (*dentry, error) {
if ino.Stat.Mask&linux.STATX_TYPE == 0 {
ctx.Warningf("can't create gofer.inode without file type")
fs.client.CloseFD(ctx, ino.ControlFD, false /* flush */)
return nil, linuxerr.EIO
}
if ino.Stat.Mode&linux.FileTypeMask == linux.ModeRegular && ino.Stat.Mask&linux.STATX_SIZE == 0 {
ctx.Warningf("can't create regular file gofer.dentry without file size")
fs.client.CloseFD(ctx, ino.ControlFD, false /* flush */)
return nil, linuxerr.EIO
}
isDir := ino.Stat.Mode&linux.FileTypeMask == linux.ModeDirectory
inoKey := inoKeyFromStatx(&ino.Stat)
// Common case. Performance hack which is used to allocate the dentry
// and its inode together in the heap. This will help reduce allocations and memory
// fragmentation. This is more cache friendly too.
// Obviously in case of hard link and if the inode already exists,
// we just re-use the inode and heap allocate just the dentry struct.
temp := struct {
d dentry
i lisafsInode
}{}
// Force new inode creation for directory inodes to avoid hard-linking directories.
// This also avoids a correctness issue when a directory is bind-mounted on the host:
// different paths (e.g., /mnt/ and /mnt/a/b/c if /mnt/a/b/c is a bind mount of /mnt/)
// can return the same device ID and inode number from a stat call.
temp.d.inode = fs.getOrCreateInode(inoKey /* dontCache = */, isDir,
func() { fs.client.CloseFD(ctx, ino.ControlFD, false /* flush */) },
func() *inode {
temp.i = lisafsInode{
inode: inode{
fs: fs,
inoKey: inoKey,
ino: fs.inoFromKey(inoKey),
rdevMajor: ino.Stat.RdevMajor,
rdevMinor: ino.Stat.RdevMinor,
mode: atomicbitops.FromUint32(uint32(ino.Stat.Mode)),
uid: atomicbitops.FromUint32(uint32(fs.opts.dfltuid)),
gid: atomicbitops.FromUint32(uint32(fs.opts.dfltgid)),
blockSize: atomicbitops.FromUint32(hostarch.PageSize),
readFD: atomicbitops.FromInt32(-1),
writeFD: atomicbitops.FromInt32(-1),
mmapFD: atomicbitops.FromInt32(-1),
},
controlFD: fs.client.NewFD(ino.ControlFD),
}
temp.i.inode.init(&temp.i)
inode := &temp.i.inode
if ino.Stat.Mask&linux.STATX_UID != 0 {
inode.uid = atomicbitops.FromUint32(dentryUID(lisafs.UID(ino.Stat.UID)))
}
if ino.Stat.Mask&linux.STATX_GID != 0 {
inode.gid = atomicbitops.FromUint32(dentryGID(lisafs.GID(ino.Stat.GID)))
}
if ino.Stat.Mask&linux.STATX_SIZE != 0 {
inode.size = atomicbitops.FromUint64(ino.Stat.Size)
}
if ino.Stat.Blksize != 0 {
inode.blockSize = atomicbitops.FromUint32(ino.Stat.Blksize)
}
if ino.Stat.Mask&linux.STATX_ATIME != 0 {
inode.atime = atomicbitops.FromInt64(dentryTimestamp(ino.Stat.Atime))
} else {
inode.atime = atomicbitops.FromInt64(fs.clock.Now().Nanoseconds())
}
if ino.Stat.Mask&linux.STATX_MTIME != 0 {
inode.mtime = atomicbitops.FromInt64(dentryTimestamp(ino.Stat.Mtime))
} else {
inode.mtime = atomicbitops.FromInt64(fs.clock.Now().Nanoseconds())
}
if ino.Stat.Mask&linux.STATX_CTIME != 0 {
inode.ctime = atomicbitops.FromInt64(dentryTimestamp(ino.Stat.Ctime))
} else {
// Approximate ctime with mtime if ctime isn't available.
inode.ctime = atomicbitops.FromInt64(inode.mtime.Load())
}
if ino.Stat.Mask&linux.STATX_BTIME != 0 {
inode.btime = atomicbitops.FromInt64(dentryTimestamp(ino.Stat.Btime))
inode.btimeValid = atomicbitops.FromBool(true)
}
if ino.Stat.Mask&linux.STATX_NLINK != 0 {
inode.nlink = atomicbitops.FromUint32(ino.Stat.Nlink)
} else {
if isDir {
inode.nlink = atomicbitops.FromUint32(2)
} else {
inode.nlink = atomicbitops.FromUint32(1)
}
}
return inode
})
temp.d.init()
// If this dentry adopted a cached inode that already carries a bound-socket
// endpoint (a hard link to, or re-walk of, an existing bound socket), hold
// the extra reference that dentry.refs keeps for endpoint-bearing dentries.
// The bind path takes this reference itself, over a fresh inode.
if temp.d.inode.endpoint != nil {
temp.d.IncRef()
}
fs.syncMu.Lock()
fs.syncableDentries.PushBack(&temp.d.syncableListEntry)
fs.syncMu.Unlock()
return &temp.d, nil
}
func (i *lisafsInode) openHandle(ctx context.Context, flags uint32) (handle, error) {
openFD, hostFD, err := i.controlFD.OpenAt(ctx, flags)
if err != nil {
return noHandle, err
}
return handle{
fdLisa: i.controlFD.Client().NewFD(openFD),
fd: int32(hostFD),
}, nil
}
func (i *lisafsInode) updateHandles(ctx context.Context, h handle, readable, writable bool) {
// Switch to new LISAFS FDs. Note that the read, write and mmap host FDs are
// updated separately.
oldReadFD := lisafs.InvalidFDID
if readable {
oldReadFD = i.readFDLisa.ID()
i.readFDLisa = h.fdLisa
}
oldWriteFD := lisafs.InvalidFDID
if writable {
oldWriteFD = i.writeFDLisa.ID()
i.writeFDLisa = h.fdLisa
}
// NOTE(b/141991141): Close old FDs before making new fids visible (by
// unlocking i.handleMu).
if oldReadFD.Ok() {
i.fs.client.CloseFD(ctx, oldReadFD, false /* flush */)
}
if oldWriteFD.Ok() && oldReadFD != oldWriteFD {
i.fs.client.CloseFD(ctx, oldWriteFD, false /* flush */)
}
}
// +checklocks:i.metadataMu
func (i *lisafsInode) updateMetadataLocked(ctx context.Context, h handle) error {
handleMuRLocked := false
if !h.fdLisa.Ok() {
// Use open FDs in preferenece to the control FD. This may be significantly
// more efficient in some implementations. Prefer a writable FD over a
// readable one since some filesystem implementations may update a writable
// FD's metadata after writes, without making metadata updates immediately
// visible to read-only FDs representing the same file.
i.handleMu.RLock()
switch {
case i.writeFDLisa.Ok():
h.fdLisa = i.writeFDLisa
handleMuRLocked = true
case i.readFDLisa.Ok():
h.fdLisa = i.readFDLisa
handleMuRLocked = true
default:
h.fdLisa = i.controlFD
i.handleMu.RUnlock()
}
}
var stat lisafs.Statx
err := h.fdLisa.StatTo(ctx, &stat)
if handleMuRLocked {
// handleMu must be released before updateMetadataFromStatxLocked().
i.handleMu.RUnlock() // +checklocksforce: complex case.
}
if err != nil {
return err
}
i.updateMetadataFromStatxLocked(&stat)
return nil
}
// updateMetadataFromStatxLocked is called to update d's metadata after an update
// from the remote filesystem.
// +checklocks:i.inode.metadataMu
func (i *lisafsInode) updateMetadataFromStatxLocked(stat *lisafs.Statx) {
if stat.Mask&linux.STATX_TYPE != 0 {
if got, want := stat.Mode&linux.FileTypeMask, i.inode.fileType(); uint32(got) != want {
panic(fmt.Sprintf("lisafsInode file type changed from %#o to %#o", want, got))
}
}
if stat.Mask&linux.STATX_MODE != 0 {
i.inode.mode.Store(uint32(stat.Mode))
}
if stat.Mask&linux.STATX_UID != 0 {
i.inode.uid.Store(dentryUID(lisafs.UID(stat.UID)))
}
if stat.Mask&linux.STATX_GID != 0 {
i.inode.gid.Store(dentryGID(lisafs.GID(stat.GID)))
}
if stat.Blksize != 0 {
i.inode.blockSize.Store(stat.Blksize)
}
// Don't override newer client-defined timestamps with old server-defined
// ones.
if stat.Mask&linux.STATX_ATIME != 0 && i.inode.atimeDirty.Load() == 0 {
i.inode.atime.Store(dentryTimestamp(stat.Atime))
}
if stat.Mask&linux.STATX_MTIME != 0 && i.inode.mtimeDirty.Load() == 0 {
i.inode.mtime.Store(dentryTimestamp(stat.Mtime))
}
if stat.Mask&linux.STATX_CTIME != 0 {
i.inode.ctime.Store(dentryTimestamp(stat.Ctime))
}
if stat.Mask&linux.STATX_BTIME != 0 {
i.inode.btime.Store(dentryTimestamp(stat.Btime))
i.inode.btimeValid.Store(true)
} else {
i.inode.btimeValid.Store(false)
}
if stat.Mask&linux.STATX_NLINK != 0 {
i.inode.nlink.Store(stat.Nlink)
}
if stat.Mask&linux.STATX_SIZE != 0 {
i.updateSizeLocked(stat.Size)
}
}
func chmod(ctx context.Context, controlFD lisafs.ClientFD, mode uint16) error {
setStat := linux.Statx{
Mask: linux.STATX_MODE,
Mode: mode,
}
_, failureErr, err := controlFD.SetStat(ctx, &setStat)
if err != nil {
return err
}
return failureErr
}
func (i *lisafsInode) destroy(ctx context.Context, d *dentry) {
if i.readFDLisa.Ok() && i.readFDLisa.ID() != i.writeFDLisa.ID() {
i.readFDLisa.Close(ctx, false /* flush */)
}
if i.writeFDLisa.Ok() {
i.writeFDLisa.Close(ctx, false /* flush */)
}
if i.controlFD.Ok() {
// Close the control FD.
// Propagate the Close RPCs immediately to the server if:
// - The dentry being destroyed is a deleted regular file. This is to
// release the disk space on remote immediately.
// - The filesystem is shared with external writers, who may try to delete
// files and attempt to unmount the filesystem. gVisor should not be
// caching FDs here as it would prevent external cleanup.
// This will flush the Close RPCs for read/write lisa FDs above as well.
flushClose := i.fs.opts.interop == InteropModeShared || (d.isDeleted() && i.isRegularFile())
i.controlFD.Close(ctx, flushClose)
}
}
func (i *lisafsInode) getRemoteChild(ctx context.Context, name string) (*dentry, error) {
childInode, err := i.controlFD.Walk(ctx, name)
if err != nil {
return nil, err
}
return i.fs.newLisafsDentry(ctx, &childInode)
}
// Preconditions:
// - fs.renameMu must be locked.
// - i.opMu must be locked.
// - i.isDir().
// - !rp.done() && rp.Component() is not "." or "..".
//
// Postcondition: The returned dentry is already cached appropriately.
func (i *lisafsInode) getRemoteChildAndWalkPathLocked(ctx context.Context, rp resolvingPath, ds **[]*dentry, d *dentry) (*dentry, error) {
// Collect as many path components as possible to walk.
var namesArr [16]string // arbitrarily sized array to help avoid slice allocation.
names := namesArr[:0]
rp.getComponents(func(name string) bool {
if name == "." {
return true
}
if name == ".." {
return false
}
names = append(names, name)
return true
})
// Walk as much of the path as possible in 1 RPC.
_, inodes, err := i.controlFD.WalkMultiple(ctx, names)
if err != nil {
return nil, err
}
if len(inodes) == 0 {
// i.opMu is locked. So a new child could not have appeared concurrently.
// It should be safe to mark this as a negative entry.
d.childrenMu.Lock()
defer d.childrenMu.Unlock()
d.cacheNegativeLookupLocked(names[0])
return nil, linuxerr.ENOENT
}
// Add the walked inodes into the dentry tree.
startParent := d
curParent := startParent
curParentLock := func() {
if curParent != startParent {
curParent.opMu.RLock()
}
curParent.childrenMu.Lock()
}
curParentUnlock := func() {
curParent.childrenMu.Unlock()
if curParent != startParent {
curParent.opMu.RUnlock() // +checklocksforce: locked via curParentLock().
}
}
var ret *dentry
var dentryCreationErr error
for i := range inodes {
if dentryCreationErr != nil {
d.inode.fs.client.CloseFD(ctx, inodes[i].ControlFD, false /* flush */)
continue
}
curParentLock()
// Did we race with another walk + cache operation?
child, ok := curParent.children[names[i]] // +checklocksforce: locked via curParentLock()
if ok && child != nil {
// We raced. Clean up the new inode and proceed with
// the cached child.
d.inode.fs.client.CloseFD(ctx, inodes[i].ControlFD, false /* flush */)
} else {
// Create and cache the new dentry.
var err error
child, err = d.inode.fs.newLisafsDentry(ctx, &inodes[i])
if err != nil {
dentryCreationErr = err
curParentUnlock()
continue
}
curParent.cacheNewChildLocked(child, names[i]) // +checklocksforce: locked via curParentLock().
}
curParentUnlock()
// For now, child has 0 references, so our caller should call
// child.checkCachingLocked(). curParent gained a ref so we should also
// call curParent.checkCachingLocked() so it can be removed from the cache
// if needed. We only do that for the first iteration because all
// subsequent parents would have already been added to ds.
if i == 0 {
*ds = appendDentry(*ds, curParent)
}
*ds = appendDentry(*ds, child)
curParent = child
if i == 0 {
ret = child
}
}
return ret, dentryCreationErr
}
func (i *lisafsInode) newChildDentry(ctx context.Context, childIno *lisafs.Inode, childName string) (*dentry, error) {
child, err := i.fs.newLisafsDentry(ctx, childIno)
if err != nil {
if err := i.controlFD.UnlinkAt(ctx, childName, 0 /* flags */); err != nil {
log.Warningf("failed to clean up created child %q after newDentry() failed: %v", childName, err)
}
}
return child, err
}
func (i *lisafsInode) mknod(ctx context.Context, name string, creds *auth.Credentials, opts *vfs.MknodOptions) (*dentry, error) {
if _, ok := opts.Endpoint.(transport.HostBoundEndpoint); !ok {
childInode, err := i.controlFD.MknodAt(ctx, name, opts.Mode, lisafs.UID(creds.EffectiveKUID), lisafs.GID(creds.EffectiveKGID), opts.DevMinor, opts.DevMajor)
if err != nil {
return nil, err
}
return i.newChildDentry(ctx, &childInode, name)
}
// This mknod(2) is coming from unix bind(2), as opts.Endpoint is set.
sockType := opts.Endpoint.(transport.Endpoint).Type()
childInode, boundSocketFD, err := i.controlFD.BindAt(ctx, sockType, name, opts.Mode, lisafs.UID(creds.EffectiveKUID), lisafs.GID(creds.EffectiveKGID))
if err != nil {
return nil, err
}
hbep := opts.Endpoint.(transport.HostBoundEndpoint)
if err := hbep.SetBoundSocketFD(ctx, boundSocketFD); err != nil {
if err := i.controlFD.UnlinkAt(ctx, name, 0 /* flags */); err != nil {
log.Warningf("failed to clean up socket which was created by BindAt RPC: %v", err)
}
i.fs.client.CloseFD(ctx, childInode.ControlFD, false /* flush */)
return nil, err
}
child, err := i.newChildDentry(ctx, &childInode, name)
if err != nil {
hbep.ResetBoundSocketFD(ctx)
return nil, err
}
// Set the endpoint on the newly created child dentry, and take the
// corresponding extra dentry reference.
child.inode.endpoint = opts.Endpoint
child.IncRef()
return child, nil
}
func (i *lisafsInode) link(ctx context.Context, target *lisafsInode, name string) (*dentry, error) {
linkInode, err := i.controlFD.LinkAt(ctx, target.controlFD.ID(), name)
if err != nil {
return nil, err
}
return i.newChildDentry(ctx, &linkInode, name)
}
func (i *lisafsInode) mkdir(ctx context.Context, name string, mode linux.FileMode, uid auth.KUID, gid auth.KGID, createDentry bool) (*dentry, error) {
childDirInode, err := i.controlFD.MkdirAt(ctx, name, mode, lisafs.UID(uid), lisafs.GID(gid))
if err != nil {
return nil, err
}
if !createDentry {
return nil, nil
}
return i.newChildDentry(ctx, &childDirInode, name)
}
func (i *lisafsInode) symlink(ctx context.Context, name, target string, creds *auth.Credentials) (*dentry, error) {
symlinkInode, err := i.controlFD.SymlinkAt(ctx, name, target, lisafs.UID(creds.EffectiveKUID), lisafs.GID(creds.EffectiveKGID))
if err != nil {
return nil, err
}
return i.newChildDentry(ctx, &symlinkInode, name)
}
func (i *lisafsInode) openCreate(ctx context.Context, name string, flags uint32, mode linux.FileMode, uid auth.KUID, gid auth.KGID, createDentry bool) (*dentry, handle, error) {
ino, openFD, hostFD, err := i.controlFD.OpenCreateAt(ctx, name, flags, mode, lisafs.UID(uid), lisafs.GID(gid))
if err != nil {
return nil, noHandle, err
}
h := handle{
fdLisa: i.fs.client.NewFD(openFD),
fd: int32(hostFD),
}
if !createDentry {
return nil, h, nil
}
child, err := i.fs.newLisafsDentry(ctx, &ino)
if err != nil {
h.close(ctx)
return nil, noHandle, err
}
return child, h, nil
}
// lisafsGetdentsCount is the number of bytes of dirents to read from the
// server in each Getdents RPC. This value is consistent with vfs1 client.
const lisafsGetdentsCount = int32(64 * 1024)
// Preconditions:
// - getDirents may not be called concurrently with another getDirents call.
func (i *lisafsInode) getDirentsLocked(ctx context.Context, recordDirent func(name string, key inoKey, dType uint8)) error {
// shouldSeek0 indicates whether the server should SEEK to 0 before reading
// directory entries.
shouldSeek0 := true
for {
count := lisafsGetdentsCount
if shouldSeek0 {
// See lisafs.Getdents64Req.Count.
count = -count
shouldSeek0 = false
}
dirents, err := i.readFDLisa.Getdents64(ctx, count)
if err != nil {
return err
}
if len(dirents) == 0 {
return nil
}
for i := range dirents {
name := string(dirents[i].Name)
if name == "." || name == ".." {
continue
}
recordDirent(name, inoKey{
ino: uint64(dirents[i].Ino),
devMinor: uint32(dirents[i].DevMinor),
devMajor: uint32(dirents[i].DevMajor),
}, uint8(dirents[i].Type))
}
}
}
func flush(ctx context.Context, fd lisafs.ClientFD) error {
if fd.Ok() {
return fd.Flush(ctx)
}
return nil
}
func (i *lisafsInode) statfs(ctx context.Context) (linux.Statfs, error) {
var statFS lisafs.StatFS
if err := i.controlFD.StatFSTo(ctx, &statFS); err != nil {
return linux.Statfs{}, err
}
return linux.Statfs{
BlockSize: statFS.BlockSize,
FragmentSize: statFS.BlockSize,
Blocks: statFS.Blocks,
BlocksFree: statFS.BlocksFree,
BlocksAvailable: statFS.BlocksAvailable,
Files: statFS.Files,
FilesFree: statFS.FilesFree,
NameLength: statFS.NameLength,
}, nil
}
func (i *lisafsInode) restoreInode(ctx context.Context, inode *lisafs.Inode, opts *vfs.CompleteRestoreOptions, d *dentry) error {
i.controlFD = i.fs.client.NewFD(inode.ControlFD)
// Gofers do not preserve inoKey across checkpoint/restore, so:
//
// - We must assume that the remote filesystem did not change in a way that
// would invalidate dentries, since we can't revalidate dentries by
// checking inoKey.
//
// - We need to associate the new inoKey with the existing i.ino.
i.inoKey = inoKeyFromStatx(&inode.Stat)
i.fs.inoMu.Lock()
i.fs.inoByKey[i.inoKey] = i.ino
i.fs.inoMu.Unlock()
// inodeByKey contains non-directory inodes only.
if !d.isDir() {
i.fs.inodeMu.Lock()
i.fs.inodeByKey[i.inoKey] = &i.inode
i.fs.inodeMu.Unlock()
}
// Check metadata stability before updating metadata.
i.metadataMu.Lock()
defer i.metadataMu.Unlock()
if i.isRegularFile() {
if opts.ValidateFileSizes {
if inode.Stat.Mask&linux.STATX_SIZE == 0 {
return vfs.ErrCorruption{Err: fmt.Errorf("gofer.dentry(%q in mount %q).restoreFile: file size validation failed: file size not available", genericDebugPathname(i.fs, d), i.fs.iopts.UniqueID)}
}
if i.size.RacyLoad() != inode.Stat.Size {
return vfs.ErrCorruption{Err: fmt.Errorf("gofer.dentry(%q in mount %q).restoreFile: file size validation failed: size changed from %d to %d", genericDebugPathname(i.fs, d), i.fs.iopts.UniqueID, i.size.Load(), inode.Stat.Size)}
}
}
if opts.ValidateFileModificationTimestamps {
if inode.Stat.Mask&linux.STATX_MTIME == 0 {
return vfs.ErrCorruption{Err: fmt.Errorf("gofer.dentry(%q in mount %q).restoreFile: mtime validation failed: mtime not available", genericDebugPathname(i.fs, d), i.fs.iopts.UniqueID)}
}
if want := dentryTimestamp(inode.Stat.Mtime); i.mtime.RacyLoad() != want {
return vfs.ErrCorruption{Err: fmt.Errorf("gofer.dentry(%q in mount %q).restoreFile: mtime validation failed: mtime changed from %+v to %+v", genericDebugPathname(i.fs, d), i.fs.iopts.UniqueID, linux.NsecToStatxTimestamp(i.mtime.RacyLoad()), linux.NsecToStatxTimestamp(want))}
}
}
}
if !i.cachedMetadataAuthoritative() {
i.updateMetadataFromStatxLocked(&inode.Stat)
}
if rw, ok := i.fs.savedDentryRW[d]; ok {
if err := d.ensureSharedHandle(ctx, rw.read, rw.write, false /* trunc */); err != nil {
return fmt.Errorf("failed to restore file handles (read=%t, write=%t) for %q in mount %q: %w", rw.read, rw.write, genericDebugPathname(i.fs, d), i.fs.iopts.UniqueID, err)
}
}
return nil
}
// doRevalidationLisafs stats all dentries in `state`. It will update or
// invalidate dentries in the cache based on the result.
//
// Preconditions:
// - fs.renameMu must be locked.
// - InteropModeShared is in effect.
func doRevalidationLisafs(ctx context.Context, vfsObj *vfs.VirtualFilesystem, state *revalidateState, ds **[]*dentry) error {
start := state.start.inode.impl.(*lisafsInode)
// Populate state.names.
state.names = state.names[:0] // For sanity.
if state.refreshStart {
state.names = append(state.names, "")
}
for _, d := range state.dentries {
state.names = append(state.names, d.name)
}
// Lock metadata on all dentries *before* getting attributes for them.
if state.refreshStart {
start.inode.metadataMu.Lock()
defer start.inode.metadataMu.Unlock()
}
for _, d := range state.dentries {
d.inode.metadataMu.Lock()
}
// lastUnlockedDentry keeps track of the dentries in state.dentries that have
// already had their metadataMu unlocked. Avoid defer unlock in the loop
// above to avoid heap allocation.
lastUnlockedDentry := -1
defer func() {
// Advance to the first unevaluated dentry and unlock the remaining
// dentries.
for lastUnlockedDentry++; lastUnlockedDentry < len(state.dentries); lastUnlockedDentry++ {
state.dentries[lastUnlockedDentry].inode.metadataMu.Unlock()
}
}()
// Make WalkStat RPC.
stats, err := start.controlFD.WalkStat(ctx, state.names)
if err != nil {
return err
}
if state.refreshStart {
if len(stats) > 0 {
// First dentry is where the search is starting, just update attributes
// since it cannot be replaced.
start.updateMetadataFromStatxLocked(&stats[0]) // +checklocksforce: see above.
stats = stats[1:]
}
}
for i := 0; i < len(state.dentries); i++ {
d := state.dentries[i]
found := i < len(stats)
// Note that synthetic dentries will always fail this comparison check.
if !found ||
d.inode.inoKey != inoKeyFromStatx(&stats[i]) ||
(stats[i].Mask&linux.STATX_TYPE != 0 && uint32(stats[i].Mode&linux.FileTypeMask) != d.inode.fileType()) {
d.inode.metadataMu.Unlock()
lastUnlockedDentry = i
if !found && d.inode.isSynthetic() {
// We have a synthetic file, and no remote file has arisen to replace
// it.
return nil
}
// The file at this path has changed or no longer exists. Mark the
// dentry invalidated.
d.invalidate(ctx, vfsObj, ds)
return nil
}
// The file at this path hasn't changed. Just update cached metadata.
d.inode.impl.(*lisafsInode).updateMetadataFromStatxLocked(&stats[i]) // +checklocksforce: see above.
d.inode.metadataMu.Unlock()
lastUnlockedDentry = i
}
return nil
}