github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/fsconfig.go (about)

     1  package wazero
     2  
     3  import (
     4  	"io/fs"
     5  
     6  	experimentalsys "github.com/wasilibs/wazerox/experimental/sys"
     7  	"github.com/wasilibs/wazerox/internal/sys"
     8  	"github.com/wasilibs/wazerox/internal/sysfs"
     9  )
    10  
    11  // FSConfig configures filesystem paths the embedding host allows the wasm
    12  // guest to access. Unconfigured paths are not allowed, so functions like
    13  // `path_open` result in unsupported errors (e.g. syscall.ENOSYS).
    14  //
    15  // # Guest Path
    16  //
    17  // `guestPath` is the name of the path the guest should use a filesystem for, or
    18  // empty for any files.
    19  //
    20  // All `guestPath` paths are normalized, specifically removing any leading or
    21  // trailing slashes. This means "/", "./" or "." all coerce to empty "".
    22  //
    23  // Multiple `guestPath` values can be configured, but the last longest match
    24  // wins. For example, if "tmp", then "" were added, a request to open
    25  // "tmp/foo.txt" use the filesystem associated with "tmp" even though a wider
    26  // path, "" (all files), was added later.
    27  //
    28  // A `guestPath` of "." coerces to the empty string "" because the current
    29  // directory is handled by the guest. In other words, the guest resolves ites
    30  // current directory prior to requesting files.
    31  //
    32  // More notes on `guestPath`
    33  //   - Go compiled with runtime.GOOS=js do not pay attention to this value.
    34  //     It only works with root mounts ("").
    35  //   - Working directories are typically tracked in wasm, though possible some
    36  //     relative paths are requested. For example, TinyGo may attempt to resolve
    37  //     a path "../.." in unit tests.
    38  //   - Zig uses the first path name it sees as the initial working directory of
    39  //     the process.
    40  //
    41  // # Scope
    42  //
    43  // Configuration here is module instance scoped. This means you can use the
    44  // same configuration for multiple calls to Runtime.InstantiateModule. Each
    45  // module will have a different file descriptor table. Any errors accessing
    46  // resources allowed here are deferred to instantiation time of each module.
    47  //
    48  // Any host resources present at the time of configuration, but deleted before
    49  // Runtime.InstantiateModule will trap/panic when the guest wasm initializes or
    50  // calls functions like `fd_read`.
    51  //
    52  // # Windows
    53  //
    54  // While wazero supports Windows as a platform, all known compilers use POSIX
    55  // conventions at runtime. For example, even when running on Windows, paths
    56  // used by wasm are separated by forward slash (/), not backslash (\).
    57  //
    58  // # Notes
    59  //
    60  //   - This is an interface for decoupling, not third-party implementations.
    61  //     All implementations are in wazero.
    62  //   - FSConfig is immutable. Each WithXXX function returns a new instance
    63  //     including the corresponding change.
    64  //   - RATIONALE.md includes design background and relationship to WebAssembly
    65  //     System Interfaces (WASI).
    66  type FSConfig interface {
    67  	// WithDirMount assigns a directory at `dir` to any paths beginning at
    68  	// `guestPath`.
    69  	//
    70  	// For example, `dirPath` as / (or c:\ in Windows), makes the entire host
    71  	// volume writeable to the path on the guest. The `guestPath` is always a
    72  	// POSIX style path, slash (/) delimited, even if run on Windows.
    73  	//
    74  	// If the same `guestPath` was assigned before, this overrides its value,
    75  	// retaining the original precedence. See the documentation of FSConfig for
    76  	// more details on `guestPath`.
    77  	//
    78  	// # Isolation
    79  	//
    80  	// The guest will have full access to this directory including escaping it
    81  	// via relative path lookups like "../../". Full access includes operations
    82  	// such as creating or deleting files, limited to any host level access
    83  	// controls.
    84  	//
    85  	// # os.DirFS
    86  	//
    87  	// This configuration optimizes for WASI compatability which is sometimes
    88  	// at odds with the behavior of os.DirFS. Hence, this will not behave
    89  	// exactly the same as os.DirFS. See /RATIONALE.md for more.
    90  	WithDirMount(dir, guestPath string) FSConfig
    91  
    92  	// WithReadOnlyDirMount assigns a directory at `dir` to any paths
    93  	// beginning at `guestPath`.
    94  	//
    95  	// This is the same as WithDirMount except only read operations are
    96  	// permitted. However, escaping the directory via relative path lookups
    97  	// like "../../" is still allowed.
    98  	WithReadOnlyDirMount(dir, guestPath string) FSConfig
    99  
   100  	// WithFSMount assigns a fs.FS file system for any paths beginning at
   101  	// `guestPath`.
   102  	//
   103  	// If the same `guestPath` was assigned before, this overrides its value,
   104  	// retaining the original precedence. See the documentation of FSConfig for
   105  	// more details on `guestPath`.
   106  	//
   107  	// # Isolation
   108  	//
   109  	// fs.FS does not restrict the ability to overwrite returned files via
   110  	// io.Writer. Moreover, os.DirFS documentation includes important notes
   111  	// about isolation, which also applies to fs.Sub. As of Go 1.19, the
   112  	// built-in file-systems are not jailed (chroot). See
   113  	// https://github.com/golang/go/issues/42322
   114  	//
   115  	// # os.DirFS
   116  	//
   117  	// Due to limited control and functionality available in os.DirFS, we
   118  	// advise using WithDirMount instead. There will be behavior differences
   119  	// between os.DirFS and WithDirMount, as the latter biases towards what's
   120  	// expected from WASI implementations.
   121  	//
   122  	// # Custom fs.FileInfo
   123  	//
   124  	// The underlying implementation supports data not usually in fs.FileInfo
   125  	// when `info.Sys` returns *sys.Stat_t. For example, a custom fs.FS can use
   126  	// this approach to generate or mask sys.Inode data. Such a filesystem
   127  	// needs to decorate any functions that can return fs.FileInfo:
   128  	//
   129  	//   - `Stat` as defined on `fs.File` (always)
   130  	//   - `Readdir` as defined on `os.File` (if defined)
   131  	//
   132  	// See sys.NewStat_t for examples.
   133  	WithFSMount(fs fs.FS, guestPath string) FSConfig
   134  }
   135  
   136  type fsConfig struct {
   137  	// fs are the currently configured filesystems.
   138  	fs []experimentalsys.FS
   139  	// guestPaths are the user-supplied names of the filesystems, retained for
   140  	// error messages and fmt.Stringer.
   141  	guestPaths []string
   142  	// guestPathToFS are the normalized paths to the currently configured
   143  	// filesystems, used for de-duplicating.
   144  	guestPathToFS map[string]int
   145  }
   146  
   147  // NewFSConfig returns a FSConfig that can be used for configuring module instantiation.
   148  func NewFSConfig() FSConfig {
   149  	return &fsConfig{guestPathToFS: map[string]int{}}
   150  }
   151  
   152  // clone makes a deep copy of this module config.
   153  func (c *fsConfig) clone() *fsConfig {
   154  	ret := *c // copy except slice and maps which share a ref
   155  	ret.fs = make([]experimentalsys.FS, 0, len(c.fs))
   156  	ret.fs = append(ret.fs, c.fs...)
   157  	ret.guestPaths = make([]string, 0, len(c.guestPaths))
   158  	ret.guestPaths = append(ret.guestPaths, c.guestPaths...)
   159  	ret.guestPathToFS = make(map[string]int, len(c.guestPathToFS))
   160  	for key, value := range c.guestPathToFS {
   161  		ret.guestPathToFS[key] = value
   162  	}
   163  	return &ret
   164  }
   165  
   166  // WithDirMount implements FSConfig.WithDirMount
   167  func (c *fsConfig) WithDirMount(dir, guestPath string) FSConfig {
   168  	return c.WithSysFSMount(sysfs.DirFS(dir), guestPath)
   169  }
   170  
   171  // WithReadOnlyDirMount implements FSConfig.WithReadOnlyDirMount
   172  func (c *fsConfig) WithReadOnlyDirMount(dir, guestPath string) FSConfig {
   173  	return c.WithSysFSMount(&sysfs.ReadFS{FS: sysfs.DirFS(dir)}, guestPath)
   174  }
   175  
   176  // WithFSMount implements FSConfig.WithFSMount
   177  func (c *fsConfig) WithFSMount(fs fs.FS, guestPath string) FSConfig {
   178  	var adapted experimentalsys.FS
   179  	if fs != nil {
   180  		adapted = &sysfs.AdaptFS{FS: fs}
   181  	}
   182  	return c.WithSysFSMount(adapted, guestPath)
   183  }
   184  
   185  // WithSysFSMount implements sysfs.FSConfig
   186  func (c *fsConfig) WithSysFSMount(fs experimentalsys.FS, guestPath string) FSConfig {
   187  	if _, ok := fs.(experimentalsys.UnimplementedFS); ok {
   188  		return c // don't add fake paths.
   189  	}
   190  	cleaned := sys.StripPrefixesAndTrailingSlash(guestPath)
   191  	ret := c.clone()
   192  	if i, ok := ret.guestPathToFS[cleaned]; ok {
   193  		ret.fs[i] = fs
   194  		ret.guestPaths[i] = guestPath
   195  	} else if fs != nil {
   196  		ret.guestPathToFS[cleaned] = len(ret.fs)
   197  		ret.fs = append(ret.fs, fs)
   198  		ret.guestPaths = append(ret.guestPaths, guestPath)
   199  	}
   200  	return ret
   201  }
   202  
   203  // preopens returns the possible nil index-correlated preopened filesystems
   204  // with guest paths.
   205  func (c *fsConfig) preopens() ([]experimentalsys.FS, []string) {
   206  	preopenCount := len(c.fs)
   207  	if preopenCount == 0 {
   208  		return nil, nil
   209  	}
   210  	fs := make([]experimentalsys.FS, len(c.fs))
   211  	copy(fs, c.fs)
   212  	guestPaths := make([]string, len(c.guestPaths))
   213  	copy(guestPaths, c.guestPaths)
   214  	return fs, guestPaths
   215  }