diff options
Diffstat (limited to 'runsc/fsgofer')
-rw-r--r-- | runsc/fsgofer/BUILD | 33 | ||||
-rw-r--r-- | runsc/fsgofer/fsgofer.go | 937 | ||||
-rw-r--r-- | runsc/fsgofer/fsgofer_test.go | 576 | ||||
-rw-r--r-- | runsc/fsgofer/fsgofer_unsafe.go | 58 |
4 files changed, 1604 insertions, 0 deletions
diff --git a/runsc/fsgofer/BUILD b/runsc/fsgofer/BUILD new file mode 100644 index 000000000..24e172f48 --- /dev/null +++ b/runsc/fsgofer/BUILD @@ -0,0 +1,33 @@ +package(licenses = ["notice"]) # Apache 2.0 + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "fsgofer", + srcs = [ + "fsgofer.go", + "fsgofer_unsafe.go", + ], + importpath = "gvisor.googlesource.com/gvisor/runsc/fsgofer", + visibility = [ + "//runsc:__subpackages__", + ], + deps = [ + "//pkg/abi/linux", + "//pkg/fd", + "//pkg/log", + "//pkg/p9", + "@org_golang_x_sys//unix:go_default_library", + ], +) + +go_test( + name = "fsgofer_test", + size = "small", + srcs = ["fsgofer_test.go"], + embed = [":fsgofer"], + deps = [ + "//pkg/log", + "//pkg/p9", + ], +) diff --git a/runsc/fsgofer/fsgofer.go b/runsc/fsgofer/fsgofer.go new file mode 100644 index 000000000..5ddc75a9d --- /dev/null +++ b/runsc/fsgofer/fsgofer.go @@ -0,0 +1,937 @@ +// 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 fsgofer implements p9.File giving access to local files using +// a simple mapping from a path prefix that is added to the path requested +// by the sandbox. Ex: +// +// prefix: "/docker/imgs/alpine" +// app path: /bin/ls => /docker/imgs/alpine/bin/ls +package fsgofer + +import ( + "fmt" + "io" + "math" + "os" + "path" + "path/filepath" + "strings" + "sync" + "syscall" + + "golang.org/x/sys/unix" + "gvisor.googlesource.com/gvisor/pkg/abi/linux" + "gvisor.googlesource.com/gvisor/pkg/fd" + "gvisor.googlesource.com/gvisor/pkg/log" + "gvisor.googlesource.com/gvisor/pkg/p9" +) + +const ( + // invalidMode is set to a value that doesn't match any other valid + // modes to ensure an unopened/closed file fails all mode checks. + invalidMode = p9.OpenFlags(math.MaxUint32) + + openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC +) + +type fileType int + +const ( + regular fileType = iota + directory + symlink +) + +// String implements fmt.Stringer. +func (f fileType) String() string { + switch f { + case regular: + return "regular" + case directory: + return "directory" + case symlink: + return "symlink" + } + return "unknown" +} + +// Config sets configuration options for each attach point. +type Config struct { + // ROMount is set to true if this is a readonly mount. + ROMount bool + + // LazyOpenForWrite makes the underlying file to be opened in RDONLY + // mode initially and be reopened in case write access is desired. + // This is done to workaround the behavior in 'overlay2' that + // copies the entire file up eagerly when it's opened in write mode + // even if the file is never actually written to. + LazyOpenForWrite bool +} + +type attachPoint struct { + prefix string + conf Config +} + +// NewAttachPoint creates a new attacher that gives local file +// access to all files under 'prefix'. +func NewAttachPoint(prefix string, c Config) p9.Attacher { + return &attachPoint{prefix: prefix, conf: c} +} + +// Attach implements p9.Attacher. +func (a *attachPoint) Attach(appPath string) (p9.File, error) { + if !path.IsAbs(appPath) { + return nil, fmt.Errorf("invalid path %q", appPath) + } + + root := filepath.Join(a.prefix, appPath) + f, err := os.OpenFile(root, openFlags|syscall.O_RDONLY, 0) + if err != nil { + return nil, fmt.Errorf("unable to open file %q, err: %v", root, err) + } + stat, err := stat(int(f.Fd())) + if err != nil { + return nil, fmt.Errorf("failed to stat file %q, err: %v", root, err) + } + return newLocalFile(a.conf, f, root, stat) +} + +func makeQID(stat syscall.Stat_t) p9.QID { + return p9.QID{ + Type: p9.FileMode(stat.Mode).QIDType(), + Path: stat.Ino, + } +} + +func isNameValid(name string) bool { + if name == "" || name == "." || name == ".." { + log.Warningf("Invalid name: %s", name) + return false + } + if strings.IndexByte(name, '/') >= 0 { + log.Warningf("Invalid name: %s", name) + return false + } + return true +} + +// localFile implements p9.File wrapping a local file. The underlying file +// is opened during Walk() and stored in 'controlFile' to be used with other +// operations. The mode in which the file is opened varies depending on the +// configuration (see below). 'controlFile' is dup'ed when Walk(nil) is called +// to clone the file. +// +// 'openedFile' is assigned when Open() is called. If requested open mode is +// a subset of controlFile's mode, it's possible to use the same file. If mode +// is not a subset, then another file is opened. Consequently, 'openedFile' +// could have a mode wider than requested and must be verified before read/write +// operations. Before the file is opened and after it's closed, 'mode' is set to +// an invalid value to prevent an unopened file from being used. +// +// localFile has 2 modes of operation based on the configuration: +// +// ** conf.lazyRWOpen == false ** +// This is the preferred mode. 'controlFile' is opened in RW mode in Walk() +// and used across all functions. The file is never reopened as the mode will +// always be a super set of the requested open mode. This reduces the number of +// syscalls required per operation and makes it resilient to renames anywhere +// in the path to the file. +// +// ** conf.lazyRWOpen == true ** +// This mode is used for better performance with 'overlay2' storage driver. +// overlay2 eagerly copies the entire file up when it's opened in write mode +// which makes the mode above perform badly when serveral of files are opened +// for read (esp. startup). In this mode, 'controlFile' is opened as readonly +// (or O_PATH for symlinks). Reopening the file is required if write mode +// is requested in Open(). +type localFile struct { + p9.DefaultWalkGetAttr + + // mu protects 'hostPath' when file is renamed. + mu sync.Mutex + + // TODO: hostPath is not safe to use as path needs to be walked + // everytime (and can change underneath us). Remove all usages. + hostPath string + + // controlFile is opened when localFile is created and it's never nil. + controlFile *os.File + + // openedFile is nil until localFile is opened. It may point to controlFile + // or be a new file struct. See struct comment for more details. + openedFile *os.File + + // mode is the mode in which the file was opened. Set to invalidMode + // if localFile isn't opened. + mode p9.OpenFlags + + ft fileType + + conf Config + + // readDirMu protects against concurrent Readdir calls. + readDirMu sync.Mutex +} + +func openAnyFile(parent *localFile, name string) (*os.File, string, error) { + // Attempt to open file in the following mode in order: + // 1. RDWR: for files with rw mounts and LazyOpenForWrite disabled + // 2. RDONLY: for directories, ro mounts or LazyOpenForWrite enabled + // 3. PATH: for symlinks + modes := []int{syscall.O_RDWR, syscall.O_RDONLY, unix.O_PATH} + symlinkIdx := len(modes) - 1 + + startIdx := 0 + if parent.conf.ROMount || parent.conf.LazyOpenForWrite { + // Skip attempt to open in RDWR based on configuration. + startIdx = 1 + } + + var err error + var fd int + for i := startIdx; i < len(modes); i++ { + fd, err = syscall.Openat(parent.controlFD(), name, openFlags|modes[i], 0) + if err == nil { + // openat succeeded, we're done. + break + } + switch e := extractErrno(err); e { + case syscall.ENOENT: + // File doesn't exist, no point in retrying. + return nil, "", e + case syscall.ELOOP: + if i < symlinkIdx { + // File was opened with O_NOFOLLOW, so this error can only happen when + // trying ot open a symlink. Jump straight to flags compatible with symlink. + i = symlinkIdx - 1 + } + } + // openat failed. Try again with next mode, preserving 'err' in + // case this was the last attempt. + log.Debugf("Attempt %d to open file failed, mode: %#x, path: %s/%s, err: %v", i, openFlags|modes[i], parent.controlFile.Name(), name, err) + } + if err != nil { + // All attempts to open file have failed, return the last error. + log.Debugf("Failed to open file, path: %s/%s, err: %v", parent.controlFile.Name(), name, err) + return nil, "", extractErrno(err) + } + + parent.mu.Lock() + defer parent.mu.Unlock() + newPath := path.Join(parent.hostPath, name) + + return os.NewFile(uintptr(fd), newPath), newPath, nil +} + +func newLocalFile(conf Config, file *os.File, path string, stat syscall.Stat_t) (*localFile, error) { + var ft fileType + switch stat.Mode & syscall.S_IFMT { + case syscall.S_IFREG: + ft = regular + case syscall.S_IFDIR: + ft = directory + case syscall.S_IFLNK: + ft = symlink + default: + return nil, syscall.EINVAL + } + return &localFile{ + hostPath: path, + controlFile: file, + conf: conf, + mode: invalidMode, + ft: ft, + }, nil +} + +// newFDMaybe creates a fd.FD from a file, dup'ing the FD and setting it as +// non-blocking. If anything fails, returns nil. It's better to have a file +// without host FD, than to fail the operation. +func newFDMaybe(file *os.File) *fd.FD { + fd, err := fd.NewFromFile(file) + if err != nil { + return nil + } + + // fd is blocking; non-blocking is required. + if err := syscall.SetNonblock(fd.FD(), true); err != nil { + fd.Close() + return nil + } + return fd +} + +func stat(fd int) (syscall.Stat_t, error) { + var stat syscall.Stat_t + if err := syscall.Fstat(fd, &stat); err != nil { + return syscall.Stat_t{}, err + } + return stat, nil +} + +func fchown(fd int, uid p9.UID, gid p9.GID) error { + return syscall.Fchownat(fd, "", int(uid), int(gid), linux.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW) +} + +func (l *localFile) controlFD() int { + return int(l.controlFile.Fd()) +} + +func (l *localFile) openedFD() int { + if l.openedFile == nil { + panic(fmt.Sprintf("trying to use an unopened file: %q", l.controlFile.Name())) + } + return int(l.openedFile.Fd()) +} + +// Open implements p9.File. +func (l *localFile) Open(mode p9.OpenFlags) (*fd.FD, p9.QID, uint32, error) { + if l.openedFile != nil { + panic(fmt.Sprintf("attempting to open already opened file: %q", l.controlFile.Name())) + } + + // Check if control file can be used or if a new open must be created. + var newFile *os.File + if mode == p9.ReadOnly || !l.conf.LazyOpenForWrite { + log.Debugf("Open reusing control file, mode: %v, %q", mode, l.controlFile.Name()) + newFile = l.controlFile + } else { + // Ideally reopen would call name_to_handle_at (with empty name) and open_by_handle_at + // to reopen the file without using 'hostPath'. However, name_to_handle_at and + // open_by_handle_at aren't supported by overlay2. + log.Debugf("Open reopening file, mode: %v, %q", mode, l.controlFile.Name()) + var err error + + l.mu.Lock() + newFile, err = os.OpenFile(l.hostPath, openFlags|mode.OSFlags(), 0) + if err != nil { + l.mu.Unlock() + return nil, p9.QID{}, 0, extractErrno(err) + } + l.mu.Unlock() + } + + stat, err := stat(int(newFile.Fd())) + if err != nil { + newFile.Close() + return nil, p9.QID{}, 0, extractErrno(err) + } + + var fd *fd.FD + if stat.Mode&syscall.S_IFMT == syscall.S_IFREG { + // Donate FD for regular files only. + fd = newFDMaybe(newFile) + } + + // Set fields on success + l.openedFile = newFile + l.mode = mode + return fd, makeQID(stat), 0, nil +} + +// Create implements p9.File. +func (l *localFile) Create(name string, mode p9.OpenFlags, perm p9.FileMode, uid p9.UID, gid p9.GID) (*fd.FD, p9.File, p9.QID, uint32, error) { + if l.conf.ROMount { + return nil, nil, p9.QID{}, 0, syscall.EBADF + } + if !isNameValid(name) { + return nil, nil, p9.QID{}, 0, syscall.EINVAL + } + + // Use a single file for both 'controlFile' and 'openedFile'. Mode must include read for control + // and whichever else was requested by caller. Note that resulting file might have a wider mode + // than needed for each particular case. + flags := openFlags | syscall.O_CREAT | syscall.O_EXCL + if mode == p9.WriteOnly { + flags |= syscall.O_RDWR + } else { + flags |= mode.OSFlags() + } + + fd, err := syscall.Openat(l.controlFD(), name, flags, uint32(perm.Permissions())) + if err != nil { + return nil, nil, p9.QID{}, 0, extractErrno(err) + } + if err := fchown(fd, uid, gid); err != nil { + syscall.Close(fd) + return nil, nil, p9.QID{}, 0, extractErrno(err) + } + stat, err := stat(fd) + if err != nil { + syscall.Close(fd) + return nil, nil, p9.QID{}, 0, extractErrno(err) + } + + l.mu.Lock() + defer l.mu.Unlock() + + cPath := path.Join(l.hostPath, name) + f := os.NewFile(uintptr(fd), cPath) + c := &localFile{ + hostPath: cPath, + controlFile: f, + openedFile: f, + mode: mode, + conf: l.conf, + } + return newFDMaybe(c.openedFile), c, makeQID(stat), 0, nil +} + +// Mkdir implements p9.File. +func (l *localFile) Mkdir(name string, perm p9.FileMode, uid p9.UID, gid p9.GID) (p9.QID, error) { + if l.conf.ROMount { + return p9.QID{}, syscall.EBADF + } + + if !isNameValid(name) { + return p9.QID{}, syscall.EINVAL + } + + if err := syscall.Mkdirat(l.controlFD(), name, uint32(perm.Permissions())); err != nil { + return p9.QID{}, extractErrno(err) + } + + // Open directory to change ownership and stat it. + flags := syscall.O_DIRECTORY | syscall.O_RDONLY | openFlags + fd, err := syscall.Openat(l.controlFD(), name, flags, 0) + if err != nil { + return p9.QID{}, extractErrno(err) + } + defer syscall.Close(fd) + + if err := fchown(fd, uid, gid); err != nil { + return p9.QID{}, extractErrno(err) + } + stat, err := stat(fd) + if err != nil { + return p9.QID{}, extractErrno(err) + } + return makeQID(stat), nil +} + +// Walk implements p9.File. +func (l *localFile) Walk(names []string) ([]p9.QID, p9.File, error) { + // Duplicate current file if 'names' is empty. + if len(names) == 0 { + newFd, err := syscall.Dup(l.controlFD()) + if err != nil { + return nil, nil, extractErrno(err) + } + stat, err := stat(newFd) + if err != nil { + syscall.Close(newFd) + return nil, nil, extractErrno(err) + } + + l.mu.Lock() + defer l.mu.Unlock() + + c := &localFile{ + hostPath: l.hostPath, + controlFile: os.NewFile(uintptr(newFd), l.hostPath), + mode: invalidMode, + conf: l.conf, + } + return []p9.QID{makeQID(stat)}, c, nil + } + + var qids []p9.QID + last := l + for _, name := range names { + if !isNameValid(name) { + return nil, nil, syscall.EINVAL + } + + f, path, err := openAnyFile(last, name) + if err != nil { + return nil, nil, extractErrno(err) + } + stat, err := stat(int(f.Fd())) + if err != nil { + return nil, nil, extractErrno(err) + } + c, err := newLocalFile(last.conf, f, path, stat) + if err != nil { + return nil, nil, extractErrno(err) + } + + qids = append(qids, makeQID(stat)) + last = c + } + return qids, last, nil +} + +// StatFS implements p9.File. +func (l *localFile) StatFS() (p9.FSStat, error) { + var s syscall.Statfs_t + if err := syscall.Fstatfs(l.controlFD(), &s); err != nil { + return p9.FSStat{}, extractErrno(err) + } + + // Populate with what's available. + return p9.FSStat{ + Type: uint32(s.Type), + BlockSize: uint32(s.Bsize), + Blocks: s.Blocks, + BlocksFree: s.Bfree, + BlocksAvailable: s.Bavail, + Files: s.Files, + FilesFree: s.Ffree, + NameLength: uint32(s.Namelen), + }, nil +} + +// FSync implements p9.File. +func (l *localFile) FSync() error { + if l.openedFile == nil { + return syscall.EBADF + } + if err := l.openedFile.Sync(); err != nil { + return extractErrno(err) + } + return nil +} + +// GetAttr implements p9.File. +func (l *localFile) GetAttr(_ p9.AttrMask) (p9.QID, p9.AttrMask, p9.Attr, error) { + stat, err := stat(l.controlFD()) + if err != nil { + return p9.QID{}, p9.AttrMask{}, p9.Attr{}, extractErrno(err) + } + + attr := p9.Attr{ + Mode: p9.FileMode(stat.Mode), + UID: p9.UID(stat.Uid), + GID: p9.GID(stat.Gid), + NLink: stat.Nlink, + RDev: stat.Rdev, + Size: uint64(stat.Size), + BlockSize: uint64(stat.Blksize), + Blocks: uint64(stat.Blocks), + ATimeSeconds: uint64(stat.Atim.Sec), + ATimeNanoSeconds: uint64(stat.Atim.Nsec), + MTimeSeconds: uint64(stat.Mtim.Sec), + MTimeNanoSeconds: uint64(stat.Mtim.Nsec), + CTimeSeconds: uint64(stat.Ctim.Sec), + CTimeNanoSeconds: uint64(stat.Ctim.Nsec), + } + valid := p9.AttrMask{ + Mode: true, + UID: true, + GID: true, + NLink: true, + RDev: true, + Size: true, + Blocks: true, + ATime: true, + MTime: true, + CTime: true, + } + + return makeQID(stat), valid, attr, nil +} + +// SetAttr implements p9.File. Due to mismatch in file API, options +// cannot be changed atomicaly and user may see partial changes when +// an error happens. +func (l *localFile) SetAttr(valid p9.SetAttrMask, attr p9.SetAttr) error { + if l.conf.ROMount { + return syscall.EBADF + } + + allowed := p9.SetAttrMask{ + Permissions: true, + UID: true, + GID: true, + Size: true, + ATime: true, + MTime: true, + ATimeNotSystemTime: true, + MTimeNotSystemTime: true, + } + + if valid.Empty() { + // Nothing to do. + return nil + } + + // Handle all the sanity checks up front so that the client gets a + // consistent result that is not attribute dependent. + if !valid.IsSubsetOf(allowed) { + log.Warningf("SetAttr() failed for %q, mask: %v", l.controlFile.Name(), valid) + return syscall.EPERM + } + + fd := l.controlFD() + if l.conf.LazyOpenForWrite && l.ft == regular { + // Regular files are opened in RO mode when lazy open is set. + // Thus it needs to be reopened here for write. + f, err := os.OpenFile(l.hostPath, openFlags|os.O_WRONLY, 0) + if err != nil { + return extractErrno(err) + } + defer f.Close() + fd = int(f.Fd()) + } + + // The semantics are to either return an error if no changes were made, + // or no error if *all* changes were made. Well, this can be impossible + // if the filesystem rejects at least one of the changes, especially + // since some operations are not easy to undo atomically. + // + // This could be made better if SetAttr actually returned the changes + // it did make, so the client can at least know what has changed. So + // we at least attempt to make all of the changes and return a generic + // error if any of them fails, which at least doesn't bias any change + // over another. + var err error + if valid.Permissions { + if cerr := syscall.Fchmod(fd, uint32(attr.Permissions)); cerr != nil { + log.Debugf("SetAttr fchmod failed %q, err: %v", l.hostPath, cerr) + err = extractErrno(cerr) + } + } + + if valid.Size { + if terr := syscall.Ftruncate(fd, int64(attr.Size)); terr != nil { + log.Debugf("SetAttr ftruncate failed %q, err: %v", l.hostPath, terr) + err = extractErrno(terr) + } + } + + if valid.ATime || valid.MTime { + utimes := [2]syscall.Timespec{ + syscall.Timespec{Sec: 0, Nsec: linux.UTIME_OMIT}, + syscall.Timespec{Sec: 0, Nsec: linux.UTIME_OMIT}, + } + if valid.ATime { + if valid.ATimeNotSystemTime { + utimes[0].Sec = int64(attr.ATimeSeconds) + utimes[0].Nsec = int64(attr.ATimeNanoSeconds) + } else { + utimes[0].Nsec = linux.UTIME_NOW + } + } + if valid.MTime { + if valid.MTimeNotSystemTime { + utimes[1].Sec = int64(attr.MTimeSeconds) + utimes[1].Nsec = int64(attr.MTimeNanoSeconds) + } else { + utimes[1].Nsec = linux.UTIME_NOW + } + } + + if l.ft == symlink { + // utimensat operates different that other syscalls. To operate on a + // symlink it *requires* AT_SYMLINK_NOFOLLOW with dirFD and a non-empty + // name. + f, err := os.OpenFile(path.Dir(l.hostPath), openFlags|unix.O_PATH, 0) + if err != nil { + return extractErrno(err) + } + defer f.Close() + + if terr := utimensat(int(f.Fd()), path.Base(l.hostPath), utimes, linux.AT_SYMLINK_NOFOLLOW); terr != nil { + log.Debugf("SetAttr utimens failed %q, err: %v", l.hostPath, terr) + err = extractErrno(terr) + } + } else { + // Directories and regular files can operate directly on the fd + // using empty name. + if terr := utimensat(fd, "", utimes, 0); terr != nil { + log.Debugf("SetAttr utimens failed %q, err: %v", l.hostPath, terr) + err = extractErrno(terr) + } + } + } + + if valid.UID || valid.GID { + uid := -1 + if valid.UID { + uid = int(attr.UID) + } + gid := -1 + if valid.GID { + gid = int(attr.GID) + } + if oerr := syscall.Fchownat(fd, "", uid, gid, linux.AT_EMPTY_PATH|linux.AT_SYMLINK_NOFOLLOW); oerr != nil { + log.Debugf("SetAttr fchownat failed %q, err: %v", l.hostPath, oerr) + err = extractErrno(oerr) + } + } + + return err +} + +// Remove implements p9.File. +// +// This is deprecated in favor of UnlinkAt. +func (*localFile) Remove() error { + return syscall.ENOSYS +} + +// Rename implements p9.File. +func (l *localFile) Rename(directory p9.File, name string) error { + if l.conf.ROMount { + return syscall.EBADF + } + if !isNameValid(name) { + return syscall.EINVAL + } + + l.mu.Lock() + defer l.mu.Unlock() + + // TODO: change to renameat(2) + parent := directory.(*localFile) + newPath := path.Join(parent.hostPath, name) + if err := os.Rename(l.hostPath, newPath); err != nil { + return extractErrno(err) + } + + // Update path on success. + // TODO: this doesn't cover cases where any of the + // parents have been renamed. + l.hostPath = newPath + return nil +} + +// RenameAt implements p9.File.RenameAt. +// +// Code still uses [deprecated] Rename(). +func (*localFile) RenameAt(_ string, _ p9.File, _ string) error { + return syscall.ENOSYS +} + +// ReadAt implements p9.File. +func (l *localFile) ReadAt(p []byte, offset uint64) (int, error) { + if l.mode != p9.ReadOnly && l.mode != p9.ReadWrite { + return 0, syscall.EBADF + } + if l.openedFile == nil { + return 0, syscall.EBADF + } + + r, err := l.openedFile.ReadAt(p, int64(offset)) + switch err { + case nil, io.EOF: + return r, nil + default: + return r, extractErrno(err) + } +} + +// WriteAt implements p9.File. +func (l *localFile) WriteAt(p []byte, offset uint64) (int, error) { + if l.mode != p9.WriteOnly && l.mode != p9.ReadWrite { + return 0, syscall.EBADF + } + if l.openedFile == nil { + return 0, syscall.EBADF + } + + w, err := l.openedFile.WriteAt(p, int64(offset)) + if err != nil { + return w, extractErrno(err) + } + return w, nil +} + +// Symlink implements p9.File. +func (l *localFile) Symlink(target, newName string, uid p9.UID, gid p9.GID) (p9.QID, error) { + if l.conf.ROMount { + return p9.QID{}, syscall.EBADF + } + if !isNameValid(newName) { + return p9.QID{}, syscall.EINVAL + } + + if err := unix.Symlinkat(target, l.controlFD(), newName); err != nil { + return p9.QID{}, extractErrno(err) + } + + // Open symlink to change ownership and stat it. + fd, err := syscall.Openat(l.controlFD(), newName, unix.O_PATH|openFlags, 0) + if err != nil { + return p9.QID{}, extractErrno(err) + } + defer syscall.Close(fd) + + if err := fchown(fd, uid, gid); err != nil { + return p9.QID{}, extractErrno(err) + } + stat, err := stat(fd) + if err != nil { + return p9.QID{}, extractErrno(err) + } + return makeQID(stat), nil +} + +// Link implements p9.File. +func (l *localFile) Link(target p9.File, newName string) error { + if l.conf.ROMount { + return syscall.EBADF + } + if !isNameValid(newName) { + return syscall.EINVAL + } + + targetFile := target.(*localFile) + if err := unix.Linkat(targetFile.controlFD(), "", l.controlFD(), newName, linux.AT_EMPTY_PATH); err != nil { + return extractErrno(err) + } + return nil +} + +// Mknod implements p9.File. +// +// Not implemented. +func (*localFile) Mknod(_ string, _ p9.FileMode, _ uint32, _ uint32, _ p9.UID, _ p9.GID) (p9.QID, error) { + return p9.QID{}, syscall.ENOSYS +} + +// UnlinkAt implements p9.File. +func (l *localFile) UnlinkAt(name string, flags uint32) error { + if l.conf.ROMount { + return syscall.EBADF + } + if !isNameValid(name) { + return syscall.EINVAL + } + if err := unix.Unlinkat(l.controlFD(), name, int(flags)); err != nil { + return extractErrno(err) + } + return nil +} + +// Readdir implements p9.File. +func (l *localFile) Readdir(offset uint64, count uint32) ([]p9.Dirent, error) { + if l.mode != p9.ReadOnly && l.mode != p9.ReadWrite { + return nil, syscall.EBADF + } + if l.openedFile == nil { + return nil, syscall.EBADF + } + + // Readdirnames is a cursor over directories, so seek back to 0 to ensure it's + // reading all directory contents. Take a lock because this operation is stateful. + l.readDirMu.Lock() + if _, err := l.openedFile.Seek(0, 0); err != nil { + l.readDirMu.Unlock() + return nil, extractErrno(err) + } + names, err := l.openedFile.Readdirnames(-1) + if err != nil { + l.readDirMu.Unlock() + return nil, extractErrno(err) + } + l.readDirMu.Unlock() + + var dirents []p9.Dirent + for i := int(offset); i >= 0 && i < len(names); i++ { + stat, err := statAt(l.openedFD(), names[i]) + if err != nil { + continue + } + qid := makeQID(stat) + dirents = append(dirents, p9.Dirent{ + QID: qid, + Type: qid.Type, + Name: names[i], + Offset: uint64(i + 1), + }) + } + return dirents, nil +} + +// Readlink implements p9.File. +func (l *localFile) Readlink() (string, error) { + // Shamelessly stolen from os.Readlink (added upper bound limit to buffer). + for len := 128; len < 1024*1024; len *= 2 { + b := make([]byte, len) + n, err := unix.Readlinkat(l.controlFD(), "", b) + if err != nil { + return "", extractErrno(err) + } + if n < len { + return string(b[:n]), nil + } + } + return "", syscall.ENOMEM +} + +// Flush implements p9.File. +func (l *localFile) Flush() error { + return nil +} + +// Connect implements p9.File. +func (l *localFile) Connect(p9.ConnectFlags) (*fd.FD, error) { + return nil, syscall.ECONNREFUSED +} + +// Close implements p9.File. +func (l *localFile) Close() error { + err := l.controlFile.Close() + + // Close only once in case opened and control files point to + // the same os.File struct. + if l.openedFile != nil && l.openedFile != l.controlFile { + err = l.openedFile.Close() + } + + l.openedFile = nil + l.controlFile = nil + l.mode = invalidMode + return err +} + +// extractErrno tries to determine the errno. +func extractErrno(err error) syscall.Errno { + if err == nil { + // This should never happen. The likely result will be that + // some user gets the frustration "error: SUCCESS" message. + log.Warningf("extractErrno called with nil error!") + return 0 + } + + switch err { + case os.ErrNotExist: + return syscall.ENOENT + case os.ErrExist: + return syscall.EEXIST + case os.ErrPermission: + return syscall.EACCES + case os.ErrInvalid: + return syscall.EINVAL + } + + // See if it's an errno or a common wrapped error. + switch e := err.(type) { + case syscall.Errno: + return e + case *os.PathError: + return extractErrno(e.Err) + case *os.LinkError: + return extractErrno(e.Err) + case *os.SyscallError: + return extractErrno(e.Err) + } + + // Fall back to EIO. + log.Debugf("Unknown error: %v, defaulting to EIO", err) + return syscall.EIO +} diff --git a/runsc/fsgofer/fsgofer_test.go b/runsc/fsgofer/fsgofer_test.go new file mode 100644 index 000000000..7d834d596 --- /dev/null +++ b/runsc/fsgofer/fsgofer_test.go @@ -0,0 +1,576 @@ +// 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 fsgofer + +import ( + "fmt" + "io/ioutil" + "os" + "syscall" + "testing" + + "gvisor.googlesource.com/gvisor/pkg/log" + "gvisor.googlesource.com/gvisor/pkg/p9" +) + +func init() { + log.SetLevel(log.Debug) + + allConfs = append(allConfs, rwConfs...) + allConfs = append(allConfs, roConfs...) +} + +var ( + allTypes = []fileType{regular, directory, symlink} + + // allConfs is set in init() above. + allConfs []Config + + rwConfs = []Config{ + Config{ROMount: false, LazyOpenForWrite: false}, + Config{ROMount: false, LazyOpenForWrite: true}, + } + roConfs = []Config{ + Config{ROMount: true, LazyOpenForWrite: false}, + Config{ROMount: true, LazyOpenForWrite: true}, + } +) + +type state struct { + root *localFile + file *localFile + conf Config + ft fileType +} + +func (s state) String() string { + return fmt.Sprintf("lazyopen(%v)-%v", s.conf.LazyOpenForWrite, s.ft) +} + +func runAll(t *testing.T, test func(*testing.T, state)) { + runCustom(t, allTypes, allConfs, test) +} + +func runCustom(t *testing.T, types []fileType, confs []Config, test func(*testing.T, state)) { + for _, c := range confs { + t.Logf("Config: %+v", c) + + for _, ft := range types { + t.Logf("File type: %v", ft) + + path, name, err := setup(ft) + if err != nil { + t.Fatalf("%v", err) + } + defer os.RemoveAll(path) + + a := NewAttachPoint(path, c) + root, err := a.Attach("/") + if err != nil { + t.Fatalf("Attach(%q) failed, err: %v", "/", err) + } + + _, file, err := root.Walk([]string{name}) + if err != nil { + root.Close() + t.Fatalf("root.Walk({%q}) failed, err: %v", "symlink", err) + } + + st := state{root: root.(*localFile), file: file.(*localFile), conf: c, ft: ft} + test(t, st) + file.Close() + root.Close() + } + } +} + +func setup(ft fileType) (string, string, error) { + path, err := ioutil.TempDir("", "root-") + if err != nil { + return "", "", fmt.Errorf("ioutil.TempDir() failed, err: %v", err) + } + + // First attach with writable configuiration to setup tree. + a := NewAttachPoint(path, Config{}) + root, err := a.Attach("/") + if err != nil { + return "", "", fmt.Errorf("Attach(%q) failed, err: %v", "/", err) + } + defer root.Close() + + var name string + switch ft { + case regular: + name = "file" + _, f, _, _, err := root.Create(name, p9.ReadWrite, 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid())) + if err != nil { + return "", "", fmt.Errorf("createFile(root, %q) failed, err: %v", "test", err) + } + defer f.Close() + case directory: + name = "dir" + if _, err := root.Mkdir(name, 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != nil { + return "", "", fmt.Errorf("root.MkDir(%q) failed, err: %v", name, err) + } + case symlink: + name = "symlink" + if _, err := root.Symlink("/some/target", name, p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != nil { + return "", "", fmt.Errorf("root.Symlink(%q) failed, err: %v", name, err) + } + default: + panic(fmt.Sprintf("unknown file type %v", ft)) + } + return path, name, nil +} + +func createFile(dir *localFile, name string) (*localFile, error) { + _, f, _, _, err := dir.Create(name, p9.ReadWrite, 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid())) + if err != nil { + return nil, err + } + return f.(*localFile), nil +} + +func TestReadWrite(t *testing.T) { + runCustom(t, []fileType{directory}, rwConfs, func(t *testing.T, s state) { + child, err := createFile(s.file, "test") + if err != nil { + t.Fatalf("%v: createFile() failed, err: %v", s, err) + } + defer child.Close() + b := []byte("foobar") + w, err := child.WriteAt(b, 0) + if err != nil { + t.Fatalf("%v: Write() failed, err: %v", s, err) + } + if w != len(b) { + t.Fatalf("%v: Write() was partial, got: %d, expected: %d", s, w, len(b)) + } + for _, test := range []struct { + flags p9.OpenFlags + read bool + write bool + }{ + {flags: p9.ReadOnly, read: true, write: false}, + {flags: p9.WriteOnly, read: false, write: true}, + {flags: p9.ReadWrite, read: true, write: true}, + } { + _, l, err := s.file.Walk([]string{"test"}) + if err != nil { + t.Fatalf("%v: Walk(%s) failed, err: %v", s, "test", err) + } + if _, _, _, err := l.Open(test.flags); err != nil { + t.Fatalf("%v: Open(%v) failed, err: %v", s, test.flags, err) + } + + w, err = l.WriteAt(b, 0) + if test.write { + if err != nil { + t.Fatalf("%v, %v: WriteAt() failed, err: %v", s, test.flags, err) + } + if w != len(b) { + t.Fatalf("%v, %v: WriteAt() was partial, got: %d, expected: %d", s, test.flags, w, len(b)) + } + } else { + if err == nil { + t.Fatalf("%v, %v: WriteAt() should have failed", s, test.flags) + } + } + + rBuf := make([]byte, len(b)) + r, err := l.ReadAt(rBuf, 0) + if test.read { + if err != nil { + t.Fatalf("%v, %v: ReadAt() failed, err: %v", s, test.flags, err) + } + if r != len(rBuf) { + t.Fatalf("%v, %v: ReadAt() was partial, got: %d, expected: %d", s, test.flags, r, len(rBuf)) + } + if string(rBuf) != "foobar" { + t.Fatalf("%v, %v: ReadAt() wrong data, got: %s, expected: %s", s, test.flags, string(rBuf), "foobar") + } + } else { + if err == nil { + t.Fatalf("%v, %v: ReadAt() should have failed", s, test.flags) + } + } + } + }) +} + +func TestCreate(t *testing.T) { + runCustom(t, []fileType{directory}, rwConfs, func(t *testing.T, s state) { + for i, test := range []struct { + flags p9.OpenFlags + read bool + }{ + {flags: p9.WriteOnly, read: false}, + {flags: p9.ReadWrite, read: true}, + } { + _, l, _, _, err := s.file.Create(fmt.Sprintf("test-%d", i), test.flags, 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid())) + if err != nil { + t.Fatalf("%v, %v: WriteAt() failed, err: %v", s, test.flags, err) + } + + b := []byte("foobar") + w, err := l.WriteAt(b, 0) + if err != nil { + t.Fatalf("%v, %v: WriteAt() failed, err: %v", s, test.flags, err) + } + if w != len(b) { + t.Fatalf("%v, %v: WriteAt() was partial, got: %d, expected: %d", s, test.flags, w, len(b)) + } + + rBuf := make([]byte, len(b)) + r, err := l.ReadAt(rBuf, 0) + if test.read { + if err != nil { + t.Fatalf("%v, %v: ReadAt() failed, err: %v", s, test.flags, err) + } + if r != len(rBuf) { + t.Fatalf("%v, %v: ReadAt() was partial, got: %d, expected: %d", s, test.flags, r, len(rBuf)) + } + if string(rBuf) != "foobar" { + t.Fatalf("%v, %v: ReadAt() wrong data, got: %s, expected: %s", s, test.flags, string(rBuf), "foobar") + } + } else { + if err == nil { + t.Fatalf("%v, %v: ReadAt() should have failed", s, test.flags) + } + } + } + }) +} + +func TestUnopened(t *testing.T) { + runCustom(t, []fileType{regular}, allConfs, func(t *testing.T, s state) { + b := []byte("foobar") + if _, err := s.file.WriteAt(b, 0); err != syscall.EBADF { + t.Errorf("%v: WriteAt() should have failed, got: %v, expected: syscall.EBADF", s, err) + } + if _, err := s.file.ReadAt(b, 0); err != syscall.EBADF { + t.Errorf("%v: ReadAt() should have failed, got: %v, expected: syscall.EBADF", s, err) + } + if _, err := s.file.Readdir(0, 100); err != syscall.EBADF { + t.Errorf("%v: Readdir() should have failed, got: %v, expected: syscall.EBADF", s, err) + } + if err := s.file.FSync(); err != syscall.EBADF { + t.Errorf("%v: FSync() should have failed, got: %v, expected: syscall.EBADF", s, err) + } + }) +} + +func SetGetAttr(l *localFile, valid p9.SetAttrMask, attr p9.SetAttr) (p9.Attr, error) { + if err := l.SetAttr(valid, attr); err != nil { + return p9.Attr{}, err + } + _, _, a, err := l.GetAttr(p9.AttrMask{}) + if err != nil { + return p9.Attr{}, err + } + return a, nil +} + +func TestSetAttrPerm(t *testing.T) { + runCustom(t, allTypes, rwConfs, func(t *testing.T, s state) { + valid := p9.SetAttrMask{Permissions: true} + attr := p9.SetAttr{Permissions: 0777} + got, err := SetGetAttr(s.file, valid, attr) + if s.ft == symlink { + if err == nil { + t.Fatalf("%v: SetGetAttr(valid, %v) should have failed", s, attr.Permissions) + } + } else { + if err != nil { + t.Fatalf("%v: SetGetAttr(valid, %v) failed, err: %v", s, attr.Permissions, err) + } + if got.Mode.Permissions() != attr.Permissions { + t.Errorf("%v: wrong permission, got: %v, expected: %v", s, got.Mode.Permissions(), attr.Permissions) + } + } + }) +} + +func TestSetAttrSize(t *testing.T) { + runCustom(t, allTypes, rwConfs, func(t *testing.T, s state) { + for _, size := range []uint64{1024, 0, 1024 * 1024} { + valid := p9.SetAttrMask{Size: true} + attr := p9.SetAttr{Size: size} + got, err := SetGetAttr(s.file, valid, attr) + if s.ft == symlink || s.ft == directory { + if err == nil { + t.Fatalf("%v: SetGetAttr(valid, %v) should have failed", s, attr.Permissions) + } + // Run for one size only, they will all fail the same way. + return + } + if err != nil { + t.Fatalf("%v: SetGetAttr(valid, %v) failed, err: %v", s, attr.Size, err) + } + if got.Size != size { + t.Errorf("%v: wrong size, got: %v, expected: %v", s, got.Size, size) + } + } + }) +} + +func TestSetAttrTime(t *testing.T) { + runCustom(t, allTypes, rwConfs, func(t *testing.T, s state) { + valid := p9.SetAttrMask{ATime: true, ATimeNotSystemTime: true} + attr := p9.SetAttr{ATimeSeconds: 123, ATimeNanoSeconds: 456} + got, err := SetGetAttr(s.file, valid, attr) + if err != nil { + t.Fatalf("%v: SetGetAttr(valid, %v:%v) failed, err: %v", s, attr.ATimeSeconds, attr.ATimeNanoSeconds, err) + } + if got.ATimeSeconds != 123 { + t.Errorf("%v: wrong ATimeSeconds, got: %v, expected: %v", s, got.ATimeSeconds, 123) + } + if got.ATimeNanoSeconds != 456 { + t.Errorf("%v: wrong ATimeNanoSeconds, got: %v, expected: %v", s, got.ATimeNanoSeconds, 456) + } + + valid = p9.SetAttrMask{MTime: true, MTimeNotSystemTime: true} + attr = p9.SetAttr{MTimeSeconds: 789, MTimeNanoSeconds: 012} + got, err = SetGetAttr(s.file, valid, attr) + if err != nil { + t.Fatalf("%v: SetGetAttr(valid, %v:%v) failed, err: %v", s, attr.MTimeSeconds, attr.MTimeNanoSeconds, err) + } + if got.MTimeSeconds != 789 { + t.Errorf("%v: wrong MTimeSeconds, got: %v, expected: %v", s, got.MTimeSeconds, 789) + } + if got.MTimeNanoSeconds != 012 { + t.Errorf("%v: wrong MTimeNanoSeconds, got: %v, expected: %v", s, got.MTimeNanoSeconds, 012) + } + }) +} + +func TestSetAttrOwner(t *testing.T) { + if os.Getuid() != 0 { + t.Skipf("SetAttr(owner) test requires CAP_CHOWN, running as %d", os.Getuid()) + } + + runCustom(t, allTypes, rwConfs, func(t *testing.T, s state) { + newUID := os.Getuid() + 1 + valid := p9.SetAttrMask{UID: true} + attr := p9.SetAttr{UID: p9.UID(newUID)} + got, err := SetGetAttr(s.file, valid, attr) + if err != nil { + t.Fatalf("%v: SetGetAttr(valid, %v) failed, err: %v", s, attr.UID, err) + } + if got.UID != p9.UID(newUID) { + t.Errorf("%v: wrong uid, got: %v, expected: %v", s, got.UID, newUID) + } + }) +} + +func TestLink(t *testing.T) { + if os.Getuid() != 0 { + t.Skipf("Link test requires CAP_DAC_READ_SEARCH, running as %d", os.Getuid()) + } + runCustom(t, allTypes, rwConfs, func(t *testing.T, s state) { + const dirName = "linkdir" + const linkFile = "link" + if _, err := s.root.Mkdir(dirName, 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != nil { + t.Fatalf("%v: MkDir(%s) failed, err: %v", s, dirName, err) + } + _, dir, err := s.root.Walk([]string{dirName}) + if err != nil { + t.Fatalf("%v: Walk({%s}) failed, err: %v", s, dirName, err) + } + + err = dir.Link(s.file, linkFile) + if s.ft == directory { + if err != syscall.EPERM { + t.Errorf("%v: Link(target, %s) should have failed, got: %v, expected: syscall.EPERM", s, linkFile, err) + } + return + } + if err != nil { + t.Errorf("%v: Link(target, %s) failed, err: %v", s, linkFile, err) + } + }) +} + +func TestROMountChecks(t *testing.T) { + runCustom(t, allTypes, roConfs, func(t *testing.T, s state) { + if _, _, _, _, err := s.file.Create("..", p9.ReadWrite, 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != syscall.EBADF { + t.Errorf("%v: Create() should have failed, got: %v, expected: syscall.EBADF", s, err) + } + if _, err := s.file.Mkdir("..", 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != syscall.EBADF { + t.Errorf("%v: MkDir() should have failed, got: %v, expected: syscall.EBADF", s, err) + } + if err := s.file.Rename(s.file, ".."); err != syscall.EBADF { + t.Errorf("%v: Rename() should have failed, got: %v, expected: syscall.EBADF", s, err) + } + if _, err := s.file.Symlink("some_place", "..", p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != syscall.EBADF { + t.Errorf("%v: Symlink() should have failed, got: %v, expected: syscall.EBADF", s, err) + } + if err := s.file.UnlinkAt("..", 0); err != syscall.EBADF { + t.Errorf("%v: UnlinkAt() should have failed, got: %v, expected: syscall.EBADF", s, err) + } + if err := s.file.Link(s.file, ".."); err != syscall.EBADF { + t.Errorf("%v: Link() should have failed, got: %v, expected: syscall.EBADF", s, err) + } + + valid := p9.SetAttrMask{Size: true} + attr := p9.SetAttr{Size: 0} + if err := s.file.SetAttr(valid, attr); err != syscall.EBADF { + t.Errorf("%v: SetAttr() should have failed, got: %v, expected: syscall.EBADF", s, err) + } + }) +} + +func TestInvalidName(t *testing.T) { + runCustom(t, []fileType{regular}, rwConfs, func(t *testing.T, s state) { + if _, _, _, _, err := s.file.Create("..", p9.ReadWrite, 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != syscall.EINVAL { + t.Errorf("%v: Create() should have failed, got: %v, expected: syscall.EINVAL", s, err) + } + if _, _, err := s.file.Walk([]string{".."}); err != syscall.EINVAL { + t.Errorf("%v: Walk() should have failed, got: %v, expected: syscall.EINVAL", s, err) + } + if _, err := s.file.Mkdir("..", 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != syscall.EINVAL { + t.Errorf("%v: MkDir() should have failed, got: %v, expected: syscall.EINVAL", s, err) + } + if err := s.file.Rename(s.file, ".."); err != syscall.EINVAL { + t.Errorf("%v: Rename() should have failed, got: %v, expected: syscall.EINVAL", s, err) + } + if _, err := s.file.Symlink("some_place", "..", p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != syscall.EINVAL { + t.Errorf("%v: Symlink() should have failed, got: %v, expected: syscall.EINVAL", s, err) + } + if err := s.file.UnlinkAt("..", 0); err != syscall.EINVAL { + t.Errorf("%v: UnlinkAt() should have failed, got: %v, expected: syscall.EINVAL", s, err) + } + if err := s.file.Link(s.file, ".."); err != syscall.EINVAL { + t.Errorf("%v: Link() should have failed, got: %v, expected: syscall.EINVAL", s, err) + } + }) +} + +func TestIsNameValid(t *testing.T) { + valid := []string{ + "name", + "123", + "!@#$%^&*()", + ".name", + "..name", + "...", + } + for _, s := range valid { + if got := isNameValid(s); !got { + t.Errorf("isNameValid(%s) failed, got: %v, expected: true", s, got) + } + } + invalid := []string{ + ".", + "..", + "name/name", + "/name", + "name/", + } + for _, s := range invalid { + if got := isNameValid(s); got { + t.Errorf("isNameValid(%s) failed, got: %v, expected: false", s, got) + } + } +} + +func TestWalkNotFound(t *testing.T) { + runCustom(t, []fileType{directory}, allConfs, func(t *testing.T, s state) { + if _, _, err := s.file.Walk([]string{"nobody-here"}); err != syscall.ENOENT { + t.Errorf("%v: Walk(%q) should have failed, got: %v, expected: syscall.ENOENT", s, "nobody-here", err) + } + }) +} + +func TestWalkDup(t *testing.T) { + runAll(t, func(t *testing.T, s state) { + _, dup, err := s.file.Walk([]string{}) + if err != nil { + t.Fatalf("%v: Walk(nil) failed, err: %v", s, err) + } + // Check that 'dup' is usable. + if _, _, _, err := dup.GetAttr(p9.AttrMask{}); err != nil { + t.Errorf("%v: GetAttr() failed, err: %v", s, err) + } + }) +} + +func TestReaddir(t *testing.T) { + runCustom(t, []fileType{directory}, rwConfs, func(t *testing.T, s state) { + name := "dir" + if _, err := s.file.Mkdir(name, 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != nil { + t.Fatalf("%v: MkDir(%s) failed, err: %v", s, name, err) + } + name = "symlink" + if _, err := s.file.Symlink("/some/target", name, p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != nil { + t.Fatalf("%v: Symlink(%q) failed, err: %v", s, name, err) + } + name = "file" + _, f, _, _, err := s.file.Create(name, p9.ReadWrite, 0555, p9.UID(os.Getuid()), p9.GID(os.Getgid())) + if err != nil { + t.Fatalf("%v: createFile(root, %q) failed, err: %v", s, name, err) + } + f.Close() + + if _, _, _, err := s.file.Open(p9.ReadOnly); err != nil { + t.Fatalf("%v: Open(ReadOnly) failed, err: %v", s, err) + } + + dirents, err := s.file.Readdir(0, 10) + if err != nil { + t.Fatalf("%v: Readdir(0, 10) failed, err: %v", s, err) + } + if len(dirents) != 3 { + t.Fatalf("%v: Readdir(0, 10) wrong number of items, got: %v, expected: 3", s, len(dirents)) + } + var dir, symlink, file bool + for _, d := range dirents { + switch d.Name { + case "dir": + if d.Type != p9.TypeDir { + t.Errorf("%v: dirent.Type got: %v, expected: %v", s, d.Type, p9.TypeDir) + } + dir = true + case "symlink": + if d.Type != p9.TypeSymlink { + t.Errorf("%v: dirent.Type got: %v, expected: %v", s, d.Type, p9.TypeSymlink) + } + symlink = true + case "file": + if d.Type != p9.TypeRegular { + t.Errorf("%v: dirent.Type got: %v, expected: %v", s, d.Type, p9.TypeRegular) + } + file = true + default: + t.Errorf("%v: dirent.Name got: %v", s, d.Name) + } + + _, f, err := s.file.Walk([]string{d.Name}) + if err != nil { + t.Fatalf("%v: Walk({%s}) failed, err: %v", s, d.Name, err) + } + _, _, a, err := f.GetAttr(p9.AttrMask{}) + if err != nil { + t.Fatalf("%v: GetAttr() failed, err: %v", s, err) + } + if d.Type != a.Mode.QIDType() { + t.Errorf("%v: dirent.Type different than GetAttr().Mode.QIDType(), got: %v, expected: %v", s, d.Type, a.Mode.QIDType()) + } + } + if !dir || !symlink || !file { + t.Errorf("%v: Readdir(0, 10) wrong files returned, dir: %v, symlink: %v, file: %v", s, dir, symlink, file) + } + }) +} diff --git a/runsc/fsgofer/fsgofer_unsafe.go b/runsc/fsgofer/fsgofer_unsafe.go new file mode 100644 index 000000000..e676809ac --- /dev/null +++ b/runsc/fsgofer/fsgofer_unsafe.go @@ -0,0 +1,58 @@ +// 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 fsgofer + +import ( + "syscall" + "unsafe" + + "gvisor.googlesource.com/gvisor/pkg/abi/linux" +) + +func statAt(dirFd int, name string) (syscall.Stat_t, error) { + nameBytes, err := syscall.BytePtrFromString(name) + if err != nil { + return syscall.Stat_t{}, extractErrno(err) + } + namePtr := uintptr(unsafe.Pointer(nameBytes)) + + var stat syscall.Stat_t + statPtr := uintptr(unsafe.Pointer(&stat)) + + if _, _, err := syscall.Syscall6(syscall.SYS_NEWFSTATAT, uintptr(dirFd), namePtr, statPtr, linux.AT_SYMLINK_NOFOLLOW, 0, 0); err != 0 { + return syscall.Stat_t{}, err + } + return stat, nil +} + +func utimensat(dirFd int, name string, times [2]syscall.Timespec, flags int) error { + // utimensat(2) doesn't accept empty name, instead name must be nil to make it + // operate directly on 'dirFd' unlike other *at syscalls. + var namePtr uintptr + if name != "" { + nameBytes, err := syscall.BytePtrFromString(name) + if err != nil { + return extractErrno(err) + } + namePtr = uintptr(unsafe.Pointer(nameBytes)) + } + + timesPtr := uintptr(unsafe.Pointer(×[0])) + + if _, _, err := syscall.Syscall6(syscall.SYS_UTIMENSAT, uintptr(dirFd), namePtr, timesPtr, uintptr(flags), 0, 0); err != 0 { + return err + } + return nil +} |