github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/fsconfig.go (about) 1 package wazero 2 3 import ( 4 "io/fs" 5 6 experimentalsys "github.com/bananabytelabs/wazero/experimental/sys" 7 "github.com/bananabytelabs/wazero/internal/sys" 8 "github.com/bananabytelabs/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 // - 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 }