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 }