Files
wazero/internal/sysfs/rootfs.go
Crypt Keeper 36bf277534 sysfs: requires all methods to return syscall.Errno (#1264)
This forces all syscall functions, notably filesystem, to return numeric
codes as opposed to mapping in two different areas. The result of this
change is better consolidation in call sites of `sysfs.FS`, while
further refactoring is needed to address consolidation of file errors.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2023-03-22 07:47:57 +01:00

509 lines
15 KiB
Go

package sysfs
import (
"fmt"
"io"
"io/fs"
"strings"
"syscall"
"time"
"github.com/tetratelabs/wazero/internal/platform"
)
func NewRootFS(fs []FS, guestPaths []string) (FS, error) {
switch len(fs) {
case 0:
return UnimplementedFS{}, nil
case 1:
if StripPrefixesAndTrailingSlash(guestPaths[0]) == "" {
return fs[0], nil
}
}
ret := &CompositeFS{
string: stringFS(fs, guestPaths),
fs: make([]FS, len(fs)),
guestPaths: make([]string, len(fs)),
cleanedGuestPaths: make([]string, len(fs)),
rootGuestPaths: map[string]int{},
rootIndex: -1,
}
copy(ret.guestPaths, guestPaths)
copy(ret.fs, fs)
for i, guestPath := range guestPaths {
// Clean the prefix in the same way path matches will.
cleaned := StripPrefixesAndTrailingSlash(guestPath)
if cleaned == "" {
if ret.rootIndex != -1 {
return nil, fmt.Errorf("multiple root filesystems are invalid: %s", ret.string)
}
ret.rootIndex = i
} else if strings.HasPrefix(cleaned, "..") {
// ../ mounts are special cased and aren't returned in a directory
// listing, so we can ignore them for now.
} else if strings.Contains(cleaned, "/") {
return nil, fmt.Errorf("only single-level guest paths allowed: %s", ret.string)
} else {
ret.rootGuestPaths[cleaned] = i
}
ret.cleanedGuestPaths[i] = cleaned
}
// Ensure there is always a root match to keep runtime logic simpler.
if ret.rootIndex == -1 {
ret.rootIndex = len(fs)
ret.cleanedGuestPaths = append(ret.cleanedGuestPaths, "")
ret.fs = append(ret.fs, &fakeRootFS{})
}
return ret, nil
}
type CompositeFS struct {
UnimplementedFS
// string is cached for convenience.
string string
// fs is index-correlated with cleanedGuestPaths
fs []FS
// guestPaths are the original paths supplied by the end user, cleaned as
// cleanedGuestPaths.
guestPaths []string
// cleanedGuestPaths to match in precedence order, ascending.
cleanedGuestPaths []string
// rootGuestPaths are cleanedGuestPaths that exist directly under root, such as
// "tmp".
rootGuestPaths map[string]int
// rootIndex is the index in fs that is the root filesystem
rootIndex int
}
// String implements fmt.Stringer
func (c *CompositeFS) String() string {
return c.string
}
func stringFS(fs []FS, guestPaths []string) string {
var ret strings.Builder
ret.WriteString("[")
writeMount(&ret, fs[0], guestPaths[0])
for i, f := range fs[1:] {
ret.WriteString(" ")
writeMount(&ret, f, guestPaths[i+1])
}
ret.WriteString("]")
return ret.String()
}
func writeMount(ret *strings.Builder, f FS, guestPath string) {
ret.WriteString(f.String())
ret.WriteString(":")
ret.WriteString(guestPath)
if _, ok := f.(*readFS); ok {
ret.WriteString(":ro")
}
}
// GuestPaths returns the underlying pre-open paths in original order.
func (c *CompositeFS) GuestPaths() (guestPaths []string) {
return c.guestPaths
}
// FS returns the underlying filesystems in original order.
func (c *CompositeFS) FS() (fs []FS) {
fs = make([]FS, len(c.guestPaths))
copy(fs, c.fs)
return
}
// Open implements the same method as documented on fs.FS
func (c *CompositeFS) Open(name string) (fs.File, error) {
return fsOpen(c, name)
}
// OpenFile implements FS.OpenFile
func (c *CompositeFS) OpenFile(path string, flag int, perm fs.FileMode) (f fs.File, err syscall.Errno) {
matchIndex, relativePath := c.chooseFS(path)
f, err = c.fs[matchIndex].OpenFile(relativePath, flag, perm)
if err != 0 {
return
}
// Ensure the root directory listing includes any prefix mounts.
if matchIndex == c.rootIndex {
switch path {
case ".", "/", "":
if len(c.rootGuestPaths) > 0 {
f = &openRootDir{c: c, f: f.(fs.ReadDirFile)}
}
}
}
return
}
// An openRootDir is a root directory open for reading, which has mounts inside
// of it.
type openRootDir struct {
c *CompositeFS
f fs.ReadDirFile // the directory file itself
dirents []fs.DirEntry // the directory contents
direntsI int // the read offset, an index into the files slice
}
func (d *openRootDir) Close() error { return d.f.Close() }
func (d *openRootDir) Stat() (fs.FileInfo, error) { return d.f.Stat() }
func (d *openRootDir) Read([]byte) (int, error) {
return 0, &fs.PathError{Op: "read", Path: "/", Err: syscall.EISDIR}
}
// readDir reads the directory fully into d.dirents, replacing any entries that
// correspond to prefix matches or appending them to the end.
func (d *openRootDir) readDir() (err error) {
if d.dirents, err = d.f.ReadDir(-1); err != nil {
return
}
remaining := make(map[string]int, len(d.c.rootGuestPaths))
for k, v := range d.c.rootGuestPaths {
remaining[k] = v
}
for i, e := range d.dirents {
if fsI, ok := remaining[e.Name()]; ok {
if d.dirents[i], err = d.rootEntry(e.Name(), fsI); err != nil {
return
}
delete(remaining, e.Name())
}
}
var di fs.DirEntry
for n, fsI := range remaining {
if di, err = d.rootEntry(n, fsI); err != nil {
return
}
d.dirents = append(d.dirents, di)
}
return
}
func (d *openRootDir) rootEntry(name string, fsI int) (fs.DirEntry, error) {
if st, err := d.c.fs[fsI].Stat("."); err != 0 {
return nil, err
} else {
return &dirInfo{name, st}, nil
}
}
// dirInfo is a DirEntry based on a FileInfo.
type dirInfo struct {
// name is needed to retain the stat info for a mount, knowing the
// directory is masked. For example, we don't want to leak the underlying
// host directory name.
name string
stat platform.Stat_t
}
func (i *dirInfo) Name() string { return i.name }
func (i *dirInfo) Type() fs.FileMode { return i.stat.Mode.Type() }
func (i *dirInfo) Info() (fs.FileInfo, error) { return i, nil }
func (i *dirInfo) Size() int64 { return i.stat.Size }
func (i *dirInfo) Mode() fs.FileMode { return i.stat.Mode }
func (i *dirInfo) ModTime() time.Time {
return time.Unix(i.stat.Mtim/1e9, i.stat.Mtim%1e9)
}
func (i *dirInfo) IsDir() bool { return i.stat.Mode.IsDir() }
func (i *dirInfo) Sys() interface{} { return nil }
func (d *openRootDir) ReadDir(count int) ([]fs.DirEntry, error) {
if d.dirents == nil {
if err := d.readDir(); err != nil {
return nil, err
}
}
// logic similar to go:embed
n := len(d.dirents) - d.direntsI
if n == 0 {
if count <= 0 {
return nil, nil
}
return nil, io.EOF
}
if count > 0 && n > count {
n = count
}
list := make([]fs.DirEntry, n)
for i := range list {
list[i] = d.dirents[d.direntsI+i]
}
d.direntsI += n
return list, nil
}
// Lstat implements FS.Lstat
func (c *CompositeFS) Lstat(path string) (platform.Stat_t, syscall.Errno) {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Lstat(relativePath)
}
// Stat implements FS.Stat
func (c *CompositeFS) Stat(path string) (platform.Stat_t, syscall.Errno) {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Stat(relativePath)
}
// Mkdir implements FS.Mkdir
func (c *CompositeFS) Mkdir(path string, perm fs.FileMode) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Mkdir(relativePath, perm)
}
// Chmod implements FS.Chmod
func (c *CompositeFS) Chmod(path string, perm fs.FileMode) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Chmod(relativePath, perm)
}
// Chown implements FS.Chown
func (c *CompositeFS) Chown(path string, uid, gid int) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Chown(relativePath, uid, gid)
}
// Lchown implements FS.Lchown
func (c *CompositeFS) Lchown(path string, uid, gid int) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Lchown(relativePath, uid, gid)
}
// Rename implements FS.Rename
func (c *CompositeFS) Rename(from, to string) syscall.Errno {
fromFS, fromPath := c.chooseFS(from)
toFS, toPath := c.chooseFS(to)
if fromFS != toFS {
return syscall.ENOSYS // not yet anyway
}
return c.fs[fromFS].Rename(fromPath, toPath)
}
// Readlink implements FS.Readlink
func (c *CompositeFS) Readlink(path string) (string, syscall.Errno) {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Readlink(relativePath)
}
// Link implements FS.Link.
func (c *CompositeFS) Link(oldName, newName string) syscall.Errno {
fromFS, oldNamePath := c.chooseFS(oldName)
toFS, newNamePath := c.chooseFS(newName)
if fromFS != toFS {
return syscall.ENOSYS // not yet anyway
}
return c.fs[fromFS].Link(oldNamePath, newNamePath)
}
// Utimens implements FS.Utimens
func (c *CompositeFS) Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Utimens(relativePath, times, symlinkFollow)
}
// Symlink implements FS.Symlink
func (c *CompositeFS) Symlink(oldName, link string) (err syscall.Errno) {
fromFS, oldNamePath := c.chooseFS(oldName)
toFS, linkPath := c.chooseFS(link)
if fromFS != toFS {
return syscall.ENOSYS // not yet anyway
}
return c.fs[fromFS].Symlink(oldNamePath, linkPath)
}
// Truncate implements FS.Truncate
func (c *CompositeFS) Truncate(path string, size int64) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Truncate(relativePath, size)
}
// Rmdir implements FS.Rmdir
func (c *CompositeFS) Rmdir(path string) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Rmdir(relativePath)
}
// Unlink implements FS.Unlink
func (c *CompositeFS) Unlink(path string) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Unlink(relativePath)
}
// chooseFS chooses the best fs and the relative path to use for the input.
func (c *CompositeFS) chooseFS(path string) (matchIndex int, relativePath string) {
matchIndex = -1
matchPrefixLen := 0
pathI, pathLen := stripPrefixesAndTrailingSlash(path)
// Last is the highest precedence, so we iterate backwards. The last longest
// match wins. e.g. the pre-open "tmp" wins vs "" regardless of order.
for i := len(c.fs) - 1; i >= 0; i-- {
prefix := c.cleanedGuestPaths[i]
if eq, match := hasPathPrefix(path, pathI, pathLen, prefix); eq {
// When the input equals the prefix, there cannot be a longer match
// later. The relative path is the FS root, so return empty string.
matchIndex = i
relativePath = ""
return
} else if match {
// Check to see if this is a longer match
prefixLen := len(prefix)
if prefixLen > matchPrefixLen || matchIndex == -1 {
matchIndex = i
matchPrefixLen = prefixLen
}
} // Otherwise, keep looking for a match
}
// Now, we know the path != prefix, but it matched an existing fs, because
// setup ensures there's always a root filesystem.
// If this was a root path match the cleaned path is the relative one to
// pass to the underlying filesystem.
if matchPrefixLen == 0 {
// Avoid re-slicing when the input is already clean
if pathI == 0 && len(path) == pathLen {
relativePath = path
} else {
relativePath = path[pathI:pathLen]
}
return
}
// Otherwise, it is non-root match: the relative path is past "$prefix/"
pathI += matchPrefixLen + 1 // e.g. prefix=foo, path=foo/bar -> bar
relativePath = path[pathI:pathLen]
return
}
// hasPathPrefix compares an input path against a prefix, both cleaned by
// stripPrefixesAndTrailingSlash. This returns a pair of eq, match to allow an
// early short circuit on match.
//
// Note: This is case-sensitive because POSIX paths are compared case
// sensitively.
func hasPathPrefix(path string, pathI, pathLen int, prefix string) (eq, match bool) {
matchLen := pathLen - pathI
if prefix == "" {
return matchLen == 0, true // e.g. prefix=, path=foo
}
prefixLen := len(prefix)
// reset pathLen temporarily to represent the length to match as opposed to
// the length of the string (that may contain leading slashes).
if matchLen == prefixLen {
if pathContainsPrefix(path, pathI, prefixLen, prefix) {
return true, true // e.g. prefix=bar, path=bar
}
return false, false
} else if matchLen < prefixLen {
return false, false // e.g. prefix=fooo, path=foo
}
if path[pathI+prefixLen] != '/' {
return false, false // e.g. prefix=foo, path=fooo
}
// Not equal, but maybe a match. e.g. prefix=foo, path=foo/bar
return false, pathContainsPrefix(path, pathI, prefixLen, prefix)
}
// pathContainsPrefix is faster than strings.HasPrefix even if we didn't cache
// the index,len. See benchmarks.
func pathContainsPrefix(path string, pathI, prefixLen int, prefix string) bool {
for i := 0; i < prefixLen; i++ {
if path[pathI] != prefix[i] {
return false // e.g. prefix=bar, path=foo or foo/bar
}
pathI++
}
return true // e.g. prefix=foo, path=foo or foo/bar
}
func StripPrefixesAndTrailingSlash(path string) string {
pathI, pathLen := stripPrefixesAndTrailingSlash(path)
return path[pathI:pathLen]
}
// stripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the
// result index begins with another string. A result of "." coerces to the
// empty string "" because the current directory is handled by the guest.
//
// Results are the offset/len pair which is an optimization to avoid re-slicing
// overhead, as this function is called for every path operation.
//
// Note: Relative paths should be handled by the guest, as that's what knows
// what the current directory is. However, paths that escape the current
// directory e.g. "../.." have been found in `tinygo test` and this
// implementation takes care to avoid it.
func stripPrefixesAndTrailingSlash(path string) (pathI, pathLen int) {
// strip trailing slashes
pathLen = len(path)
for ; pathLen > 0 && path[pathLen-1] == '/'; pathLen-- {
}
pathI = 0
loop:
for pathI < pathLen {
switch path[pathI] {
case '/':
pathI++
case '.':
nextI := pathI + 1
if nextI < pathLen && path[nextI] == '/' {
pathI = nextI + 1
} else if nextI == pathLen {
pathI = nextI
} else {
break loop
}
default:
break loop
}
}
return
}
type fakeRootFS struct{ UnimplementedFS }
// OpenFile implements FS.OpenFile
func (*fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, syscall.Errno) {
switch path {
case ".", "/", "":
return fakeRootDir{}, 0
}
return nil, syscall.ENOENT
}
type fakeRootDir struct{}
func (fakeRootDir) Close() (err error) { return }
func (fakeRootDir) Stat() (fs.FileInfo, error) { return fakeRootDirInfo{}, nil }
func (fakeRootDir) Read([]byte) (int, error) {
return 0, &fs.PathError{Op: "read", Path: "/", Err: syscall.EISDIR}
}
type fakeRootDirInfo struct{}
func (fakeRootDirInfo) Name() string { return "/" }
func (fakeRootDirInfo) Size() int64 { return 0 }
func (fakeRootDirInfo) Mode() fs.FileMode { return fs.ModeDir | 0o500 }
func (fakeRootDirInfo) ModTime() time.Time { return time.Unix(0, 0) }
func (fakeRootDirInfo) IsDir() bool { return true }
func (fakeRootDirInfo) Sys() interface{} { return nil }
func (fakeRootDir) ReadDir(int) (dirents []fs.DirEntry, err error) { return }