diff options
Diffstat (limited to 'pkg/sentry/fs/ramfs')
-rw-r--r-- | pkg/sentry/fs/ramfs/BUILD | 6 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/dir.go | 223 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/file.go | 150 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/ramfs.go | 441 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/socket.go | 48 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/symlink.go | 67 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/test/BUILD | 16 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/test/test.go | 46 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/tree.go | 3 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/tree_test.go | 2 |
10 files changed, 265 insertions, 737 deletions
diff --git a/pkg/sentry/fs/ramfs/BUILD b/pkg/sentry/fs/ramfs/BUILD index a93ad6240..a476c9cce 100644 --- a/pkg/sentry/fs/ramfs/BUILD +++ b/pkg/sentry/fs/ramfs/BUILD @@ -6,8 +6,6 @@ go_library( name = "ramfs", srcs = [ "dir.go", - "file.go", - "ramfs.go", "socket.go", "symlink.go", "tree.go", @@ -15,14 +13,12 @@ go_library( importpath = "gvisor.googlesource.com/gvisor/pkg/sentry/fs/ramfs", visibility = ["//pkg/sentry:internal"], deps = [ - "//pkg/secio", + "//pkg/abi/linux", "//pkg/sentry/context", "//pkg/sentry/fs", "//pkg/sentry/fs/anon", "//pkg/sentry/fs/fsutil", "//pkg/sentry/kernel/time", - "//pkg/sentry/memmap", - "//pkg/sentry/safemem", "//pkg/sentry/socket/unix/transport", "//pkg/sentry/usermem", "//pkg/syserror", diff --git a/pkg/sentry/fs/ramfs/dir.go b/pkg/sentry/fs/ramfs/dir.go index 0a911b155..729f37694 100644 --- a/pkg/sentry/fs/ramfs/dir.go +++ b/pkg/sentry/fs/ramfs/dir.go @@ -18,10 +18,12 @@ import ( "sync" "syscall" + "gvisor.googlesource.com/gvisor/pkg/abi/linux" "gvisor.googlesource.com/gvisor/pkg/sentry/context" "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs/fsutil" + ktime "gvisor.googlesource.com/gvisor/pkg/sentry/kernel/time" "gvisor.googlesource.com/gvisor/pkg/sentry/socket/unix/transport" - "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" "gvisor.googlesource.com/gvisor/pkg/syserror" ) @@ -47,7 +49,17 @@ type CreateOps struct { // // +stateify savable type Dir struct { - Entry + fsutil.InodeGenericChecker `state:"nosave"` + fsutil.InodeIsDirTruncate `state:"nosave"` + fsutil.InodeNoopRelease `state:"nosave"` + fsutil.InodeNoopWriteOut `state:"nosave"` + fsutil.InodeNotMappable `state:"nosave"` + fsutil.InodeNotSocket `state:"nosave"` + fsutil.InodeNotSymlink `state:"nosave"` + fsutil.InodeVirtual `state:"nosave"` + + fsutil.InodeSimpleAttributes + fsutil.InodeSimpleExtendedAttributes // CreateOps may be provided. // @@ -64,17 +76,23 @@ type Dir struct { children map[string]*fs.Inode // dentryMap is a sortedDentryMap containing entries for all children. - // Its entries ar kept up-to-date with d.children. + // Its entries are kept up-to-date with d.children. dentryMap *fs.SortedDentryMap } -// InitDir initializes a directory. -func (d *Dir) InitDir(ctx context.Context, contents map[string]*fs.Inode, owner fs.FileOwner, perms fs.FilePermissions) { - d.InitEntry(ctx, owner, perms) +var _ fs.InodeOperations = (*Dir)(nil) + +// NewDir returns a new Dir with the given contents and attributes. +func NewDir(ctx context.Context, contents map[string]*fs.Inode, owner fs.FileOwner, perms fs.FilePermissions) *Dir { + d := &Dir{ + InodeSimpleAttributes: fsutil.NewInodeSimpleAttributes(ctx, owner, perms, linux.RAMFS_MAGIC), + } + if contents == nil { contents = make(map[string]*fs.Inode) } d.children = contents + // Build the entries map ourselves, rather than calling addChildLocked, // because it will be faster. entries := make(map[string]fs.DentAttr, len(contents)) @@ -88,6 +106,8 @@ func (d *Dir) InitDir(ctx context.Context, contents map[string]*fs.Inode, owner // Directories have an extra link, corresponding to '.'. d.AddLink() + + return d } // addChildLocked add the child inode, inheriting its reference. @@ -124,17 +144,24 @@ func (d *Dir) FindChild(name string) (*fs.Inode, bool) { return child, ok } +// Children returns the names and DentAttrs of all children. It can be used to +// implement Readdir for types that embed ramfs.Dir. +func (d *Dir) Children() ([]string, map[string]fs.DentAttr) { + d.mu.Lock() + defer d.mu.Unlock() + return d.dentryMap.GetAll() +} + // removeChildLocked attempts to remove an entry from this directory. -// This Entry's mutex must be held. It returns the removed Inode. func (d *Dir) removeChildLocked(ctx context.Context, name string) (*fs.Inode, error) { inode, ok := d.children[name] if !ok { - return nil, ErrNotFound + return nil, syserror.EACCES } delete(d.children, name) d.dentryMap.Remove(name) - d.Entry.NotifyModification(ctx) + d.NotifyModification(ctx) // If the child was a subdirectory, then we must decrement this dir's // link count which was the child's ".." directory entry. @@ -143,7 +170,7 @@ func (d *Dir) removeChildLocked(ctx context.Context, name string) (*fs.Inode, er } // Update ctime. - inode.NotifyStatusChange(ctx) + inode.InodeOperations.NotifyStatusChange(ctx) // Given we're now removing this inode to the directory we must also // decrease its link count. Similarly it is increased in addChildLocked. @@ -152,8 +179,8 @@ func (d *Dir) removeChildLocked(ctx context.Context, name string) (*fs.Inode, er return inode, nil } -// RemoveEntry attempts to remove an entry from this directory. -func (d *Dir) RemoveEntry(ctx context.Context, name string) error { +// Remove removes the named non-directory. +func (d *Dir) Remove(ctx context.Context, _ *fs.Inode, name string) error { d.mu.Lock() defer d.mu.Unlock() inode, err := d.removeChildLocked(ctx, name) @@ -166,27 +193,23 @@ func (d *Dir) RemoveEntry(ctx context.Context, name string) error { return nil } -// Remove removes the named non-directory. -func (d *Dir) Remove(ctx context.Context, dir *fs.Inode, name string) error { - return d.RemoveEntry(ctx, name) -} - // RemoveDirectory removes the named directory. -func (d *Dir) RemoveDirectory(ctx context.Context, dir *fs.Inode, name string) error { +func (d *Dir) RemoveDirectory(ctx context.Context, _ *fs.Inode, name string) error { d.mu.Lock() defer d.mu.Unlock() - n, err := d.walkLocked(ctx, name) + // Get the child and make sure it is not empty. + childInode, err := d.walkLocked(ctx, name) if err != nil { return err } - dirCtx := &fs.DirCtx{} - if _, err := n.HandleOps().DeprecatedReaddir(ctx, dirCtx, 0); err != nil { + if ok, err := hasChildren(ctx, childInode); err != nil { return err + } else if ok { + return syserror.ENOTEMPTY } - if len(dirCtx.DentAttrs()) > 0 { - return ErrNotEmpty - } + + // Child was empty. Proceed with removal. inode, err := d.removeChildLocked(ctx, name) if err != nil { return err @@ -195,11 +218,11 @@ func (d *Dir) RemoveDirectory(ctx context.Context, dir *fs.Inode, name string) e // Remove our reference on the inode. inode.DecRef() - return err + return nil } // Lookup loads an inode at p into a Dirent. -func (d *Dir) Lookup(ctx context.Context, dir *fs.Inode, p string) (*fs.Dirent, error) { +func (d *Dir) Lookup(ctx context.Context, _ *fs.Inode, p string) (*fs.Dirent, error) { d.mu.Lock() defer d.mu.Unlock() @@ -214,9 +237,9 @@ func (d *Dir) Lookup(ctx context.Context, dir *fs.Inode, p string) (*fs.Dirent, return fs.NewDirent(inode, p), nil } -// walkLocked must be called with this Entry's mutex held. +// walkLocked must be called with d.mu held. func (d *Dir) walkLocked(ctx context.Context, p string) (*fs.Inode, error) { - d.Entry.NotifyAccess(ctx) + d.NotifyAccess(ctx) // Lookup a child node. if inode, ok := d.children[p]; ok { @@ -244,7 +267,7 @@ func (d *Dir) createInodeOperationsCommon(ctx context.Context, name string, make } d.addChildLocked(name, inode) - d.Entry.NotifyModification(ctx) + d.NotifyModification(ctx) return inode, nil } @@ -252,7 +275,7 @@ func (d *Dir) createInodeOperationsCommon(ctx context.Context, name string, make // Create creates a new Inode with the given name and returns its File. func (d *Dir) Create(ctx context.Context, dir *fs.Inode, name string, flags fs.FileFlags, perms fs.FilePermissions) (*fs.File, error) { if d.CreateOps == nil || d.CreateOps.NewFile == nil { - return nil, ErrDenied + return nil, syserror.EACCES } inode, err := d.createInodeOperationsCommon(ctx, name, func() (*fs.Inode, error) { @@ -274,7 +297,7 @@ func (d *Dir) Create(ctx context.Context, dir *fs.Inode, name string, flags fs.F // CreateLink returns a new link. func (d *Dir) CreateLink(ctx context.Context, dir *fs.Inode, oldname, newname string) error { if d.CreateOps == nil || d.CreateOps.NewSymlink == nil { - return ErrDenied + return syserror.EACCES } _, err := d.createInodeOperationsCommon(ctx, newname, func() (*fs.Inode, error) { return d.NewSymlink(ctx, dir, oldname) @@ -292,10 +315,10 @@ func (d *Dir) CreateHardLink(ctx context.Context, dir *fs.Inode, target *fs.Inod // The link count will be incremented in addChildLocked. d.addChildLocked(name, target) - d.Entry.NotifyModification(ctx) + d.NotifyModification(ctx) // Update ctime. - target.NotifyStatusChange(ctx) + target.InodeOperations.NotifyStatusChange(ctx) return nil } @@ -303,7 +326,7 @@ func (d *Dir) CreateHardLink(ctx context.Context, dir *fs.Inode, target *fs.Inod // CreateDirectory returns a new subdirectory. func (d *Dir) CreateDirectory(ctx context.Context, dir *fs.Inode, name string, perms fs.FilePermissions) error { if d.CreateOps == nil || d.CreateOps.NewDir == nil { - return ErrDenied + return syserror.EACCES } _, err := d.createInodeOperationsCommon(ctx, name, func() (*fs.Inode, error) { return d.NewDir(ctx, dir, perms) @@ -316,7 +339,7 @@ func (d *Dir) CreateDirectory(ctx context.Context, dir *fs.Inode, name string, p // Bind implements fs.InodeOperations.Bind. func (d *Dir) Bind(ctx context.Context, dir *fs.Inode, name string, ep transport.BoundEndpoint, perms fs.FilePermissions) (*fs.Dirent, error) { if d.CreateOps == nil || d.CreateOps.NewBoundEndpoint == nil { - return nil, ErrDenied + return nil, syserror.EACCES } inode, err := d.createInodeOperationsCommon(ctx, name, func() (*fs.Inode, error) { return d.NewBoundEndpoint(ctx, dir, ep, perms) @@ -335,7 +358,7 @@ func (d *Dir) Bind(ctx context.Context, dir *fs.Inode, name string, ep transport // CreateFifo implements fs.InodeOperations.CreateFifo. func (d *Dir) CreateFifo(ctx context.Context, dir *fs.Inode, name string, perms fs.FilePermissions) error { if d.CreateOps == nil || d.CreateOps.NewFifo == nil { - return ErrDenied + return syserror.EACCES } _, err := d.createInodeOperationsCommon(ctx, name, func() (*fs.Inode, error) { return d.NewFifo(ctx, dir, perms) @@ -343,29 +366,125 @@ func (d *Dir) CreateFifo(ctx context.Context, dir *fs.Inode, name string, perms return err } -func (d *Dir) readdirLocked(ctx context.Context, dirCtx *fs.DirCtx, offset int) (int, error) { - // Serialize the entries in dentryMap. - n, err := fs.GenericReaddir(dirCtx, d.dentryMap) +// GetFile implements fs.InodeOperations.GetFile. +func (d *Dir) GetFile(ctx context.Context, dirent *fs.Dirent, flags fs.FileFlags) (*fs.File, error) { + flags.Pread = true + return fs.NewFile(ctx, dirent, flags, &dirFileOperations{dir: d}), nil +} - // Touch the access time. - d.Entry.NotifyAccess(ctx) +// Rename implements fs.InodeOperations.Rename. +func (*Dir) Rename(ctx context.Context, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string) error { + return Rename(ctx, oldParent.InodeOperations, oldName, newParent.InodeOperations, newName) +} +// dirFileOperations implements fs.FileOperations for a ramfs directory. +// +// +stateify savable +type dirFileOperations struct { + fsutil.DirFileOperations `state:"nosave"` + + // dirCursor contains the name of the last directory entry that was + // serialized. + dirCursor string + + // dir is the ramfs dir that this file corresponds to. + dir *Dir +} + +var _ fs.FileOperations = (*dirFileOperations)(nil) + +// Seek implements fs.FileOperations.Seek. +func (dfo *dirFileOperations) Seek(ctx context.Context, file *fs.File, whence fs.SeekWhence, offset int64) (int64, error) { + return fsutil.SeekWithDirCursor(ctx, file, whence, offset, &dfo.dirCursor) +} + +// IterateDir implements DirIterator.IterateDir. +func (dfo *dirFileOperations) IterateDir(ctx context.Context, dirCtx *fs.DirCtx, offset int) (int, error) { + dfo.dir.mu.Lock() + defer dfo.dir.mu.Unlock() + + n, err := fs.GenericReaddir(dirCtx, dfo.dir.dentryMap) return offset + n, err } -// DeprecatedReaddir emits the entries contained in this directory. -func (d *Dir) DeprecatedReaddir(ctx context.Context, dirCtx *fs.DirCtx, offset int) (int, error) { - d.mu.Lock() - defer d.mu.Unlock() - return d.readdirLocked(ctx, dirCtx, offset) +// Readdir implements FileOperations.Readdir. +func (dfo *dirFileOperations) Readdir(ctx context.Context, file *fs.File, serializer fs.DentrySerializer) (int64, error) { + root := fs.RootFromContext(ctx) + defer root.DecRef() + dirCtx := &fs.DirCtx{ + Serializer: serializer, + DirCursor: &dfo.dirCursor, + } + dfo.dir.mu.Lock() + dfo.dir.InodeSimpleAttributes.Unstable.AccessTime = ktime.NowFromContext(ctx) + dfo.dir.mu.Unlock() + return fs.DirentReaddir(ctx, file.Dirent, dfo, root, dirCtx, file.Offset()) } -// DeprecatedPreadv always returns ErrIsDirectory -func (*Dir) DeprecatedPreadv(context.Context, usermem.IOSequence, int64) (int64, error) { - return 0, ErrIsDirectory +// hasChildren is a helper method that determines whether an arbitrary inode +// (not necessarily ramfs) has any children. +func hasChildren(ctx context.Context, inode *fs.Inode) (bool, error) { + // Take an extra ref on inode which will be given to the dirent and + // dropped when that dirent is destroyed. + inode.IncRef() + d := fs.NewTransientDirent(inode) + defer d.DecRef() + + file, err := inode.GetFile(ctx, d, fs.FileFlags{Read: true}) + if err != nil { + return false, err + } + defer file.DecRef() + + ser := &fs.CollectEntriesSerializer{} + if err := file.Readdir(ctx, ser); err != nil { + return false, err + } + // We will always write "." and "..", so ignore those two. + if ser.Written() > 2 { + return true, nil + } + return false, nil } -// DeprecatedPwritev always returns ErrIsDirectory -func (*Dir) DeprecatedPwritev(context.Context, usermem.IOSequence, int64) (int64, error) { - return 0, ErrIsDirectory +// Rename renames from a *ramfs.Dir to another *ramfs.Dir. +func Rename(ctx context.Context, oldParent fs.InodeOperations, oldName string, newParent fs.InodeOperations, newName string) error { + op, ok := oldParent.(*Dir) + if !ok { + return syserror.EXDEV + } + np, ok := newParent.(*Dir) + if !ok { + return syserror.EXDEV + } + + np.mu.Lock() + defer np.mu.Unlock() + + // Check whether the ramfs entry to be replaced is a non-empty directory. + if replaced, ok := np.children[newName]; ok { + if fs.IsDir(replaced.StableAttr) { + if ok, err := hasChildren(ctx, replaced); err != nil { + return err + } else if ok { + return syserror.ENOTEMPTY + } + } + } + + // Be careful, we may have already grabbed this mutex above. + if op != np { + op.mu.Lock() + defer op.mu.Unlock() + } + + // Do the swap. + n := op.children[oldName] + op.removeChildLocked(ctx, oldName) + np.addChildLocked(newName, n) + + // Update ctime. + n.InodeOperations.NotifyStatusChange(ctx) + + return nil } diff --git a/pkg/sentry/fs/ramfs/file.go b/pkg/sentry/fs/ramfs/file.go deleted file mode 100644 index b7fc98ffc..000000000 --- a/pkg/sentry/fs/ramfs/file.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2018 Google LLC -// -// 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 ramfs - -import ( - "io" - "sync" - - "gvisor.googlesource.com/gvisor/pkg/secio" - "gvisor.googlesource.com/gvisor/pkg/sentry/context" - "gvisor.googlesource.com/gvisor/pkg/sentry/fs" - "gvisor.googlesource.com/gvisor/pkg/sentry/safemem" - "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" - "gvisor.googlesource.com/gvisor/pkg/syserror" -) - -// File represents a unique file. It uses a simple byte slice as storage, and -// thus should only be used for small files. -// -// A File is not mappable. -// -// +stateify savable -type File struct { - Entry - - // mu protects the fields below. - mu sync.Mutex `state:"nosave"` - - // data tracks backing data for the file. - data []byte -} - -// InitFile initializes a file. -func (f *File) InitFile(ctx context.Context, owner fs.FileOwner, perms fs.FilePermissions) { - f.InitEntry(ctx, owner, perms) -} - -// UnstableAttr returns unstable attributes of this ramfs file. -func (f *File) UnstableAttr(ctx context.Context, inode *fs.Inode) (fs.UnstableAttr, error) { - f.mu.Lock() - defer f.mu.Unlock() - - uattr, _ := f.Entry.UnstableAttr(ctx, inode) - uattr.Size = int64(len(f.data)) - uattr.Usage = f.usageLocked() - - return uattr, nil -} - -// usageLocked returns the disk usage. Caller must hold f.mu. -func (f *File) usageLocked() int64 { - return int64(len(f.data)) -} - -// Append appends the given data. This is for internal use. -func (f *File) Append(data []byte) { - f.mu.Lock() - defer f.mu.Unlock() - f.data = append(f.data, data...) -} - -// Truncate truncates this node. -func (f *File) Truncate(ctx context.Context, inode *fs.Inode, l int64) error { - f.mu.Lock() - defer f.mu.Unlock() - if l < int64(len(f.data)) { - // Remove excess bytes. - f.data = f.data[:l] - return nil - } else if l > int64(len(f.data)) { - // Create a new slice with size l, and copy f.data into it. - d := make([]byte, l) - copy(d, f.data) - f.data = d - } - f.Entry.NotifyModification(ctx) - return nil -} - -// ReadAt implements io.ReaderAt. -func (f *File) ReadAt(data []byte, offset int64) (int, error) { - if offset < 0 { - return 0, ErrInvalidOp - } - if offset >= int64(len(f.data)) { - return 0, io.EOF - } - n := copy(data, f.data[offset:]) - // Did we read past the end? - if offset+int64(len(data)) >= int64(len(f.data)) { - return n, io.EOF - } - return n, nil -} - -// DeprecatedPreadv reads into a collection of slices from a given offset. -func (f *File) DeprecatedPreadv(ctx context.Context, dst usermem.IOSequence, offset int64) (int64, error) { - f.mu.Lock() - defer f.mu.Unlock() - if offset >= int64(len(f.data)) { - return 0, io.EOF - } - n, err := dst.CopyOut(ctx, f.data[offset:]) - if n > 0 { - f.Entry.NotifyAccess(ctx) - } - return int64(n), err -} - -// WriteAt implements io.WriterAt. -func (f *File) WriteAt(data []byte, offset int64) (int, error) { - if offset < 0 { - return 0, ErrInvalidOp - } - newLen := offset + int64(len(data)) - if newLen < 0 { - // Overflow. - return 0, syserror.EINVAL - } - if newLen > int64(len(f.data)) { - // Copy f.data into new slice with expanded length. - d := make([]byte, newLen) - copy(d, f.data) - f.data = d - } - return copy(f.data[offset:], data), nil -} - -// DeprecatedPwritev writes from a collection of slices at a given offset. -func (f *File) DeprecatedPwritev(ctx context.Context, src usermem.IOSequence, offset int64) (int64, error) { - f.mu.Lock() - defer f.mu.Unlock() - n, err := src.CopyInTo(ctx, safemem.FromIOWriter{secio.NewOffsetWriter(f, offset)}) - if n > 0 { - f.Entry.NotifyModification(ctx) - } - return n, err -} diff --git a/pkg/sentry/fs/ramfs/ramfs.go b/pkg/sentry/fs/ramfs/ramfs.go deleted file mode 100644 index d77688a34..000000000 --- a/pkg/sentry/fs/ramfs/ramfs.go +++ /dev/null @@ -1,441 +0,0 @@ -// Copyright 2018 Google LLC -// -// 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 ramfs implements an in-memory file system that can be associated with -// any device. -package ramfs - -import ( - "errors" - "sync" - "syscall" - - "gvisor.googlesource.com/gvisor/pkg/sentry/context" - "gvisor.googlesource.com/gvisor/pkg/sentry/fs" - "gvisor.googlesource.com/gvisor/pkg/sentry/fs/fsutil" - ktime "gvisor.googlesource.com/gvisor/pkg/sentry/kernel/time" - "gvisor.googlesource.com/gvisor/pkg/sentry/memmap" - "gvisor.googlesource.com/gvisor/pkg/sentry/socket/unix/transport" - "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" - "gvisor.googlesource.com/gvisor/pkg/syserror" - "gvisor.googlesource.com/gvisor/pkg/waiter" -) - -var ( - // ErrInvalidOp indicates the operation is not valid. - ErrInvalidOp = errors.New("invalid operation") - - // ErrDenied indicates the operation was denied. - ErrDenied = errors.New("operation denied") - - // ErrNotFound indicates that a node was not found on a walk. - ErrNotFound = errors.New("node not found") - - // ErrCrossDevice indicates a cross-device link or rename. - ErrCrossDevice = errors.New("can't link across filesystems") - - // ErrIsDirectory indicates that the operation failed because - // the node is a directory. - ErrIsDirectory = errors.New("is a directory") - - // ErrNotDirectory indicates that the operation failed because - // the node is a not directory. - ErrNotDirectory = errors.New("not a directory") - - // ErrNotEmpty indicates that the operation failed because the - // directory is not empty. - ErrNotEmpty = errors.New("directory not empty") -) - -// Entry represents common internal state for file and directory nodes. -// This may be used by other packages to easily create ramfs files. -// -// +stateify savable -type Entry struct { - waiter.AlwaysReady `state:"nosave"` - fsutil.NoMappable `state:"nosave"` - fsutil.NoopWriteOut `state:"nosave"` - fsutil.InodeNotSocket `state:"nosave"` - - // mu protects the fields below. - mu sync.Mutex `state:"nosave"` - - // unstable is unstable attributes. - unstable fs.UnstableAttr - - // xattrs are the extended attributes of the Entry. - xattrs map[string][]byte -} - -// InitEntry initializes an entry. -func (e *Entry) InitEntry(ctx context.Context, owner fs.FileOwner, p fs.FilePermissions) { - e.InitEntryWithAttr(ctx, fs.WithCurrentTime(ctx, fs.UnstableAttr{ - Owner: owner, - Perms: p, - // Always start unlinked. - Links: 0, - })) -} - -// InitEntryWithAttr initializes an entry with a complete set of attributes. -func (e *Entry) InitEntryWithAttr(ctx context.Context, uattr fs.UnstableAttr) { - e.unstable = uattr - e.xattrs = make(map[string][]byte) -} - -// UnstableAttr implements fs.InodeOperations.UnstableAttr. -func (e *Entry) UnstableAttr(ctx context.Context, inode *fs.Inode) (fs.UnstableAttr, error) { - e.mu.Lock() - attr := e.unstable - e.mu.Unlock() - return attr, nil -} - -// Check implements fs.InodeOperations.Check. -func (*Entry) Check(ctx context.Context, inode *fs.Inode, p fs.PermMask) bool { - return fs.ContextCanAccessFile(ctx, inode, p) -} - -// Getxattr implements fs.InodeOperations.Getxattr. -func (e *Entry) Getxattr(inode *fs.Inode, name string) ([]byte, error) { - // Hot path. Avoid defers. - e.mu.Lock() - value, ok := e.xattrs[name] - e.mu.Unlock() - if ok { - return value, nil - } - return nil, syserror.ENOATTR -} - -// Setxattr implements fs.InodeOperations.Setxattr. -func (e *Entry) Setxattr(inode *fs.Inode, name string, value []byte) error { - e.mu.Lock() - e.xattrs[name] = value - e.mu.Unlock() - return nil -} - -// Listxattr implements fs.InodeOperations.Listxattr. -func (e *Entry) Listxattr(inode *fs.Inode) (map[string]struct{}, error) { - e.mu.Lock() - names := make(map[string]struct{}, len(e.xattrs)) - for name := range e.xattrs { - names[name] = struct{}{} - } - e.mu.Unlock() - return names, nil -} - -// GetFile returns a fs.File backed by the dirent argument and flags. -func (*Entry) GetFile(ctx context.Context, d *fs.Dirent, flags fs.FileFlags) (*fs.File, error) { - return fsutil.NewHandle(ctx, d, flags, d.Inode.HandleOps()), nil -} - -// SetPermissions always sets the permissions. -func (e *Entry) SetPermissions(ctx context.Context, inode *fs.Inode, p fs.FilePermissions) bool { - e.mu.Lock() - e.unstable.Perms = p - e.unstable.StatusChangeTime = ktime.NowFromContext(ctx) - e.mu.Unlock() - return true -} - -// SetOwner always sets ownership. -func (e *Entry) SetOwner(ctx context.Context, inode *fs.Inode, owner fs.FileOwner) error { - e.mu.Lock() - if owner.UID.Ok() { - e.unstable.Owner.UID = owner.UID - } - if owner.GID.Ok() { - e.unstable.Owner.GID = owner.GID - } - e.mu.Unlock() - return nil -} - -// SetTimestamps sets the timestamps. -func (e *Entry) SetTimestamps(ctx context.Context, inode *fs.Inode, ts fs.TimeSpec) error { - if ts.ATimeOmit && ts.MTimeOmit { - return nil - } - - e.mu.Lock() - now := ktime.NowFromContext(ctx) - if !ts.ATimeOmit { - if ts.ATimeSetSystemTime { - e.unstable.AccessTime = now - } else { - e.unstable.AccessTime = ts.ATime - } - } - if !ts.MTimeOmit { - if ts.MTimeSetSystemTime { - e.unstable.ModificationTime = now - } else { - e.unstable.ModificationTime = ts.MTime - } - } - e.unstable.StatusChangeTime = now - e.mu.Unlock() - return nil -} - -// NotifyStatusChange updates the status change time (ctime). -func (e *Entry) NotifyStatusChange(ctx context.Context) { - e.mu.Lock() - e.unstable.StatusChangeTime = ktime.NowFromContext(ctx) - e.mu.Unlock() -} - -// StatusChangeTime returns the last status change time for this node. -func (e *Entry) StatusChangeTime() ktime.Time { - e.mu.Lock() - t := e.unstable.StatusChangeTime - e.mu.Unlock() - return t -} - -// NotifyModification updates the modification time and the status change time. -func (e *Entry) NotifyModification(ctx context.Context) { - e.mu.Lock() - now := ktime.NowFromContext(ctx) - e.unstable.ModificationTime = now - e.unstable.StatusChangeTime = now - e.mu.Unlock() -} - -// ModificationTime returns the last modification time for this node. -func (e *Entry) ModificationTime() ktime.Time { - e.mu.Lock() - t := e.unstable.ModificationTime - e.mu.Unlock() - return t -} - -// NotifyAccess updates the access time. -func (e *Entry) NotifyAccess(ctx context.Context) { - e.mu.Lock() - now := ktime.NowFromContext(ctx) - e.unstable.AccessTime = now - e.mu.Unlock() -} - -// AccessTime returns the last access time for this node. -func (e *Entry) AccessTime() ktime.Time { - e.mu.Lock() - t := e.unstable.AccessTime - e.mu.Unlock() - return t -} - -// Permissions returns permissions on this entry. -func (e *Entry) Permissions() fs.FilePermissions { - e.mu.Lock() - p := e.unstable.Perms - e.mu.Unlock() - return p -} - -// Lookup is not supported by default. -func (*Entry) Lookup(context.Context, *fs.Inode, string) (*fs.Dirent, error) { - return nil, ErrInvalidOp -} - -// Create is not supported by default. -func (*Entry) Create(context.Context, *fs.Inode, string, fs.FileFlags, fs.FilePermissions) (*fs.File, error) { - return nil, ErrInvalidOp -} - -// CreateLink is not supported by default. -func (*Entry) CreateLink(context.Context, *fs.Inode, string, string) error { - return ErrInvalidOp -} - -// CreateHardLink is not supported by default. -func (*Entry) CreateHardLink(context.Context, *fs.Inode, *fs.Inode, string) error { - return ErrInvalidOp -} - -// IsVirtual returns true. -func (*Entry) IsVirtual() bool { - return true -} - -// CreateDirectory is not supported by default. -func (*Entry) CreateDirectory(context.Context, *fs.Inode, string, fs.FilePermissions) error { - return ErrInvalidOp -} - -// Bind is not supported by default. -func (*Entry) Bind(context.Context, *fs.Inode, string, transport.BoundEndpoint, fs.FilePermissions) (*fs.Dirent, error) { - return nil, ErrInvalidOp -} - -// CreateFifo implements fs.InodeOperations.CreateFifo. CreateFifo is not supported by -// default. -func (*Entry) CreateFifo(context.Context, *fs.Inode, string, fs.FilePermissions) error { - return ErrInvalidOp -} - -// Remove is not supported by default. -func (*Entry) Remove(context.Context, *fs.Inode, string) error { - return ErrInvalidOp -} - -// RemoveDirectory is not supported by default. -func (*Entry) RemoveDirectory(context.Context, *fs.Inode, string) error { - return ErrInvalidOp -} - -// StatFS always returns ENOSYS. -func (*Entry) StatFS(context.Context) (fs.Info, error) { - return fs.Info{}, syscall.ENOSYS -} - -// Rename implements fs.InodeOperations.Rename. -func (e *Entry) Rename(ctx context.Context, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string) error { - return Rename(ctx, oldParent.InodeOperations, oldName, newParent.InodeOperations, newName) -} - -// Rename renames from a *ramfs.Dir to another *ramfs.Dir. -func Rename(ctx context.Context, oldParent fs.InodeOperations, oldName string, newParent fs.InodeOperations, newName string) error { - op, ok := oldParent.(*Dir) - if !ok { - return ErrCrossDevice - } - np, ok := newParent.(*Dir) - if !ok { - return ErrCrossDevice - } - - np.mu.Lock() - defer np.mu.Unlock() - - // Check whether the ramfs entry to be replaced is a non-empty directory. - if replaced, ok := np.children[newName]; ok { - if fs.IsDir(replaced.StableAttr) { - // FIXME: simplify by pinning children of ramfs-backed directories - // in the Dirent tree: this allows us to generalize ramfs operations without - // relying on an implementation of Readdir (which may do anything, like require - // that the file be open ... which would be reasonable). - dirCtx := &fs.DirCtx{} - _, err := replaced.HandleOps().DeprecatedReaddir(ctx, dirCtx, 0) - if err != nil { - return err - } - attrs := dirCtx.DentAttrs() - - // ramfs-backed directories should not contain "." and "..", but we do this - // just in case. - delete(attrs, ".") - delete(attrs, "..") - - // If the directory to be replaced is not empty, reject the rename. - if len(attrs) != 0 { - return ErrNotEmpty - } - } - } - - // Be careful, we may have already grabbed this mutex above. - if op != np { - op.mu.Lock() - defer op.mu.Unlock() - } - - // Do the swap. - n := op.children[oldName] - op.removeChildLocked(ctx, oldName) - np.addChildLocked(newName, n) - - // Update ctime. - n.NotifyStatusChange(ctx) - - return nil -} - -// Truncate is not supported by default. -func (*Entry) Truncate(context.Context, *fs.Inode, int64) error { - return ErrInvalidOp -} - -// Readlink always returns ENOLINK. -func (*Entry) Readlink(context.Context, *fs.Inode) (string, error) { - return "", syscall.ENOLINK -} - -// Getlink always returns ENOLINK. -func (*Entry) Getlink(context.Context, *fs.Inode) (*fs.Dirent, error) { - return nil, syscall.ENOLINK -} - -// Release is a no-op. -func (e *Entry) Release(context.Context) {} - -// AddLink implements InodeOperationss.AddLink. -func (e *Entry) AddLink() { - e.mu.Lock() - e.unstable.Links++ - e.mu.Unlock() -} - -// DropLink implements InodeOperationss.DropLink. -func (e *Entry) DropLink() { - e.mu.Lock() - e.unstable.Links-- - e.mu.Unlock() -} - -// DeprecatedReaddir is not supported by default. -func (*Entry) DeprecatedReaddir(context.Context, *fs.DirCtx, int) (int, error) { - return 0, ErrNotDirectory -} - -// DeprecatedPreadv always returns ErrInvalidOp. -func (*Entry) DeprecatedPreadv(context.Context, usermem.IOSequence, int64) (int64, error) { - return 0, ErrInvalidOp -} - -// DeprecatedPwritev always returns ErrInvalidOp. -func (*Entry) DeprecatedPwritev(context.Context, usermem.IOSequence, int64) (int64, error) { - return 0, ErrInvalidOp -} - -// DeprecatedFsync is a noop. -func (*Entry) DeprecatedFsync() error { - // Ignore, this is in memory. - return nil -} - -// DeprecatedFlush always returns nil. -func (*Entry) DeprecatedFlush() error { - return nil -} - -// DeprecatedMappable implements fs.InodeOperations.DeprecatedMappable. -func (*Entry) DeprecatedMappable(context.Context, *fs.Inode) (memmap.Mappable, bool) { - return nil, false -} - -func init() { - // Register ramfs errors. - syserror.AddErrorTranslation(ErrInvalidOp, syscall.EINVAL) - syserror.AddErrorTranslation(ErrDenied, syscall.EACCES) - syserror.AddErrorTranslation(ErrNotFound, syscall.ENOENT) - syserror.AddErrorTranslation(ErrCrossDevice, syscall.EXDEV) - syserror.AddErrorTranslation(ErrIsDirectory, syscall.EISDIR) - syserror.AddErrorTranslation(ErrNotDirectory, syscall.ENOTDIR) - syserror.AddErrorTranslation(ErrNotEmpty, syscall.ENOTEMPTY) -} diff --git a/pkg/sentry/fs/ramfs/socket.go b/pkg/sentry/fs/ramfs/socket.go index 8c81478c8..2c1295897 100644 --- a/pkg/sentry/fs/ramfs/socket.go +++ b/pkg/sentry/fs/ramfs/socket.go @@ -15,25 +15,42 @@ package ramfs import ( + "gvisor.googlesource.com/gvisor/pkg/abi/linux" "gvisor.googlesource.com/gvisor/pkg/sentry/context" "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs/fsutil" "gvisor.googlesource.com/gvisor/pkg/sentry/socket/unix/transport" + "gvisor.googlesource.com/gvisor/pkg/waiter" ) // Socket represents a socket. // // +stateify savable type Socket struct { - Entry + fsutil.InodeGenericChecker `state:"nosave"` + fsutil.InodeNoopRelease `state:"nosave"` + fsutil.InodeNoopWriteOut `state:"nosave"` + fsutil.InodeNotDirectory `state:"nosave"` + fsutil.InodeNotMappable `state:"nosave"` + fsutil.InodeNotSymlink `state:"nosave"` + fsutil.InodeNotTruncatable `state:"nosave"` + fsutil.InodeVirtual `state:"nosave"` + + fsutil.InodeSimpleAttributes + fsutil.InodeSimpleExtendedAttributes // ep is the bound endpoint. ep transport.BoundEndpoint } -// InitSocket initializes a socket. -func (s *Socket) InitSocket(ctx context.Context, ep transport.BoundEndpoint, owner fs.FileOwner, perms fs.FilePermissions) { - s.InitEntry(ctx, owner, perms) - s.ep = ep +var _ fs.InodeOperations = (*Socket)(nil) + +// NewSocket returns a new Socket. +func NewSocket(ctx context.Context, ep transport.BoundEndpoint, owner fs.FileOwner, perms fs.FilePermissions) *Socket { + return &Socket{ + InodeSimpleAttributes: fsutil.NewInodeSimpleAttributes(ctx, owner, perms, linux.SOCKFS_MAGIC), + ep: ep, + } } // BoundEndpoint returns the socket data. @@ -42,3 +59,24 @@ func (s *Socket) BoundEndpoint(*fs.Inode, string) transport.BoundEndpoint { // care about the path argument. return s.ep } + +// GetFile implements fs.FileOperations.GetFile. +func (s *Socket) GetFile(ctx context.Context, dirent *fs.Dirent, flags fs.FileFlags) (*fs.File, error) { + return fs.NewFile(ctx, dirent, flags, &socketFileOperations{}), nil +} + +// +stateify savable +type socketFileOperations struct { + waiter.AlwaysReady `state:"nosave"` + fsutil.FileNoIoctl `state:"nosave"` + fsutil.FileNoMMap `state:"nosave"` + fsutil.FileNoopFlush `state:"nosave"` + fsutil.FileNoopFsync `state:"nosave"` + fsutil.FileNoopRelease `state:"nosave"` + fsutil.FileNoRead `state:"nosave"` + fsutil.FileNoSeek `state:"nosave"` + fsutil.FileNotDirReaddir `state:"nosave"` + fsutil.FileNoWrite `state:"nosave"` +} + +var _ fs.FileOperations = (*socketFileOperations)(nil) diff --git a/pkg/sentry/fs/ramfs/symlink.go b/pkg/sentry/fs/ramfs/symlink.go index a21fac2c7..47dae380b 100644 --- a/pkg/sentry/fs/ramfs/symlink.go +++ b/pkg/sentry/fs/ramfs/symlink.go @@ -15,44 +15,55 @@ package ramfs import ( - "sync" - + "gvisor.googlesource.com/gvisor/pkg/abi/linux" "gvisor.googlesource.com/gvisor/pkg/sentry/context" "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs/fsutil" + "gvisor.googlesource.com/gvisor/pkg/waiter" ) // Symlink represents a symlink. // // +stateify savable type Symlink struct { - Entry + fsutil.InodeGenericChecker `state:"nosave"` + fsutil.InodeNoopRelease `state:"nosave"` + fsutil.InodeNoopWriteOut `state:"nosave"` + fsutil.InodeNotDirectory `state:"nosave"` + fsutil.InodeNotMappable `state:"nosave"` + fsutil.InodeNotTruncatable `state:"nosave"` + fsutil.InodeNotSocket `state:"nosave"` + fsutil.InodeVirtual `state:"nosave"` - mu sync.Mutex `state:"nosave"` + fsutil.InodeSimpleAttributes + fsutil.InodeSimpleExtendedAttributes // Target is the symlink target. Target string } -// InitSymlink initializes a symlink, pointing to the given target. -// A symlink is assumed to always have permissions 0777. -func (s *Symlink) InitSymlink(ctx context.Context, owner fs.FileOwner, target string) { - s.InitEntry(ctx, owner, fs.FilePermsFromMode(0777)) - s.Target = target +var _ fs.InodeOperations = (*Symlink)(nil) + +// NewSymlink returns a new Symlink. +func NewSymlink(ctx context.Context, owner fs.FileOwner, target string) *Symlink { + // A symlink is assumed to always have permissions 0777. + return &Symlink{ + InodeSimpleAttributes: fsutil.NewInodeSimpleAttributes(ctx, owner, fs.FilePermsFromMode(0777), linux.RAMFS_MAGIC), + Target: target, + } } // UnstableAttr returns all attributes of this ramfs symlink. func (s *Symlink) UnstableAttr(ctx context.Context, inode *fs.Inode) (fs.UnstableAttr, error) { - uattr, _ := s.Entry.UnstableAttr(ctx, inode) + uattr, err := s.InodeSimpleAttributes.UnstableAttr(ctx, inode) + if err != nil { + return fs.UnstableAttr{}, err + } uattr.Size = int64(len(s.Target)) uattr.Usage = uattr.Size return uattr, nil } -// Check implements InodeOperations.Check. -func (s *Symlink) Check(ctx context.Context, inode *fs.Inode, p fs.PermMask) bool { - return fs.ContextCanAccessFile(ctx, inode, p) -} - // SetPermissions on a symlink is always rejected. func (s *Symlink) SetPermissions(context.Context, *fs.Inode, fs.FilePermissions) bool { return false @@ -60,10 +71,7 @@ func (s *Symlink) SetPermissions(context.Context, *fs.Inode, fs.FilePermissions) // Readlink reads the symlink value. func (s *Symlink) Readlink(ctx context.Context, _ *fs.Inode) (string, error) { - s.mu.Lock() - defer s.mu.Unlock() - - s.Entry.NotifyAccess(ctx) + s.NotifyAccess(ctx) return s.Target, nil } @@ -72,3 +80,24 @@ func (s *Symlink) Readlink(ctx context.Context, _ *fs.Inode) (string, error) { func (*Symlink) Getlink(context.Context, *fs.Inode) (*fs.Dirent, error) { return nil, fs.ErrResolveViaReadlink } + +// GetFile implements fs.FileOperations.GetFile. +func (s *Symlink) GetFile(ctx context.Context, dirent *fs.Dirent, flags fs.FileFlags) (*fs.File, error) { + return fs.NewFile(ctx, dirent, flags, &symlinkFileOperations{}), nil +} + +// +stateify savable +type symlinkFileOperations struct { + waiter.AlwaysReady `state:"nosave"` + fsutil.FileNoIoctl `state:"nosave"` + fsutil.FileNoMMap `state:"nosave"` + fsutil.FileNoopFlush `state:"nosave"` + fsutil.FileNoopFsync `state:"nosave"` + fsutil.FileNoopRelease `state:"nosave"` + fsutil.FileNoRead `state:"nosave"` + fsutil.FileNoSeek `state:"nosave"` + fsutil.FileNotDirReaddir `state:"nosave"` + fsutil.FileNoWrite `state:"nosave"` +} + +var _ fs.FileOperations = (*symlinkFileOperations)(nil) diff --git a/pkg/sentry/fs/ramfs/test/BUILD b/pkg/sentry/fs/ramfs/test/BUILD deleted file mode 100644 index 187eac49d..000000000 --- a/pkg/sentry/fs/ramfs/test/BUILD +++ /dev/null @@ -1,16 +0,0 @@ -package(licenses = ["notice"]) # Apache 2.0 - -load("//tools/go_stateify:defs.bzl", "go_library") - -go_library( - name = "test", - testonly = 1, - srcs = ["test.go"], - importpath = "gvisor.googlesource.com/gvisor/pkg/sentry/fs/ramfs/test", - visibility = ["//pkg/sentry:internal"], - deps = [ - "//pkg/sentry/context", - "//pkg/sentry/fs", - "//pkg/sentry/fs/ramfs", - ], -) diff --git a/pkg/sentry/fs/ramfs/test/test.go b/pkg/sentry/fs/ramfs/test/test.go deleted file mode 100644 index 11bff7729..000000000 --- a/pkg/sentry/fs/ramfs/test/test.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2018 Google LLC -// -// 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 test provides a simple ramfs-based filesystem for use in testing. -package test - -import ( - "gvisor.googlesource.com/gvisor/pkg/sentry/context" - "gvisor.googlesource.com/gvisor/pkg/sentry/fs" - "gvisor.googlesource.com/gvisor/pkg/sentry/fs/ramfs" -) - -// Dir is a simple ramfs.Dir that supports save/restore as-is. -type Dir struct { - ramfs.Dir -} - -// NewDir returns a simple ramfs directory with the passed contents. -func NewDir(ctx context.Context, contents map[string]*fs.Inode, perms fs.FilePermissions) *Dir { - d := &Dir{} - d.InitDir(ctx, contents, fs.RootOwner, perms) - return d -} - -// File is a simple ramfs.File that supports save/restore as-is. -type File struct { - ramfs.File -} - -// NewFile returns a simple ramfs File. -func NewFile(ctx context.Context, perms fs.FilePermissions) *File { - f := &File{} - f.InitFile(ctx, fs.RootOwner, perms) - return f -} diff --git a/pkg/sentry/fs/ramfs/tree.go b/pkg/sentry/fs/ramfs/tree.go index 29a70f698..f6d5ffdec 100644 --- a/pkg/sentry/fs/ramfs/tree.go +++ b/pkg/sentry/fs/ramfs/tree.go @@ -60,8 +60,7 @@ func makeSubdir(ctx context.Context, msrc *fs.MountSource, root *Dir, subdir str // emptyDir returns an empty *ramfs.Dir that is traversable but not writable. func emptyDir(ctx context.Context, msrc *fs.MountSource) *fs.Inode { - dir := &Dir{} - dir.InitDir(ctx, make(map[string]*fs.Inode), fs.RootOwner, fs.FilePermsFromMode(0555)) + dir := NewDir(ctx, make(map[string]*fs.Inode), fs.RootOwner, fs.FilePermsFromMode(0555)) return fs.NewInode(dir, msrc, fs.StableAttr{ DeviceID: anon.PseudoDevice.DeviceID(), InodeID: anon.PseudoDevice.NextIno(), diff --git a/pkg/sentry/fs/ramfs/tree_test.go b/pkg/sentry/fs/ramfs/tree_test.go index 54df2143c..8bee9cfc1 100644 --- a/pkg/sentry/fs/ramfs/tree_test.go +++ b/pkg/sentry/fs/ramfs/tree_test.go @@ -22,7 +22,7 @@ import ( ) func TestMakeDirectoryTree(t *testing.T) { - mount := fs.NewNonCachingMountSource(nil, fs.MountSourceFlags{}) + mount := fs.NewPseudoMountSource() for _, test := range []struct { name string |