diff options
Diffstat (limited to 'pkg/sentry/fs/ramfs')
-rw-r--r-- | pkg/sentry/fs/ramfs/BUILD | 62 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/dir.go | 364 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/file.go | 148 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/ramfs.go | 433 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/socket.go | 42 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/symlink.go | 72 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/test/BUILD | 31 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/test/test.go | 46 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/tree.go | 71 | ||||
-rw-r--r-- | pkg/sentry/fs/ramfs/tree_test.go | 79 |
10 files changed, 1348 insertions, 0 deletions
diff --git a/pkg/sentry/fs/ramfs/BUILD b/pkg/sentry/fs/ramfs/BUILD new file mode 100644 index 000000000..663a1aeb9 --- /dev/null +++ b/pkg/sentry/fs/ramfs/BUILD @@ -0,0 +1,62 @@ +package(licenses = ["notice"]) # Apache 2.0 + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") +load("//tools/go_stateify:defs.bzl", "go_stateify") + +go_stateify( + name = "ramfs_state", + srcs = [ + "dir.go", + "file.go", + "ramfs.go", + "socket.go", + "symlink.go", + ], + out = "ramfs_state.go", + package = "ramfs", +) + +go_library( + name = "ramfs", + srcs = [ + "dir.go", + "file.go", + "ramfs.go", + "ramfs_state.go", + "socket.go", + "symlink.go", + "tree.go", + ], + importpath = "gvisor.googlesource.com/gvisor/pkg/sentry/fs/ramfs", + visibility = ["//pkg/sentry:internal"], + deps = [ + "//pkg/amutex", + "//pkg/log", + "//pkg/refs", + "//pkg/secio", + "//pkg/sentry/context", + "//pkg/sentry/device", + "//pkg/sentry/fs", + "//pkg/sentry/fs/anon", + "//pkg/sentry/fs/fsutil", + "//pkg/sentry/kernel/time", + "//pkg/sentry/memmap", + "//pkg/sentry/safemem", + "//pkg/sentry/usermem", + "//pkg/state", + "//pkg/syserror", + "//pkg/tcpip/transport/unix", + "//pkg/waiter", + ], +) + +go_test( + name = "ramfs_test", + size = "small", + srcs = ["tree_test.go"], + embed = [":ramfs"], + deps = [ + "//pkg/sentry/context/contexttest", + "//pkg/sentry/fs", + ], +) diff --git a/pkg/sentry/fs/ramfs/dir.go b/pkg/sentry/fs/ramfs/dir.go new file mode 100644 index 000000000..bf4cd8dfd --- /dev/null +++ b/pkg/sentry/fs/ramfs/dir.go @@ -0,0 +1,364 @@ +// Copyright 2018 Google Inc. +// +// 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 ( + "sync" + "syscall" + + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" + "gvisor.googlesource.com/gvisor/pkg/syserror" + "gvisor.googlesource.com/gvisor/pkg/tcpip/transport/unix" +) + +// CreateOps represents operations to create different file types. +type CreateOps struct { + // NewDir creates a new directory. + NewDir func(ctx context.Context, dir *fs.Inode, perms fs.FilePermissions) (*fs.Inode, error) + + // NewFile creates a new file. + NewFile func(ctx context.Context, dir *fs.Inode, perms fs.FilePermissions) (*fs.Inode, error) + + // NewSymlink creates a new symlink with permissions 0777. + NewSymlink func(ctx context.Context, dir *fs.Inode, target string) (*fs.Inode, error) + + // NewBoundEndpoint creates a new socket. + NewBoundEndpoint func(ctx context.Context, dir *fs.Inode, ep unix.BoundEndpoint, perms fs.FilePermissions) (*fs.Inode, error) + + // NewFifo creates a new fifo. + NewFifo func(ctx context.Context, dir *fs.Inode, perm fs.FilePermissions) (*fs.Inode, error) +} + +// Dir represents a single directory in the filesystem. +type Dir struct { + Entry + + // CreateOps may be provided. + // + // These may only be modified during initialization (while the application + // is not running). No sychronization is performed when accessing these + // operations during syscalls. + *CreateOps `state:"nosave"` + + // mu protects the fields below. + mu sync.Mutex `state:"nosave"` + + // children are inodes that are in this directory. A reference is held + // on each inode while it is in the map. + children map[string]*fs.Inode + + // dentryMap is a sortedDentryMap containing entries for all children. + // Its entries ar 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) + 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)) + for name, inode := range contents { + entries[name] = fs.DentAttr{ + Type: inode.StableAttr.Type, + InodeID: inode.StableAttr.InodeID, + } + } + d.dentryMap = fs.NewSortedDentryMap(entries) + + // Directories have an extra link, corresponding to '.'. + d.AddLink() +} + +// addChildLocked add the child inode, inheriting its reference. +func (d *Dir) addChildLocked(name string, inode *fs.Inode) { + d.children[name] = inode + d.dentryMap.Add(name, fs.DentAttr{ + Type: inode.StableAttr.Type, + InodeID: inode.StableAttr.InodeID, + }) + + // If the child is a directory, increment this dir's link count, + // corresponding to '..' from the subdirectory. + if fs.IsDir(inode.StableAttr) { + d.AddLink() + } + + // Given we're now adding this inode to the directory we must also + // increase its link count. Similiarly we decremented it in removeChildLocked. + inode.AddLink() +} + +// AddChild adds a child to this dir. +func (d *Dir) AddChild(ctx context.Context, name string, inode *fs.Inode) { + d.mu.Lock() + defer d.mu.Unlock() + d.addChildLocked(name, inode) +} + +// FindChild returns (child, true) if the directory contains name. +func (d *Dir) FindChild(name string) (*fs.Inode, bool) { + d.mu.Lock() + defer d.mu.Unlock() + child, ok := d.children[name] + return child, ok +} + +// 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 + } + + delete(d.children, name) + d.dentryMap.Remove(name) + d.Entry.NotifyModification(ctx) + + // If the child was a subdirectory, then we must decrement this dir's + // link count which was the child's ".." directory entry. + if fs.IsDir(inode.StableAttr) { + d.DropLink() + } + + // Update ctime. + inode.NotifyStatusChange(ctx) + + // Given we're now removing this inode to the directory we must also + // decrease its link count. Similiarly it is increased in addChildLocked. + inode.DropLink() + + return inode, nil +} + +// RemoveEntry attempts to remove an entry from this directory. +func (d *Dir) RemoveEntry(ctx context.Context, name string) error { + d.mu.Lock() + defer d.mu.Unlock() + inode, err := d.removeChildLocked(ctx, name) + if err != nil { + return err + } + + // Remove our reference on the inode. + inode.DecRef() + 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 { + d.mu.Lock() + defer d.mu.Unlock() + + n, err := d.walkLocked(ctx, name) + if err != nil { + return err + } + dirCtx := &fs.DirCtx{} + if _, err := n.HandleOps().DeprecatedReaddir(ctx, dirCtx, 0); err != nil { + return err + } + if len(dirCtx.DentAttrs()) > 0 { + return ErrNotEmpty + } + inode, err := d.removeChildLocked(ctx, name) + if err != nil { + return err + } + + // Remove our reference on the inode. + inode.DecRef() + + return err +} + +// Lookup loads an inode at p into a Dirent. +func (d *Dir) Lookup(ctx context.Context, dir *fs.Inode, p string) (*fs.Dirent, error) { + d.mu.Lock() + defer d.mu.Unlock() + + inode, err := d.walkLocked(ctx, p) + if err != nil { + return nil, err + } + + // Take a reference on the inode before returning it. This reference + // is owned by the dirent we are about to create. + inode.IncRef() + return fs.NewDirent(inode, p), nil +} + +// walkLocked must be called with this Entry's mutex held. +func (d *Dir) walkLocked(ctx context.Context, p string) (*fs.Inode, error) { + d.Entry.NotifyAccess(ctx) + + // Lookup a child node. + if inode, ok := d.children[p]; ok { + return inode, nil + } + + // fs.InodeOperations.Lookup returns syserror.ENOENT if p + // does not exist. + return nil, syserror.ENOENT +} + +// createInodeOperationsCommon creates a new child node at this dir by calling +// makeInodeOperations. It is the common logic for creating a new child. +func (d *Dir) createInodeOperationsCommon(ctx context.Context, name string, makeInodeOperations func() (*fs.Inode, error)) (*fs.Inode, error) { + d.mu.Lock() + defer d.mu.Unlock() + + if _, ok := d.children[name]; ok { + return nil, syscall.EEXIST + } + + inode, err := makeInodeOperations() + if err != nil { + return nil, err + } + + d.addChildLocked(name, inode) + d.Entry.NotifyModification(ctx) + + return inode, nil +} + +// 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 + } + + inode, err := d.createInodeOperationsCommon(ctx, name, func() (*fs.Inode, error) { + return d.NewFile(ctx, dir, perms) + }) + if err != nil { + return nil, err + } + + // Take an extra ref on inode, which will be owned by the dirent. + inode.IncRef() + + // Create the Dirent and corresponding file. + created := fs.NewDirent(inode, name) + defer created.DecRef() + return created.Inode.GetFile(ctx, created, flags) +} + +// 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 + } + _, err := d.createInodeOperationsCommon(ctx, newname, func() (*fs.Inode, error) { + return d.NewSymlink(ctx, dir, oldname) + }) + return err +} + +// CreateHardLink creates a new hard link. +func (d *Dir) CreateHardLink(ctx context.Context, dir *fs.Inode, target *fs.Inode, name string) error { + d.mu.Lock() + defer d.mu.Unlock() + + // Take an extra reference on the inode and add it to our children. + target.IncRef() + + // The link count will be incremented in addChildLocked. + d.addChildLocked(name, target) + d.Entry.NotifyModification(ctx) + + // Update ctime. + target.NotifyStatusChange(ctx) + + return nil +} + +// 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 + } + _, err := d.createInodeOperationsCommon(ctx, name, func() (*fs.Inode, error) { + return d.NewDir(ctx, dir, perms) + }) + // TODO: Support updating status times, as those should be + // updated by links. + return err +} + +// Bind implements fs.InodeOperations.Bind. +func (d *Dir) Bind(ctx context.Context, dir *fs.Inode, name string, ep unix.BoundEndpoint, perms fs.FilePermissions) error { + if d.CreateOps == nil || d.CreateOps.NewBoundEndpoint == nil { + return ErrDenied + } + _, err := d.createInodeOperationsCommon(ctx, name, func() (*fs.Inode, error) { + return d.NewBoundEndpoint(ctx, dir, ep, perms) + }) + if err == syscall.EEXIST { + return syscall.EADDRINUSE + } + return err +} + +// 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 + } + _, err := d.createInodeOperationsCommon(ctx, name, func() (*fs.Inode, error) { + return d.NewFifo(ctx, dir, 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) + + // Touch the access time. + d.Entry.NotifyAccess(ctx) + + 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) +} + +// DeprecatedPreadv always returns ErrIsDirectory +func (*Dir) DeprecatedPreadv(context.Context, usermem.IOSequence, int64) (int64, error) { + return 0, ErrIsDirectory +} + +// DeprecatedPwritev always returns ErrIsDirectory +func (*Dir) DeprecatedPwritev(context.Context, usermem.IOSequence, int64) (int64, error) { + return 0, ErrIsDirectory +} diff --git a/pkg/sentry/fs/ramfs/file.go b/pkg/sentry/fs/ramfs/file.go new file mode 100644 index 000000000..e8363c3e2 --- /dev/null +++ b/pkg/sentry/fs/ramfs/file.go @@ -0,0 +1,148 @@ +// Copyright 2018 Google Inc. +// +// 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. +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 new file mode 100644 index 000000000..04f2d38de --- /dev/null +++ b/pkg/sentry/fs/ramfs/ramfs.go @@ -0,0 +1,433 @@ +// Copyright 2018 Google Inc. +// +// 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/usermem" + "gvisor.googlesource.com/gvisor/pkg/syserror" + "gvisor.googlesource.com/gvisor/pkg/tcpip/transport/unix" + "gvisor.googlesource.com/gvisor/pkg/waiter" +) + +var ( + // ErrInvalidOp indicates the operation is not valid. + ErrInvalidOp = errors.New("invalid operation") + + // ErrDenied indicates the operation was denid. + 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. +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() + defer e.mu.Unlock() + return e.unstable, 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) { + e.mu.Lock() + defer e.mu.Unlock() + if value, ok := e.xattrs[name]; 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() + defer e.mu.Unlock() + e.xattrs[name] = value + return nil +} + +// Listxattr implements fs.InodeOperations.Listxattr. +func (e *Entry) Listxattr(inode *fs.Inode) (map[string]struct{}, error) { + e.mu.Lock() + defer e.mu.Unlock() + names := make(map[string]struct{}, len(e.xattrs)) + for name := range e.xattrs { + names[name] = struct{}{} + } + 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() + defer e.mu.Unlock() + e.unstable.Perms = p + e.unstable.StatusChangeTime = ktime.NowFromContext(ctx) + return true +} + +// SetOwner always sets ownership. +func (e *Entry) SetOwner(ctx context.Context, inode *fs.Inode, owner fs.FileOwner) error { + e.mu.Lock() + defer e.mu.Unlock() + if owner.UID.Ok() { + e.unstable.Owner.UID = owner.UID + } + if owner.GID.Ok() { + e.unstable.Owner.GID = owner.GID + } + 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() + defer e.mu.Unlock() + + 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 + return nil +} + +// NotifyStatusChange updates the status change time (ctime). +func (e *Entry) NotifyStatusChange(ctx context.Context) { + e.mu.Lock() + defer e.mu.Unlock() + e.unstable.StatusChangeTime = ktime.NowFromContext(ctx) +} + +// StatusChangeTime returns the last status change time for this node. +func (e *Entry) StatusChangeTime() ktime.Time { + e.mu.Lock() + defer e.mu.Unlock() + return e.unstable.StatusChangeTime +} + +// NotifyModification updates the modification time and the status change time. +func (e *Entry) NotifyModification(ctx context.Context) { + e.mu.Lock() + defer e.mu.Unlock() + now := ktime.NowFromContext(ctx) + e.unstable.ModificationTime = now + e.unstable.StatusChangeTime = now +} + +// ModificationTime returns the last modification time for this node. +func (e *Entry) ModificationTime() ktime.Time { + e.mu.Lock() + defer e.mu.Unlock() + return e.unstable.ModificationTime +} + +// NotifyAccess updates the access time. +func (e *Entry) NotifyAccess(ctx context.Context) { + e.mu.Lock() + defer e.mu.Unlock() + now := ktime.NowFromContext(ctx) + e.unstable.AccessTime = now +} + +// AccessTime returns the last access time for this node. +func (e *Entry) AccessTime() ktime.Time { + e.mu.Lock() + defer e.mu.Unlock() + return e.unstable.AccessTime +} + +// Permissions returns permissions on this entry. +func (e *Entry) Permissions() fs.FilePermissions { + e.mu.Lock() + defer e.mu.Unlock() + return e.unstable.Perms +} + +// 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, unix.BoundEndpoint, fs.FilePermissions) error { + return 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() + defer e.mu.Unlock() + e.unstable.Links++ +} + +// DropLink implements InodeOperationss.DropLink. +func (e *Entry) DropLink() { + e.mu.Lock() + defer e.mu.Unlock() + e.unstable.Links-- +} + +// 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 new file mode 100644 index 000000000..b0c79325f --- /dev/null +++ b/pkg/sentry/fs/ramfs/socket.go @@ -0,0 +1,42 @@ +// Copyright 2018 Google Inc. +// +// 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 ( + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/tcpip/transport/unix" +) + +// Socket represents a socket. +type Socket struct { + Entry + + // ep is the bound endpoint. + ep unix.BoundEndpoint +} + +// InitSocket initializes a socket. +func (s *Socket) InitSocket(ctx context.Context, ep unix.BoundEndpoint, owner fs.FileOwner, perms fs.FilePermissions) { + s.InitEntry(ctx, owner, perms) + s.ep = ep +} + +// BoundEndpoint returns the socket data. +func (s *Socket) BoundEndpoint(*fs.Inode, string) unix.BoundEndpoint { + // ramfs only supports stored sentry internal sockets. Only gofer sockets + // care about the path argument. + return s.ep +} diff --git a/pkg/sentry/fs/ramfs/symlink.go b/pkg/sentry/fs/ramfs/symlink.go new file mode 100644 index 000000000..9bbf78619 --- /dev/null +++ b/pkg/sentry/fs/ramfs/symlink.go @@ -0,0 +1,72 @@ +// Copyright 2018 Google Inc. +// +// 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 ( + "sync" + + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" +) + +// Symlink represents a symlink. +type Symlink struct { + Entry + + mu sync.Mutex `state:"nosave"` + + // 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 +} + +// 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.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 +} + +// 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) + return s.Target, nil +} + +// Getlink returns ErrResolveViaReadlink, falling back to walking to the result +// of Readlink(). +func (*Symlink) Getlink(context.Context, *fs.Inode) (*fs.Dirent, error) { + return nil, fs.ErrResolveViaReadlink +} diff --git a/pkg/sentry/fs/ramfs/test/BUILD b/pkg/sentry/fs/ramfs/test/BUILD new file mode 100644 index 000000000..074b0f5ad --- /dev/null +++ b/pkg/sentry/fs/ramfs/test/BUILD @@ -0,0 +1,31 @@ +package(licenses = ["notice"]) # Apache 2.0 + +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//tools/go_stateify:defs.bzl", "go_stateify") + +go_stateify( + name = "test_state", + srcs = [ + "test.go", + ], + out = "test_state.go", + package = "test", +) + +go_library( + name = "test", + testonly = 1, + srcs = [ + "test.go", + "test_state.go", + ], + importpath = "gvisor.googlesource.com/gvisor/pkg/sentry/fs/ramfs/test", + visibility = ["//pkg/sentry:internal"], + deps = [ + "//pkg/sentry/context", + "//pkg/sentry/device", + "//pkg/sentry/fs", + "//pkg/sentry/fs/ramfs", + "//pkg/state", + ], +) diff --git a/pkg/sentry/fs/ramfs/test/test.go b/pkg/sentry/fs/ramfs/test/test.go new file mode 100644 index 000000000..fb669558f --- /dev/null +++ b/pkg/sentry/fs/ramfs/test/test.go @@ -0,0 +1,46 @@ +// Copyright 2018 Google Inc. +// +// 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 new file mode 100644 index 000000000..1fb335f74 --- /dev/null +++ b/pkg/sentry/fs/ramfs/tree.go @@ -0,0 +1,71 @@ +// Copyright 2018 Google Inc. +// +// 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 ( + "fmt" + "path" + "strings" + + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs/anon" + "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" +) + +// MakeDirectoryTree constructs a ramfs tree of all directories containing +// subdirs. Each element of subdir must be a clean path, and cannot be empty or +// "/". +func MakeDirectoryTree(ctx context.Context, msrc *fs.MountSource, subdirs []string) (*fs.Inode, error) { + root := emptyDir(ctx, msrc) + for _, subdir := range subdirs { + if path.Clean(subdir) != subdir { + return nil, fmt.Errorf("cannot add subdir at an unclean path: %q", subdir) + } + if subdir == "" || subdir == "/" { + return nil, fmt.Errorf("cannot add subdir at %q", subdir) + } + makeSubdir(ctx, msrc, root.InodeOperations.(*Dir), subdir) + } + return root, nil +} + +// makeSubdir installs into root each component of subdir. The final component is +// a *ramfs.Dir. +func makeSubdir(ctx context.Context, msrc *fs.MountSource, root *Dir, subdir string) { + for _, c := range strings.Split(subdir, "/") { + if len(c) == 0 { + continue + } + child, ok := root.FindChild(c) + if !ok { + child = emptyDir(ctx, msrc) + root.AddChild(ctx, c, child) + } + root = child.InodeOperations.(*Dir) + } +} + +// 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)) + return fs.NewInode(dir, msrc, fs.StableAttr{ + DeviceID: anon.PseudoDevice.DeviceID(), + InodeID: anon.PseudoDevice.NextIno(), + BlockSize: usermem.PageSize, + Type: fs.Directory, + }) +} diff --git a/pkg/sentry/fs/ramfs/tree_test.go b/pkg/sentry/fs/ramfs/tree_test.go new file mode 100644 index 000000000..68e2929d5 --- /dev/null +++ b/pkg/sentry/fs/ramfs/tree_test.go @@ -0,0 +1,79 @@ +// Copyright 2018 Google Inc. +// +// 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 ( + "testing" + + "gvisor.googlesource.com/gvisor/pkg/sentry/context/contexttest" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" +) + +func TestMakeDirectoryTree(t *testing.T) { + mount := fs.NewNonCachingMountSource(nil, fs.MountSourceFlags{}) + + for _, test := range []struct { + name string + subdirs []string + }{ + { + name: "abs paths", + subdirs: []string{ + "/tmp", + "/tmp/a/b", + "/tmp/a/c/d", + "/tmp/c", + "/proc", + "/dev/a/b", + "/tmp", + }, + }, + { + name: "rel paths", + subdirs: []string{ + "tmp", + "tmp/a/b", + "tmp/a/c/d", + "tmp/c", + "proc", + "dev/a/b", + "tmp", + }, + }, + } { + ctx := contexttest.Context(t) + tree, err := MakeDirectoryTree(ctx, mount, test.subdirs) + if err != nil { + t.Errorf("%s: failed to make ramfs tree, got error %v, want nil", test.name, err) + continue + } + + // Expect to be able to find each of the paths. + mm, err := fs.NewMountNamespace(ctx, tree) + if err != nil { + t.Errorf("%s: failed to create mount manager: %v", test.name, err) + continue + } + root := mm.Root() + defer mm.DecRef() + + for _, p := range test.subdirs { + if _, err := mm.FindInode(ctx, root, nil, p, 0); err != nil { + t.Errorf("%s: failed to find node %s: %v", test.name, p, err) + break + } + } + } +} |