github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/fsconfig.go (about)

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