From 298b5f36122ebc9ae32d64157132d91fff2eba8f Mon Sep 17 00:00:00 2001 From: Lucas Manning Date: Wed, 1 Feb 2023 17:18:31 -0800 Subject: [PATCH] Refactor FUSE inode implementation. This change replaces the FUSE inode implementations with a custom inode type that implementsthe kernfs.Inode interface. It also cleans up some of the attribute related functions and gets the open_test to pass. PiperOrigin-RevId: 506465698 --- pkg/abi/linux/fuse.go | 16 +- pkg/sentry/fsimpl/fuse/BUILD | 5 + pkg/sentry/fsimpl/fuse/connection.go | 2 +- pkg/sentry/fsimpl/fuse/dev.go | 23 - pkg/sentry/fsimpl/fuse/file.go | 31 +- pkg/sentry/fsimpl/fuse/fusefs.go | 674 +------------------- pkg/sentry/fsimpl/fuse/inode.go | 837 +++++++++++++++++++++++++ pkg/sentry/fsimpl/fuse/read_write.go | 21 +- pkg/sentry/fsimpl/fuse/regular_file.go | 43 +- test/runner/fuse/fuse.go | 2 +- test/syscalls/BUILD | 1 + 11 files changed, 932 insertions(+), 723 deletions(-) create mode 100644 pkg/sentry/fsimpl/fuse/inode.go diff --git a/pkg/abi/linux/fuse.go b/pkg/abi/linux/fuse.go index 5291f572e..891bbbff5 100644 --- a/pkg/abi/linux/fuse.go +++ b/pkg/abi/linux/fuse.go @@ -348,11 +348,11 @@ type FUSEAttr struct { _ uint32 } -// FUSEGetAttrOut is the reply sent by the daemon to the kernel -// for FUSEGetAttrIn. +// FUSEAttrOut is the reply sent by the daemon to the kernel +// for FUSEGetAttrIn and FUSESetAttrIn. // // +marshal -type FUSEGetAttrOut struct { +type FUSEAttrOut struct { // AttrValid and AttrValidNsec describe the attribute cache duration AttrValid uint64 @@ -1054,3 +1054,13 @@ type FUSEFsyncIn struct { // padding _ uint32 } + +// FUSEAccessIn is the request sent by the kernel to the daemon when checking +// permissions on a file. +// +// +marshal +type FUSEAccessIn struct { + Mask uint32 + // padding + _ uint32 +} diff --git a/pkg/sentry/fsimpl/fuse/BUILD b/pkg/sentry/fsimpl/fuse/BUILD index 65d4ae37d..48d71dbe4 100644 --- a/pkg/sentry/fsimpl/fuse/BUILD +++ b/pkg/sentry/fsimpl/fuse/BUILD @@ -36,6 +36,7 @@ go_library( "directory.go", "file.go", "fusefs.go", + "inode.go", "inode_refs.go", "read_write.go", "register.go", @@ -58,8 +59,12 @@ go_library( "//pkg/safemem", "//pkg/sentry/fsimpl/devtmpfs", "//pkg/sentry/fsimpl/kernfs", + "//pkg/sentry/fsutil", "//pkg/sentry/kernel", "//pkg/sentry/kernel/auth", + "//pkg/sentry/kernel/pipe", + "//pkg/sentry/kernel/time", + "//pkg/sentry/memmap", "//pkg/sentry/vfs", "//pkg/sync", "//pkg/usermem", diff --git a/pkg/sentry/fsimpl/fuse/connection.go b/pkg/sentry/fsimpl/fuse/connection.go index 3ad7e9624..2d52d3847 100644 --- a/pkg/sentry/fsimpl/fuse/connection.go +++ b/pkg/sentry/fsimpl/fuse/connection.go @@ -181,7 +181,7 @@ type connection struct { dontMask bool // noOpen if FUSE server doesn't support open operation. - // This flag only influence performance, not correctness of the program. + // This flag only influences performance, not correctness of the program. noOpen bool } diff --git a/pkg/sentry/fsimpl/fuse/dev.go b/pkg/sentry/fsimpl/fuse/dev.go index 5ce222c26..8d4f13263 100644 --- a/pkg/sentry/fsimpl/fuse/dev.go +++ b/pkg/sentry/fsimpl/fuse/dev.go @@ -145,7 +145,6 @@ func (fd *DeviceFD) Read(ctx context.Context, dst usermem.IOSequence, opts vfs.R if !fd.connected() { return 0, linuxerr.EPERM } - // We require that any Read done on this filesystem have a sane minimum // read buffer. It must have the capacity for the fixed parts of any request // header (Linux uses the request header and the FUSEWriteIn header for this @@ -162,14 +161,6 @@ func (fd *DeviceFD) Read(ctx context.Context, dst usermem.IOSequence, opts vfs.R if dst.NumBytes() < int64(minBuffSize) { return 0, linuxerr.EINVAL } - return fd.readLocked(ctx, dst, opts) -} - -// readLocked implements the reading of the fuse device while locked with DeviceFD.mu. -// -// Preconditions: dst is large enough for any reasonable request. -// +checklocks:fd.mu -func (fd *DeviceFD) readLocked(ctx context.Context, dst usermem.IOSequence, opts vfs.ReadOptions) (int64, error) { // Find the first valid request. For the normal case this loop only executes // once. var req *Request @@ -209,7 +200,6 @@ func (fd *DeviceFD) readLocked(ctx context.Context, dst usermem.IOSequence, opts fd.numActiveRequests-- delete(fd.completions, req.hdr.Unique) } - return int64(n), nil } @@ -231,12 +221,6 @@ func (fd *DeviceFD) PWrite(ctx context.Context, src usermem.IOSequence, offset i func (fd *DeviceFD) Write(ctx context.Context, src usermem.IOSequence, opts vfs.WriteOptions) (int64, error) { fd.mu.Lock() defer fd.mu.Unlock() - return fd.writeLocked(ctx, src, opts) -} - -// writeLocked implements writing to the fuse device while locked with DeviceFD.mu. -// +checklocks:fd.mu -func (fd *DeviceFD) writeLocked(ctx context.Context, src usermem.IOSequence, opts vfs.WriteOptions) (int64, error) { if !fd.connected() { return 0, linuxerr.EPERM } @@ -273,13 +257,6 @@ func (fd *DeviceFD) writeLocked(ctx context.Context, src usermem.IOSequence, opt func (fd *DeviceFD) Readiness(mask waiter.EventMask) waiter.EventMask { fd.mu.Lock() defer fd.mu.Unlock() - return fd.readinessLocked(mask) -} - -// readinessLocked implements checking the readiness of the fuse device while -// locked with DeviceFD.mu. -// +checklocks:fd.mu -func (fd *DeviceFD) readinessLocked(mask waiter.EventMask) waiter.EventMask { var ready waiter.EventMask if !fd.connected() { diff --git a/pkg/sentry/fsimpl/fuse/file.go b/pkg/sentry/fsimpl/fuse/file.go index 0ef1fcc85..16927af24 100644 --- a/pkg/sentry/fsimpl/fuse/file.go +++ b/pkg/sentry/fsimpl/fuse/file.go @@ -36,10 +36,10 @@ type fileDescription struct { // the file handle used in userspace. Fh uint64 - // Nonseekable is indicate cannot perform seek on a file. + // Nonseekable indicates we cannot perform seek on a file. Nonseekable bool - // DirectIO suggest fuse to use direct io operation. + // DirectIO suggests that fuse use direct IO operations. DirectIO bool // OpenFlag is the flag returned by open. @@ -77,16 +77,19 @@ func (fd *fileDescription) Release(ctx context.Context) { Fh: fd.Fh, Flags: fd.statusFlags(), } - // TODO(gvisor.dev/issue/3245): add logic when we support file lock owner. + // TODO(gvisor.dev/issue/3245): add logic when we support file lock owners. + inode := fd.inode() + inode.attrMu.Lock() + defer inode.attrMu.Unlock() var opcode linux.FUSEOpcode - if fd.inode().Mode().IsDir() { + if inode.filemode().IsDir() { opcode = linux.FUSE_RELEASEDIR } else { opcode = linux.FUSE_RELEASE } kernelTask := kernel.TaskFromContext(ctx) - // Ignoring errors and FUSE server reply is analogous to Linux's behavior. - req := conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), fd.inode().nodeID, opcode, &in) + // Ignoring errors and FUSE server replies is analogous to Linux's behavior. + req := conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), inode.nodeID, opcode, &in) // The reply will be ignored since no callback is defined in asyncCallBack(). conn.CallAsync(kernelTask, req) } @@ -127,15 +130,21 @@ func (fd *fileDescription) Stat(ctx context.Context, opts vfs.StatOptions) (linu func (fd *fileDescription) SetStat(ctx context.Context, opts vfs.SetStatOptions) error { fs := fd.filesystem() creds := auth.CredentialsFromContext(ctx) - return fd.inode().setAttr(ctx, fs, creds, opts, true, fd.Fh) + inode := fd.inode() + inode.attrMu.Lock() + defer inode.attrMu.Unlock() + return inode.setAttr(ctx, fs, creds, opts, fhOptions{useFh: true, fh: fd.Fh}) } // Sync implements vfs.FileDescriptionImpl.Sync. func (fd *fileDescription) Sync(ctx context.Context) error { - if fd.inode().Mode().IsDir() { + inode := fd.inode() + inode.attrMu.Lock() + defer inode.attrMu.Unlock() + if inode.filemode().IsDir() { return linuxerr.EPERM } - conn := fd.inode().fs.conn + conn := inode.fs.conn // no need to proceed if FUSE server doesn't implement Open. if conn.noOpen { return linuxerr.EINVAL @@ -146,8 +155,8 @@ func (fd *fileDescription) Sync(ctx context.Context) error { Fh: fd.Fh, FsyncFlags: fd.statusFlags(), } - // Ignoring errors and FUSE server reply is analogous to Linux's behavior. - req := conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), fd.inode().nodeID, linux.FUSE_FSYNC, &in) + // Ignoring errors and FUSE server replies is analogous to Linux's behavior. + req := conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), inode.nodeID, linux.FUSE_FSYNC, &in) // The reply will be ignored since no callback is defined in asyncCallBack(). conn.CallAsync(kernelTask, req) return nil diff --git a/pkg/sentry/fsimpl/fuse/fusefs.go b/pkg/sentry/fsimpl/fuse/fusefs.go index c01097452..b3de44c95 100644 --- a/pkg/sentry/fsimpl/fuse/fusefs.go +++ b/pkg/sentry/fsimpl/fuse/fusefs.go @@ -18,18 +18,15 @@ package fuse import ( "math" "strconv" - "sync" "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/log" - "gvisor.dev/gvisor/pkg/marshal" - "gvisor.dev/gvisor/pkg/marshal/primitive" "gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs" "gvisor.dev/gvisor/pkg/sentry/kernel" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" + "gvisor.dev/gvisor/pkg/sentry/kernel/time" "gvisor.dev/gvisor/pkg/sentry/vfs" ) @@ -96,6 +93,9 @@ type filesystem struct { // opts is the options the fusefs is initialized with. opts *filesystemOptions + + // clock is a real-time clock used to set timestamps in file operations. + clock time.Clock } // Name implements vfs.FilesystemType.Name. @@ -271,6 +271,7 @@ func newFUSEFilesystem(ctx context.Context, vfsObj *vfs.VirtualFilesystem, fsTyp devMinor: devMinor, opts: opts, conn: fuseFD.conn, + clock: time.RealtimeClockFromContext(ctx), } fs.VFSFilesystem().Init(vfsObj, fsType, fs) return fs, nil @@ -287,60 +288,11 @@ func (fs *filesystem) MountOptions() string { return fs.opts.mopts } -// NewFhData is returned by newEntry. -type NewFhData struct { - // fh is the file handler. - fh uint64 - - // flags is the flags of the file. - flags uint32 -} - -// inode implements kernfs.Inode. -// -// +stateify savable -type inode struct { - inodeRefs - kernfs.InodeAlwaysValid - kernfs.InodeAttrs - kernfs.InodeDirectoryNoNewChildren - kernfs.InodeNotSymlink - kernfs.InodeWatches - kernfs.OrderedChildren - - // the owning filesystem. fs is immutable. - fs *filesystem - - // metaDataMu protects the metadata of this inode. - metadataMu sync.Mutex - - nodeID uint64 - - locks vfs.FileLocks - - // size of the file. - size atomicbitops.Uint64 - - // attributeVersion is the version of inode's attributes. - attributeVersion atomicbitops.Uint64 - - // attributeTime is the remaining vaild time of attributes. - attributeTime uint64 - - // version of the inode. - version uint64 - - // link is result of following a symbolic link. - link string - - // if newEntry got a new Fh from server it saves it here, until returned by Open - isNewFh bool - newFhData NewFhData -} - func (fs *filesystem) newRoot(ctx context.Context, creds *auth.Credentials, mode linux.FileMode) *kernfs.Dentry { i := &inode{fs: fs, nodeID: 1} - i.InodeAttrs.Init(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, 1, linux.ModeDirectory|0755) + i.attrMu.Lock() + i.init(creds, linux.UNNAMED_MAJOR, fs.devMinor, 1, linux.ModeDirectory|0755) + i.attrMu.Unlock() i.OrderedChildren.Init(kernfs.OrderedChildrenOptions{}) i.InitRefs() @@ -352,615 +304,11 @@ func (fs *filesystem) newRoot(ctx context.Context, creds *auth.Credentials, mode func (fs *filesystem) newInode(ctx context.Context, nodeID uint64, attr linux.FUSEAttr) kernfs.Inode { i := &inode{fs: fs, nodeID: nodeID} creds := auth.Credentials{EffectiveKGID: auth.KGID(attr.UID), EffectiveKUID: auth.KUID(attr.UID)} - i.InodeAttrs.Init(ctx, &creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), linux.FileMode(attr.Mode)) + i.attrMu.Lock() + i.init(&creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), linux.FileMode(attr.Mode)) i.size.Store(attr.Size) + i.attrMu.Unlock() i.OrderedChildren.Init(kernfs.OrderedChildrenOptions{}) i.InitRefs() return i } - -// CheckPermissions implements kernfs.Inode.CheckPermissions. -func (i *inode) CheckPermissions(ctx context.Context, creds *auth.Credentials, ats vfs.AccessTypes) error { - // Since FUSE operations are ultimately backed by a userspace process (the - // fuse daemon), allowing a process to call into fusefs grants the daemon - // ptrace-like capabilities over the calling process. Because of this, by - // default FUSE only allows the mount owner to interact with the - // filesystem. This explicitly excludes setuid/setgid processes. - // - // This behaviour can be overriden with the 'allow_other' mount option. - // - // See fs/fuse/dir.c:fuse_allow_current_process() in Linux. - if !i.fs.opts.allowOther { - if creds.RealKUID != i.fs.opts.uid || - creds.EffectiveKUID != i.fs.opts.uid || - creds.SavedKUID != i.fs.opts.uid || - creds.RealKGID != i.fs.opts.gid || - creds.EffectiveKGID != i.fs.opts.gid || - creds.SavedKGID != i.fs.opts.gid { - return linuxerr.EACCES - } - } - - // By default, fusefs delegates all permission checks to the server. - // However, standard unix permission checks can be enabled with the - // default_permissions mount option. - if i.fs.opts.defaultPermissions { - return i.InodeAttrs.CheckPermissions(ctx, creds, ats) - } - return nil -} - -// Open implements kernfs.Inode.Open. -func (i *inode) Open(ctx context.Context, rp *vfs.ResolvingPath, d *kernfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) { - opts.Flags &= linux.O_ACCMODE | linux.O_CREAT | linux.O_EXCL | linux.O_TRUNC | - linux.O_DIRECTORY | linux.O_NOFOLLOW | linux.O_NONBLOCK | linux.O_NOCTTY | - linux.O_APPEND - isDir := i.InodeAttrs.Mode().IsDir() - // return error if specified to open directory but inode is not a directory. - if !isDir && opts.Mode.IsDir() { - return nil, linuxerr.ENOTDIR - } - if opts.Flags&linux.O_LARGEFILE == 0 && i.size.Load() > linux.MAX_NON_LFS { - return nil, linuxerr.EOVERFLOW - } - - var fd *fileDescription - var fdImpl vfs.FileDescriptionImpl - if isDir { - directoryFD := &directoryFD{} - fd = &(directoryFD.fileDescription) - fdImpl = directoryFD - } else { - regularFD := ®ularFileFD{} - fd = &(regularFD.fileDescription) - fdImpl = regularFD - } - fd.LockFD.Init(&i.locks) - // FOPEN_KEEP_CACHE is the defualt flag for noOpen. - fd.OpenFlag = linux.FOPEN_KEEP_CACHE - - // Only send open request when FUSE server support open or is opening a directory. - if i.isNewFh { - // use Fh from NewEntry - fd.OpenFlag = i.newFhData.flags - fd.Fh = i.newFhData.fh - i.isNewFh = false - } else if !i.fs.conn.noOpen || isDir { - kernelTask := kernel.TaskFromContext(ctx) - if kernelTask == nil { - log.Warningf("fusefs.Inode.Open: couldn't get kernel task from context") - return nil, linuxerr.EINVAL - } - - // Build the request. - var opcode linux.FUSEOpcode - if isDir { - opcode = linux.FUSE_OPENDIR - } else { - opcode = linux.FUSE_OPEN - } - - in := linux.FUSEOpenIn{Flags: opts.Flags & ^uint32(linux.O_CREAT|linux.O_EXCL|linux.O_NOCTTY)} - if !i.fs.conn.atomicOTrunc { - in.Flags &= ^uint32(linux.O_TRUNC) - } - - // Send the request and receive the reply. - req := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), i.nodeID, opcode, &in) - res, err := i.fs.conn.Call(kernelTask, req) - if err != nil { - return nil, err - } - if err := res.Error(); linuxerr.Equals(linuxerr.ENOSYS, err) && !isDir { - i.fs.conn.noOpen = true - } else if err != nil { - return nil, err - } else { - out := linux.FUSEOpenOut{} - if err := res.UnmarshalPayload(&out); err != nil { - return nil, err - } - - // Process the reply. - fd.OpenFlag = out.OpenFlag - fd.Fh = out.Fh - } - } - if isDir { - fd.OpenFlag &= ^uint32(linux.FOPEN_DIRECT_IO) - } - - // TODO(gvisor.dev/issue/3234): invalidate mmap after implemented it for FUSE Inode - fd.DirectIO = fd.OpenFlag&linux.FOPEN_DIRECT_IO != 0 - fdOptions := &vfs.FileDescriptionOptions{} - if fd.OpenFlag&linux.FOPEN_NONSEEKABLE != 0 { - fdOptions.DenyPRead = true - fdOptions.DenyPWrite = true - fd.Nonseekable = true - } - - // If atomicOTrunc and O_TRUNC are set, just update the inode's version number - // and set its size to 0 since the truncation is handled by the FUSE daemon. - // Otherwise send a separate SETATTR to truncate the file size. - if opts.Flags&linux.O_TRUNC != 0 { - if i.fs.conn.atomicOTrunc { - i.fs.conn.mu.Lock() - i.attributeVersion.Store(i.fs.conn.attributeVersion.Add(1)) - i.size.Store(0) - i.fs.conn.mu.Unlock() - i.attributeTime = 0 - } else { - opts := vfs.SetStatOptions{Stat: linux.Statx{Size: 0, Mask: linux.STATX_SIZE}} - i.setAttr(ctx, i.fs.VFSFilesystem(), auth.CredentialsFromContext(ctx), opts, true, i.newFhData.fh) - } - } - - if err := fd.vfsfd.Init(fdImpl, opts.Flags, rp.Mount(), d.VFSDentry(), fdOptions); err != nil { - return nil, err - } - return &fd.vfsfd, nil -} - -// Lookup implements kernfs.Inode.Lookup. -func (i *inode) Lookup(ctx context.Context, name string) (kernfs.Inode, error) { - in := linux.FUSELookupIn{Name: linux.CString(name)} - return i.newEntry(ctx, name, 0, linux.FUSE_LOOKUP, &in) -} - -// Keep implements kernfs.Inode.Keep. -func (i *inode) Keep() bool { - // Return true so that kernfs keeps the new dentry pointing to this - // inode in the dentry tree. This is needed because inodes created via - // Lookup are not temporary. They might refer to existing files on server - // that can be Unlink'd/Rmdir'd. - return true -} - -// IterDirents implements kernfs.Inode.IterDirents. -func (*inode) IterDirents(ctx context.Context, mnt *vfs.Mount, callback vfs.IterDirentsCallback, offset, relOffset int64) (int64, error) { - return offset, nil -} - -// NewFile implements kernfs.Inode.NewFile. -func (i *inode) NewFile(ctx context.Context, name string, opts vfs.OpenOptions) (kernfs.Inode, error) { - opts.Flags &= linux.O_ACCMODE | linux.O_CREAT | linux.O_EXCL | linux.O_TRUNC | - linux.O_DIRECTORY | linux.O_NOFOLLOW | linux.O_NONBLOCK | linux.O_NOCTTY - kernelTask := kernel.TaskFromContext(ctx) - if kernelTask == nil { - log.Warningf("fusefs.Inode.NewFile: couldn't get kernel task from context", i.nodeID) - return nil, linuxerr.EINVAL - } - in := linux.FUSECreateIn{ - CreateMeta: linux.FUSECreateMeta{ - Flags: opts.Flags, - Mode: uint32(opts.Mode) | linux.S_IFREG, - Umask: uint32(kernelTask.FSContext().Umask()), - }, - Name: linux.CString(name), - } - return i.newEntry(ctx, name, linux.S_IFREG, linux.FUSE_CREATE, &in) -} - -// NewNode implements kernfs.Inode.NewNode. -func (i *inode) NewNode(ctx context.Context, name string, opts vfs.MknodOptions) (kernfs.Inode, error) { - in := linux.FUSEMknodIn{ - MknodMeta: linux.FUSEMknodMeta{ - Mode: uint32(opts.Mode), - Rdev: linux.MakeDeviceID(uint16(opts.DevMajor), opts.DevMinor), - Umask: uint32(kernel.TaskFromContext(ctx).FSContext().Umask()), - }, - Name: linux.CString(name), - } - return i.newEntry(ctx, name, opts.Mode.FileType(), linux.FUSE_MKNOD, &in) -} - -// NewSymlink implements kernfs.Inode.NewSymlink. -func (i *inode) NewSymlink(ctx context.Context, name, target string) (kernfs.Inode, error) { - in := linux.FUSESymlinkIn{ - Name: linux.CString(name), - Target: linux.CString(target), - } - return i.newEntry(ctx, name, linux.S_IFLNK, linux.FUSE_SYMLINK, &in) -} - -// Unlink implements kernfs.Inode.Unlink. -func (i *inode) Unlink(ctx context.Context, name string, child kernfs.Inode) error { - kernelTask := kernel.TaskFromContext(ctx) - if kernelTask == nil { - log.Warningf("fusefs.Inode.newEntry: couldn't get kernel task from context", i.nodeID) - return linuxerr.EINVAL - } - in := linux.FUSEUnlinkIn{Name: linux.CString(name)} - req := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), i.nodeID, linux.FUSE_UNLINK, &in) - res, err := i.fs.conn.Call(kernelTask, req) - if err != nil { - return err - } - // only return error, discard res. - return res.Error() -} - -// NewDir implements kernfs.Inode.NewDir. -func (i *inode) NewDir(ctx context.Context, name string, opts vfs.MkdirOptions) (kernfs.Inode, error) { - in := linux.FUSEMkdirIn{ - MkdirMeta: linux.FUSEMkdirMeta{ - Mode: uint32(opts.Mode), - Umask: uint32(kernel.TaskFromContext(ctx).FSContext().Umask()), - }, - Name: linux.CString(name), - } - return i.newEntry(ctx, name, linux.S_IFDIR, linux.FUSE_MKDIR, &in) -} - -// RmDir implements kernfs.Inode.RmDir. -func (i *inode) RmDir(ctx context.Context, name string, child kernfs.Inode) error { - fusefs := i.fs - task, creds := kernel.TaskFromContext(ctx), auth.CredentialsFromContext(ctx) - - in := linux.FUSERmDirIn{Name: linux.CString(name)} - req := fusefs.conn.NewRequest(creds, uint32(task.ThreadID()), i.nodeID, linux.FUSE_RMDIR, &in) - res, err := i.fs.conn.Call(task, req) - if err != nil { - return err - } - return res.Error() -} - -func (i *inode) Rename(ctx context.Context, oldname, newname string, child, dstDir kernfs.Inode) error { - fusefs := i.fs - task, creds := kernel.TaskFromContext(ctx), auth.CredentialsFromContext(ctx) - - dstDirInode, ok := dstDir.(*inode) - if !ok { - return linuxerr.EXDEV - } - - in := linux.FUSERenameIn{ - Newdir: primitive.Uint64(dstDirInode.nodeID), - Oldname: linux.CString(oldname), - Newname: linux.CString(newname), - } - req := fusefs.conn.NewRequest(creds, uint32(task.ThreadID()), i.nodeID, linux.FUSE_RENAME, &in) - res, err := i.fs.conn.Call(task, req) - if err != nil { - return err - } - return res.Error() -} - -// newEntry calls FUSE server for entry creation and allocates corresponding entry according to response. -// Shared by FUSE_MKNOD, FUSE_MKDIR, FUSE_SYMLINK, FUSE_LINK and FUSE_LOOKUP. -func (i *inode) newEntry(ctx context.Context, name string, fileType linux.FileMode, opcode linux.FUSEOpcode, payload marshal.Marshallable) (kernfs.Inode, error) { - kernelTask := kernel.TaskFromContext(ctx) - if kernelTask == nil { - log.Warningf("fusefs.Inode.newEntry: couldn't get kernel task from context", i.nodeID) - return nil, linuxerr.EINVAL - } - req := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), i.nodeID, opcode, payload) - res, err := i.fs.conn.Call(kernelTask, req) - if err != nil { - return nil, err - } - if err := res.Error(); err != nil { - return nil, err - } - out := linux.FUSECreateOut{} - if opcode == linux.FUSE_CREATE { - if err := res.UnmarshalPayload(&out); err != nil { - return nil, err - } - } else { - if err := res.UnmarshalPayload(&out.FUSEEntryOut); err != nil { - return nil, err - } - } - if opcode != linux.FUSE_LOOKUP && ((out.Attr.Mode&linux.S_IFMT)^uint32(fileType) != 0 || out.NodeID == 0 || out.NodeID == linux.FUSE_ROOT_ID) { - return nil, linuxerr.EIO - } - child := i.fs.newInode(ctx, out.NodeID, out.Attr) - if opcode == linux.FUSE_CREATE { - // File handler is returned by fuse server at a time of file create. - // Save it temporary in a created child, so Open could return it when invoked - // to be sure after fh is consumed reset 'isNewFh' flag of inode - childI, ok := child.(*inode) - if ok { - childI.isNewFh = true - childI.newFhData.fh = out.FUSEOpenOut.Fh - childI.newFhData.flags = out.FUSEOpenOut.OpenFlag - } - } - return child, nil -} - -// Getlink implements kernfs.Inode.Getlink. -func (i *inode) Getlink(ctx context.Context, mnt *vfs.Mount) (vfs.VirtualDentry, string, error) { - path, err := i.Readlink(ctx, mnt) - return vfs.VirtualDentry{}, path, err -} - -// Readlink implements kernfs.Inode.Readlink. -func (i *inode) Readlink(ctx context.Context, mnt *vfs.Mount) (string, error) { - if i.Mode().FileType()&linux.S_IFLNK == 0 { - return "", linuxerr.EINVAL - } - if len(i.link) == 0 { - kernelTask := kernel.TaskFromContext(ctx) - if kernelTask == nil { - log.Warningf("fusefs.Inode.Readlink: couldn't get kernel task from context") - return "", linuxerr.EINVAL - } - req := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), i.nodeID, linux.FUSE_READLINK, &linux.FUSEEmptyIn{}) - res, err := i.fs.conn.Call(kernelTask, req) - if err != nil { - return "", err - } - i.link = string(res.data[res.hdr.SizeBytes():]) - if !mnt.Options().ReadOnly { - i.attributeTime = 0 - } - } - return i.link, nil -} - -// getFUSEAttr returns a linux.FUSEAttr of this inode stored in local cache. -// TODO(gvisor.dev/issue/3679): Add support for other fields. -func (i *inode) getFUSEAttr() linux.FUSEAttr { - return linux.FUSEAttr{ - Ino: i.Ino(), - Size: i.size.Load(), - Mode: uint32(i.Mode()), - } -} - -// statFromFUSEAttr makes attributes from linux.FUSEAttr to linux.Statx. The -// opts.Sync attribute is ignored since the synchronization is handled by the -// FUSE server. -func statFromFUSEAttr(attr linux.FUSEAttr, mask, devMinor uint32) linux.Statx { - var stat linux.Statx - stat.Blksize = attr.BlkSize - stat.DevMajor, stat.DevMinor = linux.UNNAMED_MAJOR, devMinor - - rdevMajor, rdevMinor := linux.DecodeDeviceID(attr.Rdev) - stat.RdevMajor, stat.RdevMinor = uint32(rdevMajor), rdevMinor - - if mask&linux.STATX_MODE != 0 { - stat.Mode = uint16(attr.Mode) - } - if mask&linux.STATX_NLINK != 0 { - stat.Nlink = attr.Nlink - } - if mask&linux.STATX_UID != 0 { - stat.UID = attr.UID - } - if mask&linux.STATX_GID != 0 { - stat.GID = attr.GID - } - if mask&linux.STATX_ATIME != 0 { - stat.Atime = linux.StatxTimestamp{ - Sec: int64(attr.Atime), - Nsec: attr.AtimeNsec, - } - } - if mask&linux.STATX_MTIME != 0 { - stat.Mtime = linux.StatxTimestamp{ - Sec: int64(attr.Mtime), - Nsec: attr.MtimeNsec, - } - } - if mask&linux.STATX_CTIME != 0 { - stat.Ctime = linux.StatxTimestamp{ - Sec: int64(attr.Ctime), - Nsec: attr.CtimeNsec, - } - } - if mask&linux.STATX_INO != 0 { - stat.Ino = attr.Ino - } - if mask&linux.STATX_SIZE != 0 { - stat.Size = attr.Size - } - if mask&linux.STATX_BLOCKS != 0 { - stat.Blocks = attr.Blocks - } - return stat -} - -// getAttr gets the attribute of this inode by issuing a FUSE_GETATTR request -// or read from local cache. It updates the corresponding attributes if -// necessary. -func (i *inode) getAttr(ctx context.Context, fs *vfs.Filesystem, opts vfs.StatOptions, flags uint32, fh uint64) (linux.FUSEAttr, error) { - attributeVersion := i.fs.conn.attributeVersion.Load() - - // TODO(gvisor.dev/issue/3679): send the request only if - // - invalid local cache for fields specified in the opts.Mask - // - forced update - // - i.attributeTime expired - // If local cache is still valid, return local cache. - // Currently we always send a request, - // and we always set the metadata with the new result, - // unless attributeVersion has changed. - - task := kernel.TaskFromContext(ctx) - if task == nil { - log.Warningf("couldn't get kernel task from context") - return linux.FUSEAttr{}, linuxerr.EINVAL - } - - creds := auth.CredentialsFromContext(ctx) - - in := linux.FUSEGetAttrIn{ - GetAttrFlags: flags, - Fh: fh, - } - req := i.fs.conn.NewRequest(creds, uint32(task.ThreadID()), i.nodeID, linux.FUSE_GETATTR, &in) - res, err := i.fs.conn.Call(task, req) - if err != nil { - return linux.FUSEAttr{}, err - } - if err := res.Error(); err != nil { - return linux.FUSEAttr{}, err - } - - var out linux.FUSEGetAttrOut - if err := res.UnmarshalPayload(&out); err != nil { - return linux.FUSEAttr{}, err - } - - // Local version is newer, return the local one. - // Skip the update. - if attributeVersion != 0 && i.attributeVersion.Load() > attributeVersion { - return i.getFUSEAttr(), nil - } - - // Set the metadata of kernfs.InodeAttrs. - if err := i.InodeAttrs.SetStat(ctx, fs, creds, vfs.SetStatOptions{ - Stat: statFromFUSEAttr(out.Attr, linux.STATX_ALL, i.fs.devMinor), - }); err != nil { - return linux.FUSEAttr{}, err - } - - // Set the size if no error (after SetStat() check). - i.size.Store(out.Attr.Size) - - return out.Attr, nil -} - -// reviseAttr attempts to update the attributes for internal purposes -// by calling getAttr with a pre-specified mask. -// Used by read, write, lseek. -func (i *inode) reviseAttr(ctx context.Context, flags uint32, fh uint64) error { - // Never need atime for internal purposes. - _, err := i.getAttr(ctx, i.fs.VFSFilesystem(), vfs.StatOptions{ - Mask: linux.STATX_BASIC_STATS &^ linux.STATX_ATIME, - }, flags, fh) - return err -} - -// Stat implements kernfs.Inode.Stat. -func (i *inode) Stat(ctx context.Context, fs *vfs.Filesystem, opts vfs.StatOptions) (linux.Statx, error) { - attr, err := i.getAttr(ctx, fs, opts, 0, 0) - if err != nil { - return linux.Statx{}, err - } - - return statFromFUSEAttr(attr, opts.Mask, i.fs.devMinor), nil -} - -// DecRef implements kernfs.Inode.DecRef. -func (i *inode) DecRef(ctx context.Context) { - i.inodeRefs.DecRef(func() { i.Destroy(ctx) }) -} - -// StatFS implements kernfs.Inode.StatFS. -func (i *inode) StatFS(ctx context.Context, fs *vfs.Filesystem) (linux.Statfs, error) { - task := kernel.TaskFromContext(ctx) - if task == nil { - log.Warningf("couldn't get kernel task from context") - return linux.Statfs{}, linuxerr.EINVAL - } - - req := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(task.ThreadID()), i.nodeID, - linux.FUSE_STATFS, &linux.FUSEEmptyIn{}, - ) - res, err := i.fs.conn.Call(task, req) - if err != nil { - return linux.Statfs{}, err - } - if err := res.Error(); err != nil { - return linux.Statfs{}, err - } - - var out linux.FUSEStatfsOut - if err := res.UnmarshalPayload(&out); err != nil { - return linux.Statfs{}, err - } - - return linux.Statfs{ - Type: linux.FUSE_SUPER_MAGIC, - Blocks: uint64(out.Blocks), - BlocksFree: out.BlocksFree, - BlocksAvailable: out.BlocksAvailable, - Files: out.Files, - FilesFree: out.FilesFree, - BlockSize: int64(out.BlockSize), - NameLength: uint64(out.NameLength), - FragmentSize: int64(out.FragmentSize), - }, nil -} - -// fattrMaskFromStats converts vfs.SetStatOptions.Stat.Mask to linux stats mask -// aligned with the attribute mask defined in include/linux/fs.h. -func fattrMaskFromStats(mask uint32) uint32 { - var fuseAttrMask uint32 - maskMap := map[uint32]uint32{ - linux.STATX_MODE: linux.FATTR_MODE, - linux.STATX_UID: linux.FATTR_UID, - linux.STATX_GID: linux.FATTR_GID, - linux.STATX_SIZE: linux.FATTR_SIZE, - linux.STATX_ATIME: linux.FATTR_ATIME, - linux.STATX_MTIME: linux.FATTR_MTIME, - linux.STATX_CTIME: linux.FATTR_CTIME, - } - for statxMask, fattrMask := range maskMap { - if mask&statxMask != 0 { - fuseAttrMask |= fattrMask - } - } - return fuseAttrMask -} - -// SetStat implements kernfs.Inode.SetStat. -func (i *inode) SetStat(ctx context.Context, fs *vfs.Filesystem, creds *auth.Credentials, opts vfs.SetStatOptions) error { - return i.setAttr(ctx, fs, creds, opts, false, 0) -} - -func (i *inode) setAttr(ctx context.Context, fs *vfs.Filesystem, creds *auth.Credentials, opts vfs.SetStatOptions, useFh bool, fh uint64) error { - conn := i.fs.conn - task := kernel.TaskFromContext(ctx) - if task == nil { - log.Warningf("couldn't get kernel task from context") - return linuxerr.EINVAL - } - - // We should retain the original file type when assigning new mode. - fileType := uint16(i.Mode()) & linux.S_IFMT - fattrMask := fattrMaskFromStats(opts.Stat.Mask) - if useFh { - fattrMask |= linux.FATTR_FH - } - in := linux.FUSESetAttrIn{ - Valid: fattrMask, - Fh: fh, - Size: opts.Stat.Size, - Atime: uint64(opts.Stat.Atime.Sec), - Mtime: uint64(opts.Stat.Mtime.Sec), - Ctime: uint64(opts.Stat.Ctime.Sec), - AtimeNsec: opts.Stat.Atime.Nsec, - MtimeNsec: opts.Stat.Mtime.Nsec, - CtimeNsec: opts.Stat.Ctime.Nsec, - Mode: uint32(fileType | opts.Stat.Mode), - UID: opts.Stat.UID, - GID: opts.Stat.GID, - } - req := conn.NewRequest(creds, uint32(task.ThreadID()), i.nodeID, linux.FUSE_SETATTR, &in) - res, err := conn.Call(task, req) - if err != nil { - return err - } - if err := res.Error(); err != nil { - return err - } - out := linux.FUSEGetAttrOut{} - if err := res.UnmarshalPayload(&out); err != nil { - return err - } - - // Set the metadata of kernfs.InodeAttrs. - if err := i.InodeAttrs.SetStat(ctx, fs, creds, vfs.SetStatOptions{ - Stat: statFromFUSEAttr(out.Attr, linux.STATX_ALL, i.fs.devMinor), - }); err != nil { - return err - } - - return nil -} diff --git a/pkg/sentry/fsimpl/fuse/inode.go b/pkg/sentry/fsimpl/fuse/inode.go new file mode 100644 index 000000000..002755c3f --- /dev/null +++ b/pkg/sentry/fsimpl/fuse/inode.go @@ -0,0 +1,837 @@ +// 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 fuse + +import ( + "fmt" + "sync" + + "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/log" + "gvisor.dev/gvisor/pkg/marshal" + "gvisor.dev/gvisor/pkg/marshal/primitive" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs" + "gvisor.dev/gvisor/pkg/sentry/kernel" + "gvisor.dev/gvisor/pkg/sentry/kernel/auth" + "gvisor.dev/gvisor/pkg/sentry/vfs" +) + +// +stateify savable +type fileHandle struct { + new bool + handle uint64 + flags uint32 +} + +// inode implements kernfs.Inode. +// +// +stateify savable +type inode struct { + inodeRefs + kernfs.InodeAlwaysValid + kernfs.InodeDirectoryNoNewChildren + kernfs.InodeNotSymlink + kernfs.InodeWatches + kernfs.OrderedChildren + + // the owning filesystem. fs is immutable. + fs *filesystem + + // nodeID is a unique id which identifies the inode between userspace + // and the sentry. Immutable. + nodeID uint64 + + // attrVersion is the version of the last attribute change. + attrVersion atomicbitops.Uint64 + + // attrTime is the time until the attributes are valid. + attrTime uint64 + + // link is result of following a symbolic link. + link string + + // fh caches the file handle returned by the server from a FUSE_CREATE request + // so we don't have to send a separate FUSE_OPEN request. + fh fileHandle + + locks vfs.FileLocks + watches vfs.Watches + + // attrMu protects the attributes of this inode. + attrMu sync.Mutex + + // +checklocks:attrMu + ino atomicbitops.Uint64 // Stat data, not accessed for path walking. + // +checklocks:attrMu + uid atomicbitops.Uint32 // auth.KUID, but stored as raw uint32 for sync/atomic. + // +checklocks:attrMu + gid atomicbitops.Uint32 // auth.KGID, but... + // +checklocks:attrMu + mode atomicbitops.Uint32 // File type and mode. + + // Timestamps in nanoseconds from the unix epoch. + // +checklocks:attrMu + atime atomicbitops.Int64 + // +checklocks:attrMu + mtime atomicbitops.Int64 + // +checklocks:attrMu + ctime atomicbitops.Int64 + + // +checklocks:attrMu + size atomicbitops.Uint64 + + // nlink counts the number of hard links to this inode. It's updated and + // accessed used atomic operations but not protected by attrMu. + nlink atomicbitops.Uint32 + + // +checklocks:attrMu + blockSize atomicbitops.Uint32 // 0 if unknown. +} + +func (i *inode) Mode() linux.FileMode { + i.attrMu.Lock() + defer i.attrMu.Unlock() + return i.filemode() +} + +// +checklocks:i.attrMu +func (i *inode) filemode() linux.FileMode { + return linux.FileMode(i.mode.Load()) +} + +// touchCMTime updates the ctime and mtime attributes to be the current time. +// +// +checklocks:i.attrMu +func (i *inode) touchCMtime() { + now := i.fs.clock.Now().Nanoseconds() + i.mtime.Store(now) + i.ctime.Store(now) +} + +// touchAtime updates the atime attribut to be the current time. +// +// +checklocks:i.attrMu +func (i *inode) touchAtime() { + i.atime.Store(i.fs.clock.Now().Nanoseconds()) +} + +// +checklocks:i.attrMu +func (i *inode) init(creds *auth.Credentials, devMajor, devMinor uint32, nodeid uint64, mode linux.FileMode) { + if mode.FileType() == 0 { + panic(fmt.Sprintf("No file type specified in 'mode' for InodeAttrs.Init(): mode=0%o", mode)) + } + + nlink := uint32(1) + if mode.FileType() == linux.ModeDirectory { + nlink = 2 + } + i.nodeID = nodeid + i.ino.Store(nodeid) + i.mode.Store(uint32(mode)) + i.uid.Store(uint32(creds.EffectiveKUID)) + i.gid.Store(uint32(creds.EffectiveKGID)) + i.nlink.Store(nlink) + i.blockSize.Store(hostarch.PageSize) + + now := i.fs.clock.Now().Nanoseconds() + i.atime.Store(now) + i.mtime.Store(now) + i.ctime.Store(now) +} + +// CheckPermissions implements kernfs.Inode.CheckPermissions. +func (i *inode) CheckPermissions(ctx context.Context, creds *auth.Credentials, ats vfs.AccessTypes) error { + // Since FUSE operations are ultimately backed by a userspace process (the + // fuse daemon), allowing a process to call into fusefs grants the daemon + // ptrace-like capabilities over the calling process. Because of this, by + // default FUSE only allows the mount owner to interact with the + // filesystem. This explicitly excludes setuid/setgid processes. + // + // This behaviour can be overriden with the 'allow_other' mount option. + // + // See fs/fuse/dir.c:fuse_allow_current_process() in Linux. + if !i.fs.opts.allowOther { + if creds.RealKUID != i.fs.opts.uid || + creds.EffectiveKUID != i.fs.opts.uid || + creds.SavedKUID != i.fs.opts.uid || + creds.RealKGID != i.fs.opts.gid || + creds.EffectiveKGID != i.fs.opts.gid || + creds.SavedKGID != i.fs.opts.gid { + return linuxerr.EACCES + } + } + + // By default, fusefs delegates all permission checks to the server. + // However, standard unix permission checks can be enabled with the + // default_permissions mount option. + i.attrMu.Lock() + defer i.attrMu.Unlock() + refreshed := false + opts := vfs.StatOptions{Mask: linux.STATX_MODE | linux.STATX_UID | linux.STATX_GID} + if i.fs.opts.defaultPermissions || (ats.MayExec() && i.filemode().FileType() == linux.S_IFREG) { + if uint64(i.fs.clock.Now().Nanoseconds()) > i.attrTime { + refreshed = true + if _, err := i.getAttr(ctx, i.fs.VFSFilesystem(), opts, 0, 0); err != nil { + return err + } + } + } + + if i.fs.opts.defaultPermissions || (ats.MayExec() && i.filemode().FileType() == linux.S_IFREG) { + err := vfs.GenericCheckPermissions(creds, ats, linux.FileMode(i.mode.Load()), auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())) + if linuxerr.Equals(linuxerr.EACCES, err) && !refreshed { + if _, err := i.getAttr(ctx, i.fs.VFSFilesystem(), opts, 0, 0); err != nil { + return err + } + return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(i.mode.Load()), auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())) + } + return err + } else if ats.MayRead() || ats.MayWrite() || ats.MayExec() { + kernelTask := kernel.TaskFromContext(ctx) + if kernelTask == nil { + log.Warningf("fusefs.Inode.CheckPermissions: couldn't get kernel task from context") + return linuxerr.EINVAL + } + in := linux.FUSEAccessIn{Mask: uint32(ats)} + req := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), i.nodeID, linux.FUSE_ACCESS, &in) + res, err := i.fs.conn.Call(kernelTask, req) + if err != nil { + return err + } + return res.Error() + } + return nil +} + +// Open implements kernfs.Inode.Open. +func (i *inode) Open(ctx context.Context, rp *vfs.ResolvingPath, d *kernfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) { + opts.Flags &= linux.O_ACCMODE | linux.O_CREAT | linux.O_EXCL | linux.O_TRUNC | + linux.O_DIRECTORY | linux.O_NOFOLLOW | linux.O_NONBLOCK | linux.O_NOCTTY | + linux.O_APPEND | linux.O_DIRECT + i.attrMu.Lock() + defer i.attrMu.Unlock() + if opts.Flags&linux.O_LARGEFILE == 0 && i.size.Load() > linux.MAX_NON_LFS { + return nil, linuxerr.EOVERFLOW + } + + var ( + fd *fileDescription + fdImpl vfs.FileDescriptionImpl + opcode linux.FUSEOpcode + ) + switch i.filemode().FileType() { + case linux.S_IFREG: + regularFD := ®ularFileFD{} + fd = &(regularFD.fileDescription) + fdImpl = regularFD + opcode = linux.FUSE_OPEN + case linux.S_IFDIR: + if opts.Flags&linux.O_CREAT != 0 { + return nil, linuxerr.EISDIR + } + if ats := vfs.AccessTypesForOpenFlags(&opts); ats.MayWrite() { + return nil, linuxerr.EISDIR + } + if opts.Flags&linux.O_DIRECT != 0 { + return nil, linuxerr.EINVAL + } + directoryFD := &directoryFD{} + fd = &(directoryFD.fileDescription) + fdImpl = directoryFD + opcode = linux.FUSE_OPENDIR + case linux.S_IFLNK: + return nil, linuxerr.ELOOP + } + + fd.LockFD.Init(&i.locks) + // FOPEN_KEEP_CACHE is the defualt flag for noOpen. + fd.OpenFlag = linux.FOPEN_KEEP_CACHE + + if i.fh.new { + fd.OpenFlag = i.fh.flags + fd.Fh = i.fh.handle + i.fh.new = false + // Only send an open request when the FUSE server supports open or is + // opening a directory. + } else if !i.fs.conn.noOpen || i.filemode().IsDir() { + kernelTask := kernel.TaskFromContext(ctx) + if kernelTask == nil { + log.Warningf("fusefs.Inode.Open: couldn't get kernel task from context") + return nil, linuxerr.EINVAL + } + + in := linux.FUSEOpenIn{Flags: opts.Flags & ^uint32(linux.O_CREAT|linux.O_EXCL|linux.O_NOCTTY)} + // Truncating with SETATTR instead of O_TRUNC, so clear the flag. + if !i.fs.conn.atomicOTrunc { + in.Flags &= ^uint32(linux.O_TRUNC) + } + + req := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), i.nodeID, opcode, &in) + res, err := i.fs.conn.Call(kernelTask, req) + if err != nil { + return nil, err + } + if err := res.Error(); err != nil { + if linuxerr.Equals(linuxerr.ENOSYS, err) && !i.filemode().IsDir() { + i.fs.conn.noOpen = true + } else { + return nil, err + } + } else { + out := linux.FUSEOpenOut{} + if err := res.UnmarshalPayload(&out); err != nil { + return nil, err + } + fd.OpenFlag = out.OpenFlag + fd.Fh = out.Fh + } + } + if i.filemode().IsDir() { + fd.OpenFlag &= ^uint32(linux.FOPEN_DIRECT_IO) + } + + // TODO(gvisor.dev/issue/3234): invalidate mmap after implemented it for FUSE Inode + fd.DirectIO = fd.OpenFlag&linux.FOPEN_DIRECT_IO != 0 + fdOptions := &vfs.FileDescriptionOptions{} + if fd.OpenFlag&linux.FOPEN_NONSEEKABLE != 0 { + fdOptions.DenyPRead = true + fdOptions.DenyPWrite = true + fd.Nonseekable = true + } + + // If atomicOTrunc and O_TRUNC are set, just update the inode's version number + // and set its size to 0 since the truncation is handled by the FUSE daemon. + // Otherwise send a separate SETATTR to truncate the file size. + if opts.Flags&linux.O_TRUNC != 0 && i.filemode().FileType() == linux.S_IFREG { + if i.fs.conn.atomicOTrunc { + i.fs.conn.mu.Lock() + i.attrVersion.Store(i.fs.conn.attributeVersion.Add(1)) + i.fs.conn.mu.Unlock() + i.size.Store(0) + i.touchCMtime() + } else { + opts := vfs.SetStatOptions{Stat: linux.Statx{Size: 0, Mask: linux.STATX_SIZE}} + i.setAttr(ctx, i.fs.VFSFilesystem(), auth.CredentialsFromContext(ctx), opts, fhOptions{useFh: true, fh: i.fh.handle}) + } + } + + if err := fd.vfsfd.Init(fdImpl, opts.Flags, rp.Mount(), d.VFSDentry(), fdOptions); err != nil { + return nil, err + } + return &fd.vfsfd, nil +} + +// Lookup implements kernfs.Inode.Lookup. +func (i *inode) Lookup(ctx context.Context, name string) (kernfs.Inode, error) { + in := linux.FUSELookupIn{Name: linux.CString(name)} + return i.newEntry(ctx, name, 0, linux.FUSE_LOOKUP, &in) +} + +// Keep implements kernfs.Inode.Keep. +func (i *inode) Keep() bool { + // Return true so that kernfs keeps the new dentry pointing to this + // inode in the dentry tree. This is needed because inodes created via + // Lookup are not temporary. They might refer to existing files on server + // that can be Unlink'd/Rmdir'd. + return true +} + +// IterDirents implements kernfs.Inode.IterDirents. +func (*inode) IterDirents(ctx context.Context, mnt *vfs.Mount, callback vfs.IterDirentsCallback, offset, relOffset int64) (int64, error) { + return offset, nil +} + +// NewFile implements kernfs.Inode.NewFile. +func (i *inode) NewFile(ctx context.Context, name string, opts vfs.OpenOptions) (kernfs.Inode, error) { + opts.Flags &= linux.O_ACCMODE | linux.O_CREAT | linux.O_EXCL | linux.O_TRUNC | + linux.O_DIRECTORY | linux.O_NOFOLLOW | linux.O_NONBLOCK | linux.O_NOCTTY + kernelTask := kernel.TaskFromContext(ctx) + if kernelTask == nil { + log.Warningf("fusefs.Inode.NewFile: couldn't get kernel task from context", i.nodeID) + return nil, linuxerr.EINVAL + } + in := linux.FUSECreateIn{ + CreateMeta: linux.FUSECreateMeta{ + Flags: opts.Flags, + Mode: uint32(opts.Mode) | linux.S_IFREG, + Umask: uint32(kernelTask.FSContext().Umask()), + }, + Name: linux.CString(name), + } + return i.newEntry(ctx, name, linux.S_IFREG, linux.FUSE_CREATE, &in) +} + +// NewNode implements kernfs.Inode.NewNode. +func (i *inode) NewNode(ctx context.Context, name string, opts vfs.MknodOptions) (kernfs.Inode, error) { + in := linux.FUSEMknodIn{ + MknodMeta: linux.FUSEMknodMeta{ + Mode: uint32(opts.Mode), + Rdev: linux.MakeDeviceID(uint16(opts.DevMajor), opts.DevMinor), + Umask: uint32(kernel.TaskFromContext(ctx).FSContext().Umask()), + }, + Name: linux.CString(name), + } + return i.newEntry(ctx, name, opts.Mode.FileType(), linux.FUSE_MKNOD, &in) +} + +// NewSymlink implements kernfs.Inode.NewSymlink. +func (i *inode) NewSymlink(ctx context.Context, name, target string) (kernfs.Inode, error) { + in := linux.FUSESymlinkIn{ + Name: linux.CString(name), + Target: linux.CString(target), + } + return i.newEntry(ctx, name, linux.S_IFLNK, linux.FUSE_SYMLINK, &in) +} + +// Unlink implements kernfs.Inode.Unlink. +func (i *inode) Unlink(ctx context.Context, name string, child kernfs.Inode) error { + kernelTask := kernel.TaskFromContext(ctx) + if kernelTask == nil { + log.Warningf("fusefs.Inode.newEntry: couldn't get kernel task from context", i.nodeID) + return linuxerr.EINVAL + } + in := linux.FUSEUnlinkIn{Name: linux.CString(name)} + req := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), i.nodeID, linux.FUSE_UNLINK, &in) + res, err := i.fs.conn.Call(kernelTask, req) + if err != nil { + return err + } + // only return error, discard res. + return res.Error() +} + +// NewDir implements kernfs.Inode.NewDir. +func (i *inode) NewDir(ctx context.Context, name string, opts vfs.MkdirOptions) (kernfs.Inode, error) { + in := linux.FUSEMkdirIn{ + MkdirMeta: linux.FUSEMkdirMeta{ + Mode: uint32(opts.Mode), + Umask: uint32(kernel.TaskFromContext(ctx).FSContext().Umask()), + }, + Name: linux.CString(name), + } + return i.newEntry(ctx, name, linux.S_IFDIR, linux.FUSE_MKDIR, &in) +} + +// RmDir implements kernfs.Inode.RmDir. +func (i *inode) RmDir(ctx context.Context, name string, child kernfs.Inode) error { + fusefs := i.fs + task, creds := kernel.TaskFromContext(ctx), auth.CredentialsFromContext(ctx) + + in := linux.FUSERmDirIn{Name: linux.CString(name)} + req := fusefs.conn.NewRequest(creds, uint32(task.ThreadID()), i.nodeID, linux.FUSE_RMDIR, &in) + res, err := i.fs.conn.Call(task, req) + if err != nil { + return err + } + return res.Error() +} + +// Rename implements kernfs.Inode.Rename. +func (i *inode) Rename(ctx context.Context, oldname, newname string, child, dstDir kernfs.Inode) error { + fusefs := i.fs + task, creds := kernel.TaskFromContext(ctx), auth.CredentialsFromContext(ctx) + + dstDirInode, ok := dstDir.(*inode) + if !ok { + return linuxerr.EXDEV + } + + in := linux.FUSERenameIn{ + Newdir: primitive.Uint64(dstDirInode.nodeID), + Oldname: linux.CString(oldname), + Newname: linux.CString(newname), + } + req := fusefs.conn.NewRequest(creds, uint32(task.ThreadID()), i.nodeID, linux.FUSE_RENAME, &in) + res, err := i.fs.conn.Call(task, req) + if err != nil { + return err + } + return res.Error() +} + +// newEntry calls FUSE server for entry creation and allocates corresponding +// entry according to response. Shared by FUSE_MKNOD, FUSE_MKDIR, FUSE_SYMLINK, +// FUSE_LINK and FUSE_LOOKUP. +func (i *inode) newEntry(ctx context.Context, name string, fileType linux.FileMode, opcode linux.FUSEOpcode, payload marshal.Marshallable) (kernfs.Inode, error) { + kernelTask := kernel.TaskFromContext(ctx) + if kernelTask == nil { + log.Warningf("fusefs.Inode.newEntry: couldn't get kernel task from context", i.nodeID) + return nil, linuxerr.EINVAL + } + req := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), i.nodeID, opcode, payload) + res, err := i.fs.conn.Call(kernelTask, req) + if err != nil { + return nil, err + } + if err := res.Error(); err != nil { + return nil, err + } + out := linux.FUSECreateOut{} + if opcode == linux.FUSE_CREATE { + if err := res.UnmarshalPayload(&out); err != nil { + return nil, err + } + } else { + if err := res.UnmarshalPayload(&out.FUSEEntryOut); err != nil { + return nil, err + } + } + if opcode != linux.FUSE_LOOKUP && ((out.Attr.Mode&linux.S_IFMT)^uint32(fileType) != 0 || out.NodeID == 0 || out.NodeID == linux.FUSE_ROOT_ID) { + return nil, linuxerr.EIO + } + child := i.fs.newInode(ctx, out.NodeID, out.Attr) + if opcode == linux.FUSE_CREATE { + // File handler is returned by fuse server at a time of file create. + // Save it temporary in a created child, so Open could return it when invoked + // to be sure after fh is consumed reset 'isNewFh' flag of inode + childI, ok := child.(*inode) + if ok { + childI.fh.new = true + childI.fh.handle = out.FUSEOpenOut.Fh + childI.fh.flags = out.FUSEOpenOut.OpenFlag + } + } + return child, nil +} + +// Getlink implements kernfs.Inode.Getlink. +func (i *inode) Getlink(ctx context.Context, mnt *vfs.Mount) (vfs.VirtualDentry, string, error) { + path, err := i.Readlink(ctx, mnt) + return vfs.VirtualDentry{}, path, err +} + +// Readlink implements kernfs.Inode.Readlink. +func (i *inode) Readlink(ctx context.Context, mnt *vfs.Mount) (string, error) { + i.attrMu.Lock() + defer i.attrMu.Unlock() + if i.filemode().FileType()&linux.S_IFLNK == 0 { + return "", linuxerr.EINVAL + } + if len(i.link) == 0 { + kernelTask := kernel.TaskFromContext(ctx) + if kernelTask == nil { + log.Warningf("fusefs.Inode.Readlink: couldn't get kernel task from context") + return "", linuxerr.EINVAL + } + req := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), i.nodeID, linux.FUSE_READLINK, &linux.FUSEEmptyIn{}) + res, err := i.fs.conn.Call(kernelTask, req) + if err != nil { + return "", err + } + i.link = string(res.data[res.hdr.SizeBytes():]) + if !mnt.Options().ReadOnly { + i.attrTime = 0 + } + } + return i.link, nil +} + +// getFUSEAttr returns a linux.FUSEAttr of this inode stored in local cache. +// +// +checklocks:i.attrMu +func (i *inode) getFUSEAttr() linux.FUSEAttr { + return linux.FUSEAttr{ + Ino: i.nodeID, + UID: i.uid.Load(), + GID: i.gid.Load(), + Size: i.size.Load(), + Mode: uint32(i.filemode()), + BlkSize: i.blockSize.Load(), + Atime: uint64(i.atime.Load()), + Mtime: uint64(i.mtime.Load()), + Ctime: uint64(i.ctime.Load()), + Nlink: i.nlink.Load(), + } +} + +// statFromFUSEAttr makes attributes from linux.FUSEAttr to linux.Statx. The +// opts.Sync attribute is ignored since the synchronization is handled by the +// FUSE server. +func statFromFUSEAttr(attr linux.FUSEAttr, mask, devMinor uint32) linux.Statx { + var stat linux.Statx + stat.Blksize = attr.BlkSize + stat.DevMajor, stat.DevMinor = linux.UNNAMED_MAJOR, devMinor + + rdevMajor, rdevMinor := linux.DecodeDeviceID(attr.Rdev) + stat.RdevMajor, stat.RdevMinor = uint32(rdevMajor), rdevMinor + + if mask&linux.STATX_MODE != 0 { + stat.Mode = uint16(attr.Mode) + } + if mask&linux.STATX_NLINK != 0 { + stat.Nlink = attr.Nlink + } + if mask&linux.STATX_UID != 0 { + stat.UID = attr.UID + } + if mask&linux.STATX_GID != 0 { + stat.GID = attr.GID + } + if mask&linux.STATX_ATIME != 0 { + stat.Atime = linux.StatxTimestamp{ + Sec: int64(attr.Atime), + Nsec: attr.AtimeNsec, + } + } + if mask&linux.STATX_MTIME != 0 { + stat.Mtime = linux.StatxTimestamp{ + Sec: int64(attr.Mtime), + Nsec: attr.MtimeNsec, + } + } + if mask&linux.STATX_CTIME != 0 { + stat.Ctime = linux.StatxTimestamp{ + Sec: int64(attr.Ctime), + Nsec: attr.CtimeNsec, + } + } + if mask&linux.STATX_INO != 0 { + stat.Ino = attr.Ino + } + if mask&linux.STATX_SIZE != 0 { + stat.Size = attr.Size + } + if mask&linux.STATX_BLOCKS != 0 { + stat.Blocks = attr.Blocks + } + return stat +} + +// getAttr gets the attribute of this inode by issuing a FUSE_GETATTR request +// or read from local cache. It updates the corresponding attributes if +// necessary. +// +// +checklocks:i.attrMu +func (i *inode) getAttr(ctx context.Context, fs *vfs.Filesystem, opts vfs.StatOptions, flags uint32, fh uint64) (linux.FUSEAttr, error) { + // TODO(gvisor.dev/issue/3679): send the request only if + // - invalid local cache for fields specified in the opts.Mask + // - forced update + // - i.attributeTime expired + // If local cache is still valid, return local cache. + // Currently we always send a request, + // and we always set the metadata with the new result, + // unless attributeVersion has changed. + + task := kernel.TaskFromContext(ctx) + if task == nil { + log.Warningf("couldn't get kernel task from context") + return linux.FUSEAttr{}, linuxerr.EINVAL + } + + creds := auth.CredentialsFromContext(ctx) + + in := linux.FUSEGetAttrIn{ + GetAttrFlags: flags, + Fh: fh, + } + req := i.fs.conn.NewRequest(creds, uint32(task.ThreadID()), i.nodeID, linux.FUSE_GETATTR, &in) + res, err := i.fs.conn.Call(task, req) + if err != nil { + return linux.FUSEAttr{}, err + } + if err := res.Error(); err != nil { + return linux.FUSEAttr{}, err + } + var out linux.FUSEAttrOut + if err := res.UnmarshalPayload(&out); err != nil { + return linux.FUSEAttr{}, err + } + + // Local version is newer, return the local one. + i.fs.conn.mu.Lock() + attributeVersion := i.fs.conn.attributeVersion.Load() + if attributeVersion != 0 && i.attrVersion.Load() > attributeVersion { + i.fs.conn.mu.Unlock() + return i.getFUSEAttr(), nil + } + i.fs.conn.mu.Unlock() + i.updateAttrs(out.Attr, out.AttrValid) + return out.Attr, nil +} + +// reviseAttr attempts to update the attributes for internal purposes +// by calling getAttr with a pre-specified mask. +// Used by read, write, lseek. +// +// +checklocks:i.attrMu +func (i *inode) reviseAttr(ctx context.Context, flags uint32, fh uint64) error { + // Never need atime for internal purposes. + _, err := i.getAttr(ctx, i.fs.VFSFilesystem(), vfs.StatOptions{ + Mask: linux.STATX_BASIC_STATS &^ linux.STATX_ATIME, + }, flags, fh) + return err +} + +// Stat implements kernfs.Inode.Stat. +func (i *inode) Stat(ctx context.Context, fs *vfs.Filesystem, opts vfs.StatOptions) (linux.Statx, error) { + i.attrMu.Lock() + defer i.attrMu.Unlock() + attr, err := i.getAttr(ctx, fs, opts, 0, 0) + if err != nil { + return linux.Statx{}, err + } + + return statFromFUSEAttr(attr, opts.Mask, i.fs.devMinor), nil +} + +// DecRef implements kernfs.Inode.DecRef. +func (i *inode) DecRef(ctx context.Context) { + i.inodeRefs.DecRef(func() { i.Destroy(ctx) }) +} + +// StatFS implements kernfs.Inode.StatFS. +func (i *inode) StatFS(ctx context.Context, fs *vfs.Filesystem) (linux.Statfs, error) { + task := kernel.TaskFromContext(ctx) + if task == nil { + log.Warningf("couldn't get kernel task from context") + return linux.Statfs{}, linuxerr.EINVAL + } + + req := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(task.ThreadID()), i.nodeID, + linux.FUSE_STATFS, &linux.FUSEEmptyIn{}, + ) + res, err := i.fs.conn.Call(task, req) + if err != nil { + return linux.Statfs{}, err + } + if err := res.Error(); err != nil { + return linux.Statfs{}, err + } + + var out linux.FUSEStatfsOut + if err := res.UnmarshalPayload(&out); err != nil { + return linux.Statfs{}, err + } + + return linux.Statfs{ + Type: linux.FUSE_SUPER_MAGIC, + Blocks: uint64(out.Blocks), + BlocksFree: out.BlocksFree, + BlocksAvailable: out.BlocksAvailable, + Files: out.Files, + FilesFree: out.FilesFree, + BlockSize: int64(out.BlockSize), + NameLength: uint64(out.NameLength), + FragmentSize: int64(out.FragmentSize), + }, nil +} + +// fattrMaskFromStats converts vfs.SetStatOptions.Stat.Mask to linux stats mask +// aligned with the attribute mask defined in include/linux/fs.h. +func fattrMaskFromStats(mask uint32) uint32 { + var fuseAttrMask uint32 + maskMap := map[uint32]uint32{ + linux.STATX_MODE: linux.FATTR_MODE, + linux.STATX_UID: linux.FATTR_UID, + linux.STATX_GID: linux.FATTR_GID, + linux.STATX_SIZE: linux.FATTR_SIZE, + linux.STATX_ATIME: linux.FATTR_ATIME, + linux.STATX_MTIME: linux.FATTR_MTIME, + linux.STATX_CTIME: linux.FATTR_CTIME, + } + for statxMask, fattrMask := range maskMap { + if mask&statxMask != 0 { + fuseAttrMask |= fattrMask + } + } + return fuseAttrMask +} + +// SetStat implements kernfs.Inode.SetStat. +func (i *inode) SetStat(ctx context.Context, fs *vfs.Filesystem, creds *auth.Credentials, opts vfs.SetStatOptions) error { + if opts.Stat.Mask == 0 { + return nil + } + i.attrMu.Lock() + defer i.attrMu.Unlock() + return i.setAttr(ctx, fs, creds, opts, fhOptions{useFh: false}) +} + +type fhOptions struct { + useFh bool + fh uint64 +} + +// +checklocks:i.attrMu +func (i *inode) setAttr(ctx context.Context, fs *vfs.Filesystem, creds *auth.Credentials, opts vfs.SetStatOptions, fhOpts fhOptions) error { + task := kernel.TaskFromContext(ctx) + if task == nil { + log.Warningf("couldn't get kernel task from context") + return linuxerr.EINVAL + } + + // We should retain the original file type when assigning a new mode. + fattrMask := fattrMaskFromStats(opts.Stat.Mask) + if fhOpts.useFh { + fattrMask |= linux.FATTR_FH + } + in := linux.FUSESetAttrIn{ + Valid: fattrMask, + Fh: fhOpts.fh, + Size: opts.Stat.Size, + Atime: uint64(opts.Stat.Atime.Sec), + Mtime: uint64(opts.Stat.Mtime.Sec), + Ctime: uint64(opts.Stat.Ctime.Sec), + AtimeNsec: opts.Stat.Atime.Nsec, + MtimeNsec: opts.Stat.Mtime.Nsec, + CtimeNsec: opts.Stat.Ctime.Nsec, + Mode: uint32(uint16(i.filemode().FileType()) | opts.Stat.Mode), + UID: opts.Stat.UID, + GID: opts.Stat.GID, + } + req := i.fs.conn.NewRequest(creds, uint32(task.ThreadID()), i.nodeID, linux.FUSE_SETATTR, &in) + res, err := i.fs.conn.Call(task, req) + if err != nil { + return err + } + if err := res.Error(); err != nil { + return err + } + out := linux.FUSEAttrOut{} + if err := res.UnmarshalPayload(&out); err != nil { + return err + } + i.updateAttrs(out.Attr, out.AttrValid) + return nil +} + +// +checklocks:i.attrMu +func (i *inode) updateAttrs(attr linux.FUSEAttr, attrTimeout uint64) { + i.fs.conn.mu.Lock() + i.attrVersion.Store(i.fs.conn.attributeVersion.Add(1)) + i.fs.conn.mu.Unlock() + i.attrTime = attrTimeout + + i.ino.Store(attr.Ino) + + i.mode.Store((attr.Mode & 07777) | (i.mode.Load() & linux.S_IFMT)) + i.uid.Store(attr.UID) + i.gid.Store(attr.GID) + + i.atime.Store(int64(attr.Atime)) + i.mtime.Store(int64(attr.Mtime)) + i.ctime.Store(int64(attr.Ctime)) + + i.size.Store(attr.Size) + i.nlink.Store(attr.Nlink) + + if !i.fs.opts.defaultPermissions { + i.mode.Store(i.mode.Load() & ^uint32(linux.S_ISVTX)) + } +} diff --git a/pkg/sentry/fsimpl/fuse/read_write.go b/pkg/sentry/fsimpl/fuse/read_write.go index 5e4fbb8b4..f41e33f6d 100644 --- a/pkg/sentry/fsimpl/fuse/read_write.go +++ b/pkg/sentry/fsimpl/fuse/read_write.go @@ -44,7 +44,7 @@ func (fs *filesystem) ReadInPages(ctx context.Context, fd *regularFileFD, off ui // Round up to a multiple of page size. readSize, _ := hostarch.PageRoundUp(uint64(size)) - // One request cannnot exceed either maxRead or maxPages. + // One request cannot exceed either maxRead or maxPages. maxPages := fs.conn.maxRead >> hostarch.PageShift if maxPages > uint32(fs.conn.maxPages) { maxPages = uint32(fs.conn.maxPages) @@ -109,7 +109,7 @@ func (fs *filesystem) ReadInPages(ctx context.Context, fd *regularFileFD, off ui pagesRead += pagesCanRead } - defer fs.ReadCallback(ctx, fd, off, size, sizeRead, attributeVersion) + defer fs.ReadCallback(ctx, fd.inode(), off, size, sizeRead, attributeVersion) // +checklocksforce: fd.inode() locks are held during fd operations. // No bytes returned: offset >= EOF. if len(outs) == 0 { @@ -121,14 +121,13 @@ func (fs *filesystem) ReadInPages(ctx context.Context, fd *regularFileFD, off ui // ReadCallback updates several information after receiving a read response. // Due to readahead, sizeRead can be larger than size. -func (fs *filesystem) ReadCallback(ctx context.Context, fd *regularFileFD, off uint64, size uint32, sizeRead uint32, attributeVersion uint64) { +// +// +checklocks:i.attrMu +func (fs *filesystem) ReadCallback(ctx context.Context, i *inode, off uint64, size uint32, sizeRead uint32, attributeVersion uint64) { // TODO(gvisor.dev/issue/3247): support async read. // If this is called by an async read, correctly process it. // May need to update the signature. - - i := fd.inode() - i.InodeAttrs.TouchAtime(ctx, fd.vfsfd.Mount()) - + i.touchAtime() // Reached EOF. if sizeRead < size { // TODO(gvisor.dev/issue/3630): If we have writeback cache, then we need to fill this hole. @@ -137,8 +136,8 @@ func (fs *filesystem) ReadCallback(ctx context.Context, fd *regularFileFD, off u // Update existing size. newSize := off + uint64(sizeRead) fs.conn.mu.Lock() - if attributeVersion == i.attributeVersion.Load() && newSize < i.size.Load() { - i.attributeVersion.Store(i.fs.conn.attributeVersion.Add(1)) + if attributeVersion == i.attrVersion.Load() && newSize < i.size.Load() { + i.attrVersion.Store(i.fs.conn.attributeVersion.Add(1)) i.size.Store(newSize) } fs.conn.mu.Unlock() @@ -156,7 +155,7 @@ func (fs *filesystem) Write(ctx context.Context, fd *regularFileFD, off uint64, return 0, linuxerr.EINVAL } - // One request cannnot exceed either maxWrite or maxPages. + // One request cannot exceed either maxWrite or maxPages. maxWrite := uint32(fs.conn.maxPages) << hostarch.PageShift if maxWrite > fs.conn.maxWrite { maxWrite = fs.conn.maxWrite @@ -230,7 +229,5 @@ func (fs *filesystem) Write(ctx context.Context, fd *regularFileFD, off uint64, break } } - inode.InodeAttrs.TouchCMtime(ctx) - return written, nil } diff --git a/pkg/sentry/fsimpl/fuse/regular_file.go b/pkg/sentry/fsimpl/fuse/regular_file.go index 8c14a1c64..a048bd77b 100644 --- a/pkg/sentry/fsimpl/fuse/regular_file.go +++ b/pkg/sentry/fsimpl/fuse/regular_file.go @@ -35,6 +35,30 @@ type regularFileFD struct { offMu sync.Mutex } +// Seek implements vfs.FileDescriptionImpl.Seek. +func (fd *regularFileFD) Seek(ctx context.Context, offset int64, whence int32) (int64, error) { + fd.offMu.Lock() + defer fd.offMu.Unlock() + inode := fd.inode() + inode.attrMu.Lock() + defer inode.attrMu.Unlock() + switch whence { + case linux.SEEK_SET: + // use offset as specified + case linux.SEEK_CUR: + offset += fd.off + case linux.SEEK_END: + offset += int64(inode.size.Load()) + default: + return 0, linuxerr.EINVAL + } + if offset < 0 { + return 0, linuxerr.EINVAL + } + fd.off = offset + return offset, nil +} + // PRead implements vfs.FileDescriptionImpl.PRead. func (fd *regularFileFD) PRead(ctx context.Context, dst usermem.IOSequence, offset int64, opts vfs.ReadOptions) (int64, error) { if offset < 0 { @@ -61,6 +85,8 @@ func (fd *regularFileFD) PRead(ctx context.Context, dst usermem.IOSequence, offs // TODO(gvisor.dev/issue/3678): Add direct IO support. inode := fd.inode() + inode.attrMu.Lock() + defer inode.attrMu.Unlock() // Reading beyond EOF, update file size if outdated. if uint64(offset+size) > inode.size.Load() { @@ -140,7 +166,7 @@ func (fd *regularFileFD) Write(ctx context.Context, src usermem.IOSequence, opts // pwrite returns the number of bytes written, final offset and error. The // final offset should be ignored by PWrite. -func (fd *regularFileFD) pwrite(ctx context.Context, src usermem.IOSequence, offset int64, opts vfs.WriteOptions) (written, finalOff int64, err error) { +func (fd *regularFileFD) pwrite(ctx context.Context, src usermem.IOSequence, offset int64, opts vfs.WriteOptions) (int64, int64, error) { if offset < 0 { return 0, offset, linuxerr.EINVAL } @@ -153,8 +179,8 @@ func (fd *regularFileFD) pwrite(ctx context.Context, src usermem.IOSequence, off } inode := fd.inode() - inode.metadataMu.Lock() - defer inode.metadataMu.Unlock() + inode.attrMu.Lock() + defer inode.attrMu.Unlock() // If the file is opened with O_APPEND, update offset to file size. // Note: since our Open() implements the interface of kernfs, @@ -177,7 +203,7 @@ func (fd *regularFileFD) pwrite(ctx context.Context, src usermem.IOSequence, off return 0, offset, linuxerr.EINVAL } - srclen, err = vfs.CheckLimit(ctx, offset, srclen) + srclen, err := vfs.CheckLimit(ctx, offset, srclen) if err != nil { return 0, offset, err } @@ -217,13 +243,12 @@ func (fd *regularFileFD) pwrite(ctx context.Context, src usermem.IOSequence, off return 0, offset, linuxerr.EIO } - written = int64(n) - finalOff = offset + written - + written := int64(n) + finalOff := offset + written if finalOff > int64(inode.size.Load()) { inode.size.Store(uint64(finalOff)) inode.fs.conn.attributeVersion.Add(1) } - - return + inode.touchCMtime() + return written, finalOff, nil } diff --git a/test/runner/fuse/fuse.go b/test/runner/fuse/fuse.go index b4e53f1ee..df8299296 100644 --- a/test/runner/fuse/fuse.go +++ b/test/runner/fuse/fuse.go @@ -33,7 +33,7 @@ func main() { log.Warningf("could not create loopback root: %v", err) os.Exit(1) } - opts := &fuse.MountOptions{DirectMount: true, Debug: true} + opts := &fuse.MountOptions{DirectMount: true, Debug: true, Options: []string{"default_permissions"}} rawFS := fs.NewNodeFS(loopbackRoot, &fs.Options{Logger: golog.Default()}) server, err := fuse.NewServer(rawFS, "/tmp", opts) if err != nil { diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD index ffaaf9f13..05a370854 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -386,6 +386,7 @@ syscall_test( add_overlay = True, shard_count = more_shards, test = "//test/syscalls/linux:open_test", + use_fusefs = True, ) syscall_test(