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 }