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
755 lines
26 KiB
Go
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
|
|
}
|