github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/internal/sys/fs.go (about)

     1  package sys
     2  
     3  import (
     4  	"io"
     5  	"io/fs"
     6  	"net"
     7  
     8  	"github.com/bananabytelabs/wazero/experimental/sys"
     9  	"github.com/bananabytelabs/wazero/internal/descriptor"
    10  	"github.com/bananabytelabs/wazero/internal/fsapi"
    11  	socketapi "github.com/bananabytelabs/wazero/internal/sock"
    12  	"github.com/bananabytelabs/wazero/internal/sysfs"
    13  )
    14  
    15  const (
    16  	FdStdin int32 = iota
    17  	FdStdout
    18  	FdStderr
    19  	// FdPreopen is the file descriptor of the first pre-opened directory.
    20  	//
    21  	// # Why file descriptor 3?
    22  	//
    23  	// While not specified, the most common WASI implementation, wasi-libc,
    24  	// expects POSIX style file descriptor allocation, where the lowest
    25  	// available number is used to open the next file. Since 1 and 2 are taken
    26  	// by stdout and stderr, the next is 3.
    27  	//   - https://github.com/WebAssembly/WASI/issues/122
    28  	//   - https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_14
    29  	//   - https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-16/libc-bottom-half/sources/preopens.c#L215
    30  	FdPreopen
    31  )
    32  
    33  const modeDevice = fs.ModeDevice | 0o640
    34  
    35  // FileEntry maps a path to an open file in a file system.
    36  type FileEntry struct {
    37  	// Name is the name of the directory up to its pre-open, or the pre-open
    38  	// name itself when IsPreopen.
    39  	//
    40  	// # Notes
    41  	//
    42  	//   - This can drift on rename.
    43  	//   - This relates to the guest path, which is not the real file path
    44  	//     except if the entire host filesystem was made available.
    45  	Name string
    46  
    47  	// IsPreopen is a directory that is lazily opened.
    48  	IsPreopen bool
    49  
    50  	// FS is the filesystem associated with the pre-open.
    51  	FS sys.FS
    52  
    53  	// File is always non-nil.
    54  	File fsapi.File
    55  
    56  	// direntCache is nil until DirentCache was called.
    57  	direntCache *DirentCache
    58  }
    59  
    60  // DirentCache gets or creates a DirentCache for this file or returns an error.
    61  //
    62  // # Errors
    63  //
    64  // A zero sys.Errno is success. The below are expected otherwise:
    65  //   - sys.ENOSYS: the implementation does not support this function.
    66  //   - sys.EBADF: the dir was closed or not readable.
    67  //   - sys.ENOTDIR: the file was not a directory.
    68  //
    69  // # Notes
    70  //
    71  //   - See /RATIONALE.md for design notes.
    72  func (f *FileEntry) DirentCache() (*DirentCache, sys.Errno) {
    73  	if dir := f.direntCache; dir != nil {
    74  		return dir, 0
    75  	}
    76  
    77  	// Require the file to be a directory vs a late error on the same.
    78  	if isDir, errno := f.File.IsDir(); errno != 0 {
    79  		return nil, errno
    80  	} else if !isDir {
    81  		return nil, sys.ENOTDIR
    82  	}
    83  
    84  	// Generate the dotEntries only once.
    85  	if dotEntries, errno := synthesizeDotEntries(f); errno != 0 {
    86  		return nil, errno
    87  	} else {
    88  		f.direntCache = &DirentCache{f: f.File, dotEntries: dotEntries}
    89  	}
    90  
    91  	return f.direntCache, 0
    92  }
    93  
    94  // DirentCache is a caching abstraction of sys.File Readdir.
    95  //
    96  // This is special-cased for "wasi_snapshot_preview1.fd_readdir", and may be
    97  // unneeded, or require changes, to support preview1 or preview2.
    98  //   - The position of the dirents are serialized as `d_next`. For reasons
    99  //     described below, any may need to be re-read. This accepts any positions
   100  //     in the cache, rather than track the position of the last dirent.
   101  //   - dot entries ("." and "..") must be returned. See /RATIONALE.md for why.
   102  //   - An sys.Dirent Name is variable length, it could exceed memory size and
   103  //     need to be re-read.
   104  //   - Multiple dirents may be returned. It is more efficient to read from the
   105  //     underlying file in bulk vs one-at-a-time.
   106  //
   107  // The last results returned by Read are cached, but entries before that
   108  // position are not. This support re-reading entries that couldn't fit into
   109  // memory without accidentally caching all entries in a large directory. This
   110  // approach is sometimes called a sliding window.
   111  type DirentCache struct {
   112  	// f is the underlying file
   113  	f sys.File
   114  
   115  	// dotEntries are the "." and ".." entries added when the directory is
   116  	// initialized.
   117  	dotEntries []sys.Dirent
   118  
   119  	// dirents are the potentially unread directory entries.
   120  	//
   121  	// Internal detail: nil is different from zero length. Zero length is an
   122  	// exhausted directory (eof). nil means the re-read.
   123  	dirents []sys.Dirent
   124  
   125  	// countRead is the total count of dirents read since last rewind.
   126  	countRead uint64
   127  
   128  	// eof is true when the underlying file is at EOF. This avoids re-reading
   129  	// the directory when it is exhausted. Entires in an exhausted directory
   130  	// are not visible until it is rewound via calling Read with `pos==0`.
   131  	eof bool
   132  }
   133  
   134  // synthesizeDotEntries generates a slice of the two elements "." and "..".
   135  func synthesizeDotEntries(f *FileEntry) ([]sys.Dirent, sys.Errno) {
   136  	dotIno, errno := f.File.Ino()
   137  	if errno != 0 {
   138  		return nil, errno
   139  	}
   140  	result := [2]sys.Dirent{}
   141  	result[0] = sys.Dirent{Name: ".", Ino: dotIno, Type: fs.ModeDir}
   142  	// See /RATIONALE.md for why we don't attempt to get an inode for ".." and
   143  	// why in wasi-libc this won't fan-out either.
   144  	result[1] = sys.Dirent{Name: "..", Ino: 0, Type: fs.ModeDir}
   145  	return result[:], 0
   146  }
   147  
   148  // exhaustedDirents avoids allocating empty slices.
   149  var exhaustedDirents = [0]sys.Dirent{}
   150  
   151  // Read is similar to and returns the same errors as `Readdir` on sys.File.
   152  // The main difference is this caches entries returned, resulting in multiple
   153  // valid positions to read from.
   154  //
   155  // When zero, `pos` means rewind to the beginning of this directory. This
   156  // implies a rewind (Seek to zero on the underlying sys.File), unless the
   157  // initial entries are still cached.
   158  //
   159  // When non-zero, `pos` is the zero based index of all dirents returned since
   160  // last rewind. Only entries beginning at `pos` are cached for subsequent
   161  // calls. A non-zero `pos` before the cache returns sys.ENOENT for reasons
   162  // described on DirentCache documentation.
   163  //
   164  // Up to `n` entries are cached and returned. When `n` exceeds the cache, the
   165  // difference are read from the underlying sys.File via `Readdir`. EOF is
   166  // when `len(dirents)` returned are less than `n`.
   167  func (d *DirentCache) Read(pos uint64, n uint32) (dirents []sys.Dirent, errno sys.Errno) {
   168  	switch {
   169  	case pos > d.countRead: // farther than read or negative coerced to uint64.
   170  		return nil, sys.ENOENT
   171  	case pos == 0 && d.dirents != nil:
   172  		// Rewind if we have already read entries. This allows us to see new
   173  		// entries added after the directory was opened.
   174  		if _, errno = d.f.Seek(0, io.SeekStart); errno != 0 {
   175  			return
   176  		}
   177  		d.dirents = nil // dump cache
   178  		d.countRead = 0
   179  	}
   180  
   181  	if n == 0 {
   182  		return // special case no entries.
   183  	}
   184  
   185  	if d.dirents == nil {
   186  		// Always populate dot entries, which makes min len(dirents) == 2.
   187  		d.dirents = d.dotEntries
   188  		d.countRead = 2
   189  		d.eof = false
   190  
   191  		if countToRead := int(n - 2); countToRead <= 0 {
   192  			return
   193  		} else if dirents, errno = d.f.Readdir(countToRead); errno != 0 {
   194  			return
   195  		} else if countRead := len(dirents); countRead > 0 {
   196  			d.eof = countRead < countToRead
   197  			d.dirents = append(d.dotEntries, dirents...)
   198  			d.countRead += uint64(countRead)
   199  		}
   200  
   201  		return d.cachedDirents(n), 0
   202  	}
   203  
   204  	// Reset our cache to the first entry being read.
   205  	cacheStart := d.countRead - uint64(len(d.dirents))
   206  	if pos < cacheStart {
   207  		// We don't currently allow reads before our cache because Seek(0) is
   208  		// the only portable way. Doing otherwise requires skipping, which we
   209  		// won't do unless wasi-testsuite starts requiring it. Implementing
   210  		// this would allow re-reading a large directory, so care would be
   211  		// needed to not buffer the entire directory in memory while skipping.
   212  		errno = sys.ENOENT
   213  		return
   214  	} else if posInCache := pos - cacheStart; posInCache != 0 {
   215  		if uint64(len(d.dirents)) == posInCache {
   216  			// Avoid allocation re-slicing to zero length.
   217  			d.dirents = exhaustedDirents[:]
   218  		} else {
   219  			d.dirents = d.dirents[posInCache:]
   220  		}
   221  	}
   222  
   223  	// See if we need more entries.
   224  	if countToRead := int(n) - len(d.dirents); countToRead > 0 && !d.eof {
   225  		// Try to read more, which could fail.
   226  		if dirents, errno = d.f.Readdir(countToRead); errno != 0 {
   227  			return
   228  		}
   229  
   230  		// Append the next read entries if we weren't at EOF.
   231  		if countRead := len(dirents); countRead > 0 {
   232  			d.eof = countRead < countToRead
   233  			d.dirents = append(d.dirents, dirents...)
   234  			d.countRead += uint64(countRead)
   235  		}
   236  	}
   237  
   238  	return d.cachedDirents(n), 0
   239  }
   240  
   241  // cachedDirents returns up to `n` dirents from the cache.
   242  func (d *DirentCache) cachedDirents(n uint32) []sys.Dirent {
   243  	direntCount := uint32(len(d.dirents))
   244  	switch {
   245  	case direntCount == 0:
   246  		return nil
   247  	case direntCount > n:
   248  		return d.dirents[:n]
   249  	}
   250  	return d.dirents
   251  }
   252  
   253  type FSContext struct {
   254  	// rootFS is the root ("/") mount.
   255  	rootFS sys.FS
   256  
   257  	// openedFiles is a map of file descriptor numbers (>=FdPreopen) to open files
   258  	// (or directories) and defaults to empty.
   259  	// TODO: This is unguarded, so not goroutine-safe!
   260  	openedFiles FileTable
   261  }
   262  
   263  // FileTable is a specialization of the descriptor.Table type used to map file
   264  // descriptors to file entries.
   265  type FileTable = descriptor.Table[int32, *FileEntry]
   266  
   267  // RootFS returns a possibly unimplemented root filesystem. Any files that
   268  // should be added to the table should be inserted via InsertFile.
   269  //
   270  // TODO: This is only used by GOOS=js and tests: Remove when we remove GOOS=js
   271  // (after Go 1.22 is released).
   272  func (c *FSContext) RootFS() sys.FS {
   273  	if rootFS := c.rootFS; rootFS == nil {
   274  		return sys.UnimplementedFS{}
   275  	} else {
   276  		return rootFS
   277  	}
   278  }
   279  
   280  // LookupFile returns a file if it is in the table.
   281  func (c *FSContext) LookupFile(fd int32) (*FileEntry, bool) {
   282  	return c.openedFiles.Lookup(fd)
   283  }
   284  
   285  // OpenFile opens the file into the table and returns its file descriptor.
   286  // The result must be closed by CloseFile or Close.
   287  func (c *FSContext) OpenFile(fs sys.FS, path string, flag sys.Oflag, perm fs.FileMode) (int32, sys.Errno) {
   288  	if f, errno := fs.OpenFile(path, flag, perm); errno != 0 {
   289  		return 0, errno
   290  	} else {
   291  		fe := &FileEntry{FS: fs, File: fsapi.Adapt(f)}
   292  		if path == "/" || path == "." {
   293  			fe.Name = ""
   294  		} else {
   295  			fe.Name = path
   296  		}
   297  		if newFD, ok := c.openedFiles.Insert(fe); !ok {
   298  			return 0, sys.EBADF
   299  		} else {
   300  			return newFD, 0
   301  		}
   302  	}
   303  }
   304  
   305  // Renumber assigns the file pointed by the descriptor `from` to `to`.
   306  func (c *FSContext) Renumber(from, to int32) sys.Errno {
   307  	fromFile, ok := c.openedFiles.Lookup(from)
   308  	if !ok || to < 0 {
   309  		return sys.EBADF
   310  	} else if fromFile.IsPreopen {
   311  		return sys.ENOTSUP
   312  	}
   313  
   314  	// If toFile is already open, we close it to prevent windows lock issues.
   315  	//
   316  	// The doc is unclear and other implementations do nothing for already-opened To FDs.
   317  	// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno
   318  	// https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasi-common/src/snapshots/preview_1.rs#L531-L546
   319  	if toFile, ok := c.openedFiles.Lookup(to); ok {
   320  		if toFile.IsPreopen {
   321  			return sys.ENOTSUP
   322  		}
   323  		_ = toFile.File.Close()
   324  	}
   325  
   326  	c.openedFiles.Delete(from)
   327  	if !c.openedFiles.InsertAt(fromFile, to) {
   328  		return sys.EBADF
   329  	}
   330  	return 0
   331  }
   332  
   333  // SockAccept accepts a sock.TCPConn into the file table and returns its file
   334  // descriptor.
   335  func (c *FSContext) SockAccept(sockFD int32, nonblock bool) (int32, sys.Errno) {
   336  	var sock socketapi.TCPSock
   337  	if e, ok := c.LookupFile(sockFD); !ok || !e.IsPreopen {
   338  		return 0, sys.EBADF // Not a preopen
   339  	} else if sock, ok = e.File.(socketapi.TCPSock); !ok {
   340  		return 0, sys.EBADF // Not a sock
   341  	}
   342  
   343  	conn, errno := sock.Accept()
   344  	if errno != 0 {
   345  		return 0, errno
   346  	}
   347  
   348  	fe := &FileEntry{File: fsapi.Adapt(conn)}
   349  
   350  	if nonblock {
   351  		if errno = fe.File.SetNonblock(true); errno != 0 {
   352  			_ = conn.Close()
   353  			return 0, errno
   354  		}
   355  	}
   356  
   357  	if newFD, ok := c.openedFiles.Insert(fe); !ok {
   358  		return 0, sys.EBADF
   359  	} else {
   360  		return newFD, 0
   361  	}
   362  }
   363  
   364  // CloseFile returns any error closing the existing file.
   365  func (c *FSContext) CloseFile(fd int32) (errno sys.Errno) {
   366  	f, ok := c.openedFiles.Lookup(fd)
   367  	if !ok {
   368  		return sys.EBADF
   369  	}
   370  	if errno = f.File.Close(); errno != 0 {
   371  		return errno
   372  	}
   373  	c.openedFiles.Delete(fd)
   374  	return errno
   375  }
   376  
   377  // Close implements io.Closer
   378  func (c *FSContext) Close() (err error) {
   379  	// Close any files opened in this context
   380  	c.openedFiles.Range(func(fd int32, entry *FileEntry) bool {
   381  		if errno := entry.File.Close(); errno != 0 {
   382  			err = errno // This means err returned == the last non-nil error.
   383  		}
   384  		return true
   385  	})
   386  	// A closed FSContext cannot be reused so clear the state.
   387  	c.openedFiles = FileTable{}
   388  	return
   389  }
   390  
   391  // InitFSContext initializes a FSContext with stdio streams and optional
   392  // pre-opened filesystems and TCP listeners.
   393  func (c *Context) InitFSContext(
   394  	stdin io.Reader,
   395  	stdout, stderr io.Writer,
   396  	fs []sys.FS, guestPaths []string,
   397  	tcpListeners []*net.TCPListener,
   398  ) (err error) {
   399  	inFile, err := stdinFileEntry(stdin)
   400  	if err != nil {
   401  		return err
   402  	}
   403  	c.fsc.openedFiles.Insert(inFile)
   404  	outWriter, err := stdioWriterFileEntry("stdout", stdout)
   405  	if err != nil {
   406  		return err
   407  	}
   408  	c.fsc.openedFiles.Insert(outWriter)
   409  	errWriter, err := stdioWriterFileEntry("stderr", stderr)
   410  	if err != nil {
   411  		return err
   412  	}
   413  	c.fsc.openedFiles.Insert(errWriter)
   414  
   415  	for i, fs := range fs {
   416  		guestPath := guestPaths[i]
   417  
   418  		if StripPrefixesAndTrailingSlash(guestPath) == "" {
   419  			// Default to bind to '/' when guestPath is effectively empty.
   420  			guestPath = "/"
   421  			c.fsc.rootFS = fs
   422  		}
   423  		c.fsc.openedFiles.Insert(&FileEntry{
   424  			FS:        fs,
   425  			Name:      guestPath,
   426  			IsPreopen: true,
   427  			File:      &lazyDir{fs: fs},
   428  		})
   429  	}
   430  
   431  	for _, tl := range tcpListeners {
   432  		c.fsc.openedFiles.Insert(&FileEntry{IsPreopen: true, File: fsapi.Adapt(sysfs.NewTCPListenerFile(tl))})
   433  	}
   434  	return nil
   435  }
   436  
   437  // StripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the
   438  // result index begins with another string. A result of "." coerces to the
   439  // empty string "" because the current directory is handled by the guest.
   440  //
   441  // Results are the offset/len pair which is an optimization to avoid re-slicing
   442  // overhead, as this function is called for every path operation.
   443  //
   444  // Note: Relative paths should be handled by the guest, as that's what knows
   445  // what the current directory is. However, paths that escape the current
   446  // directory e.g. "../.." have been found in `tinygo test` and this
   447  // implementation takes care to avoid it.
   448  func StripPrefixesAndTrailingSlash(path string) string {
   449  	// strip trailing slashes
   450  	pathLen := len(path)
   451  	for ; pathLen > 0 && path[pathLen-1] == '/'; pathLen-- {
   452  	}
   453  
   454  	pathI := 0
   455  loop:
   456  	for pathI < pathLen {
   457  		switch path[pathI] {
   458  		case '/':
   459  			pathI++
   460  		case '.':
   461  			nextI := pathI + 1
   462  			if nextI < pathLen && path[nextI] == '/' {
   463  				pathI = nextI + 1
   464  			} else if nextI == pathLen {
   465  				pathI = nextI
   466  			} else {
   467  				break loop
   468  			}
   469  		default:
   470  			break loop
   471  		}
   472  	}
   473  	return path[pathI:pathLen]
   474  }