github.com/ejcx/wazero@v1.1.0/fsconfig.go (about)

     1  package wazero
     2  
     3  import (
     4  	"io/fs"
     5  
     6  	"github.com/tetratelabs/wazero/internal/fsapi"
     7  	"github.com/tetratelabs/wazero/internal/sysfs"
     8  )
     9  
    10  // FSConfig configures filesystem paths the embedding host allows the wasm
    11  // guest to access. Unconfigured paths are not allowed, so functions like
    12  // `path_open` result in unsupported errors (e.g. syscall.ENOSYS).
    13  //
    14  // # Guest Path
    15  //
    16  // `guestPath` is the name of the path the guest should use a filesystem for, or
    17  // empty for any files.
    18  //
    19  // All `guestPath` paths are normalized, specifically removing any leading or
    20  // trailing slashes. This means "/", "./" or "." all coerce to empty "".
    21  //
    22  // Multiple `guestPath` values can be configured, but the last longest match
    23  // wins. For example, if "tmp", then "" were added, a request to open
    24  // "tmp/foo.txt" use the filesystem associated with "tmp" even though a wider
    25  // path, "" (all files), was added later.
    26  //
    27  // A `guestPath` of "." coerces to the empty string "" because the current
    28  // directory is handled by the guest. In other words, the guest resolves ites
    29  // current directory prior to requesting files.
    30  //
    31  // More notes on `guestPath`
    32  //   - Go compiled with runtime.GOOS=js do not pay attention to this value.
    33  //     Hence, you need to normalize the filesystem with NewRootFS to ensure
    34  //     paths requested resolve as expected.
    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  	WithFSMount(fs fs.FS, guestPath string) FSConfig
   122  }
   123  
   124  type fsConfig struct {
   125  	// fs are the currently configured filesystems.
   126  	fs []fsapi.FS
   127  	// guestPaths are the user-supplied names of the filesystems, retained for
   128  	// error messages and fmt.Stringer.
   129  	guestPaths []string
   130  	// guestPathToFS are the normalized paths to the currently configured
   131  	// filesystems, used for de-duplicating.
   132  	guestPathToFS map[string]int
   133  }
   134  
   135  // NewFSConfig returns a FSConfig that can be used for configuring module instantiation.
   136  func NewFSConfig() FSConfig {
   137  	return &fsConfig{guestPathToFS: map[string]int{}}
   138  }
   139  
   140  // clone makes a deep copy of this module config.
   141  func (c *fsConfig) clone() *fsConfig {
   142  	ret := *c // copy except slice and maps which share a ref
   143  	ret.fs = make([]fsapi.FS, 0, len(c.fs))
   144  	ret.fs = append(ret.fs, c.fs...)
   145  	ret.guestPaths = make([]string, 0, len(c.guestPaths))
   146  	ret.guestPaths = append(ret.guestPaths, c.guestPaths...)
   147  	ret.guestPathToFS = make(map[string]int, len(c.guestPathToFS))
   148  	for key, value := range c.guestPathToFS {
   149  		ret.guestPathToFS[key] = value
   150  	}
   151  	return &ret
   152  }
   153  
   154  // WithDirMount implements FSConfig.WithDirMount
   155  func (c *fsConfig) WithDirMount(dir, guestPath string) FSConfig {
   156  	return c.withMount(sysfs.NewDirFS(dir), guestPath)
   157  }
   158  
   159  // WithReadOnlyDirMount implements FSConfig.WithReadOnlyDirMount
   160  func (c *fsConfig) WithReadOnlyDirMount(dir, guestPath string) FSConfig {
   161  	return c.withMount(sysfs.NewReadFS(sysfs.NewDirFS(dir)), guestPath)
   162  }
   163  
   164  // WithFSMount implements FSConfig.WithFSMount
   165  func (c *fsConfig) WithFSMount(fs fs.FS, guestPath string) FSConfig {
   166  	return c.withMount(sysfs.Adapt(fs), guestPath)
   167  }
   168  
   169  func (c *fsConfig) withMount(fs fsapi.FS, guestPath string) FSConfig {
   170  	cleaned := sysfs.StripPrefixesAndTrailingSlash(guestPath)
   171  	ret := c.clone()
   172  	if i, ok := ret.guestPathToFS[cleaned]; ok {
   173  		ret.fs[i] = fs
   174  		ret.guestPaths[i] = guestPath
   175  	} else {
   176  		ret.guestPathToFS[cleaned] = len(ret.fs)
   177  		ret.fs = append(ret.fs, fs)
   178  		ret.guestPaths = append(ret.guestPaths, guestPath)
   179  	}
   180  	return ret
   181  }
   182  
   183  func (c *fsConfig) toFS() (fsapi.FS, error) {
   184  	return sysfs.NewRootFS(c.fs, c.guestPaths)
   185  }