From d50c0c685b265f63f3f0dee9cf96552638ddb775 Mon Sep 17 00:00:00 2001 From: Achille Roussel Date: Wed, 9 Aug 2023 00:20:07 -0700 Subject: [PATCH] sandbox: remove Name method from File interface Signed-off-by: Achille Roussel --- internal/sandbox/dirfs_unix.go | 231 ++++++++++----------- internal/sandbox/fs.go | 238 ++++++++++++++++++--- internal/sandbox/ocifs/file.go | 39 ++-- internal/sandbox/ocifs/ocifs.go | 17 +- internal/sandbox/rootfs.go | 286 -------------------------- internal/sandbox/rootfs_test.go | 27 --- internal/sandbox/tarfs/dir.go | 31 +-- internal/sandbox/tarfs/file.go | 11 +- internal/sandbox/tarfs/placeholder.go | 2 +- internal/sandbox/tarfs/symlink.go | 2 +- internal/sandbox/tarfs/tarfs.go | 4 +- internal/sandbox/wasi_test.go | 2 +- 12 files changed, 359 insertions(+), 531 deletions(-) delete mode 100644 internal/sandbox/rootfs.go delete mode 100644 internal/sandbox/rootfs_test.go diff --git a/internal/sandbox/dirfs_unix.go b/internal/sandbox/dirfs_unix.go index 0bb93f29..b573cd00 100644 --- a/internal/sandbox/dirfs_unix.go +++ b/internal/sandbox/dirfs_unix.go @@ -3,7 +3,7 @@ package sandbox import ( "fmt" "io/fs" - "os" + "sync/atomic" "github.com/stealthrocket/timecraft/internal/sandbox/fspath" "golang.org/x/sys/unix" @@ -28,134 +28,149 @@ func (fsys *dirFS) Open(name string, flags int, mode fs.FileMode) (File, error) func (fsys *dirFS) openRoot() (File, error) { dirfd, err := openat(unix.AT_FDCWD, fsys.root, O_DIRECTORY, 0) if err != nil { - return nil, &fs.PathError{Op: "open", Path: "/", Err: err} + return nil, err } - return &dirFile{fsys: fsys, fd: dirfd, name: "/"}, nil + return newFile(nil, dirfd), nil +} + +func newFile(dir *dirFile, fd int) *dirFile { + f := &dirFile{dir: dir, fd: fd} + if dir != nil { + dir.ref() + } + f.ref() + return f } type dirFile struct { - fsys *dirFS + dir *dirFile + refc atomic.Int32 + once atomic.Int32 fd int - name string +} + +func (f *dirFile) ref() { + f.refc.Add(1) +} + +func (f *dirFile) unref() { + if f.refc.Add(-1) == 0 { + closeTraceError(f.fd) + f.fd = -1 + if f.dir != nil { + f.dir.unref() + } + } } func (f *dirFile) String() string { - return fmt.Sprintf("&sandbox.dirFile{fd:%d,name:%q}", f.fd, f.name) + return fmt.Sprintf("&sandbox.dirFile{fd:%d}", f.fd) } func (f *dirFile) Fd() uintptr { return uintptr(f.fd) } -func (f *dirFile) Name() string { - return f.name -} - func (f *dirFile) Close() error { - fd := f.fd - f.fd = -1 - if fd >= 0 { - closeTraceError(fd) + if f.once.Swap(1) == 0 { + f.unref() } return nil } +func (f *dirFile) openSelf() (File, error) { + fd, err := dup(f.fd) + if err != nil { + return nil, err + } + return newFile(f.dir, fd), nil +} + +func (f *dirFile) openParent() (File, error) { + if f.dir != nil { // not already at the root? + f = f.dir + } + return f.openSelf() +} + +func (f *dirFile) openRoot() (File, error) { + for f.dir != nil { // walk up to root + f = f.dir + } + return f.openSelf() +} + +func (f *dirFile) openFile(name string, flags int, mode fs.FileMode) (File, error) { + fd, err := openat(f.fd, name, flags|O_NOFOLLOW, uint32(mode.Perm())) + if err != nil { + return nil, err + } + return newFile(f, fd), nil +} + +func (f *dirFile) open(name string, flags int, mode fs.FileMode) (File, error) { + switch name { + case ".": + return f.openSelf() + case "..": + return f.openParent() + default: + return f.openFile(name, flags, mode) + } +} + func (f *dirFile) Open(name string, flags int, mode fs.FileMode) (File, error) { if fspath.IsRoot(name) { - return f.fsys.openRoot() + return f.openRoot() } - relPath := f.join(name) return ResolvePath(f, name, flags, func(d *dirFile, name string) (File, error) { - fd, err := openat(d.fd, name, flags|O_NOFOLLOW, uint32(mode.Perm())) - if err != nil { - return nil, &fs.PathError{Op: "open", Path: relPath, Err: err} - } - return &dirFile{fsys: f.fsys, fd: fd, name: relPath}, nil + return d.open(name, flags, mode) }) } func (f *dirFile) Readv(iovs [][]byte) (int, error) { - n, err := readv(f.fd, iovs) - if err != nil { - err = &fs.PathError{Op: "read", Path: f.name, Err: err} - } - return n, err + return readv(f.fd, iovs) } func (f *dirFile) Writev(iovs [][]byte) (int, error) { - n, err := writev(f.fd, iovs) - if err != nil { - err = &fs.PathError{Op: "write", Path: f.name, Err: err} - } - return n, err + return writev(f.fd, iovs) } func (f *dirFile) Preadv(iovs [][]byte, offset int64) (int, error) { - n, err := preadv(f.fd, iovs, offset) - if err != nil { - err = &fs.PathError{Op: "pread", Path: f.name, Err: err} - } - return n, err + return preadv(f.fd, iovs, offset) } func (f *dirFile) Pwritev(iovs [][]byte, offset int64) (int, error) { - n, err := pwritev(f.fd, iovs, offset) - if err != nil { - err = &fs.PathError{Op: "pwrite", Path: f.name, Err: err} - } - return n, err + return pwritev(f.fd, iovs, offset) } func (f *dirFile) Seek(offset int64, whence int) (int64, error) { - seek, err := lseek(f.fd, offset, whence) - if err != nil { - err = &fs.PathError{Op: "seek", Path: f.name, Err: err} - } - return seek, err + return lseek(f.fd, offset, whence) } func (f *dirFile) Allocate(offset, length int64) error { - if err := fallocate(f.fd, offset, length); err != nil { - return &fs.PathError{Op: "allocate", Path: f.name, Err: err} - } - return nil + return fallocate(f.fd, offset, length) } func (f *dirFile) Truncate(size int64) error { - if err := ftruncate(f.fd, size); err != nil { - return &fs.PathError{Op: "truncate", Path: f.name, Err: err} - } - return nil + return ftruncate(f.fd, size) } func (f *dirFile) Sync() error { - if err := fsync(f.fd); err != nil { - return &fs.PathError{Op: "sync", Path: f.name, Err: err} - } - return nil + return fsync(f.fd) } func (f *dirFile) Datasync() error { - if err := fdatasync(f.fd); err != nil { - return &fs.PathError{Op: "datasync", Path: f.name, Err: err} - } - return nil + return fdatasync(f.fd) } func (f *dirFile) Flags() (int, error) { - flags, err := unix.FcntlInt(uintptr(f.fd), unix.F_GETFL, 0) - if err != nil { - return 0, &fs.PathError{Op: "fcntl", Path: f.name, Err: err} - } - return flags, nil + return unix.FcntlInt(uintptr(f.fd), unix.F_GETFL, 0) } func (f *dirFile) SetFlags(flags int) error { _, err := unix.FcntlInt(uintptr(f.fd), unix.F_SETFL, flags) - if err != nil { - return &fs.PathError{Op: "fcntl", Path: f.name, Err: err} - } - return nil + return err } func (f *dirFile) ReadDirent(buf []byte) (int, error) { @@ -173,11 +188,11 @@ func (f *dirFile) Stat(name string, flags int) (FileInfo, error) { err = fstatat(d.fd, name, &stat, AT_SYMLINK_NOFOLLOW) } if err != nil { - return FileInfo{}, &fs.PathError{Op: "stat", Path: d.join(name), Err: err} + return FileInfo{}, err } if (stat.Mode & unix.S_IFMT) == unix.S_IFLNK { if (flags & AT_SYMLINK_NOFOLLOW) == 0 { - return FileInfo{}, &fs.PathError{Op: "stat", Path: d.join(name), Err: ELOOP} + return FileInfo{}, ELOOP } } @@ -216,66 +231,45 @@ func (f *dirFile) Stat(name string, flags int) (FileInfo, error) { } func (f *dirFile) Readlink(name string, buf []byte) (int, error) { - return ResolvePath(f, name, O_NOFOLLOW, func(d *dirFile, name string) (n int, err error) { + return ResolvePath(f, name, O_NOFOLLOW, func(d *dirFile, name string) (int, error) { if name == "" { - n, err = freadlink(d.fd, buf) + return freadlink(d.fd, buf) } else { - n, err = readlinkat(d.fd, name, buf) + return readlinkat(d.fd, name, buf) } - if err != nil { - err = &fs.PathError{Op: "readlink", Path: d.join(name), Err: err} - } - return n, err }) } func (f *dirFile) Chtimes(name string, times [2]Timespec, flags int) error { - return resolvePath1(f, name, openFlags(flags), func(d *dirFile, name string) (err error) { + return resolvePath1(f, name, openFlags(flags), func(d *dirFile, name string) error { if name == "" { - err = futimens(d.fd, ×) + return futimens(d.fd, ×) } else { - err = utimensat(d.fd, name, ×, AT_SYMLINK_NOFOLLOW) - } - if err != nil { - return &fs.PathError{Op: "chtimes", Path: f.join(name), Err: err} + return utimensat(d.fd, name, ×, AT_SYMLINK_NOFOLLOW) } - return err }) } func (f *dirFile) Mkdir(name string, mode fs.FileMode) error { return resolvePath1(f, name, O_NOFOLLOW, func(d *dirFile, name string) error { - if err := mkdirat(d.fd, name, uint32(mode.Perm())); err != nil { - return &fs.PathError{Op: "mkdir", Path: f.join(name), Err: err} - } - return nil + return mkdirat(d.fd, name, uint32(mode.Perm())) }) } func (f *dirFile) Rmdir(name string) error { return resolvePath1(f, name, O_NOFOLLOW, func(d *dirFile, name string) error { - if err := unlinkat(d.fd, name, unix.AT_REMOVEDIR); err != nil { - return &fs.PathError{Op: "rmdir", Path: f.join(name), Err: err} - } - return nil + return unlinkat(d.fd, name, unix.AT_REMOVEDIR) }) } func (f1 *dirFile) Rename(oldName string, newDir File, newName string) error { f2, ok := newDir.(*dirFile) if !ok { - path1 := f1.join(oldName) - path2 := f2.join(newName) - return &os.LinkError{Op: "rename", Old: path1, New: path2, Err: EXDEV} + return EXDEV } return resolvePath1(f1, oldName, O_NOFOLLOW, func(d1 *dirFile, name1 string) error { return resolvePath1(f2, newName, O_NOFOLLOW, func(d2 *dirFile, name2 string) error { - if err := renameat(d1.fd, name1, d2.fd, name2); err != nil { - path1 := d1.join(name1) - path2 := d2.join(name2) - return &os.LinkError{Op: "rename", Old: path1, New: path2, Err: err} - } - return nil + return renameat(d1.fd, name1, d2.fd, name2) }) }) } @@ -283,45 +277,28 @@ func (f1 *dirFile) Rename(oldName string, newDir File, newName string) error { func (f1 *dirFile) Link(oldName string, newDir File, newName string, flags int) error { f2, ok := newDir.(*dirFile) if !ok { - path1 := f1.join(oldName) - path2 := f2.join(newName) - return &os.LinkError{Op: "rename", Old: path1, New: path2, Err: EXDEV} + return EXDEV } oflags := openFlags(flags) return resolvePath1(f1, oldName, oflags, func(d1 *dirFile, name1 string) error { return resolvePath1(f2, newName, oflags, func(d2 *dirFile, name2 string) error { - if err := linkat(d1.fd, name1, d2.fd, name2, 0); err != nil { - path1 := d1.join(name1) - path2 := f2.join(name2) - return &os.LinkError{Op: "link", Old: path1, New: path2, Err: err} - } - return nil + return linkat(d1.fd, name1, d2.fd, name2, 0) }) }) } func (f *dirFile) Symlink(oldName string, newName string) error { return resolvePath1(f, newName, O_NOFOLLOW, func(d *dirFile, name string) error { - if err := symlinkat(oldName, d.fd, name); err != nil { - return &fs.PathError{Op: "symlink", Path: f.join(newName), Err: err} - } - return nil + return symlinkat(oldName, d.fd, name) }) } func (f *dirFile) Unlink(name string) error { return resolvePath1(f, name, O_NOFOLLOW, func(d *dirFile, name string) error { - if err := unlinkat(d.fd, name, 0); err != nil { - return &fs.PathError{Op: "unlink", Path: f.join(name), Err: err} - } - return nil + return unlinkat(d.fd, name, 0) }) } -func (f *dirFile) join(name string) string { - return fspath.Join(f.name, name) -} - func openFlags(flags int) int { if (flags & AT_SYMLINK_NOFOLLOW) != 0 { return O_NOFOLLOW @@ -329,8 +306,8 @@ func openFlags(flags int) int { return 0 } -func resolvePath1[F File](d F, name string, flags int, do func(F, string) error) error { - _, err := ResolvePath(d, name, flags, func(d F, name string) (_ struct{}, err error) { +func resolvePath1(d *dirFile, name string, flags int, do func(*dirFile, string) error) error { + _, err := ResolvePath(d, name, flags, func(d *dirFile, name string) (_ struct{}, err error) { err = do(d, name) return }) diff --git a/internal/sandbox/fs.go b/internal/sandbox/fs.go index f81f75da..27d3eb1f 100644 --- a/internal/sandbox/fs.go +++ b/internal/sandbox/fs.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "io/fs" + "os" "os/user" "path" "strconv" @@ -35,22 +36,38 @@ type FileSystem interface { // Create creates and opens a file on a file system. The name is the location // where the file is created and the mode is used to set permissions. func Create(fsys FileSystem, name string, mode fs.FileMode) (File, error) { - return fsys.Open(name, O_CREAT|O_TRUNC|O_WRONLY, mode) + f, err := fsys.Open(name, O_CREAT|O_TRUNC|O_WRONLY, mode) + if err != nil { + err = &fs.PathError{Op: "create", Path: name, Err: err} + } + return f, err } // Open opens a file with the given name on a file system. func Open(fsys FileSystem, name string) (File, error) { - return fsys.Open(name, O_RDONLY, 0) + f, err := fsys.Open(name, O_RDONLY, 0) + if err != nil { + err = &fs.PathError{Op: "open", Path: name, Err: err} + } + return f, err } // OpenDir opens a directory with the given name on the file system. func OpenDir(fsys FileSystem, name string) (File, error) { - return fsys.Open(name, O_DIRECTORY, 0) + f, err := fsys.Open(name, O_DIRECTORY, 0) + if err != nil { + err = &fs.PathError{Op: "open", Path: name, Err: err} + } + return f, err } // OpenRoot opens the root directory of a file system. func OpenRoot(fsys FileSystem) (File, error) { - return OpenDir(fsys, "/") + f, err := OpenDir(fsys, "/") + if err != nil { + err = &fs.PathError{Op: "open", Path: "/", Err: err} + } + return f, err } // Lstat returns information about a file on a file system. @@ -58,7 +75,13 @@ func OpenRoot(fsys FileSystem) (File, error) { // Is the name points to a location where a symbolic link exists, the function // returns information about the link itself. func Lstat(fsys FileSystem, name string) (FileInfo, error) { - return withRoot2(fsys, func(dir File) (FileInfo, error) { return dir.Stat(name, AT_SYMLINK_NOFOLLOW) }) + info, err := withRoot2(fsys, func(dir File) (FileInfo, error) { + return dir.Stat(name, AT_SYMLINK_NOFOLLOW) + }) + if err != nil { + err = &fs.PathError{Op: "lstat", Path: name, Err: err} + } + return info, err } // Stat returns information about a file on a file system. @@ -66,7 +89,13 @@ func Lstat(fsys FileSystem, name string) (FileInfo, error) { // Is the name points to a location where a symbolic link exists, the function // returns information about the link target. func Stat(fsys FileSystem, name string) (FileInfo, error) { - return withRoot2(fsys, func(dir File) (FileInfo, error) { return dir.Stat(name, 0) }) + info, err := withRoot2(fsys, func(dir File) (FileInfo, error) { + return dir.Stat(name, 0) + }) + if err != nil { + err = &fs.PathError{Op: "stat", Path: name, Err: err} + } + return info, err } // ReadFile reads the content of a file on a file system. The name represents @@ -76,15 +105,12 @@ func Stat(fsys FileSystem, name string) (FileInfo, error) { func ReadFile(fsys FileSystem, name string, flags int) ([]byte, error) { f, err := fsys.Open(name, flags|O_RDONLY, 0) if err != nil { - return nil, err + return nil, &fs.PathError{Op: "read", Path: name, Err: err} } defer f.Close() s, err := f.Stat("", 0) if err != nil { - return nil, err - } - if _, err := f.Seek(0, 0); err != nil { - return nil, err + return nil, &fs.PathError{Op: "read", Path: name, Err: err} } b := make([]byte, s.Size) v := make([][]byte, 1) @@ -96,7 +122,7 @@ func ReadFile(fsys FileSystem, name string, flags int) ([]byte, error) { n += rn } if err != nil || rn == 0 { - return b[:n], err + return b[:n], &fs.PathError{Op: "read", Path: name, Err: err} } } return b, nil @@ -106,11 +132,13 @@ func ReadFile(fsys FileSystem, name string, flags int) ([]byte, error) { func WriteFile(fsys FileSystem, name string, data []byte, mode fs.FileMode) error { f, err := fsys.Open(name, O_CREAT|O_WRONLY|O_TRUNC|O_EXCL, mode) if err != nil { - return err + return &fs.PathError{Op: "write", Path: name, Err: err} } defer f.Close() - _, err = f.Writev([][]byte{data}) - return err + if _, err := f.Writev([][]byte{data}); err != nil { + return &fs.PathError{Op: "write", Path: name, Err: err} + } + return nil } // MkdirAll creates all directories to form the given path name on a file @@ -159,28 +187,54 @@ func mkdirAll(fsys FileSystem, name string, mode fs.FileMode) error { // Mkdir creates a directory on a file system. The mode is used to set the // permissions of the new directory. func Mkdir(fsys FileSystem, name string, mode fs.FileMode) error { - return withRoot1(fsys, func(dir File) error { return dir.Mkdir(name, mode) }) + if err := withRoot1(fsys, func(dir File) error { + return dir.Mkdir(name, mode) + }); err != nil { + return &fs.PathError{Op: "mkdir", Path: name, Err: err} + } + return nil } // Rmdir removes an empty directory from a file system. func Rmdir(fsys FileSystem, name string) error { - return withRoot1(fsys, func(dir File) error { return dir.Rmdir(name) }) + if err := withRoot1(fsys, func(dir File) error { + return dir.Rmdir(name) + }); err != nil { + return &fs.PathError{Op: "rmdir", Path: name, Err: err} + } + return nil } // Link creates a hard link between the old and new names passed as arguments. func Link(fsys FileSystem, oldName, newName string) error { - return withRoot1(fsys, func(dir File) error { return dir.Link(oldName, dir, newName, AT_SYMLINK_NOFOLLOW) }) + if err := withRoot1(fsys, func(dir File) error { + return dir.Link(oldName, dir, newName, AT_SYMLINK_NOFOLLOW) + }); err != nil { + return &os.LinkError{Op: "link", Old: oldName, New: newName, Err: err} + } + return nil } // Symlink creates a symbolic link to a file system location. func Symlink(fsys FileSystem, oldName, newName string) error { - return withRoot1(fsys, func(dir File) error { return dir.Symlink(oldName, newName) }) + if err := withRoot1(fsys, func(dir File) error { + return dir.Symlink(oldName, newName) + }); err != nil { + return &os.LinkError{Op: "symlink", Old: oldName, New: newName, Err: err} + } + return nil } // Readlink reads the target of a symbolic link located at the given path name // on a file system. func Readlink(fsys FileSystem, name string) (string, error) { - return withRoot2(fsys, func(dir File) (string, error) { return readlink(dir, name) }) + link, err := withRoot2(fsys, func(dir File) (string, error) { + return readlink(dir, name) + }) + if err != nil { + err = &fs.PathError{Op: "readlink", Path: name, Err: err} + } + return link, err } func readlink(dir File, name string) (string, error) { @@ -202,13 +256,23 @@ func readlink(dir File, name string) (string, error) { // Unlink removes a file or symbolic link from a file system. func Unlink(fsys FileSystem, name string) error { - return withRoot1(fsys, func(dir File) error { return dir.Unlink(name) }) + if err := withRoot1(fsys, func(dir File) error { + return dir.Unlink(name) + }); err != nil { + return &fs.PathError{Op: "unlink", Path: name, Err: err} + } + return nil } // Rename changes the name referencing a file, symbolic link, or directory on a // file system. func Rename(fsys FileSystem, oldName, newName string) error { - return withRoot1(fsys, func(dir File) error { return dir.Rename(oldName, dir, newName) }) + if err := withRoot1(fsys, func(dir File) error { + return dir.Rename(oldName, dir, newName) + }); err != nil { + return &os.LinkError{Op: "rename", Old: oldName, New: newName, Err: err} + } + return nil } func withRoot1(fsys FileSystem, do func(File) error) error { @@ -235,12 +299,6 @@ type File interface { // the file. Fd() uintptr - // Returns the canonical name of the file on the file system. - // - // Assuming the file system is not modified concurrently, a file opened at - // the location returned by this method will point to the same resource. - Name() string - // Closes the file. // // This method must be opened when the program does not need the file @@ -481,7 +539,7 @@ func (fsys *fsFileSystem) Open(name string) (fs.File, error) { if err != nil { return nil, fsError("open", name, err) } - return &fsFile{fsys: fsys.base, File: f}, nil + return &fsFile{fsys: fsys.base, name: name, File: f}, nil } func (fsys *fsFileSystem) Stat(name string) (fs.FileInfo, error) { @@ -523,6 +581,7 @@ var ( type fsFile struct { fsys FileSystem + name string dir *dirbuf File } @@ -532,7 +591,7 @@ func (f *fsFile) Stat() (fs.FileInfo, error) { if err != nil { return nil, err } - name := path.Base(f.File.Name()) + name := path.Base(f.name) return &fsFileInfo{name: name, stat: stat}, nil } @@ -666,3 +725,122 @@ func (dirent *fsDirEntry) Info() (fs.FileInfo, error) { } return &fsFileInfo{name: name, stat: stat}, nil } + +// ResolvePath is the path resolution algorithm which guarantees sandboxing of +// path access in a root FS. +// +// The algorithm walks the path name from f, calling the do function when it +// reaches a path leaf. The function may return ELOOP to indicate that a symlink +// was encountered and must be followed, in which case ResolvePath continues +// walking the path at the link target. Any other value or error returned by the +// do function will be returned immediately. +func ResolvePath[F File, R any](dir F, name string, flags int, do func(F, string) (R, error)) (ret R, err error) { + if name == "" { + return do(dir, "") + } + if fspath.HasTrailingSlash(name) { + flags |= O_DIRECTORY + } + + var lastOpenDir File + defer func() { closeFileIfNotNil(lastOpenDir) }() + + setCurrentDirectory := func(cd File) { + closeFileIfNotNil(lastOpenDir) + dir, lastOpenDir = cd.(F), cd + } + + followSymlinkDepth := 0 + followSymlink := func(symlink, target string) error { + link, err := readlink(dir, symlink) + if err != nil { + // This error may be EINVAL if the file system was modified + // concurrently and the directory entry was not pointing to a + // symbolic link anymore. + return err + } + + // Limit the maximum number of symbolic links that would be followed + // during path resolution; this ensures that if we encounter a loop, + // we will eventually abort resolving the path. + if followSymlinkDepth == MaxFollowSymlink { + return ELOOP + } + followSymlinkDepth++ + + if target != "" { + name = link + "/" + target + } else { + name = link + } + return nil + } + + for { + if fspath.IsAbs(name) { + if name = fspath.TrimLeadingSlash(name); name == "" { + name = "." + } + d, err := openRoot(dir) + if err != nil { + return ret, err + } + setCurrentDirectory(d) + } + + var elem string + elem, name = fspath.Walk(name) + + if name == "" { + doFile: + ret, err = do(dir, elem) + if err != nil { + if !errors.Is(err, ELOOP) || ((flags & O_NOFOLLOW) != 0) { + return ret, err + } + switch err := followSymlink(elem, ""); { + case errors.Is(err, nil): + continue + case errors.Is(err, EINVAL): + goto doFile + default: + return ret, err + } + } + return ret, nil + } + + if elem == "." { + // This is a minor optimization, the path contains a reference to + // the current directory, we don't need to reopen it. + continue + } + + openPath: + d, err := dir.Open(elem, openPathFlags, 0) + if err != nil { + if !errors.Is(err, ENOTDIR) { + return ret, err + } + switch err := followSymlink(elem, name); { + case errors.Is(err, nil): + continue + case errors.Is(err, EINVAL): + goto openPath + default: + return ret, err + } + } + setCurrentDirectory(d) + } +} + +func openRoot(dir File) (File, error) { + return dir.Open("/", O_DIRECTORY, 0) +} + +func closeFileIfNotNil(f File) { + if f != nil { + f.Close() + } +} diff --git a/internal/sandbox/ocifs/file.go b/internal/sandbox/ocifs/file.go index fcc4609c..a643054e 100644 --- a/internal/sandbox/ocifs/file.go +++ b/internal/sandbox/ocifs/file.go @@ -55,30 +55,36 @@ func (f *file) openSelf() (sandbox.File, error) { return nil, sandbox.EBADF } defer unref(l) + return f.fsys.newFile(l), nil +} - open := &file{ - fsys: f.fsys, - layers: l, +func (f *file) openParent() (sandbox.File, error) { + l := f.ref() + if l == nil { + return nil, sandbox.EBADF } + defer unref(l) - ref(open.layers) - return open, nil + p := l.parent + if p == nil { // already at the root? + p = l + } + + return f.fsys.newFile(p), nil } -func (f *file) openParent() (sandbox.File, error) { +func (f *file) openRoot() (sandbox.File, error) { l := f.ref() if l == nil { return nil, sandbox.EBADF } defer unref(l) - open := &file{ - fsys: f.fsys, - layers: l.parent, + for l.parent != nil { // walk up to the root + l = l.parent } - ref(open.layers) - return open, nil + return f.fsys.newFile(l), nil } func (f *file) openFile(name string, flags int, mode fs.FileMode) (sandbox.File, error) { @@ -209,7 +215,7 @@ func (f *file) Open(name string, flags int, mode fs.FileMode) (sandbox.File, err } if fspath.IsRoot(name) { - return f.fsys.openRoot() + return f.openRoot() } return sandbox.ResolvePath(f, name, flags, func(at *file, name string) (sandbox.File, error) { @@ -302,15 +308,6 @@ func (f *file) Fd() uintptr { return l.files[0].Fd() } -func (f *file) Name() string { - l := f.ref() - if l == nil { - return "" - } - defer unref(l) - return l.files[0].Name() -} - func (f *file) Readv(iovs [][]byte) (int, error) { l := f.ref() if l == nil { diff --git a/internal/sandbox/ocifs/ocifs.go b/internal/sandbox/ocifs/ocifs.go index b9230dd6..a0007d13 100644 --- a/internal/sandbox/ocifs/ocifs.go +++ b/internal/sandbox/ocifs/ocifs.go @@ -55,7 +55,7 @@ func (fsys *FileSystem) openRoot() (sandbox.File, error) { files := make([]sandbox.File, 0, len(fsys.layers)) defer func() { - closeFiles(files) + closeFiles(files) // only closed on error or panic }() for _, layer := range fsys.layers { @@ -69,11 +69,16 @@ func (fsys *FileSystem) openRoot() (sandbox.File, error) { } } - root := &file{ - fsys: fsys, - layers: &fileLayers{files: files}, - } - ref(root.layers) + root := fsys.newFile(&fileLayers{files: files}) files = nil return root, nil } + +func (fsys *FileSystem) newFile(layers *fileLayers) *file { + f := &file{ + fsys: fsys, + layers: layers, + } + ref(layers) + return f +} diff --git a/internal/sandbox/rootfs.go b/internal/sandbox/rootfs.go deleted file mode 100644 index b1c99fed..00000000 --- a/internal/sandbox/rootfs.go +++ /dev/null @@ -1,286 +0,0 @@ -package sandbox - -import ( - "errors" - "fmt" - "io/fs" - "os" - - "github.com/stealthrocket/timecraft/internal/sandbox/fspath" -) - -// RootFS wraps the given FileSystem to prevent path resolution from escaping -// its root directory. -// -// RootFS is useful to create a sandbox in combination with a DirFS pointing at -// a directory on the local file system. Operations performed on the RootFS are -// guaranteed not to escape the base directory, even in the presence of symbolic -// links pointing to parent directories of the root. -func RootFS(fsys FileSystem) FileSystem { - return &rootFS{fsys} -} - -type rootFS struct{ base FileSystem } - -func (fsys *rootFS) Open(name string, flags int, mode fs.FileMode) (File, error) { - f, err := OpenRoot(fsys.base) - if err != nil { - return nil, err - } - if fspath.IsRoot(name) { - return &rootFile{f}, nil - } - defer f.Close() - return (&rootFile{f}).Open(name, flags, mode) -} - -type rootFile struct{ File } - -func (f *rootFile) String() string { - return fmt.Sprintf("&sandbox.rootFile{%v}", f.File) -} - -func (f *rootFile) Open(name string, flags int, mode fs.FileMode) (File, error) { - file, err := ResolvePath(f.File, name, flags, func(dir File, name string) (File, error) { - return dir.Open(name, flags|O_NOFOLLOW, mode) - }) - if err != nil { - return nil, &fs.PathError{Op: "open", Path: name, Err: unwrap(err)} - } - return &rootFile{file}, nil -} - -func (f *rootFile) Stat(name string, flags int) (FileInfo, error) { - openFlags := 0 - if (flags & AT_SYMLINK_NOFOLLOW) != 0 { - openFlags |= O_NOFOLLOW - } - return withPath2("stat", f, name, openFlags, func(dir File, name string) (FileInfo, error) { - info, err := dir.Stat(name, AT_SYMLINK_NOFOLLOW) - if err == nil { - if info.Mode.Type() == fs.ModeSymlink && ((flags & AT_SYMLINK_NOFOLLOW) == 0) { - err = ELOOP - } - } - return info, err - }) -} - -func (f *rootFile) Readlink(name string, buf []byte) (int, error) { - return withPath2("readlink", f, name, AT_SYMLINK_NOFOLLOW, func(dir File, name string) (int, error) { - return dir.Readlink(name, buf) - }) -} - -func (f *rootFile) Chtimes(name string, times [2]Timespec, flags int) error { - return withPath1("chtimes", f, name, flags, func(dir File, name string) error { - return dir.Chtimes(name, times, AT_SYMLINK_NOFOLLOW) - }) -} - -func (f *rootFile) Mkdir(name string, mode fs.FileMode) error { - return withPath1("mkdir", f, name, AT_SYMLINK_NOFOLLOW, func(dir File, name string) error { - return dir.Mkdir(name, mode) - }) -} - -func (f *rootFile) Rmdir(name string) error { - return withPath1("rmdir", f, name, AT_SYMLINK_NOFOLLOW, File.Rmdir) -} - -func (f *rootFile) Rename(oldName string, newDir File, newName string) error { - f2, ok := newDir.(*rootFile) - if !ok { - path1 := fspath.Join(f.Name(), oldName) - path2 := fspath.Join(newDir.Name(), newName) - return &os.LinkError{Op: "rename", Old: path1, New: path2, Err: EXDEV} - } - return withPath3("rename", f, oldName, f2, newName, AT_SYMLINK_NOFOLLOW, File.Rename) -} - -func (f *rootFile) Link(oldName string, newDir File, newName string, flags int) error { - f2, ok := newDir.(*rootFile) - if !ok { - path1 := fspath.Join(f.Name(), oldName) - path2 := fspath.Join(newDir.Name(), newName) - return &os.LinkError{Op: "link", Old: path1, New: path2, Err: EXDEV} - } - return withPath3("link", f, oldName, f2, newName, flags, func(oldDir File, oldName string, newDir File, newName string) error { - return oldDir.Link(oldName, newDir, newName, AT_SYMLINK_NOFOLLOW) - }) -} - -func (f *rootFile) Symlink(oldName, newName string) error { - return withPath1("symlink", f, newName, AT_SYMLINK_NOFOLLOW, func(dir File, name string) error { - return dir.Symlink(oldName, name) - }) -} - -func (f *rootFile) Unlink(name string) error { - return withPath1("unlink", f, name, AT_SYMLINK_NOFOLLOW, File.Unlink) -} - -func withPath1(op string, root *rootFile, path string, flags int, do func(File, string) error) error { - _, err := ResolvePath(root.File, path, flags, func(dir File, name string) (_ struct{}, err error) { - err = do(dir, name) - return - }) - if err != nil { - err = &fs.PathError{Op: op, Path: path, Err: unwrap(err)} - } - return err -} - -func withPath2[R any](op string, root *rootFile, path string, flags int, do func(File, string) (R, error)) (ret R, err error) { - ret, err = ResolvePath(root.File, path, flags, do) - if err != nil { - err = &fs.PathError{Op: op, Path: path, Err: unwrap(err)} - } - return ret, err -} - -func withPath3(op string, f1 *rootFile, path1 string, f2 *rootFile, path2 string, flags int, do func(File, string, File, string) error) error { - err := withPath1(op, f1, path1, flags, func(dir1 File, name1 string) error { - return withPath1(op, f2, path2, flags, func(dir2 File, name2 string) error { - return do(dir1, name1, dir2, name2) - }) - }) - if err != nil { - path1 = fspath.Join(f1.Name(), path2) - path2 = fspath.Join(f2.Name(), path2) - return &os.LinkError{Op: "link", Old: path1, New: path2, Err: unwrap(err)} - } - return nil -} - -// ResolvePath is the path resolution algorithm which guarantees sandboxing of -// path access in a root FS. -// -// The algorithm walks the path name from f, calling the do function when it -// reaches a path leaf. The function may return ELOOP to indicate that a symlink -// was encountered and must be followed, in which case ResolvePath continues -// walking the path at the link target. Any other value or error returned by the -// do function will be returned immediately. -func ResolvePath[F File, R any](dir F, name string, flags int, do func(F, string) (R, error)) (ret R, err error) { - if name == "" { - return do(dir, "") - } - if fspath.HasTrailingSlash(name) { - flags |= O_DIRECTORY - } - - var lastOpenDir File - defer func() { closeFileIfNotNil(lastOpenDir) }() - - setCurrentDirectory := func(cd File) { - closeFileIfNotNil(lastOpenDir) - dir, lastOpenDir = cd.(F), cd - } - - followSymlinkDepth := 0 - followSymlink := func(symlink, target string) error { - link, err := readlink(dir, symlink) - if err != nil { - // This error may be EINVAL if the file system was modified - // concurrently and the directory entry was not pointing to a - // symbolic link anymore. - return err - } - - // Limit the maximum number of symbolic links that would be followed - // during path resolution; this ensures that if we encounter a loop, - // we will eventually abort resolving the path. - if followSymlinkDepth == MaxFollowSymlink { - return ELOOP - } - followSymlinkDepth++ - - if target != "" { - name = link + "/" + target - } else { - name = link - } - return nil - } - - depth := fspath.Depth(dir.Name()) - for { - if fspath.IsAbs(name) { - if name = fspath.TrimLeadingSlash(name); name == "" { - name = "." - } - d, err := openRoot(dir) - if err != nil { - return ret, err - } - depth = 0 - setCurrentDirectory(d) - } - - var delta int - var elem string - elem, name = fspath.Walk(name) - - if name == "" { - doFile: - ret, err = do(dir, elem) - if err != nil { - if !errors.Is(err, ELOOP) || ((flags & O_NOFOLLOW) != 0) { - return ret, err - } - switch err := followSymlink(elem, ""); { - case errors.Is(err, nil): - continue - case errors.Is(err, EINVAL): - goto doFile - default: - return ret, err - } - } - return ret, nil - } - - if elem == "." { - continue - } - - if elem == ".." { - // This check ensures that we cannot escape the root of the file - // system when accessing a parent directory. - if depth == 0 { - continue - } - delta = -1 - } else { - delta = +1 - } - - openPath: - d, err := dir.Open(elem, openPathFlags, 0) - if err != nil { - if !errors.Is(err, ENOTDIR) { - return ret, err - } - switch err := followSymlink(elem, name); { - case errors.Is(err, nil): - continue - case errors.Is(err, EINVAL): - goto openPath - default: - return ret, err - } - } - depth += delta - setCurrentDirectory(d) - } -} - -func openRoot(dir File) (File, error) { - return dir.Open("/", O_DIRECTORY, 0) -} - -func closeFileIfNotNil(f File) { - if f != nil { - f.Close() - } -} diff --git a/internal/sandbox/rootfs_test.go b/internal/sandbox/rootfs_test.go deleted file mode 100644 index 18911380..00000000 --- a/internal/sandbox/rootfs_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package sandbox_test - -import ( - "io/fs" - "testing" - - "github.com/stealthrocket/timecraft/internal/sandbox" - "github.com/stealthrocket/timecraft/internal/sandbox/sandboxtest" -) - -func TestRootFS(t *testing.T) { - t.Run("fs.FS", func(t *testing.T) { - sandboxtest.TestFS(t, func(t *testing.T, path string) fs.FS { - return sandbox.FS(sandbox.RootFS(sandbox.DirFS(path))) - }) - }) - - t.Run("sandbox.FileSystem", func(t *testing.T) { - sandboxtest.TestFileSystem(t, func(t *testing.T) sandbox.FileSystem { - return sandbox.RootFS(sandbox.DirFS(t.TempDir())) - }) - }) - - sandboxtest.TestRootFS(t, func(t *testing.T, path string) sandbox.FileSystem { - return sandbox.RootFS(sandbox.DirFS(path)) - }) -} diff --git a/internal/sandbox/tarfs/dir.go b/internal/sandbox/tarfs/dir.go index 1fbbb5fc..0002a0a0 100644 --- a/internal/sandbox/tarfs/dir.go +++ b/internal/sandbox/tarfs/dir.go @@ -2,7 +2,6 @@ package tarfs import ( "archive/tar" - "fmt" "io/fs" "path" "sort" @@ -48,8 +47,8 @@ type dirEntry struct { file fileEntry } -func (d *dir) open(fsys *FileSystem, name string) (sandbox.File, error) { - open := &openDir{fsys: fsys, name: name} +func (d *dir) open(fsys *FileSystem) (sandbox.File, error) { + open := &openDir{fsys: fsys} open.dir.Store(d) return open, nil } @@ -102,14 +101,12 @@ func (d *dir) find(name string) fileEntry { return d.ents[i].file } -func resolve[R any](fsys *FileSystem, cwd *dir, cwdName, name string, flags int, do func(fileEntry, []string) (R, error)) (R, error) { +func resolve[R any](fsys *FileSystem, cwd *dir, name string, flags int, do func(fileEntry) (R, error)) (R, error) { var zero R - pathElems := make([]string, 1, 8) - pathElems[0] = cwdName for loop := 0; loop < sandbox.MaxFollowSymlink; loop++ { if name == "" { - return do(cwd, pathElems) + return do(cwd) } var elem string @@ -117,7 +114,6 @@ func resolve[R any](fsys *FileSystem, cwd *dir, cwdName, name string, flags int, if elem == "/" { cwd = &fsys.root - pathElems = append(pathElems[:0], "/") continue } @@ -126,8 +122,6 @@ func resolve[R any](fsys *FileSystem, cwd *dir, cwdName, name string, flags int, return zero, sandbox.ENOENT } - pathElems = append(pathElems, elem) - if name != "" { switch c := f.(type) { case *symlink: @@ -153,7 +147,7 @@ func resolve[R any](fsys *FileSystem, cwd *dir, cwdName, name string, flags int, } } - return do(f, pathElems) + return do(f) } return zero, sandbox.ELOOP @@ -162,7 +156,6 @@ func resolve[R any](fsys *FileSystem, cwd *dir, cwdName, name string, flags int, type openDir struct { readOnlyFile fsys *FileSystem - name string dir atomic.Pointer[dir] mu sync.Mutex index int @@ -170,11 +163,7 @@ type openDir struct { } func (d *openDir) String() string { - return fmt.Sprintf("&tarfs.openDir{name:%q}", d.name) -} - -func (d *openDir) Name() string { - return d.name + return "&tarfs.openDir{}" } func (d *openDir) Close() error { @@ -201,11 +190,11 @@ func (d *openDir) Open(name string, flags int, mode fs.FileMode) (sandbox.File, flags |= sandbox.O_DIRECTORY } - return resolve(d.fsys, dir, d.name, name, flags, func(f fileEntry, pathElems []string) (sandbox.File, error) { + return resolve(d.fsys, dir, name, flags, func(f fileEntry) (sandbox.File, error) { if _, ok := f.(*symlink); ok { return nil, sandbox.ELOOP } - return f.open(d.fsys, path.Join(pathElems...)) + return f.open(d.fsys) }) } @@ -218,7 +207,7 @@ func (d *openDir) Stat(name string, flags int) (sandbox.FileInfo, error) { if (flags & sandbox.AT_SYMLINK_NOFOLLOW) != 0 { openFlags |= sandbox.O_NOFOLLOW } - return resolve(d.fsys, dir, d.name, name, openFlags, func(f fileEntry, _ []string) (sandbox.FileInfo, error) { + return resolve(d.fsys, dir, name, openFlags, func(f fileEntry) (sandbox.FileInfo, error) { return f.stat(), nil }) } @@ -228,7 +217,7 @@ func (d *openDir) Readlink(name string, buf []byte) (int, error) { if dir == nil { return 0, sandbox.EBADF } - return resolve(d.fsys, dir, d.name, name, sandbox.O_NOFOLLOW, func(f fileEntry, _ []string) (int, error) { + return resolve(d.fsys, dir, name, sandbox.O_NOFOLLOW, func(f fileEntry) (int, error) { if s, ok := f.(*symlink); ok { return copy(buf, s.link), nil } else { diff --git a/internal/sandbox/tarfs/file.go b/internal/sandbox/tarfs/file.go index 841c8377..3b012527 100644 --- a/internal/sandbox/tarfs/file.go +++ b/internal/sandbox/tarfs/file.go @@ -37,8 +37,8 @@ func newFile(header *tar.Header, offset int64) *file { } } -func (f *file) open(fsys *FileSystem, name string) (sandbox.File, error) { - open := &openFile{name: name} +func (f *file) open(fsys *FileSystem) (sandbox.File, error) { + open := new(openFile) open.file.Store(f) open.data = *io.NewSectionReader(fsys.data, f.offset, f.size) return open, nil @@ -67,18 +67,13 @@ func (f *file) memsize() uintptr { type openFile struct { leafFile - name string file atomic.Pointer[file] seek sync.Mutex data io.SectionReader } func (f *openFile) String() string { - return fmt.Sprintf("&tarfs.openFile{name:%q}", f.name) -} - -func (f *openFile) Name() string { - return f.name + return fmt.Sprintf("&tarfs.openFile{size:%d}", f.data.Size()) } func (f *openFile) Close() error { diff --git a/internal/sandbox/tarfs/placeholder.go b/internal/sandbox/tarfs/placeholder.go index 72765b5a..77b86c5c 100644 --- a/internal/sandbox/tarfs/placeholder.go +++ b/internal/sandbox/tarfs/placeholder.go @@ -32,7 +32,7 @@ func newPlaceholder(header *tar.Header) *placeholder { } } -func (p *placeholder) open(fsys *FileSystem, name string) (sandbox.File, error) { +func (p *placeholder) open(fsys *FileSystem) (sandbox.File, error) { return nil, syscall.EPERM } diff --git a/internal/sandbox/tarfs/symlink.go b/internal/sandbox/tarfs/symlink.go index 399c82d3..b61fb934 100644 --- a/internal/sandbox/tarfs/symlink.go +++ b/internal/sandbox/tarfs/symlink.go @@ -30,7 +30,7 @@ func newSymlink(header *tar.Header) *symlink { } } -func (s *symlink) open(fsys *FileSystem, name string) (sandbox.File, error) { +func (s *symlink) open(fsys *FileSystem) (sandbox.File, error) { return nil, sandbox.ELOOP } diff --git a/internal/sandbox/tarfs/tarfs.go b/internal/sandbox/tarfs/tarfs.go index 16bd7b7e..caae2003 100644 --- a/internal/sandbox/tarfs/tarfs.go +++ b/internal/sandbox/tarfs/tarfs.go @@ -26,7 +26,7 @@ type FileSystem struct { // Open satisfies sandbox.FileSystem. func (fsys *FileSystem) Open(name string, flags int, mode fs.FileMode) (sandbox.File, error) { - f, err := fsys.root.open(fsys, "/") + f, err := fsys.root.open(fsys) if err != nil { return nil, err } @@ -175,7 +175,7 @@ func OpenFile(f *os.File) (*FileSystem, error) { } type fileEntry interface { - open(fsys *FileSystem, name string) (sandbox.File, error) + open(fsys *FileSystem) (sandbox.File, error) stat() sandbox.FileInfo diff --git a/internal/sandbox/wasi_test.go b/internal/sandbox/wasi_test.go index 2f24c21c..2fdc9754 100644 --- a/internal/sandbox/wasi_test.go +++ b/internal/sandbox/wasi_test.go @@ -215,7 +215,7 @@ func TestSandboxWASI(t *testing.T) { } if config.RootFS != "" { - options = append(options, sandbox.Mount("/", sandbox.RootFS(sandbox.DirFS(config.RootFS)))) + options = append(options, sandbox.Mount("/", sandbox.DirFS(config.RootFS))) } sys := sandbox.New(options...)