1package wazero
2
3import (
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).
64type 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
134type 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.
146func NewFSConfig() FSConfig {
147 return &fsConfig{guestPathToFS: map[string]int{}}
148}
149
150// clone makes a deep copy of this module config.
151func (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
165func (c *fsConfig) WithDirMount(dir, guestPath string) FSConfig {
166 return c.WithSysFSMount(sysfs.DirFS(dir), guestPath)
167}
168
169// WithReadOnlyDirMount implements FSConfig.WithReadOnlyDirMount
170func (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
175func (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
184func (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.
203func (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}