github.com/tetratelabs/wazero@v1.2.1/internal/sys/fs.go (about)

     1  package sys
     2  
     3  import (
     4  	"io"
     5  	"io/fs"
     6  	"net"
     7  	"path"
     8  	"syscall"
     9  
    10  	"github.com/tetratelabs/wazero/internal/descriptor"
    11  	"github.com/tetratelabs/wazero/internal/fsapi"
    12  	"github.com/tetratelabs/wazero/internal/platform"
    13  	socketapi "github.com/tetratelabs/wazero/internal/sock"
    14  	"github.com/tetratelabs/wazero/internal/sysfs"
    15  )
    16  
    17  const (
    18  	FdStdin int32 = iota
    19  	FdStdout
    20  	FdStderr
    21  	// FdPreopen is the file descriptor of the first pre-opened directory.
    22  	//
    23  	// # Why file descriptor 3?
    24  	//
    25  	// While not specified, the most common WASI implementation, wasi-libc,
    26  	// expects POSIX style file descriptor allocation, where the lowest
    27  	// available number is used to open the next file. Since 1 and 2 are taken
    28  	// by stdout and stderr, the next is 3.
    29  	//   - https://github.com/WebAssembly/WASI/issues/122
    30  	//   - https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_14
    31  	//   - https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-16/libc-bottom-half/sources/preopens.c#L215
    32  	FdPreopen
    33  )
    34  
    35  const modeDevice = fs.ModeDevice | 0o640
    36  
    37  // FileEntry maps a path to an open file in a file system.
    38  type FileEntry struct {
    39  	// Name is the name of the directory up to its pre-open, or the pre-open
    40  	// name itself when IsPreopen.
    41  	//
    42  	// # Notes
    43  	//
    44  	//   - This can drift on rename.
    45  	//   - This relates to the guest path, which is not the real file path
    46  	//     except if the entire host filesystem was made available.
    47  	Name string
    48  
    49  	// IsPreopen is a directory that is lazily opened.
    50  	IsPreopen bool
    51  
    52  	// FS is the filesystem associated with the pre-open.
    53  	FS fsapi.FS
    54  
    55  	// File is always non-nil.
    56  	File fsapi.File
    57  }
    58  
    59  const direntBufSize = 16
    60  
    61  // Readdir is the status of a prior fs.ReadDirFile call.
    62  type Readdir struct {
    63  	// cursor is the current position in the buffer.
    64  	cursor uint64
    65  
    66  	// countRead is the total count of files read including Dirents.
    67  	//
    68  	// Notes:
    69  	//
    70  	// * countRead is the index of the next file in the list. This is
    71  	//   also the value that Cookie returns, so it should always be
    72  	//   higher or equal than the cookie given in Rewind.
    73  	//
    74  	// * this can overflow to negative, which means our implementation
    75  	//   doesn't support writing greater than max int64 entries.
    76  	//   countRead uint64
    77  	countRead uint64
    78  
    79  	// dirents is a fixed buffer of size direntBufSize. Notably,
    80  	// directory listing are not rewindable, so we keep entries around in case
    81  	// the caller mis-estimated their buffer and needs a few still cached.
    82  	//
    83  	// Note: This is wasi-specific and needs to be refactored.
    84  	// In wasi preview1, dot and dot-dot entries are required to exist, but the
    85  	// reverse is true for preview2. More importantly, preview2 holds separate
    86  	// stateful dir-entry-streams per file.
    87  	dirents []fsapi.Dirent
    88  
    89  	// dirInit seeks and reset the provider for dirents to the beginning
    90  	// and returns an initial batch (e.g. dot directories).
    91  	dirInit func() ([]fsapi.Dirent, syscall.Errno)
    92  
    93  	// dirReader fetches a new batch of direntBufSize elements.
    94  	dirReader func(n uint64) ([]fsapi.Dirent, syscall.Errno)
    95  }
    96  
    97  // NewReaddir is a constructor for Readdir. It takes a dirInit
    98  func NewReaddir(
    99  	dirInit func() ([]fsapi.Dirent, syscall.Errno),
   100  	dirReader func(n uint64) ([]fsapi.Dirent, syscall.Errno),
   101  ) (*Readdir, syscall.Errno) {
   102  	d := &Readdir{dirReader: dirReader, dirInit: dirInit}
   103  	return d, d.init()
   104  }
   105  
   106  // init resets the cursor and invokes the dirInit, dirReader
   107  // methods to reset the internal state of the Readdir struct.
   108  //
   109  // Note: this is different from Reset, because it will not short-circuit
   110  // when cursor is already 0, but it will force an unconditional reload.
   111  func (d *Readdir) init() syscall.Errno {
   112  	d.cursor = 0
   113  	d.countRead = 0
   114  	// Reset the buffer to the initial state.
   115  	initialDirents, errno := d.dirInit()
   116  	if errno != 0 {
   117  		return errno
   118  	}
   119  	if len(initialDirents) > direntBufSize {
   120  		return syscall.EINVAL
   121  	}
   122  	d.dirents = initialDirents
   123  	// Fill the buffer with more data.
   124  	count := direntBufSize - len(initialDirents)
   125  	if count == 0 {
   126  		// No need to fill up the buffer further.
   127  		return 0
   128  	}
   129  	dirents, errno := d.dirReader(uint64(count))
   130  	if errno != 0 {
   131  		return errno
   132  	}
   133  	d.dirents = append(d.dirents, dirents...)
   134  	return 0
   135  }
   136  
   137  // newReaddirFromFileEntry is a constructor for Readdir that takes a FileEntry to initialize.
   138  func newReaddirFromFileEntry(f *FileEntry) (*Readdir, syscall.Errno) {
   139  	// Generate the dotEntries only once and return it many times in the dirInit closure.
   140  	dotEntries, errno := synthesizeDotEntries(f)
   141  	if errno != 0 {
   142  		return nil, errno
   143  	}
   144  	dirInit := func() ([]fsapi.Dirent, syscall.Errno) {
   145  		// Ensure we always rewind to the beginning when we re-init.
   146  		if _, errno := f.File.Seek(0, io.SeekStart); errno != 0 {
   147  			return nil, errno
   148  		}
   149  		// Return the dotEntries that we have already generated outside the closure.
   150  		return dotEntries, 0
   151  	}
   152  	dirReader := func(n uint64) ([]fsapi.Dirent, syscall.Errno) { return f.File.Readdir(int(n)) }
   153  	return NewReaddir(dirInit, dirReader)
   154  }
   155  
   156  // synthesizeDotEntries generates a slice of the two elements "." and "..".
   157  func synthesizeDotEntries(f *FileEntry) (result []fsapi.Dirent, errno syscall.Errno) {
   158  	dotIno, errno := f.File.Ino()
   159  	if errno != 0 {
   160  		return nil, errno
   161  	}
   162  	result = append(result, fsapi.Dirent{Name: ".", Ino: dotIno, Type: fs.ModeDir})
   163  	dotDotIno := uint64(0)
   164  	if !f.IsPreopen && f.Name != "." {
   165  		if st, errno := f.FS.Stat(path.Dir(f.Name)); errno != 0 {
   166  			return nil, errno
   167  		} else {
   168  			dotDotIno = st.Ino
   169  		}
   170  	}
   171  	result = append(result, fsapi.Dirent{Name: "..", Ino: dotDotIno, Type: fs.ModeDir})
   172  	return result, 0
   173  }
   174  
   175  // Reset seeks the internal cursor to 0 and refills the buffer.
   176  func (d *Readdir) Reset() syscall.Errno {
   177  	if d.countRead == 0 {
   178  		return 0
   179  	}
   180  	return d.init()
   181  }
   182  
   183  // Skip is equivalent to calling n times Advance.
   184  func (d *Readdir) Skip(n uint64) {
   185  	end := d.countRead + n
   186  	var err syscall.Errno = 0
   187  	for d.countRead < end && err == 0 {
   188  		err = d.Advance()
   189  	}
   190  }
   191  
   192  // Cookie returns a cookie representing the current state of the ReadDir struct.
   193  //
   194  // Note: this returns the countRead field, but it is an implementation detail.
   195  func (d *Readdir) Cookie() uint64 {
   196  	return d.countRead
   197  }
   198  
   199  // Rewind seeks the internal cursor to the state represented by the cookie.
   200  // It returns a syscall.Errno if the cursor was reset and an I/O error occurred while trying to re-init.
   201  func (d *Readdir) Rewind(cookie int64) syscall.Errno {
   202  	unsignedCookie := uint64(cookie)
   203  	switch {
   204  	case cookie < 0 || unsignedCookie > d.countRead:
   205  		// the cookie can neither be negative nor can it be larger than countRead.
   206  		return syscall.EINVAL
   207  	case cookie == 0 && d.countRead == 0:
   208  		return 0
   209  	case cookie == 0 && d.countRead != 0:
   210  		// This means that there was a previous call to the dir, but cookie is reset.
   211  		// This happens when the program calls rewinddir, for example:
   212  		// https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/cloudlibc/src/libc/dirent/rewinddir.c#L10-L12
   213  		return d.Reset()
   214  	case unsignedCookie < d.countRead:
   215  		if cookie/direntBufSize != int64(d.countRead)/direntBufSize {
   216  			// The cookie is not 0, but it points into a window before the current one.
   217  			return syscall.ENOSYS
   218  		}
   219  		// We are allowed to rewind back to a previous offset within the current window.
   220  		d.countRead = unsignedCookie
   221  		d.cursor = d.countRead % direntBufSize
   222  		return 0
   223  	default:
   224  		// The cookie is valid.
   225  		return 0
   226  	}
   227  }
   228  
   229  // Peek emits the current value.
   230  // It returns syscall.ENOENT when there are no entries left in the directory.
   231  func (d *Readdir) Peek() (*fsapi.Dirent, syscall.Errno) {
   232  	switch {
   233  	case d.cursor == uint64(len(d.dirents)):
   234  		// We're past the buf size, fill it up again.
   235  		dirents, errno := d.dirReader(direntBufSize)
   236  		if errno != 0 {
   237  			return nil, errno
   238  		}
   239  		d.dirents = append(d.dirents, dirents...)
   240  		fallthrough
   241  	default: // d.cursor < direntBufSize FIXME
   242  		if d.cursor == uint64(len(d.dirents)) {
   243  			return nil, syscall.ENOENT
   244  		}
   245  		dirent := &d.dirents[d.cursor]
   246  		return dirent, 0
   247  	}
   248  }
   249  
   250  // Advance advances the internal counters and indices to the next value.
   251  // It also empties and refill the buffer with the next set of values when the internal cursor
   252  // reaches the end of it.
   253  func (d *Readdir) Advance() syscall.Errno {
   254  	if d.cursor == uint64(len(d.dirents)) {
   255  		return syscall.ENOENT
   256  	}
   257  	d.cursor++
   258  	d.countRead++
   259  	return 0
   260  }
   261  
   262  type FSContext struct {
   263  	// rootFS is the root ("/") mount.
   264  	rootFS fsapi.FS
   265  
   266  	// openedFiles is a map of file descriptor numbers (>=FdPreopen) to open files
   267  	// (or directories) and defaults to empty.
   268  	// TODO: This is unguarded, so not goroutine-safe!
   269  	openedFiles FileTable
   270  
   271  	// readdirs is a map of numeric identifiers to Readdir structs
   272  	// and defaults to empty.
   273  	// TODO: This is unguarded, so not goroutine-safe!
   274  	readdirs ReaddirTable
   275  }
   276  
   277  // FileTable is a specialization of the descriptor.Table type used to map file
   278  // descriptors to file entries.
   279  type FileTable = descriptor.Table[int32, *FileEntry]
   280  
   281  // ReaddirTable is a specialization of the descriptor.Table type used to map file
   282  // descriptors to Readdir structs.
   283  type ReaddirTable = descriptor.Table[int32, *Readdir]
   284  
   285  // RootFS returns the underlying filesystem. Any files that should be added to
   286  // the table should be inserted via InsertFile.
   287  func (c *FSContext) RootFS() fsapi.FS {
   288  	return c.rootFS
   289  }
   290  
   291  // OpenFile opens the file into the table and returns its file descriptor.
   292  // The result must be closed by CloseFile or Close.
   293  func (c *FSContext) OpenFile(fs fsapi.FS, path string, flag int, perm fs.FileMode) (int32, syscall.Errno) {
   294  	if f, errno := fs.OpenFile(path, flag, perm); errno != 0 {
   295  		return 0, errno
   296  	} else {
   297  		fe := &FileEntry{FS: fs, File: f}
   298  		if path == "/" || path == "." {
   299  			fe.Name = ""
   300  		} else {
   301  			fe.Name = path
   302  		}
   303  		if newFD, ok := c.openedFiles.Insert(fe); !ok {
   304  			return 0, syscall.EBADF
   305  		} else {
   306  			return newFD, 0
   307  		}
   308  	}
   309  }
   310  
   311  // SockAccept accepts a socketapi.TCPConn into the file table and returns
   312  // its file descriptor.
   313  func (c *FSContext) SockAccept(sockFD int32, nonblock bool) (int32, syscall.Errno) {
   314  	var sock socketapi.TCPSock
   315  	if e, ok := c.LookupFile(sockFD); !ok || !e.IsPreopen {
   316  		return 0, syscall.EBADF // Not a preopen
   317  	} else if sock, ok = e.File.(socketapi.TCPSock); !ok {
   318  		return 0, syscall.EBADF // Not a sock
   319  	}
   320  
   321  	var conn socketapi.TCPConn
   322  	var errno syscall.Errno
   323  	if conn, errno = sock.Accept(); errno != 0 {
   324  		return 0, errno
   325  	} else if nonblock {
   326  		if errno = conn.SetNonblock(true); errno != 0 {
   327  			_ = conn.Close()
   328  			return 0, errno
   329  		}
   330  	}
   331  
   332  	fe := &FileEntry{File: conn}
   333  	if newFD, ok := c.openedFiles.Insert(fe); !ok {
   334  		return 0, syscall.EBADF
   335  	} else {
   336  		return newFD, 0
   337  	}
   338  }
   339  
   340  // LookupFile returns a file if it is in the table.
   341  func (c *FSContext) LookupFile(fd int32) (*FileEntry, bool) {
   342  	return c.openedFiles.Lookup(fd)
   343  }
   344  
   345  // LookupReaddir returns a Readdir struct or creates an empty one if it was not present.
   346  //
   347  // Note: this currently assumes that idx == fd, where fd is the file descriptor of the directory.
   348  // CloseFile will delete this idx from the internal store. In the future, idx may be independent
   349  // of a file fd, and the idx may have to be disposed with an explicit CloseReaddir.
   350  func (c *FSContext) LookupReaddir(idx int32, f *FileEntry) (*Readdir, syscall.Errno) {
   351  	if item, _ := c.readdirs.Lookup(idx); item != nil {
   352  		return item, 0
   353  	} else {
   354  		item, err := newReaddirFromFileEntry(f)
   355  		if err != 0 {
   356  			return nil, err
   357  		}
   358  		ok := c.readdirs.InsertAt(item, idx)
   359  		if !ok {
   360  			return nil, syscall.EINVAL
   361  		}
   362  		return item, 0
   363  	}
   364  }
   365  
   366  // CloseReaddir delete the Readdir struct at the given index
   367  //
   368  // Note: Currently only necessary in tests. In the future, the idx will have to be disposed explicitly,
   369  // unless we maintain a map fd -> []idx, and we let CloseFile close all the idx in []idx.
   370  func (c *FSContext) CloseReaddir(idx int32) {
   371  	c.readdirs.Delete(idx)
   372  }
   373  
   374  // Renumber assigns the file pointed by the descriptor `from` to `to`.
   375  func (c *FSContext) Renumber(from, to int32) syscall.Errno {
   376  	fromFile, ok := c.openedFiles.Lookup(from)
   377  	if !ok || to < 0 {
   378  		return syscall.EBADF
   379  	} else if fromFile.IsPreopen {
   380  		return syscall.ENOTSUP
   381  	}
   382  
   383  	// If toFile is already open, we close it to prevent windows lock issues.
   384  	//
   385  	// The doc is unclear and other implementations do nothing for already-opened To FDs.
   386  	// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno
   387  	// https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasi-common/src/snapshots/preview_1.rs#L531-L546
   388  	if toFile, ok := c.openedFiles.Lookup(to); ok {
   389  		if toFile.IsPreopen {
   390  			return syscall.ENOTSUP
   391  		}
   392  		_ = toFile.File.Close()
   393  	}
   394  
   395  	c.openedFiles.Delete(from)
   396  	if !c.openedFiles.InsertAt(fromFile, to) {
   397  		return syscall.EBADF
   398  	}
   399  	return 0
   400  }
   401  
   402  // CloseFile returns any error closing the existing file.
   403  func (c *FSContext) CloseFile(fd int32) syscall.Errno {
   404  	f, ok := c.openedFiles.Lookup(fd)
   405  	if !ok {
   406  		return syscall.EBADF
   407  	}
   408  	c.openedFiles.Delete(fd)
   409  	c.readdirs.Delete(fd)
   410  	return platform.UnwrapOSError(f.File.Close())
   411  }
   412  
   413  // Close implements io.Closer
   414  func (c *FSContext) Close() (err error) {
   415  	// Close any files opened in this context
   416  	c.openedFiles.Range(func(fd int32, entry *FileEntry) bool {
   417  		if errno := entry.File.Close(); errno != 0 {
   418  			err = errno // This means err returned == the last non-nil error.
   419  		}
   420  		return true
   421  	})
   422  	// A closed FSContext cannot be reused so clear the state instead of
   423  	// using Reset.
   424  	c.openedFiles = FileTable{}
   425  	c.readdirs = ReaddirTable{}
   426  	return
   427  }
   428  
   429  // NewFSContext creates a FSContext with stdio streams and an optional
   430  // pre-opened filesystem.
   431  //
   432  // If `preopened` is not UnimplementedFS, it is inserted into
   433  // the file descriptor table as FdPreopen.
   434  func (c *Context) NewFSContext(
   435  	stdin io.Reader,
   436  	stdout, stderr io.Writer,
   437  	rootFS fsapi.FS,
   438  	tcpListeners []*net.TCPListener,
   439  ) (err error) {
   440  	c.fsc.rootFS = rootFS
   441  	inFile, err := stdinFileEntry(stdin)
   442  	if err != nil {
   443  		return err
   444  	}
   445  	c.fsc.openedFiles.Insert(inFile)
   446  	outWriter, err := stdioWriterFileEntry("stdout", stdout)
   447  	if err != nil {
   448  		return err
   449  	}
   450  	c.fsc.openedFiles.Insert(outWriter)
   451  	errWriter, err := stdioWriterFileEntry("stderr", stderr)
   452  	if err != nil {
   453  		return err
   454  	}
   455  	c.fsc.openedFiles.Insert(errWriter)
   456  
   457  	if _, ok := rootFS.(fsapi.UnimplementedFS); ok {
   458  		// don't add to the pre-opens
   459  	} else if comp, ok := rootFS.(*sysfs.CompositeFS); ok {
   460  		preopens := comp.FS()
   461  		for i, p := range comp.GuestPaths() {
   462  			c.fsc.openedFiles.Insert(&FileEntry{
   463  				FS:        preopens[i],
   464  				Name:      p,
   465  				IsPreopen: true,
   466  				File:      &lazyDir{fs: rootFS},
   467  			})
   468  		}
   469  	} else {
   470  		c.fsc.openedFiles.Insert(&FileEntry{
   471  			FS:        rootFS,
   472  			Name:      "/",
   473  			IsPreopen: true,
   474  			File:      &lazyDir{fs: rootFS},
   475  		})
   476  	}
   477  
   478  	for _, tl := range tcpListeners {
   479  		c.fsc.openedFiles.Insert(&FileEntry{IsPreopen: true, File: sysfs.NewTCPListenerFile(tl)})
   480  	}
   481  	return nil
   482  }