Files
2026-04-01 11:17:30 -07:00

664 lines
24 KiB
Go

// Copyright 2021 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 lisafs
import (
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/refs"
"gvisor.dev/gvisor/pkg/sync"
)
// FDID (file descriptor identifier) is used to identify FDs on a connection.
// Each connection has its own FDID namespace.
//
// +marshal boundCheck slice:FDIDSlice
type FDID uint64
// InvalidFDID represents an invalid FDID.
const InvalidFDID FDID = 0
// Ok returns true if f is a valid FDID.
func (f FDID) Ok() bool {
return f != InvalidFDID
}
// genericFD can represent any type of FD.
type genericFD interface {
refs.RefCounter
}
// A ControlFD is the gateway to the backing filesystem tree node. It is an
// unusual concept. This exists to provide a safe way to do path-based
// operations on the file. It performs operations that can modify the
// filesystem tree and synchronizes these operations. See ControlFDImpl for
// supported operations.
//
// It is not an inode, because multiple control FDs are allowed to exist on the
// same file. It is not a file descriptor because it is not tied to any access
// mode, i.e. a control FD can change its access mode based on the operation
// being performed.
//
// Reference Model:
// - Each control FD holds a ref on its Node for its entire lifetime.
type ControlFD struct {
controlFDRefs
controlFDEntry
// node is the filesystem node this FD is immutably associated with.
node *Node
// openFDs is a linked list of all FDs opened on this FD. As per reference
// model, all open FDs hold a ref on this FD.
openFDsMu sync.RWMutex
openFDs openFDList
// All the following fields are immutable.
// id is the unique FD identifier which identifies this FD on its connection.
id FDID
// conn is the backing connection owning this FD.
conn *Connection
// ftype is the file type of the backing inode. ftype.FileType() == ftype.
ftype linux.FileMode
// impl is the control FD implementation which embeds this struct. It
// contains all the implementation specific details.
impl ControlFDImpl
}
var _ genericFD = (*ControlFD)(nil)
// DecRef implements refs.RefCounter.DecRef. Note that the context
// parameter should never be used. It exists solely to comply with the
// refs.RefCounter interface.
func (fd *ControlFD) DecRef(context.Context) {
fd.controlFDRefs.DecRef(func() {
fd.conn.server.renameMu.RLock()
defer fd.conn.server.renameMu.RUnlock()
fd.destroyLocked()
})
}
// decRefLocked is the same as DecRef except the added precondition.
//
// Precondition: server's rename mutex must be at least read locked.
func (fd *ControlFD) decRefLocked() {
fd.controlFDRefs.DecRef(func() {
fd.destroyLocked()
})
}
// Precondition: server's rename mutex must be at least read locked.
func (fd *ControlFD) destroyLocked() {
// Update node's control FD list.
fd.node.removeFD(fd)
// Drop ref on node.
fd.node.DecRef(nil)
// Let the FD implementation clean up.
fd.impl.Close()
}
// Init must be called before first use of fd. It inserts fd into the
// filesystem tree.
//
// Preconditions:
// - server's rename mutex must be at least read locked.
// - The caller must take a ref on node which is transferred to fd.
func (fd *ControlFD) Init(c *Connection, node *Node, mode linux.FileMode, impl ControlFDImpl) {
fd.conn = c
fd.node = node
fd.impl = impl
fd.ftype = mode.FileType()
// Initialize fd with 1 ref which is transferred to c via c.insertFD().
fd.controlFDRefs.InitRefs()
// Make fd reachable/discoverable.
fd.id = c.insertFD(fd)
node.insertFD(fd)
}
// Conn returns the fd's owning connection.
func (fd *ControlFD) Conn() *Connection {
return fd.conn
}
// FileType returns the file mode only containing the file type bits.
func (fd *ControlFD) FileType() linux.FileMode {
return fd.ftype
}
// IsDir indicates whether fd represents a directory.
func (fd *ControlFD) IsDir() bool {
return fd.ftype == unix.S_IFDIR
}
// IsRegular indicates whether fd represents a regular file.
func (fd *ControlFD) IsRegular() bool {
return fd.ftype == unix.S_IFREG
}
// IsSymlink indicates whether fd represents a symbolic link.
func (fd *ControlFD) IsSymlink() bool {
return fd.ftype == unix.S_IFLNK
}
// IsSocket indicates whether fd represents a socket.
func (fd *ControlFD) IsSocket() bool {
return fd.ftype == unix.S_IFSOCK
}
// Node returns the node this FD was opened on.
func (fd *ControlFD) Node() *Node {
return fd.node
}
// RemoveFromConn removes this control FD from its owning connection.
//
// Preconditions:
// - fd should not have been returned to the client. Otherwise the client can
// still refer to it.
// - server's rename mutex must at least be read locked.
func (fd *ControlFD) RemoveFromConn() {
fd.conn.removeControlFDLocked(fd.id)
}
// safelyRead executes the given operation with the local path node locked.
// This guarantees that fd's path will not change. fn may not any change paths.
func (fd *ControlFD) safelyRead(fn func() error) error {
fd.conn.server.renameMu.RLock()
defer fd.conn.server.renameMu.RUnlock()
fd.node.opMu.RLock()
defer fd.node.opMu.RUnlock()
return fn()
}
// safelyWrite executes the given operation with the local path node locked in
// a writable fashion. This guarantees that no other operation is executing on
// this path node. fn may change paths inside fd.node.
func (fd *ControlFD) safelyWrite(fn func() error) error {
fd.conn.server.renameMu.RLock()
defer fd.conn.server.renameMu.RUnlock()
fd.node.opMu.Lock()
defer fd.node.opMu.Unlock()
return fn()
}
// safelyGlobal executes the given operation with the global path lock held.
// This guarantees that no other operations is executing concurrently on this
// server. fn may change any path.
func (fd *ControlFD) safelyGlobal(fn func() error) (err error) {
fd.conn.server.renameMu.Lock()
defer fd.conn.server.renameMu.Unlock()
return fn()
}
// forEachOpenFD executes fn on each FD opened on fd.
func (fd *ControlFD) forEachOpenFD(fn func(ofd *OpenFD)) {
fd.openFDsMu.RLock()
defer fd.openFDsMu.RUnlock()
for ofd := fd.openFDs.Front(); ofd != nil; ofd = ofd.Next() {
fn(ofd)
}
}
// OpenFD represents an open file descriptor on the protocol. It resonates
// closely with a Linux file descriptor. Its operations are limited to the
// file. Its operations are not allowed to modify or traverse the filesystem
// tree. See OpenFDImpl for the supported operations.
//
// Reference Model:
// - An OpenFD takes a reference on the control FD it was opened on.
type OpenFD struct {
openFDRefs
openFDEntry
// All the following fields are immutable.
// controlFD is the ControlFD on which this FD was opened. OpenFD holds a ref
// on controlFD for its entire lifetime.
controlFD *ControlFD
// id is the unique FD identifier which identifies this FD on its connection.
id FDID
// Access mode for this FD.
readable bool
writable bool
// impl is the open FD implementation which embeds this struct. It
// contains all the implementation specific details.
impl OpenFDImpl
}
var _ genericFD = (*OpenFD)(nil)
// ControlFD returns the control FD on which this FD was opened.
func (fd *OpenFD) ControlFD() ControlFDImpl {
return fd.controlFD.impl
}
// DecRef implements refs.RefCounter.DecRef. Note that the context
// parameter should never be used. It exists solely to comply with the
// refs.RefCounter interface.
func (fd *OpenFD) DecRef(context.Context) {
fd.openFDRefs.DecRef(func() {
fd.controlFD.openFDsMu.Lock()
fd.controlFD.openFDs.Remove(fd)
fd.controlFD.openFDsMu.Unlock()
fd.controlFD.DecRef(nil) // Drop the ref on the control FD.
fd.impl.Close()
})
}
// Init must be called before first use of fd.
func (fd *OpenFD) Init(cfd *ControlFD, flags uint32, impl OpenFDImpl) {
// Initialize fd with 1 ref which is transferred to c via c.insertFD().
fd.openFDRefs.InitRefs()
fd.controlFD = cfd
fd.id = cfd.conn.insertFD(fd)
accessMode := flags & unix.O_ACCMODE
fd.readable = accessMode == unix.O_RDONLY || accessMode == unix.O_RDWR
fd.writable = accessMode == unix.O_WRONLY || accessMode == unix.O_RDWR
fd.impl = impl
cfd.IncRef() // Holds a ref on cfd for its lifetime.
cfd.openFDsMu.Lock()
cfd.openFDs.PushBack(fd)
cfd.openFDsMu.Unlock()
}
// BoundSocketFD represents a bound socket on the server.
//
// Reference Model:
// - A BoundSocketFD takes a reference on the control FD it is bound to.
type BoundSocketFD struct {
boundSocketFDRefs
// All the following fields are immutable.
// controlFD is the ControlFD on which this FD was bound. BoundSocketFD
// holds a ref on controlFD for its entire lifetime.
controlFD *ControlFD
// id is the unique FD identifier which identifies this FD on its connection.
id FDID
// impl is the socket FD implementation which embeds this struct. It
// contains all the implementation specific details.
impl BoundSocketFDImpl
}
var _ genericFD = (*BoundSocketFD)(nil)
// ControlFD returns the control FD on which this FD was bound.
func (fd *BoundSocketFD) ControlFD() ControlFDImpl {
return fd.controlFD.impl
}
// DecRef implements refs.RefCounter.DecRef. Note that the context
// parameter should never be used. It exists solely to comply with the
// refs.RefCounter interface.
func (fd *BoundSocketFD) DecRef(context.Context) {
fd.boundSocketFDRefs.DecRef(func() {
fd.controlFD.DecRef(nil) // Drop the ref on the control FD.
fd.impl.Close()
})
}
// Init must be called before first use of fd.
func (fd *BoundSocketFD) Init(cfd *ControlFD, impl BoundSocketFDImpl) {
// Initialize fd with 1 ref which is transferred to c via c.insertFD().
fd.boundSocketFDRefs.InitRefs()
fd.controlFD = cfd
fd.id = cfd.conn.insertFD(fd)
fd.impl = impl
cfd.IncRef() // Holds a ref on cfd for its lifetime.
}
// There are four different types of guarantees provided:
//
// none: There is no concurrency guarantee. The method may be invoked
// concurrently with any other method on any other FD.
//
// read: The method is guaranteed to be exclusive of any write or global
// operation that is mutating the state of the directory tree starting at this
// node. For example, this means creating new files, symlinks, directories or
// renaming a directory entry (or renaming in to this target), but the method
// may be called concurrently with other read methods.
//
// write: The method is guaranteed to be exclusive of any read, write or global
// operation that is mutating the state of the directory tree starting at this
// node, as described in read above. There may however, be other write
// operations executing concurrently on other components in the directory tree.
//
// global: The method is guaranteed to be exclusive of any read, write or
// global operation.
// ControlFDImpl contains implementation details for a ControlFD.
// Implementations of ControlFDImpl should contain their associated ControlFD
// by value as their first field.
//
// The operations that perform path traversal or any modification to the
// filesystem tree must synchronize those modifications with the server's
// rename mutex.
type ControlFDImpl interface {
// FD returns a pointer to the embedded ControlFD.
FD() *ControlFD
// Close should clean up resources used by the control FD implementation.
// Close is called after all references on the FD have been dropped and its
// FDID has been released.
//
// On the server, Close has no concurrency guarantee.
Close()
// Stat returns the stat(2) results for this FD.
//
// On the server, Stat has a read concurrency guarantee.
Stat() (Statx, error)
// SetStat sets file attributes on the backing file. This does not correspond
// to any one Linux syscall. On Linux, this operation is performed using
// multiple syscalls like fchmod(2), fchown(2), ftruncate(2), futimesat(2)
// and so on. The implementation must only set attributes for fields
// indicated by stat.Mask. Failure to set an attribute may or may not
// terminate the entire operation. SetStat must return a uint32 which is
// interpreted as a stat mask to indicate which attribute setting attempts
// failed. If multiple attribute setting attempts failed, the returned error
// may be from any one of them.
//
// On the server, SetStat has a write concurrency guarantee.
SetStat(stat SetStatReq) (uint32, error)
// Walk walks one path component from the directory represented by this FD.
// Walk must open a ControlFD on the walked file.
//
// On the server, Walk has a read concurrency guarantee.
Walk(name string) (*ControlFD, Statx, error)
// WalkStat is capable of walking multiple path components and returning the
// stat results for each path component walked via recordStat. Stat results
// must be returned in the order of walk.
//
// In case a symlink is encountered, the walk must terminate successfully on
// the symlink including its stat result.
//
// The first path component of path may be "" which indicates that the first
// stat result returned must be of this starting directory.
//
// On the server, WalkStat has a read concurrency guarantee.
WalkStat(path StringArray, recordStat func(Statx)) error
// Open opens the control FD with the flags passed. The flags should be
// interpreted as open(2) flags.
//
// Open may also optionally return a host FD for the opened file whose
// lifecycle is independent of the OpenFD. Returns -1 if not available.
//
// N.B. The server must resolve any lazy paths when open is called.
// After this point, read and write may be called on files with no
// deletion check, so resolving in the data path is not viable.
//
// On the server, Open has a read concurrency guarantee.
Open(flags uint32) (*OpenFD, int, error)
// OpenCreate creates a regular file inside the directory represented by this
// FD and then also opens the file. The created file has perms as specified
// by mode and owners as specified by uid and gid. The file is opened with
// the specified flags.
//
// OpenCreate may also optionally return a host FD for the opened file whose
// lifecycle is independent of the OpenFD. Returns -1 if not available.
//
// N.B. The server must resolve any lazy paths when open is called.
// After this point, read and write may be called on files with no
// deletion check, so resolving in the data path is not viable.
//
// On the server, OpenCreate has a write concurrency guarantee.
OpenCreate(mode linux.FileMode, uid UID, gid GID, name string, flags uint32) (*ControlFD, Statx, *OpenFD, int, error)
// Mkdir creates a directory inside the directory represented by this FD. The
// created directory has perms as specified by mode and owners as specified
// by uid and gid.
//
// On the server, Mkdir has a write concurrency guarantee.
Mkdir(mode linux.FileMode, uid UID, gid GID, name string) (*ControlFD, Statx, error)
// Mknod creates a file inside the directory represented by this FD. The file
// type and perms are specified by mode and owners are specified by uid and
// gid. If the newly created file is a character or block device, minor and
// major specify its device number.
//
// On the server, Mkdir has a write concurrency guarantee.
Mknod(mode linux.FileMode, uid UID, gid GID, name string, minor uint32, major uint32) (*ControlFD, Statx, error)
// Symlink creates a symlink inside the directory represented by this FD. The
// symlink has owners as specified by uid and gid and points to target.
//
// On the server, Symlink has a write concurrency guarantee.
Symlink(name string, target string, uid UID, gid GID) (*ControlFD, Statx, error)
// Link creates a hard link to the file represented by this FD. The hard link
// is created inside dir with the specified name.
//
// On the server, Link has a write concurrency guarantee for dir and read
// concurrency guarantee for this file.
Link(dir ControlFDImpl, name string) (*ControlFD, Statx, error)
// StatFS returns information about the file system associated with
// this file.
//
// On the server, StatFS has read concurrency guarantee.
StatFS() (StatFS, error)
// Readlink reads the symlink's target and writes the string into the buffer
// returned by getLinkBuf which can be used to request buffer for some size.
// It returns the number of bytes written into the buffer.
//
// On the server, Readlink has a read concurrency guarantee.
Readlink(getLinkBuf func(uint32) []byte) (uint16, error)
// Connect establishes a new host-socket backed connection with a unix domain
// socket. On success it returns a non-blocking host socket FD whose
// lifecycle is independent of this ControlFD.
//
// sockType indicates the requested type of socket and can be passed as type
// argument to socket(2).
//
// On the server, Connect has a read concurrency guarantee.
Connect(sockType uint32) (int, error)
// ConnectWithCreds is a wrapper around Connect but first changes the gofer's
// euid and egid to the given uid and gid before calling Connect. It restores
// the euid and egid after Connect.
//
// On the server, ConnectWithCreds has a read concurrency guarantee.
ConnectWithCreds(sockType uint32, uid UID, gid GID) (int, error)
// BindAt creates a host unix domain socket of type sockType, bound to
// the given namt of type sockType, bound to the given name. It returns
// a ControlFD that can be used for path operations on the socket, a
// BoundSocketFD that can be used to Accept/Listen on the socket, and a
// host FD that can be used for event notifications (like new
// connections).
//
// On the server, BindAt has a write concurrency guarantee.
BindAt(name string, sockType uint32, mode linux.FileMode, uid UID, gid GID) (*ControlFD, Statx, *BoundSocketFD, int, error)
// UnlinkAt the file identified by name in this directory.
//
// Flags are Linux unlinkat(2) flags.
//
// On the server, UnlinkAt has a write concurrency guarantee.
Unlink(name string, flags uint32) error
// RenameAt renames a given file to a new name in a potentially new directory.
//
// oldName must be a name relative to this file, which must be a directory.
// newName is a name relative to newDir.
//
// On the server, RenameAt has a global concurrency guarantee.
RenameAt(oldName string, newDir ControlFDImpl, newName string) error
// RenameAt2 renames a given file to a new name in a potentially new directory.
//
// oldName must be a name relative to this file, which must be a directory.
// newName is a name relative to newDir.
//
// On the server, RenameAt2 has a global concurrency guarantee.
RenameAt2(oldName string, newDir ControlFDImpl, newName string, flags uint32) error
// Renamed is called to notify the FD implementation that the file has been
// renamed. FD implementation may update its state accordingly.
//
// On the server, Renamed has a global concurrency guarantee.
Renamed()
// GetXattr returns extended attributes of this file. It returns the number
// of bytes written into the buffer returned by getValueBuf which can be used
// to request buffer for some size.
//
// If the value is larger than size, implementations may return ERANGE to
// indicate that the buffer is too small.
//
// N.B. size may be 0, in which can the implementation must first find out
// the attribute value size using getxattr(2) by passing size=0. Then request
// a buffer large enough using getValueBuf and write the value there.
//
// On the server, GetXattr has a read concurrency guarantee.
GetXattr(name string, size uint32, getValueBuf func(uint32) []byte) (uint16, error)
// SetXattr sets extended attributes on this file.
//
// On the server, SetXattr has a write concurrency guarantee.
SetXattr(name string, value string, flags uint32) error
// ListXattr lists the names of the extended attributes on this file.
//
// Size indicates the size of the buffer that has been allocated to hold the
// attribute list. If the list would be larger than size, implementations may
// return ERANGE to indicate that the buffer is too small, but they are also
// free to ignore the hint entirely (i.e. the value returned may be larger
// than size). All size checking is done independently at the syscall layer.
//
// On the server, ListXattr has a read concurrency guarantee.
ListXattr(size uint64) (StringArray, error)
// RemoveXattr removes extended attributes on this file.
//
// On the server, RemoveXattr has a write concurrency guarantee.
RemoveXattr(name string) error
}
// OpenFDImpl contains implementation details for a OpenFD. Implementations of
// OpenFDImpl should contain their associated OpenFD by value as their first
// field.
//
// Since these operations do not perform any path traversal or any modification
// to the filesystem tree, there is no need to synchronize with rename
// operations.
type OpenFDImpl interface {
// FD returns a pointer to the embedded OpenFD.
FD() *OpenFD
// Close should clean up resources used by the open FD implementation.
// Close is called after all references on the FD have been dropped and its
// FDID has been released.
//
// On the server, Close has no concurrency guarantee.
Close()
// Stat returns the stat(2) results for this FD.
//
// On the server, Stat has a read concurrency guarantee.
Stat() (Statx, error)
// Sync is similar to fsync(2).
//
// On the server, Sync has a read concurrency guarantee.
Sync() error
// Write writes buf at offset off to the backing file via this open FD. Write
// attempts to write len(buf) bytes and returns the number of bytes written.
//
// On the server, Write has a write concurrency guarantee. See Open for
// additional requirements regarding lazy path resolution.
Write(buf []byte, off uint64) (uint64, error)
// Read reads at offset off into buf from the backing file via this open FD.
// Read attempts to read len(buf) bytes and returns the number of bytes read.
//
// On the server, Read has a read concurrency guarantee. See Open for
// additional requirements regarding lazy path resolution.
Read(buf []byte, off uint64) (uint64, error)
// Allocate allows the caller to directly manipulate the allocated disk space
// for the file. See fallocate(2) for more details.
//
// On the server, Allocate has a write concurrency guarantee.
Allocate(mode, off, length uint64) error
// Flush can be used to clean up the file state. Behavior is
// implementation-specific.
//
// On the server, Flush has a read concurrency guarantee.
Flush() error
// Getdent64 fetches directory entries for this directory and calls
// recordDirent for each dirent read. If seek0 is true, then the directory FD
// is seeked to 0 and iteration starts from the beginning.
//
// On the server, Getdent64 has a read concurrency guarantee.
Getdent64(count uint32, seek0 bool, recordDirent func(Dirent64)) error
// Renamed is called to notify the FD implementation that the file has been
// renamed. FD implementation may update its state accordingly.
//
// On the server, Renamed has a global concurrency guarantee.
Renamed()
}
// BoundSocketFDImpl represents a socket on the host filesystem that has been
// created by the sandboxed application via Bind.
type BoundSocketFDImpl interface {
// FD returns a pointer to the embedded BoundSocketFD.
FD() *BoundSocketFD
// Listen marks the socket as accepting incoming connections.
//
// On the server, Listen has a read concurrency guarantee.
Listen(backlog int32) error
// Accept takes the first pending connection and creates a new socket
// for it. The new socket FD is returned along with the peer address of
// the connecting socket (which may be empty string).
//
// On the server, Accept has a read concurrency guarantee.
Accept() (int, string, error)
// Close should clean up resources used by the bound socket FD
// implementation.
//
// Close is called after all references on the FD have been dropped and its
// FDID has been released.
//
// On the server, Close has no concurrency guarantee.
Close()
}