From 59617a24c8b1974c1dad188ff17b4ad2176de0ac Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Wed, 23 Mar 2022 12:58:55 +0800 Subject: [PATCH] wasi: renames WASIConfig to SysConfig and makes stdio defaults safer (#396) This introduces `SysConfig` to replace `WASIConfig` and formalize documentation around system calls. The only incompatible change planned after this is to switch from wasi.FS to fs.FS Implementation Notes: Defaulting to os.Stdin os.Stdout and os.Stderr doesn't make sense for the same reasons as why we don't propagate ENV or ARGV: it violates sand-boxing. Moreover, these are worse as they prevent concurrency and can also lead to console overload if accidentally not overridden. This also changes default stdin to read EOF as that is safer than reading from os.DevNull, which can run the host out of file descriptors. Finally, this removes "WithPreopens" for "WithFS" and "WithWorkDirFS", to focus on the intended result. Similar Docker, if the WorkDir isn't set, it defaults to the same as root. Signed-off-by: Adrian Cole --- RATIONALE.md | 58 ++- builder.go | 2 + config.go | 208 +++++++++- config_test.go | 279 ++++++++++++++ examples/file_system_test.go | 8 +- examples/host_func_test.go | 2 +- examples/stdio_test.go | 2 +- internal/cstring/cstring.go | 45 --- internal/cstring/cstring_test.go | 88 ----- internal/wasi/wasi.go | 157 +++----- internal/wasi/wasi_test.go | 357 ++++++++++-------- internal/wasm/global_test.go | 2 +- internal/wasm/interpreter/interpreter_test.go | 4 +- internal/wasm/jit/engine_test.go | 8 +- internal/wasm/module_context.go | 22 +- internal/wasm/store.go | 16 +- internal/wasm/store_test.go | 50 ++- internal/wasm/sys.go | 274 ++++++++++---- internal/wasm/sys_test.go | 190 ++++++---- tests/bench/bench_test.go | 2 + tests/bench/wasi_test.go | 26 +- tests/engine/adhoc_test.go | 5 + tests/spectest/spec_test.go | 6 +- wasi.go | 118 +----- wasi_test.go | 6 +- wasm.go | 4 +- 26 files changed, 1218 insertions(+), 721 deletions(-) delete mode 100644 internal/cstring/cstring.go delete mode 100644 internal/cstring/cstring_test.go diff --git a/RATIONALE.md b/RATIONALE.md index 07a167fe33..3e1e679a74 100644 --- a/RATIONALE.md +++ b/RATIONALE.md @@ -32,7 +32,7 @@ wazero shares constants and interfaces with internal code by a sharing pattern d * shared interfaces and constants go in a package under root. * Ex. package `wasi` -> `/wasi/*.go` * user code that refer to that package go into the flat root package `wazero`. - * Ex. `WASIConfig` -> `/wasi.go` + * Ex. `StartWASICommand` -> `/wasi.go` * implementation code begin in a corresponding package under `/internal`. * Ex package `internalwasi` -> `/internal/wasi/*.go` @@ -87,6 +87,62 @@ runtime vs interpreting Wasm directly (the `naivevm` interpreter). Note: `microwasm` was never specified formally, and only exists in a historical codebase of wasmtime: https://github.com/bytecodealliance/wasmtime/blob/v0.29.0/crates/lightbeam/src/microwasm.rs +## Why is `SysConfig` decoupled from WASI? + +WebAssembly System Interfaces (WASI) is a formalization of a practice that can be done anyway: Define a host function to +access a system interface, such as writing to STDOUT. WASI stalled at snapshot-01 and as of early 2022, is being +rewritten entirely. + +This instability implies a need to transition between WASI specs, which places wazero in a position that requires +decoupling. For example, if code uses two different functions to call `fd_write`, the underlying configuration must be +centralized and decoupled. Otherwise, calls using the same file descriptor number will end up writing to different +places. + +In short, wazero defined system configuration in `SysConfig`, not a WASI type. This allows end-users to switch from +one spec to another with minimal impact. This has other helpful benefits, as centralized resources are simpler to close +coherently (ex via `Module.Close`). + +### Background on `SysConfig` design +WebAssembly 1.0 (20191205) specifies some aspects to control isolation between modules ([sandboxing](https://en.wikipedia.org/wiki/Sandbox_(computer_security))). +For example, `wasm.Memory` has size constraints and each instance of it is isolated from each other. While `wasm.Memory` +can be shared, by exporting it, it is not exported by default. In fact a WebAssembly Module (Wasm) has no memory by +default. + +While memory is defined in WebAssembly 1.0 (20191205), many aspects are not. Let's use an example of `exec.Cmd` as for +example, a WebAssembly System Interfaces (WASI) command is implemented as a module with a `_start` function, and in many +ways acts similar to a process with a `main` function. + +To capture "hello world" written to the console (stdout a.k.a. file descriptor 1) in `exec.Cmd`, you would set the +`Stdout` field accordingly, perhaps to a buffer. In WebAssembly 1.0 (20191205), the only way to perform something like +this is via a host function (ex `ModuleBuilder.ExportFunction`) and internally copy memory corresponding to that string +to a buffer. + +WASI implements system interfaces with host functions. Concretely, to write to console, a WASI command `Module` imports +"fd_write" from "wasi_snapshot_preview1" and calls it with the `fd` parameter set to 1 (STDOUT). + +The [snapshot-01](https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md) version of WASI has no +means to declare configuration, although its function definitions imply configuration for example if fd 1 should exist, +and if so where should it write. Moreover, snapshot-01 was last updated in late 2020 and the specification is being +completely rewritten as of early 2022. This means WASI as defined by "snapshot-01" will not clarify aspects like which +file descriptors are required. While it is possible a subsequent version may, it is too early to tell as no version of +WASI has reached a stage near W3C recommendation. Even if it did, module authors are not required to only use WASI to +write to console, as they can define their own host functions, such as they did before WASI existed. + +wazero aims to serve Go developers as a primary function, and help them transition between WASI specifications. In +order to do this, we have to allow top-level configuration. To ensure isolation by default, `SysConfig` has WithXXX +that override defaults to no-op or empty. One `SysConfig` instance is used regardless of how many times the same WASI +functions are imported. The nil defaults allow safe concurrency in these situations, as well lower the cost when they +are never used. Finally, a one-to-one mapping with `Module` allows the module to close the `SysConfig` instead of +confusing users with another API to close. + +Naming, defaults and validation rules of aspects like `STDIN` and `Environ` are intentionally similar to other Go +libraries such as `exec.Cmd` or `syscall.SetEnv`, and differences called out where helpful. For example, there's no goal +to emulate any operating system primitive specific to Windows (such as a 'c:\' drive). Moreover, certain defaults +working with real system calls are neither relevant nor safe to inherit: For example, `exec.Cmd` defaults to read STDIN +from a real file descriptor ("/dev/null"). Defaulting to this, vs reading `io.EOF`, would be unsafe as it can exhaust +file descriptors if resources aren't managed properly. In other words, blind copying of defaults isn't wise as it can +violate isolation or endanger the embedding process. In summary, we try to be similar to normal Go code, but often need +act differently and document `SysConfig` is more about emulating, not necessarily performing real system calls. ## Implementation limitations diff --git a/builder.go b/builder.go index 726b049efa..0cc42a1bf3 100644 --- a/builder.go +++ b/builder.go @@ -71,6 +71,8 @@ type ModuleBuilder interface { Build() (*Module, error) // Instantiate is a convenience that calls Build, then Runtime.InstantiateModule + // + // Note: Fields in the builder are copied during instantiation: Later changes do not affect the instantiated result. Instantiate() (wasm.Module, error) } diff --git a/config.go b/config.go index 8bca03c2a1..f2dacfb9d9 100644 --- a/config.go +++ b/config.go @@ -2,10 +2,15 @@ package wazero import ( "context" + "errors" + "fmt" + "io" + "math" internalwasm "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm/interpreter" "github.com/tetratelabs/wazero/internal/wasm/jit" + "github.com/tetratelabs/wazero/wasi" ) // NewRuntimeConfigJIT compiles WebAssembly modules into runtime.GOARCH-specific assembly for optimal performance. @@ -77,7 +82,204 @@ type Module struct { module *internalwasm.Module } -// WithName returns a new instance which overrides the name. -func (m *Module) WithName(moduleName string) *Module { - return &Module{name: moduleName, module: m.module} +// WithName configures the module name. Defaults to what was decoded from the module source. +// +// If the source was in WebAssembly 1.0 (20191205) Binary Format, this defaults to what was decoded from the custom name +// section. Otherwise, if it was decoded from Text Format, this defaults to the module ID stripped of leading '$'. +// +// For example, if the Module was decoded from the text format `(module $math)`, the default name is "math". +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0 +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0 +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#modules%E2%91%A0%E2%91%A2 +func (m *Module) WithName(name string) *Module { + m.name = name + return m +} + +// SysConfig configures resources needed by functions that have low-level interactions with the host operating system. +// Using this, resources such as STDIN can be isolated (ex via StartWASICommandWithConfig), so that the same module can +// be safely instantiated multiple times. +// +// Note: While wazero supports Windows as a platform, host functions using SysConfig follow a UNIX dialect. +// See RATIONALE.md for design background and relationship to WebAssembly System Interfaces (WASI). +type SysConfig struct { + stdin io.Reader + stdout io.Writer + stderr io.Writer + args []string + // environ is pair-indexed to retain order similar to os.Environ. + environ []string + // environKeys allow overwriting of existing values. + environKeys map[string]int + + // preopenFD has the next FD number to use + preopenFD uint32 + // preopens are keyed on file descriptor and only include the Path and FS fields. + preopens map[uint32]*internalwasm.FileEntry + // preopenPaths allow overwriting of existing paths. + preopenPaths map[string]uint32 +} + +func NewSysConfig() *SysConfig { + return &SysConfig{ + environKeys: map[string]int{}, + preopenFD: uint32(3), // after stdin/stdout/stderr + preopens: map[uint32]*internalwasm.FileEntry{}, + preopenPaths: map[string]uint32{}, + } +} + +// WithStdin configures where standard input (file descriptor 0) is read. Defaults to return io.EOF. +// +// This reader is most commonly used by the functions like "fd_read" in wasi.ModuleSnapshotPreview1 although it could be +// used by functions imported from other modules. +// +// Note: The caller is responsible to close any io.Reader they supply: It is not closed on wasm.Module Close. +// Note: This does not default to os.Stdin as that both violates sandboxing and prevents concurrent modules. +// See https://linux.die.net/man/3/stdin +func (c *SysConfig) WithStdin(stdin io.Reader) *SysConfig { + c.stdin = stdin + return c +} + +// WithStdout configures where standard output (file descriptor 1) is written. Defaults to io.Discard. +// +// This writer is most commonly used by the functions like "fd_write" in wasi.ModuleSnapshotPreview1 although it could +// be used by functions imported from other modules. +// +// Note: The caller is responsible to close any io.Writer they supply: It is not closed on wasm.Module Close. +// Note: This does not default to os.Stdout as that both violates sandboxing and prevents concurrent modules. +// See https://linux.die.net/man/3/stdout +func (c *SysConfig) WithStdout(stdout io.Writer) *SysConfig { + c.stdout = stdout + return c +} + +// WithStderr configures where standard error (file descriptor 2) is written. Defaults to io.Discard. +// +// This writer is most commonly used by the functions like "fd_write" in wasi.ModuleSnapshotPreview1 although it could +// be used by functions imported from other modules. +// +// Note: The caller is responsible to close any io.Writer they supply: It is not closed on wasm.Module Close. +// Note: This does not default to os.Stderr as that both violates sandboxing and prevents concurrent modules. +// See https://linux.die.net/man/3/stderr +func (c *SysConfig) WithStderr(stderr io.Writer) *SysConfig { + c.stderr = stderr + return c +} + +// WithArgs assigns command-line arguments visible to an imported function that reads an arg vector (argv). Defaults to +// none. +// +// These values are commonly read by the functions like "args_get" in wasi.ModuleSnapshotPreview1 although they could be +// read by functions imported from other modules. +// +// Similar to os.Args and exec.Cmd Env, many implementations would expect a program name to be argv[0]. However, neither +// WebAssembly nor WebAssembly System Interfaces (WASI) define this. Regardless, you may choose to set the first +// argument to the same value set via WithName. +// +// Note: This does not default to os.Args as that violates sandboxing. +// Note: Runtime.InstantiateModule errs if any value is empty. +// See https://linux.die.net/man/3/argv +// See https://en.wikipedia.org/wiki/Null-terminated_string +func (c *SysConfig) WithArgs(args ...string) *SysConfig { + c.args = args + return c +} + +// WithEnv sets an environment variable visible to a Module that imports functions. Defaults to none. +// +// Validation is the same as os.Setenv on Linux and replaces any existing value. Unlike exec.Cmd Env, this does not +// default to the current process environment as that would violate sandboxing. This also does not preserve order. +// +// Environment variables are commonly read by the functions like "environ_get" in wasi.ModuleSnapshotPreview1 although +// they could be read by functions imported from other modules. +// +// While similar to process configuration, there are no assumptions that can be made about anything OS-specific. For +// example, neither WebAssembly nor WebAssembly System Interfaces (WASI) define concerns processes have, such as +// case-sensitivity on environment keys. For portability, define entries with case-insensitively unique keys. +// +// Note: Runtime.InstantiateModule errs if the key is empty or contains a NULL(0) or equals("") character. +// See https://linux.die.net/man/3/environ +// See https://en.wikipedia.org/wiki/Null-terminated_string +func (c *SysConfig) WithEnv(key, value string) *SysConfig { + // Check to see if this key already exists and update it. + if i, ok := c.environKeys[key]; ok { + c.environ[i+1] = value // environ is pair-indexed, so the value is 1 after the key. + } else { + c.environKeys[key] = len(c.environ) + c.environ = append(c.environ, key, value) + } + return c +} + +// WithFS assigns the file system to use for any paths beginning at "/". Defaults to not found. +// +// Note: This sets WithWorkDirFS to the same file-system unless already set. +func (c *SysConfig) WithFS(fs wasi.FS) *SysConfig { + c.setFS("/", fs) + return c +} + +// WithWorkDirFS indicates the file system to use for any paths beginning at ".". Defaults to the same as WithFS. +func (c *SysConfig) WithWorkDirFS(fs wasi.FS) *SysConfig { + c.setFS(".", fs) + return c +} + +// withFS is hidden especially until #394 as existing use cases should be possible by composing file systems. +// TODO: in #394 add examples on WithFS to accomplish this. +func (c *SysConfig) setFS(path string, fs wasi.FS) { + // Check to see if this key already exists and update it. + entry := &internalwasm.FileEntry{Path: path, FS: fs} + if fd, ok := c.preopenPaths[path]; ok { + c.preopens[fd] = entry + } else { + c.preopens[c.preopenFD] = entry + c.preopenPaths[path] = c.preopenFD + c.preopenFD++ + } +} + +// toSysContext creates a baseline internalwasm.SysContext configured by SysConfig. +func (c *SysConfig) toSysContext() (sys *internalwasm.SysContext, err error) { + var environ []string // Intentionally doesn't pre-allocate to reduce logic to default to nil. + // Same validation as syscall.Setenv for Linux + for i := 0; i < len(c.environ); i += 2 { + key, value := c.environ[i], c.environ[i+1] + if len(key) == 0 { + err = errors.New("environ invalid: empty key") + return + } + for j := 0; j < len(key); j++ { + if key[j] == '=' { // NUL enforced in NewSysContext + err = errors.New("environ invalid: key contains '=' character") + return + } + } + environ = append(environ, key+"="+value) + } + + // Ensure no-one set a nil FD. We do this here instead of at the call site to allow chaining as nil is unexpected. + rootFD := uint32(0) // zero is invalid + setWorkDirFS := false + preopens := c.preopens + for fd, fs := range preopens { + if fs.FS == nil { + err = fmt.Errorf("FS for %s is nil", fs.Path) + return + } else if fs.Path == "/" { + rootFD = fd + } else if fs.Path == "." { + setWorkDirFS = true + } + } + + // Default the working directory to the root FS if it exists. + if rootFD != 0 && !setWorkDirFS { + preopens[c.preopenFD] = &internalwasm.FileEntry{Path: ".", FS: preopens[rootFD].FS} + } + + return internalwasm.NewSysContext(math.MaxUint32, c.args, environ, c.stdin, c.stdout, c.stderr, preopens) } diff --git a/config_test.go b/config_test.go index b3206363d7..12c6cbc4ca 100644 --- a/config_test.go +++ b/config_test.go @@ -1,6 +1,8 @@ package wazero import ( + "io" + "math" "testing" "github.com/stretchr/testify/require" @@ -54,3 +56,280 @@ func TestRuntimeConfig_Features(t *testing.T) { }) } } + +func TestSysConfig_toSysContext(t *testing.T) { + memFS := WASIMemFS() + memFS2 := WASIMemFS() + + tests := []struct { + name string + input *SysConfig + expected *internalwasm.SysContext + }{ + { + name: "empty", + input: NewSysConfig(), + expected: requireSysContext(t, + math.MaxUint32, // max + nil, // args + nil, // environ + nil, // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ), + }, + { + name: "WithArgs", + input: NewSysConfig().WithArgs("a", "bc"), + expected: requireSysContext(t, + math.MaxUint32, // max + []string{"a", "bc"}, // args + nil, // environ + nil, // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ), + }, + { + name: "WithArgs - empty ok", // Particularly argv[0] can be empty, and we have no rules about others. + input: NewSysConfig().WithArgs("", "bc"), + expected: requireSysContext(t, + math.MaxUint32, // max + []string{"", "bc"}, // args + nil, // environ + nil, // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ), + }, + { + name: "WithArgs - second call overwrites", + input: NewSysConfig().WithArgs("a", "bc").WithArgs("bc", "a"), + expected: requireSysContext(t, + math.MaxUint32, // max + []string{"bc", "a"}, // args + nil, // environ + nil, // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ), + }, + { + name: "WithEnv", + input: NewSysConfig().WithEnv("a", "b"), + expected: requireSysContext(t, + math.MaxUint32, // max + nil, // args + []string{"a=b"}, // environ + nil, // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ), + }, + { + name: "WithEnv - empty value", + input: NewSysConfig().WithEnv("a", ""), + expected: requireSysContext(t, + math.MaxUint32, // max + nil, // args + []string{"a="}, // environ + nil, // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ), + }, + { + name: "WithEnv twice", + input: NewSysConfig().WithEnv("a", "b").WithEnv("c", "de"), + expected: requireSysContext(t, + math.MaxUint32, // max + nil, // args + []string{"a=b", "c=de"}, // environ + nil, // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ), + }, + { + name: "WithEnv overwrites", + input: NewSysConfig().WithEnv("a", "bc").WithEnv("c", "de").WithEnv("a", "de"), + expected: requireSysContext(t, + math.MaxUint32, // max + nil, // args + []string{"a=de", "c=de"}, // environ + nil, // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ), + }, + + { + name: "WithEnv twice", + input: NewSysConfig().WithEnv("a", "b").WithEnv("c", "de"), + expected: requireSysContext(t, + math.MaxUint32, // max + nil, // args + []string{"a=b", "c=de"}, // environ + nil, // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ), + }, + { + name: "WithFS", + input: NewSysConfig().WithFS(memFS), + expected: requireSysContext(t, + math.MaxUint32, // max + nil, // args + nil, // environ + nil, // stdin + nil, // stdout + nil, // stderr + map[uint32]*internalwasm.FileEntry{ // openedFiles + 3: {Path: "/", FS: memFS}, + 4: {Path: ".", FS: memFS}, + }, + ), + }, + { + name: "WithFS - overwrites", + input: NewSysConfig().WithFS(memFS).WithFS(memFS2), + expected: requireSysContext(t, + math.MaxUint32, // max + nil, // args + nil, // environ + nil, // stdin + nil, // stdout + nil, // stderr + map[uint32]*internalwasm.FileEntry{ // openedFiles + 3: {Path: "/", FS: memFS2}, + 4: {Path: ".", FS: memFS2}, + }, + ), + }, + { + name: "WithWorkDirFS", + input: NewSysConfig().WithWorkDirFS(memFS), + expected: requireSysContext(t, + math.MaxUint32, // max + nil, // args + nil, // environ + nil, // stdin + nil, // stdout + nil, // stderr + map[uint32]*internalwasm.FileEntry{ // openedFiles + 3: {Path: ".", FS: memFS}, + }, + ), + }, + { + name: "WithFS and WithWorkDirFS", + input: NewSysConfig().WithFS(memFS).WithWorkDirFS(memFS2), + expected: requireSysContext(t, + math.MaxUint32, // max + nil, // args + nil, // environ + nil, // stdin + nil, // stdout + nil, // stderr + map[uint32]*internalwasm.FileEntry{ // openedFiles + 3: {Path: "/", FS: memFS}, + 4: {Path: ".", FS: memFS2}, + }, + ), + }, + { + name: "WithWorkDirFS and WithFS", + input: NewSysConfig().WithWorkDirFS(memFS).WithFS(memFS2), + expected: requireSysContext(t, + math.MaxUint32, // max + nil, // args + nil, // environ + nil, // stdin + nil, // stdout + nil, // stderr + map[uint32]*internalwasm.FileEntry{ // openedFiles + 3: {Path: ".", FS: memFS}, + 4: {Path: "/", FS: memFS2}, + }, + ), + }, + } + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + sys, err := tc.input.toSysContext() + require.NoError(t, err) + require.Equal(t, tc.expected, sys) + }) + } +} + +func TestSysConfig_toSysContext_Errors(t *testing.T) { + tests := []struct { + name string + input *SysConfig + expectedErr string + }{ + { + name: "WithArgs - arg contains NUL", + input: NewSysConfig().WithArgs("", string([]byte{'a', 0})), + expectedErr: "args invalid: contains NUL character", + }, + { + name: "WithEnv - key contains NUL", + input: NewSysConfig().WithEnv(string([]byte{'a', 0}), "a"), + expectedErr: "environ invalid: contains NUL character", + }, + { + name: "WithEnv - value contains NUL", + input: NewSysConfig().WithEnv("a", string([]byte{'a', 0})), + expectedErr: "environ invalid: contains NUL character", + }, + { + name: "WithEnv - key contains equals", + input: NewSysConfig().WithEnv("a=", "a"), + expectedErr: "environ invalid: key contains '=' character", + }, + { + name: "WithEnv - empty key", + input: NewSysConfig().WithEnv("", "a"), + expectedErr: "environ invalid: empty key", + }, + { + name: "WithFS - nil", + input: NewSysConfig().WithFS(nil), + expectedErr: "FS for / is nil", + }, + { + name: "WithWorkDirFS - nil", + input: NewSysConfig().WithWorkDirFS(nil), + expectedErr: "FS for . is nil", + }, + } + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + _, err := tc.input.toSysContext() + require.EqualError(t, err, tc.expectedErr) + }) + } +} + +// requireSysContext ensures internalwasm.NewSysContext doesn't return an error, which makes it usable in test matrices. +func requireSysContext(t *testing.T, max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, openedFiles map[uint32]*internalwasm.FileEntry) *internalwasm.SysContext { + sys, err := internalwasm.NewSysContext(max, args, environ, stdin, stdout, stderr, openedFiles) + require.NoError(t, err) + return sys +} diff --git a/examples/file_system_test.go b/examples/file_system_test.go index e1c223c984..bc30b842f8 100644 --- a/examples/file_system_test.go +++ b/examples/file_system_test.go @@ -29,18 +29,18 @@ func Test_Cat(t *testing.T) { // First, configure where the WebAssembly Module (Wasm) console outputs to (stdout). stdoutBuf := bytes.NewBuffer(nil) - wasiConfig := wazero.NewWASIConfig().WithStdout(stdoutBuf) + sysConfig := wazero.NewSysConfig().WithStdout(stdoutBuf) // Next, configure a sandboxed filesystem to include one file. file := "cat.go" // arbitrary file memFS := wazero.WASIMemFS() err := writeFile(memFS, file, catGo) require.NoError(t, err) - wasiConfig.WithPreopens(map[string]wasi.FS{".": memFS}) + sysConfig.WithWorkDirFS(memFS) // Since this runs a main function (_start in WASI), configure the arguments. // Remember, arg[0] is the program name! - wasiConfig.WithArgs("cat", file) + sysConfig.WithArgs("cat", file) // Compile the `cat` module. compiled, err := r.CompileModule(catWasm) @@ -52,7 +52,7 @@ func Test_Cat(t *testing.T) { defer wasi.Close() // StartWASICommand runs the "_start" function which is what TinyGo compiles "main" to. - cat, err := wazero.StartWASICommandWithConfig(r, compiled, wasiConfig) + cat, err := wazero.StartWASICommandWithConfig(r, compiled, sysConfig) require.NoError(t, err) defer cat.Close() diff --git a/examples/host_func_test.go b/examples/host_func_test.go index 60ccedb8c5..fc675785c6 100644 --- a/examples/host_func_test.go +++ b/examples/host_func_test.go @@ -65,7 +65,7 @@ func Test_hostFunc(t *testing.T) { // Configure stdout (console) to write to a buffer. stdout := bytes.NewBuffer(nil) - config := wazero.NewWASIConfig().WithStdout(stdout) + config := wazero.NewSysConfig().WithStdout(stdout) // Instantiate WASI, which implements system I/O such as console output. wasi, err := r.InstantiateModule(wazero.WASISnapshotPreview1()) diff --git a/examples/stdio_test.go b/examples/stdio_test.go index 09fd5a3bd2..33cc23720a 100644 --- a/examples/stdio_test.go +++ b/examples/stdio_test.go @@ -26,7 +26,7 @@ func Test_stdio(t *testing.T) { stdinBuf := bytes.NewBuffer([]byte("WASI\n")) stdoutBuf := bytes.NewBuffer(nil) stderrBuf := bytes.NewBuffer(nil) - config := wazero.NewWASIConfig().WithStdin(stdinBuf).WithStdout(stdoutBuf).WithStderr(stderrBuf) + config := wazero.NewSysConfig().WithStdin(stdinBuf).WithStdout(stdoutBuf).WithStderr(stderrBuf) // Instantiate WASI, which implements system I/O such as console output. wasi, err := r.InstantiateModule(wazero.WASISnapshotPreview1()) diff --git a/internal/cstring/cstring.go b/internal/cstring/cstring.go deleted file mode 100644 index 6792e94efe..0000000000 --- a/internal/cstring/cstring.go +++ /dev/null @@ -1,45 +0,0 @@ -// Package cstring is named cstring because null-terminated strings are also known as CString and that avoids using -// clashing package names like "strings" or a really long one like "null-terminated-strings" -package cstring - -import ( - "fmt" - "unicode/utf8" -) - -// NullTerminatedStrings holds null-terminated strings. It ensures that -// its length and total buffer size don't exceed the max of uint32. -// NullTerminatedStrings are convenience struct for args_get and environ_get. (environ_get is not implemented yet) -// -// A Null-terminated string is a byte string with a NULL suffix ("\x00"). -// See https://en.wikipedia.org/wiki/Null-terminated_string -type NullTerminatedStrings struct { - // NullTerminatedValues are null-terminated values with a NULL suffix. - NullTerminatedValues [][]byte - TotalBufSize uint32 -} - -var EmptyNullTerminatedStrings = &NullTerminatedStrings{NullTerminatedValues: [][]byte{}} - -// NewNullTerminatedStrings creates a NullTerminatedStrings from the given string slice. It returns an error -// if the length or the total buffer size of the result NullTerminatedStrings exceeds the maxBufSize -func NewNullTerminatedStrings(maxBufSize uint32, valName string, vals ...string) (*NullTerminatedStrings, error) { - if len(vals) == 0 { - return EmptyNullTerminatedStrings, nil - } - var strings [][]byte // don't pre-allocate as this function is size bound - totalBufSize := uint32(0) - for i, arg := range vals { - if !utf8.ValidString(arg) { - return nil, fmt.Errorf("%s[%d] is not a valid UTF-8 string", valName, i) - } - argLen := uint64(len(arg)) + 1 // + 1 for "\x00"; uint64 in case this one arg is huge - nextSize := uint64(totalBufSize) + argLen - if nextSize > uint64(maxBufSize) { - return nil, fmt.Errorf("%s[%d] will exceed max buffer size %d", valName, i, maxBufSize) - } - totalBufSize = uint32(nextSize) - strings = append(strings, append([]byte(arg), 0)) - } - return &NullTerminatedStrings{NullTerminatedValues: strings, TotalBufSize: totalBufSize}, nil -} diff --git a/internal/cstring/cstring_test.go b/internal/cstring/cstring_test.go deleted file mode 100644 index e53d6b3076..0000000000 --- a/internal/cstring/cstring_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package cstring - -import ( - _ "embed" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewNullTerminatedStrings(t *testing.T) { - emptyWASIStringArray := &NullTerminatedStrings{NullTerminatedValues: [][]byte{}} - tests := []struct { - name string - input []string - expected *NullTerminatedStrings - }{ - { - name: "nil", - expected: emptyWASIStringArray, - }, - { - name: "none", - input: []string{}, - expected: emptyWASIStringArray, - }, - { - name: "two", - input: []string{"a", "bc"}, - expected: &NullTerminatedStrings{ - NullTerminatedValues: [][]byte{ - {'a', 0}, - {'b', 'c', 0}, - }, - TotalBufSize: 5, - }, - }, - { - name: "two and empty string", - input: []string{"a", "", "bc"}, - expected: &NullTerminatedStrings{ - NullTerminatedValues: [][]byte{ - {'a', 0}, - {0}, - {'b', 'c', 0}, - }, - TotalBufSize: 6, - }, - }, - { - name: "utf-8", - // "šŸ˜Ø", "šŸ¤£", and "ļøšŸƒā€ā™€ļø" have 4, 4, and 13 bytes respectively - input: []string{"šŸ˜ØšŸ¤£šŸƒ\u200dā™€ļø", "foo", "bar"}, - expected: &NullTerminatedStrings{ - NullTerminatedValues: [][]byte{ - []byte("šŸ˜ØšŸ¤£šŸƒ\u200dā™€ļø\x00"), - {'f', 'o', 'o', 0}, - {'b', 'a', 'r', 0}, - }, - TotalBufSize: 30, - }, - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - s, err := NewNullTerminatedStrings(100, "", tc.input...) - require.NoError(t, err) - require.Equal(t, tc.expected, s) - }) - } -} - -func TestNewNullTerminatedStrings_Errors(t *testing.T) { - t.Run("invalid utf-8", func(t *testing.T) { - _, err := NewNullTerminatedStrings(100, "arg", "\xff\xfe\xfd", "foo", "bar") - require.EqualError(t, err, "arg[0] is not a valid UTF-8 string") - }) - t.Run("arg[0] too large", func(t *testing.T) { - _, err := NewNullTerminatedStrings(1, "arg", "a", "bc") - require.EqualError(t, err, "arg[0] will exceed max buffer size 1") - }) - t.Run("empty arg too large due to null terminator", func(t *testing.T) { - _, err := NewNullTerminatedStrings(2, "arg", "a", "", "bc") - require.EqualError(t, err, "arg[1] will exceed max buffer size 2") - }) -} diff --git a/internal/wasi/wasi.go b/internal/wasi/wasi.go index 77f1db1c4c..b21f00bec1 100644 --- a/internal/wasi/wasi.go +++ b/internal/wasi/wasi.go @@ -2,7 +2,6 @@ package internalwasi import ( crand "crypto/rand" - "encoding/binary" "errors" "fmt" "io" @@ -1019,30 +1018,19 @@ func SnapshotPreview1Functions() (a *wasiAPI, nameToGoFunc map[string]interface{ // ArgsGet implements SnapshotPreview1.ArgsGet func (a *wasiAPI) ArgsGet(ctx wasm.Module, argv, argvBuf uint32) wasi.Errno { - sys := systemContext(ctx) - - for _, arg := range sys.Args.NullTerminatedValues { - if !ctx.Memory().WriteUint32Le(argv, argvBuf) { - return wasi.ErrnoFault - } - argv += 4 // size of uint32 - if !ctx.Memory().Write(argvBuf, arg) { - return wasi.ErrnoFault - } - argvBuf += uint32(len(arg)) - } - - return wasi.ErrnoSuccess + sys := sysContext(ctx) + return writeOffsetsAndNullTerminatedValues(ctx.Memory(), sys.Args(), argv, argvBuf) } // ArgsSizesGet implements SnapshotPreview1.ArgsSizesGet func (a *wasiAPI) ArgsSizesGet(ctx wasm.Module, resultArgc, resultArgvBufSize uint32) wasi.Errno { - sys := systemContext(ctx) + sys := sysContext(ctx) + mem := ctx.Memory() - if !ctx.Memory().WriteUint32Le(resultArgc, uint32(len(sys.Args.NullTerminatedValues))) { + if !mem.WriteUint32Le(resultArgc, uint32(len(sys.Args()))) { return wasi.ErrnoFault } - if !ctx.Memory().WriteUint32Le(resultArgvBufSize, sys.Args.TotalBufSize) { + if !mem.WriteUint32Le(resultArgvBufSize, sys.ArgsSize()) { return wasi.ErrnoFault } return wasi.ErrnoSuccess @@ -1050,31 +1038,19 @@ func (a *wasiAPI) ArgsSizesGet(ctx wasm.Module, resultArgc, resultArgvBufSize ui // EnvironGet implements SnapshotPreview1.EnvironGet func (a *wasiAPI) EnvironGet(ctx wasm.Module, environ uint32, environBuf uint32) wasi.Errno { - sys := systemContext(ctx) - - // w.environ holds the environment variables in the form of "key=val\x00", so just copies it to the linear memory. - for _, env := range sys.Environ.NullTerminatedValues { - if !ctx.Memory().WriteUint32Le(environ, environBuf) { - return wasi.ErrnoFault - } - environ += 4 // size of uint32 - if !ctx.Memory().Write(environBuf, env) { - return wasi.ErrnoFault - } - environBuf += uint32(len(env)) - } - - return wasi.ErrnoSuccess + sys := sysContext(ctx) + return writeOffsetsAndNullTerminatedValues(ctx.Memory(), sys.Environ(), environ, environBuf) } // EnvironSizesGet implements SnapshotPreview1.EnvironSizesGet func (a *wasiAPI) EnvironSizesGet(ctx wasm.Module, resultEnvironc uint32, resultEnvironBufSize uint32) wasi.Errno { - sys := systemContext(ctx) + sys := sysContext(ctx) + mem := ctx.Memory() - if !ctx.Memory().WriteUint32Le(resultEnvironc, uint32(len(sys.Environ.NullTerminatedValues))) { + if !mem.WriteUint32Le(resultEnvironc, uint32(len(sys.Environ()))) { return wasi.ErrnoFault } - if !ctx.Memory().WriteUint32Le(resultEnvironBufSize, sys.Environ.TotalBufSize) { + if !mem.WriteUint32Le(resultEnvironBufSize, sys.EnvironSize()) { return wasi.ErrnoFault } @@ -1107,19 +1083,14 @@ func (a *wasiAPI) FdAllocate(ctx wasm.Module, fd uint32, offset, len uint64) was // FdClose implements SnapshotPreview1.FdClose func (a *wasiAPI) FdClose(ctx wasm.Module, fd uint32) wasi.Errno { - sys := systemContext(ctx) + sys := sysContext(ctx) - f, ok := sys.OpenedFiles[fd] - if !ok { + if ok, err := sys.CloseFile(fd); err != nil { + return wasi.ErrnoIo + } else if !ok { return wasi.ErrnoBadf } - if f.File != nil { - f.File.Close() - } - - delete(sys.OpenedFiles, fd) - return wasi.ErrnoSuccess } @@ -1131,9 +1102,9 @@ func (a *wasiAPI) FdDatasync(ctx wasm.Module, fd uint32) wasi.Errno { // FdFdstatGet implements SnapshotPreview1.FdFdstatGet // TODO: Currently FdFdstatget implements nothing except returning fake fs_right_inheriting func (a *wasiAPI) FdFdstatGet(ctx wasm.Module, fd uint32, resultStat uint32) wasi.Errno { - sys := systemContext(ctx) + sys := sysContext(ctx) - if _, ok := sys.OpenedFiles[fd]; !ok { + if _, ok := sys.OpenedFile(fd); !ok { return wasi.ErrnoBadf } if !ctx.Memory().WriteUint64Le(resultStat+16, rightFDRead|rightFDWrite) { @@ -1144,9 +1115,9 @@ func (a *wasiAPI) FdFdstatGet(ctx wasm.Module, fd uint32, resultStat uint32) was // FdPrestatGet implements SnapshotPreview1.FdPrestatGet func (a *wasiAPI) FdPrestatGet(ctx wasm.Module, fd uint32, resultPrestat uint32) wasi.Errno { - sys := systemContext(ctx) + sys := sysContext(ctx) - entry, ok := sys.OpenedFiles[fd] + entry, ok := sys.OpenedFile(fd) if !ok || entry.Path == "" { return wasi.ErrnoBadf } @@ -1195,9 +1166,9 @@ func (a *wasiAPI) FdPread(ctx wasm.Module, fd, iovs, iovsCount uint32, offset ui // FdPrestatDirName implements SnapshotPreview1.FdPrestatDirName func (a *wasiAPI) FdPrestatDirName(ctx wasm.Module, fd uint32, pathPtr uint32, pathLen uint32) wasi.Errno { - sys := systemContext(ctx) + sys := sysContext(ctx) - f, ok := sys.OpenedFiles[fd] + f, ok := sys.OpenedFile(fd) if !ok { return wasi.ErrnoBadf } @@ -1221,19 +1192,14 @@ func (a *wasiAPI) FdPwrite(ctx wasm.Module, fd, iovs, iovsCount uint32, offset u // FdRead implements SnapshotPreview1.FdRead func (a *wasiAPI) FdRead(ctx wasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno { - sys := systemContext(ctx) + sys := sysContext(ctx) var reader io.Reader - switch fd { - case 0: - if sys.Stdin != nil { - reader = sys.Stdin - } else { - return wasi.ErrnoBadf - } - default: - f, ok := sys.OpenedFiles[fd] + if fd == 0 { + reader = sys.Stdin() + } else { + f, ok := sys.OpenedFile(fd) if !ok || f.File == nil { return wasi.ErrnoBadf } @@ -1281,9 +1247,9 @@ func (a *wasiAPI) FdRenumber(ctx wasm.Module, fd, to uint32) wasi.Errno { // FdSeek implements SnapshotPreview1.FdSeek func (a *wasiAPI) FdSeek(ctx wasm.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) wasi.Errno { - sys := systemContext(ctx) + sys := sysContext(ctx) - f, ok := sys.OpenedFiles[fd] + f, ok := sys.OpenedFile(fd) if !ok || f.File == nil { return wasi.ErrnoBadf } @@ -1319,17 +1285,17 @@ func (a *wasiAPI) FdTell(ctx wasm.Module, fd, resultOffset uint32) wasi.Errno { // FdWrite implements SnapshotPreview1.FdWrite func (a *wasiAPI) FdWrite(ctx wasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno { - sys := systemContext(ctx) + sys := sysContext(ctx) var writer io.Writer switch fd { case 1: - writer = sys.Stdout + writer = sys.Stdout() case 2: - writer = sys.Stderr + writer = sys.Stderr() default: - f, ok := sys.OpenedFiles[fd] + f, ok := sys.OpenedFile(fd) if !ok || f.File == nil { return wasi.ErrnoBadf } @@ -1420,9 +1386,9 @@ func posixOpenFlags(oFlags uint32, fsRights uint64) (pFlags int) { // PathOpen implements SnapshotPreview1.PathOpen func (a *wasiAPI) PathOpen(ctx wasm.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase, fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno wasi.Errno) { - sys := systemContext(ctx) + sys := sysContext(ctx) - dir, ok := sys.OpenedFiles[fd] + dir, ok := sys.OpenedFile(fd) if !ok || dir.FS == nil { return wasi.ErrnoBadf } @@ -1447,14 +1413,9 @@ func (a *wasiAPI) PathOpen(ctx wasm.Module, fd, dirflags, pathPtr, pathLen, ofla // when ofagDir is set, the opened file must be a directory. // TODO if oflags&oflagDir != 0 return wasi.ErrnoNotdir if stat != dir - newFD, err := a.randUnusedFD(sys) - if err != nil { + if newFD, ok := sys.OpenFile(&internalwasm.FileEntry{Path: pathName, File: f, FS: dir.FS}); !ok { return wasi.ErrnoIo - } - - sys.OpenedFiles[newFD] = &internalwasm.FileEntry{Path: pathName, File: f, FS: dir.FS} - - if !ctx.Memory().WriteUint32Le(resultOpenedFd, newFD) { + } else if !ctx.Memory().WriteUint32Le(resultOpenedFd, newFD) { return wasi.ErrnoFault } return wasi.ErrnoSuccess @@ -1551,29 +1512,11 @@ func NewAPI() *wasiAPI { } } -func systemContext(ctx wasm.Module) *internalwasm.SystemContext { +func sysContext(ctx wasm.Module) *internalwasm.SysContext { if internal, ok := ctx.(*internalwasm.ModuleContext); !ok { panic(fmt.Errorf("unsupported wasm.Module implementation: %v", ctx)) } else { - return internal.System - } -} - -func (a *wasiAPI) randUnusedFD(sys *internalwasm.SystemContext) (uint32, error) { - // TODO: consider not using random source here, as there's collision avoidance below anyway, and using the random - // source is both expensive (including allocation and decoding) and can deplete it. - rand := make([]byte, 4) - err := a.randSource(rand) - if err != nil { - return 0, err - } - // fd is actually a signed int32, and must be a positive number. - fd := binary.LittleEndian.Uint32(rand) % (1 << 31) - for { // avoid collisions on an already used file-descriptor. Note: this is not goroutine safe. - if _, ok := sys.OpenedFiles[fd]; !ok { - return fd, nil - } - fd = (fd + 1) % (1 << 31) + return internal.Sys() } } @@ -1608,3 +1551,25 @@ func requireExport(module *internalwasm.Module, moduleName string, exportName st } return exp, nil } + +func writeOffsetsAndNullTerminatedValues(mem wasm.Memory, values []string, offsets, bytes uint32) wasi.Errno { + for _, value := range values { + // Write current offset and advance it. + if !mem.WriteUint32Le(offsets, bytes) { + return wasi.ErrnoFault + } + offsets += 4 // size of uint32 + + // Write the next value to memory with a NUL terminator + if !mem.Write(bytes, []byte(value)) { + return wasi.ErrnoFault + } + bytes += uint32(len(value)) + if !mem.WriteByte(bytes, 0) { + return wasi.ErrnoFault + } + bytes++ + } + + return wasi.ErrnoSuccess +} diff --git a/internal/wasi/wasi_test.go b/internal/wasi/wasi_test.go index 61b723241a..b8d021aff2 100644 --- a/internal/wasi/wasi_test.go +++ b/internal/wasi/wasi_test.go @@ -1,12 +1,13 @@ package internalwasi import ( + "bytes" "context" _ "embed" - "encoding/binary" "errors" "fmt" "io" + "math" "math/rand" "testing" @@ -23,10 +24,7 @@ const moduleName = "test" func TestSnapshotPreview1_ArgsGet(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) - - err = sys.WithArgs("a", "bc") + sys, err := newSysContext([]string{"a", "bc"}, nil, nil) require.NoError(t, err) argv := uint32(7) // arbitrary offset @@ -41,6 +39,7 @@ func TestSnapshotPreview1_ArgsGet(t *testing.T) { } a, mod, fn := instantiateModule(t, ctx, FunctionArgsGet, ImportArgsGet, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.ArgsGet", func(t *testing.T) { maskMemory(t, mod, len(expectedMemory)) @@ -69,12 +68,11 @@ func TestSnapshotPreview1_ArgsGet(t *testing.T) { func TestSnapshotPreview1_ArgsGet_Errors(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext([]string{"a", "bc"}, nil, nil) require.NoError(t, err) - err = sys.WithArgs("a", "bc") - require.NoError(t, err) a, mod, _ := instantiateModule(t, ctx, FunctionArgsGet, ImportArgsGet, moduleName, sys) + defer mod.Close() memorySize := mod.Memory().Size() validAddress := uint32(0) // arbitrary valid address as arguments to args_get. We chose 0 here. @@ -121,10 +119,7 @@ func TestSnapshotPreview1_ArgsGet_Errors(t *testing.T) { func TestSnapshotPreview1_ArgsSizesGet(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) - - err = sys.WithArgs("a", "bc") + sys, err := newSysContext([]string{"a", "bc"}, nil, nil) require.NoError(t, err) resultArgc := uint32(1) // arbitrary offset @@ -138,6 +133,7 @@ func TestSnapshotPreview1_ArgsSizesGet(t *testing.T) { } a, mod, fn := instantiateModule(t, ctx, FunctionArgsSizesGet, ImportArgsSizesGet, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.ArgsSizesGet", func(t *testing.T) { maskMemory(t, mod, len(expectedMemory)) @@ -166,13 +162,11 @@ func TestSnapshotPreview1_ArgsSizesGet(t *testing.T) { func TestSnapshotPreview1_ArgsSizesGet_Errors(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) - - err = sys.WithArgs("a", "bc") + sys, err := newSysContext([]string{"a", "bc"}, nil, nil) require.NoError(t, err) a, mod, _ := instantiateModule(t, ctx, FunctionArgsSizesGet, ImportArgsSizesGet, moduleName, sys) + defer mod.Close() memorySize := mod.Memory().Size() validAddress := uint32(0) // arbitrary valid address as arguments to args_sizes_get. We chose 0 here. @@ -217,10 +211,7 @@ func TestSnapshotPreview1_ArgsSizesGet_Errors(t *testing.T) { func TestSnapshotPreview1_EnvironGet(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) - - err = sys.WithEnviron("a=b", "b=cd") + sys, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil) require.NoError(t, err) resultEnviron := uint32(11) // arbitrary offset @@ -236,6 +227,7 @@ func TestSnapshotPreview1_EnvironGet(t *testing.T) { } a, mod, fn := instantiateModule(t, ctx, FunctionEnvironGet, ImportEnvironGet, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.EnvironGet", func(t *testing.T) { maskMemory(t, mod, len(expectedMemory)) @@ -264,13 +256,11 @@ func TestSnapshotPreview1_EnvironGet(t *testing.T) { func TestSnapshotPreview1_EnvironGet_Errors(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) - - err = sys.WithEnviron("a=bc", "b=cd") + sys, err := newSysContext(nil, []string{"a=bc", "b=cd"}, nil) require.NoError(t, err) a, mod, _ := instantiateModule(t, ctx, FunctionEnvironGet, ImportEnvironGet, moduleName, sys) + defer mod.Close() memorySize := mod.Memory().Size() validAddress := uint32(0) // arbitrary valid address as arguments to environ_get. We chose 0 here. @@ -317,10 +307,7 @@ func TestSnapshotPreview1_EnvironGet_Errors(t *testing.T) { func TestSnapshotPreview1_EnvironSizesGet(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) - - err = sys.WithEnviron("a=b", "b=cd") + sys, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil) require.NoError(t, err) resultEnvironc := uint32(1) // arbitrary offset @@ -334,6 +321,7 @@ func TestSnapshotPreview1_EnvironSizesGet(t *testing.T) { } a, mod, fn := instantiateModule(t, ctx, FunctionEnvironSizesGet, ImportEnvironSizesGet, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.EnvironSizesGet", func(t *testing.T) { maskMemory(t, mod, len(expectedMemory)) @@ -362,13 +350,11 @@ func TestSnapshotPreview1_EnvironSizesGet(t *testing.T) { func TestSnapshotPreview1_EnvironSizesGet_Errors(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) - - err = sys.WithEnviron("a=b", "b=cd") + sys, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil) require.NoError(t, err) a, mod, _ := instantiateModule(t, ctx, FunctionEnvironSizesGet, ImportEnvironSizesGet, moduleName, sys) + defer mod.Close() memorySize := mod.Memory().Size() validAddress := uint32(0) // arbitrary valid address as arguments to environ_sizes_get. We chose 0 here. @@ -414,10 +400,11 @@ func TestSnapshotPreview1_EnvironSizesGet_Errors(t *testing.T) { // TestSnapshotPreview1_ClockResGet only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_ClockResGet(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionClockResGet, ImportClockResGet, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.ClockResGet", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.ClockResGet(mod, 0, 0)) @@ -440,10 +427,12 @@ func TestSnapshotPreview1_ClockTimeGet(t *testing.T) { } ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionClockTimeGet, ImportClockTimeGet, moduleName, sys) + defer mod.Close() + a.timeNowUnixNano = func() uint64 { return epochNanos } t.Run("SnapshotPreview1.ClockTimeGet", func(t *testing.T) { @@ -475,10 +464,12 @@ func TestSnapshotPreview1_ClockTimeGet_Errors(t *testing.T) { epochNanos := uint64(1640995200000000000) // midnight UTC 2022-01-01 ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionClockTimeGet, ImportClockTimeGet, moduleName, sys) + defer mod.Close() + a.timeNowUnixNano = func() uint64 { return epochNanos } memorySize := mod.Memory().Size() @@ -513,10 +504,11 @@ func TestSnapshotPreview1_ClockTimeGet_Errors(t *testing.T) { // TestSnapshotPreview1_FdAdvise only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdAdvise(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdAdvise, ImportFdAdvise, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdAdvise", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdAdvise(mod, 0, 0, 0, 0)) @@ -532,10 +524,11 @@ func TestSnapshotPreview1_FdAdvise(t *testing.T) { // TestSnapshotPreview1_FdAllocate only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdAllocate(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdAllocate, ImportFdAllocate, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdAllocate", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdAllocate(mod, 0, 0, 0)) @@ -554,11 +547,8 @@ func TestSnapshotPreview1_FdClose(t *testing.T) { setupFD := func() (publicwasm.Module, publicwasm.Function, *wasiAPI) { ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) - memFs := &MemFS{} - sys.OpenedFiles = map[uint32]*wasm.FileEntry{ + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ fdToClose: { Path: "/tmp", FS: memFs, @@ -567,31 +557,45 @@ func TestSnapshotPreview1_FdClose(t *testing.T) { Path: "path to keep", FS: memFs, }, - } + }) + require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdClose, ImportFdClose, moduleName, sys) return mod, fn, a } + verify := func(mod publicwasm.Module) { + // Verify fdToClose is closed and removed from the opened FDs. + _, ok := sysContext(mod).OpenedFile(fdToClose) + require.False(t, ok) + + // Verify fdToKeep is not closed + _, ok = sysContext(mod).OpenedFile(fdToKeep) + require.True(t, ok) + } + t.Run("SnapshotPreview1.FdClose", func(t *testing.T) { mod, _, api := setupFD() + defer mod.Close() errno := api.FdClose(mod, fdToClose) require.Equal(t, wasi.ErrnoSuccess, errno) - require.NotContains(t, systemContext(mod).OpenedFiles, fdToClose) // Fd is closed and removed from the opened FDs. - require.Contains(t, systemContext(mod).OpenedFiles, fdToKeep) + + verify(mod) }) t.Run(FunctionFdClose, func(t *testing.T) { mod, fn, _ := setupFD() + defer mod.Close() ret, err := fn.Call(mod, uint64(fdToClose)) require.NoError(t, err) - require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64 - require.NotContains(t, systemContext(mod).OpenedFiles, fdToClose) // Fd is closed and removed from the opened FDs. - require.Contains(t, systemContext(mod).OpenedFiles, fdToKeep) + require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64 + + verify(mod) }) t.Run("ErrnoBadF for an invalid FD", func(t *testing.T) { mod, _, api := setupFD() + defer mod.Close() errno := api.FdClose(mod, 42) // 42 is an arbitrary invalid FD require.Equal(t, wasi.ErrnoBadf, errno) @@ -601,10 +605,11 @@ func TestSnapshotPreview1_FdClose(t *testing.T) { // TestSnapshotPreview1_FdDatasync only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdDatasync(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdDatasync, ImportFdDatasync, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdDatasync", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdDatasync(mod, 0)) @@ -622,10 +627,11 @@ func TestSnapshotPreview1_FdDatasync(t *testing.T) { // TestSnapshotPreview1_FdFdstatSetFlags only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdFdstatSetFlags(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdFdstatSetFlags, ImportFdFdstatSetFlags, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdFdstatSetFlags", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdFdstatSetFlags(mod, 0, 0)) @@ -641,10 +647,11 @@ func TestSnapshotPreview1_FdFdstatSetFlags(t *testing.T) { // TestSnapshotPreview1_FdFdstatSetRights only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdFdstatSetRights(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdFdstatSetRights, ImportFdFdstatSetRights, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdFdstatSetRights", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdFdstatSetRights(mod, 0, 0, 0)) @@ -660,10 +667,11 @@ func TestSnapshotPreview1_FdFdstatSetRights(t *testing.T) { // TestSnapshotPreview1_FdFilestatGet only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdFilestatGet(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdFilestatGet, ImportFdFilestatGet, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdFilestatGet", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdFilestatGet(mod, 0, 0)) @@ -679,10 +687,11 @@ func TestSnapshotPreview1_FdFilestatGet(t *testing.T) { // TestSnapshotPreview1_FdFilestatSetSize only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdFilestatSetSize(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdFilestatSetSize, ImportFdFilestatSetSize, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdFilestatSetSize", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdFilestatSetSize(mod, 0, 0)) @@ -698,10 +707,11 @@ func TestSnapshotPreview1_FdFilestatSetSize(t *testing.T) { // TestSnapshotPreview1_FdFilestatSetTimes only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdFilestatSetTimes(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdFilestatSetTimes, ImportFdFilestatSetTimes, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdFilestatSetTimes", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdFilestatSetTimes(mod, 0, 0, 0, 0)) @@ -717,10 +727,11 @@ func TestSnapshotPreview1_FdFilestatSetTimes(t *testing.T) { // TestSnapshotPreview1_FdPread only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdPread(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdPread, ImportFdPread, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdPread", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdPread(mod, 0, 0, 0, 0, 0)) @@ -737,12 +748,11 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) { fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}}) require.NoError(t, err) - sys.OpenedFiles[fd] = &wasm.FileEntry{Path: "/tmp"} - a, mod, fn := instantiateModule(t, ctx, FunctionFdPrestatGet, ImportFdPrestatGet, moduleName, sys) + defer mod.Close() resultPrestat := uint32(1) // arbitrary offset expectedMemory := []byte{ @@ -783,12 +793,11 @@ func TestSnapshotPreview1_FdPrestatGet_Errors(t *testing.T) { validAddress := uint32(0) // Arbitrary valid address as arguments to fd_prestat_get. We chose 0 here. ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}}) require.NoError(t, err) - sys.OpenedFiles = map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}} - a, mod, _ := instantiateModule(t, ctx, FunctionFdPrestatGet, ImportFdPrestatGet, moduleName, sys) + defer mod.Close() memorySize := mod.Memory().Size() @@ -827,12 +836,11 @@ func TestSnapshotPreview1_FdPrestatDirName(t *testing.T) { fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}}) require.NoError(t, err) - sys.OpenedFiles[fd] = &wasm.FileEntry{Path: "/tmp", FS: &MemFS{}} - a, mod, fn := instantiateModule(t, ctx, FunctionFdPrestatDirName, ImportFdPrestatDirName, moduleName, sys) + defer mod.Close() path := uint32(1) // arbitrary offset pathLen := uint32(3) // shorter than len("/tmp") to test the path is written for the length of pathLen @@ -870,16 +878,15 @@ func TestSnapshotPreview1_FdPrestatDirName_Errors(t *testing.T) { fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}}) require.NoError(t, err) - sys.OpenedFiles = map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}} - pathLen := uint32(len("/tmp")) - a, mod, _ := instantiateModule(t, ctx, FunctionFdPrestatDirName, ImportFdPrestatDirName, moduleName, sys) + defer mod.Close() memorySize := mod.Memory().Size() validAddress := uint32(0) // Arbitrary valid address as arguments to fd_prestat_dir_name. We chose 0 here. + pathLen := uint32(len("/tmp")) tests := []struct { name string @@ -932,10 +939,11 @@ func TestSnapshotPreview1_FdPrestatDirName_Errors(t *testing.T) { // TestSnapshotPreview1_FdPwrite only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdPwrite(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdPwrite, ImportFdPwrite, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdPwrite", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdPwrite(mod, 0, 0, 0, 0, 0)) @@ -950,8 +958,6 @@ func TestSnapshotPreview1_FdPwrite(t *testing.T) { func TestSnapshotPreview1_FdRead(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err iovs := uint32(1) // arbitrary offset @@ -975,18 +981,16 @@ func TestSnapshotPreview1_FdRead(t *testing.T) { '?', ) - a, mod, fn := instantiateModule(t, ctx, FunctionFdRead, ImportFdRead, moduleName, sys) - // TestSnapshotPreview1_FdRead uses a matrix because setting up test files is complicated and has to be clean each time. type fdReadFn func(ctx publicwasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno tests := []struct { name string - fdRead func() fdReadFn + fdRead func(*wasiAPI, *wasm.ModuleContext, publicwasm.Function) fdReadFn }{ - {"SnapshotPreview1.FdRead", func() fdReadFn { + {"SnapshotPreview1.FdRead", func(a *wasiAPI, _ *wasm.ModuleContext, _ publicwasm.Function) fdReadFn { return a.FdRead }}, - {FunctionFdRead, func() fdReadFn { + {FunctionFdRead, func(_ *wasiAPI, mod *wasm.ModuleContext, fn publicwasm.Function) fdReadFn { return func(ctx publicwasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno { ret, err := fn.Call(mod, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize)) require.NoError(t, err) @@ -1000,13 +1004,20 @@ func TestSnapshotPreview1_FdRead(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Create a fresh file to read the contents from file, memFS := createFile(t, "test_path", []byte("wazero")) - sys.OpenedFiles[fd] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file} + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + fd: {Path: "test_path", FS: memFS, File: file}, + }) + require.NoError(t, err) + + a, mod, fn := instantiateModule(t, ctx, FunctionFdRead, ImportFdRead, moduleName, sys) + defer mod.Close() + maskMemory(t, mod, len(expectedMemory)) ok := mod.Memory().Write(0, initialMemory) require.True(t, ok) - errno := tc.fdRead()(mod, fd, iovs, iovsCount, resultSize) + errno := tc.fdRead(a, mod, fn)(mod, fd, iovs, iovsCount, resultSize) require.Equal(t, wasi.ErrnoSuccess, errno) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) @@ -1021,12 +1032,13 @@ func TestSnapshotPreview1_FdRead_Errors(t *testing.T) { file, memFS := createFile(t, "test_path", []byte{}) // file with empty contents ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + validFD: {Path: "test_path", FS: memFS, File: file}, + }) require.NoError(t, err) - sys.OpenedFiles[validFD] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file} - a, mod, _ := instantiateModule(t, ctx, FunctionFdRead, ImportFdRead, moduleName, sys) + defer mod.Close() tests := []struct { name string @@ -1111,10 +1123,11 @@ func TestSnapshotPreview1_FdRead_Errors(t *testing.T) { // TestSnapshotPreview1_FdReaddir only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdReaddir(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdReaddir, ImportFdReaddir, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdReaddir", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdReaddir(mod, 0, 0, 0, 0, 0)) @@ -1130,10 +1143,11 @@ func TestSnapshotPreview1_FdReaddir(t *testing.T) { // TestSnapshotPreview1_FdRenumber only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdRenumber(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdRenumber, ImportFdRenumber, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdRenumber", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdRenumber(mod, 0, 0)) @@ -1152,12 +1166,13 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) { file, memFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + fd: {Path: "test_path", FS: memFS, File: file}, + }) require.NoError(t, err) - sys.OpenedFiles[fd] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file} - a, mod, fn := instantiateModule(t, ctx, FunctionFdSeek, ImportFdSeek, moduleName, sys) + defer mod.Close() // TestSnapshotPreview1_FdSeek uses a matrix because setting up test files is complicated and has to be clean each time. type fdSeekFn func(ctx publicwasm.Module, fd uint32, offset uint64, whence, resultNewOffset uint32) wasi.Errno @@ -1246,12 +1261,14 @@ func TestSnapshotPreview1_FdSeek_Errors(t *testing.T) { validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err file, memFS := createFile(t, "test_path", []byte("wazero")) // arbitrary valid file with non-empty contents ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + validFD: {Path: "test_path", FS: memFS, File: file}, + }) require.NoError(t, err) - sys.OpenedFiles[validFD] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file} - a, mod, _ := instantiateModule(t, ctx, FunctionFdSeek, ImportFdSeek, moduleName, sys) + defer mod.Close() + memorySize := mod.Memory().Size() tests := []struct { @@ -1293,10 +1310,11 @@ func TestSnapshotPreview1_FdSeek_Errors(t *testing.T) { // TestSnapshotPreview1_FdSync only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdSync(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdSync, ImportFdSync, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdSync", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdSync(mod, 0)) @@ -1312,10 +1330,11 @@ func TestSnapshotPreview1_FdSync(t *testing.T) { // TestSnapshotPreview1_FdTell only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_FdTell(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdTell, ImportFdTell, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.FdTell", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.FdTell(mod, 0, 0)) @@ -1330,8 +1349,6 @@ func TestSnapshotPreview1_FdTell(t *testing.T) { func TestSnapshotPreview1_FdWrite(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err iovs := uint32(1) // arbitrary offset @@ -1355,18 +1372,16 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) { '?', ) - a, mod, fn := instantiateModule(t, ctx, FunctionFdWrite, ImportFdWrite, moduleName, sys) - // TestSnapshotPreview1_FdWrite uses a matrix because setting up test files is complicated and has to be clean each time. type fdWriteFn func(ctx publicwasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno tests := []struct { name string - fdWrite func() fdWriteFn + fdWrite func(*wasiAPI, *wasm.ModuleContext, publicwasm.Function) fdWriteFn }{ - {"SnapshotPreview1.FdWrite", func() fdWriteFn { + {"SnapshotPreview1.FdWrite", func(a *wasiAPI, _ *wasm.ModuleContext, _ publicwasm.Function) fdWriteFn { return a.FdWrite }}, - {FunctionFdWrite, func() fdWriteFn { + {FunctionFdWrite, func(_ *wasiAPI, mod *wasm.ModuleContext, fn publicwasm.Function) fdWriteFn { return func(ctx publicwasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno { ret, err := fn.Call(mod, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize)) require.NoError(t, err) @@ -1380,13 +1395,19 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Create a fresh file to write the contents to file, memFS := createFile(t, "test_path", []byte{}) - sys.OpenedFiles[fd] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file} + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + fd: {Path: "test_path", FS: memFS, File: file}, + }) + require.NoError(t, err) + + a, mod, fn := instantiateModule(t, ctx, FunctionFdWrite, ImportFdWrite, moduleName, sys) + defer mod.Close() maskMemory(t, mod, len(expectedMemory)) ok := mod.Memory().Write(0, initialMemory) require.True(t, ok) - errno := tc.fdWrite()(mod, fd, iovs, iovsCount, resultSize) + errno := tc.fdWrite(a, mod, fn)(mod, fd, iovs, iovsCount, resultSize) require.Equal(t, wasi.ErrnoSuccess, errno) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) @@ -1401,12 +1422,13 @@ func TestSnapshotPreview1_FdWrite_Errors(t *testing.T) { validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err file, memFS := createFile(t, "test_path", []byte{}) // file with empty contents ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + validFD: {Path: "test_path", FS: memFS, File: file}, + }) require.NoError(t, err) - sys.OpenedFiles[validFD] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file} - a, mod, _ := instantiateModule(t, ctx, FunctionFdWrite, ImportFdWrite, moduleName, sys) + defer mod.Close() // Setup valid test memory iovs, iovsCount := uint32(0), uint32(1) @@ -1485,10 +1507,11 @@ func createFile(t *testing.T, path string, contents []byte) (*memFile, *MemFS) { // TestSnapshotPreview1_PathCreateDirectory only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_PathCreateDirectory(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionPathCreateDirectory, ImportPathCreateDirectory, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.PathCreateDirectory", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.PathCreateDirectory(mod, 0, 0, 0)) @@ -1504,10 +1527,11 @@ func TestSnapshotPreview1_PathCreateDirectory(t *testing.T) { // TestSnapshotPreview1_PathFilestatGet only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_PathFilestatGet(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionPathFilestatGet, ImportPathFilestatGet, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.PathFilestatGet", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.PathFilestatGet(mod, 0, 0, 0, 0, 0)) @@ -1523,10 +1547,11 @@ func TestSnapshotPreview1_PathFilestatGet(t *testing.T) { // TestSnapshotPreview1_PathFilestatSetTimes only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_PathFilestatSetTimes(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionPathFilestatSetTimes, ImportPathFilestatSetTimes, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.PathFilestatSetTimes", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.PathFilestatSetTimes(mod, 0, 0, 0, 0, 0, 0, 0)) @@ -1542,10 +1567,11 @@ func TestSnapshotPreview1_PathFilestatSetTimes(t *testing.T) { // TestSnapshotPreview1_PathLink only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_PathLink(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionPathLink, ImportPathLink, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.PathLink", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.PathLink(mod, 0, 0, 0, 0, 0, 0, 0)) @@ -1572,32 +1598,15 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) { '?', // `path` is after this 'w', 'a', 'z', 'e', 'r', 'o', // path } - expectedFD := uint32(5) // arbitrary expected FD + expectedFD := byte(workdirFD + 1) expectedMemory := append( initialMemory, - '?', // `resultOpenedFd` is after this - 5, 0, 0, 0, // = epxectedFD (5) + '?', // `resultOpenedFd` is after this + expectedFD, 0, 0, 0, '?', ) - // MemFS for testing - // Create a memFS for testing that has "./wazero" file. - memFS := &MemFS{ - Files: map[string][]byte{ - "wazero": {}, - }, - } - ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) - - a, mod, fn := instantiateModule(t, ctx, FunctionPathOpen, ImportPathOpen, moduleName, sys) - // randSource is used to determine the new fd. Fix it to the expectedFD for testing. - a.randSource = func(b []byte) error { - binary.LittleEndian.PutUint32(b, expectedFD) - return nil - } // TestSnapshotPreview1_PathOpen uses a matrix because setting up test files is complicated and has to be clean each time. type pathOpenFn func(ctx publicwasm.Module, fd, dirflags, path, pathLen, oflags uint32, @@ -1605,12 +1614,12 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) { fdFlags, resultOpenedFd uint32) wasi.Errno pathOpenFns := []struct { name string - pathOpen func() pathOpenFn + pathOpen func(*wasiAPI, *wasm.ModuleContext, publicwasm.Function) pathOpenFn }{ - {"SnapshotPreview1.PathOpen", func() pathOpenFn { + {"SnapshotPreview1.PathOpen", func(a *wasiAPI, _ *wasm.ModuleContext, _ publicwasm.Function) pathOpenFn { return a.PathOpen }}, - {FunctionPathOpen, func() pathOpenFn { + {FunctionPathOpen, func(_ *wasiAPI, mod *wasm.ModuleContext, fn publicwasm.Function) pathOpenFn { return func(ctx publicwasm.Module, fd, dirflags, path, pathLen, oflags uint32, fsRightsBase, fsRightsInheriting uint64, fdFlags, resultOpenedFd uint32) wasi.Errno { @@ -1639,23 +1648,31 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) { for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { - // Set up a fresh opened FD table - sys.OpenedFiles = map[uint32]*wasm.FileEntry{ + // Create a memFS for testing that has "./wazero" file. + memFS := &MemFS{Files: map[string][]byte{"wazero": {}}} + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ workdirFD: {Path: ".", FS: memFS}, - } + }) + require.NoError(t, err) + + a, mod, fn := instantiateModule(t, ctx, FunctionPathOpen, ImportPathOpen, moduleName, sys) + defer mod.Close() maskMemory(t, mod, len(expectedMemory)) ok := mod.Memory().Write(0, initialMemory) require.True(t, ok) - errno := pf.pathOpen()(mod, tc.fd, dirflags, path, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdFlags, resultOpenedFd) + errno := pf.pathOpen(a, mod, fn)(mod, tc.fd, dirflags, path, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdFlags, resultOpenedFd) require.Equal(t, wasi.ErrnoSuccess, errno) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) require.Equal(t, expectedMemory, actual) + // verify the file was actually opened - require.Equal(t, tc.expectedPath, sys.OpenedFiles[expectedFD].Path) + f, ok := sys.OpenedFile(uint32(expectedFD)) + require.True(t, ok) + require.Equal(t, tc.expectedPath, f.Path) }) } }) @@ -1671,18 +1688,17 @@ func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) { }, } ctx := context.Background() - sys, err := wasm.NewSystemContext() - require.NoError(t, err) - - sys.OpenedFiles = map[uint32]*wasm.FileEntry{ + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ validFD: {Path: ".", FS: memFS}, - } + }) + require.NoError(t, err) a, mod, _ := instantiateModule(t, ctx, FunctionPathOpen, ImportPathOpen, moduleName, sys) + defer mod.Close() validPath := uint32(0) // arbitrary offset validPathLen := uint32(6) // the length of "wazero" - mod.Memory().Write(uint32(validPath), []byte{ + mod.Memory().Write(validPath, []byte{ 'w', 'a', 'z', 'e', 'r', 'o', // write to offset 0 (= validPath) }) // wazero is the path to the file in the memFS @@ -1739,10 +1755,11 @@ func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) { // TestSnapshotPreview1_PathReadlink only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_PathReadlink(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionPathReadlink, ImportPathReadlink, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.PathLink", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.PathReadlink(mod, 0, 0, 0, 0, 0, 0)) @@ -1758,10 +1775,11 @@ func TestSnapshotPreview1_PathReadlink(t *testing.T) { // TestSnapshotPreview1_PathRemoveDirectory only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_PathRemoveDirectory(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionPathRemoveDirectory, ImportPathRemoveDirectory, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.PathRemoveDirectory", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.PathRemoveDirectory(mod, 0, 0, 0)) @@ -1777,10 +1795,11 @@ func TestSnapshotPreview1_PathRemoveDirectory(t *testing.T) { // TestSnapshotPreview1_PathRename only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_PathRename(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionPathRename, ImportPathRename, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.PathRename", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.PathRename(mod, 0, 0, 0, 0, 0, 0)) @@ -1796,10 +1815,11 @@ func TestSnapshotPreview1_PathRename(t *testing.T) { // TestSnapshotPreview1_PathSymlink only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_PathSymlink(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionPathSymlink, ImportPathSymlink, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.PathSymlink", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.PathSymlink(mod, 0, 0, 0, 0, 0)) @@ -1815,10 +1835,11 @@ func TestSnapshotPreview1_PathSymlink(t *testing.T) { // TestSnapshotPreview1_PathUnlinkFile only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_PathUnlinkFile(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionPathUnlinkFile, ImportPathUnlinkFile, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.PathUnlinkFile", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.PathUnlinkFile(mod, 0, 0, 0)) @@ -1834,10 +1855,11 @@ func TestSnapshotPreview1_PathUnlinkFile(t *testing.T) { // TestSnapshotPreview1_PollOneoff only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_PollOneoff(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionPollOneoff, ImportPollOneoff, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.PollOneoff", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.PollOneoff(mod, 0, 0, 0, 0)) @@ -1852,7 +1874,7 @@ func TestSnapshotPreview1_PollOneoff(t *testing.T) { func TestSnapshotPreview1_ProcExit(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) tests := []struct { @@ -1873,13 +1895,14 @@ func TestSnapshotPreview1_ProcExit(t *testing.T) { // Note: Unlike most tests, this uses fn, not the 'a' result parameter. This is because currently, this function // body panics, and we expect Call to unwrap the panic. _, mod, fn := instantiateModule(t, ctx, FunctionProcExit, ImportProcExit, moduleName, sys) + defer mod.Close() for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { // When ProcExit is called, store.CallFunction returns immediately, returning the exit code as the error. - _, err := fn.Call(mod, uint64(tc.exitCode)) + _, err = fn.Call(mod, uint64(tc.exitCode)) var code wasi.ExitCode require.ErrorAs(t, err, &code) require.Equal(t, code, wasi.ExitCode(tc.exitCode)) @@ -1890,10 +1913,11 @@ func TestSnapshotPreview1_ProcExit(t *testing.T) { // TestSnapshotPreview1_ProcRaise only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_ProcRaise(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionProcRaise, ImportProcRaise, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.ProcRaise", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.ProcRaise(mod, 0)) @@ -1909,10 +1933,11 @@ func TestSnapshotPreview1_ProcRaise(t *testing.T) { // TestSnapshotPreview1_SchedYield only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_SchedYield(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionSchedYield, ImportSchedYield, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.SchedYield", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.SchedYield(mod)) @@ -1936,10 +1961,12 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) { offset := uint32(1) // offset, seed := int64(42) // and seed value ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionRandomGet, ImportRandomGet, moduleName, sys) + defer mod.Close() + a.randSource = func(p []byte) error { s := rand.NewSource(seed) rng := rand.New(s) @@ -1975,12 +2002,14 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) { func TestSnapshotPreview1_RandomGet_Errors(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) validAddress := uint32(0) // arbitrary valid address a, mod, _ := instantiateModule(t, ctx, FunctionRandomGet, ImportRandomGet, moduleName, sys) + defer mod.Close() + memorySize := mod.Memory().Size() tests := []struct { @@ -2013,10 +2042,12 @@ func TestSnapshotPreview1_RandomGet_Errors(t *testing.T) { func TestSnapshotPreview1_RandomGet_SourceError(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, _ := instantiateModule(t, ctx, FunctionRandomGet, ImportRandomGet, moduleName, sys) + defer mod.Close() + a.randSource = func(p []byte) error { return errors.New("random source error") } @@ -2028,10 +2059,11 @@ func TestSnapshotPreview1_RandomGet_SourceError(t *testing.T) { // TestSnapshotPreview1_SockRecv only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_SockRecv(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionSockRecv, ImportSockRecv, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.SockRecv", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.SockRecv(mod, 0, 0, 0, 0, 0, 0)) @@ -2047,10 +2079,11 @@ func TestSnapshotPreview1_SockRecv(t *testing.T) { // TestSnapshotPreview1_SockSend only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_SockSend(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionSockSend, ImportSockSend, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.SockSend", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.SockSend(mod, 0, 0, 0, 0, 0)) @@ -2066,10 +2099,11 @@ func TestSnapshotPreview1_SockSend(t *testing.T) { // TestSnapshotPreview1_SockShutdown only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_SockShutdown(t *testing.T) { ctx := context.Background() - sys, err := wasm.NewSystemContext() + sys, err := newSysContext(nil, nil, nil) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionSockShutdown, ImportSockShutdown, moduleName, sys) + defer mod.Close() t.Run("SnapshotPreview1.SockShutdown", func(t *testing.T) { require.Equal(t, wasi.ErrnoNosys, a.SockShutdown(mod, 0, 0)) @@ -2084,7 +2118,7 @@ func TestSnapshotPreview1_SockShutdown(t *testing.T) { const testMemoryPageSize = 1 -func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImport, moduleName string, sys *wasm.SystemContext) (*wasiAPI, *wasm.ModuleContext, publicwasm.Function) { +func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImport, moduleName string, sys *wasm.SysContext) (*wasiAPI, *wasm.ModuleContext, publicwasm.Function) { enabledFeatures := wasm.Features20191205 store := wasm.NewStore(interpreter.NewEngine(), enabledFeatures) @@ -2097,7 +2131,7 @@ func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImpo // Double-check what we created passes same validity as module-defined modules. require.NoError(t, m.Validate(enabledFeatures)) - _, err = store.Instantiate(ctx, m, m.NameSection.ModuleName) // TODO: close + _, err = store.Instantiate(ctx, m, m.NameSection.ModuleName, nil) // TODO: close require.NoError(t, err) m, err = text.DecodeModule([]byte(fmt.Sprintf(`(module @@ -2108,9 +2142,8 @@ func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImpo )`, wasiFunction, wasiImport)), enabledFeatures) require.NoError(t, err) - mod, err := store.Instantiate(ctx, m, moduleName) + mod, err := store.Instantiate(ctx, m, moduleName, sys) require.NoError(t, err) - mod.System = sys // TODO: temporary until #394 fn := mod.ExportedFunction(wasiFunction) require.NotNil(t, fn) @@ -2123,3 +2156,7 @@ func maskMemory(t *testing.T, mod publicwasm.Module, size int) { require.True(t, mod.Memory().WriteByte(i, '?')) } } + +func newSysContext(args, environ []string, openedFiles map[uint32]*wasm.FileEntry) (sys *wasm.SysContext, err error) { + return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, openedFiles) +} diff --git a/internal/wasm/global_test.go b/internal/wasm/global_test.go index 215f2bc609..2fc55e2423 100644 --- a/internal/wasm/global_test.go +++ b/internal/wasm/global_test.go @@ -260,7 +260,7 @@ func TestPublicModule_Global(t *testing.T) { s := newStore() t.Run(tc.name, func(t *testing.T) { // Instantiate the module and get the export of the above global - module, err := s.Instantiate(context.Background(), tc.module, t.Name()) + module, err := s.Instantiate(context.Background(), tc.module, t.Name(), nil) require.NoError(t, err) if global := module.ExportedGlobal("global"); tc.expected != nil { diff --git a/internal/wasm/interpreter/interpreter_test.go b/internal/wasm/interpreter/interpreter_test.go index 9fcdda0187..39e468c68d 100644 --- a/internal/wasm/interpreter/interpreter_test.go +++ b/internal/wasm/interpreter/interpreter_test.go @@ -175,7 +175,7 @@ func TestEngine_Call(t *testing.T) { // Use exported functions to simplify instantiation of a Wasm function e := NewEngine() store := wasm.NewStore(e, wasm.Features20191205) - mod, err := store.Instantiate(context.Background(), m, t.Name()) + mod, err := store.Instantiate(context.Background(), m, t.Name(), nil) require.NoError(t, err) fn := mod.ExportedFunction("fn") @@ -207,7 +207,7 @@ func TestEngine_Call_HostFn(t *testing.T) { e := NewEngine() module := &wasm.ModuleInstance{Memory: memory} - modCtx := wasm.NewModuleContext(context.Background(), wasm.NewStore(e, wasm.Features20191205), module) + modCtx := wasm.NewModuleContext(context.Background(), wasm.NewStore(e, wasm.Features20191205), module, nil) f := &wasm.FunctionInstance{ GoFunc: &hostFn, diff --git a/internal/wasm/jit/engine_test.go b/internal/wasm/jit/engine_test.go index bf350805ab..ee1a630f6e 100644 --- a/internal/wasm/jit/engine_test.go +++ b/internal/wasm/jit/engine_test.go @@ -127,7 +127,7 @@ func TestEngine_Call(t *testing.T) { // Use exported functions to simplify instantiation of a Wasm function e := NewEngine() store := wasm.NewStore(e, wasm.Features20191205) - mod, err := store.Instantiate(context.Background(), m, t.Name()) + mod, err := store.Instantiate(context.Background(), m, t.Name(), nil) require.NoError(t, err) fn := mod.ExportedFunction("fn") @@ -266,7 +266,7 @@ func TestEngine_Call_HostFn(t *testing.T) { e := NewEngine() module := &wasm.ModuleInstance{Memory: memory} - modCtx := wasm.NewModuleContext(context.Background(), wasm.NewStore(e, wasm.Features20191205), module) + modCtx := wasm.NewModuleContext(context.Background(), wasm.NewStore(e, wasm.Features20191205), module, nil) f := &wasm.FunctionInstance{ GoFunc: &hostFn, @@ -523,7 +523,7 @@ func TestSliceAllocatedOnHeap(t *testing.T) { }}) require.NoError(t, err) - _, err = store.Instantiate(context.Background(), hm, hostModuleName) + _, err = store.Instantiate(context.Background(), hm, hostModuleName, nil) require.NoError(t, err) const valueStackCorruption = "value_stack_corruption" @@ -574,7 +574,7 @@ func TestSliceAllocatedOnHeap(t *testing.T) { }, } - mi, err := store.Instantiate(context.Background(), m, t.Name()) + mi, err := store.Instantiate(context.Background(), m, t.Name(), nil) require.NoError(t, err) for _, fnName := range []string{valueStackCorruption, callStackCorruption} { diff --git a/internal/wasm/module_context.go b/internal/wasm/module_context.go index a796954247..4a22edfe55 100644 --- a/internal/wasm/module_context.go +++ b/internal/wasm/module_context.go @@ -10,8 +10,8 @@ import ( // compile time check to ensure ModuleContext implements wasm.Module var _ publicwasm.Module = &ModuleContext{} -func NewModuleContext(ctx context.Context, store *Store, instance *ModuleInstance) *ModuleContext { - return &ModuleContext{ctx: ctx, memory: instance.Memory, module: instance, store: store} +func NewModuleContext(ctx context.Context, store *Store, instance *ModuleInstance, sys *SysContext) *ModuleContext { + return &ModuleContext{ctx: ctx, memory: instance.Memory, module: instance, store: store, sys: sys} } // ModuleContext implements wasm.Module @@ -23,17 +23,16 @@ type ModuleContext struct { memory publicwasm.Memory store *Store - // System is not exposed publicly. This is currently only used by internalwasi. + // Sys is not exposed publicly. This is currently only used by internalwasi. // Note: This is a part of ModuleContext so that scope is correct and Close is coherent. - // TODO: In #394 unexport this for System() - System *SystemContext + sys *SysContext } // WithMemory allows overriding memory without re-allocation when the result would be the same. func (m *ModuleContext) WithMemory(memory *MemoryInstance) *ModuleContext { // only re-allocate if it will change the effective memory if m.memory == nil || (memory != nil && memory.Max != nil && *memory.Max > 0 && memory != m.memory) { - return &ModuleContext{module: m.module, memory: memory, ctx: m.ctx, System: m.System} + return &ModuleContext{module: m.module, memory: memory, ctx: m.ctx, sys: m.sys} } return m } @@ -48,11 +47,16 @@ func (m *ModuleContext) Context() context.Context { return m.ctx } +// Sys is exposed only for WASI. +func (m *ModuleContext) Sys() *SysContext { + return m.sys +} + // WithContext implements wasm.Module WithContext func (m *ModuleContext) WithContext(ctx context.Context) publicwasm.Module { // only re-allocate if it will change the effective context if ctx != nil && ctx != m.ctx { - return &ModuleContext{module: m.module, memory: m.memory, ctx: ctx, System: m.System} + return &ModuleContext{module: m.module, memory: m.memory, ctx: ctx, sys: m.sys} } return m } @@ -61,7 +65,9 @@ func (m *ModuleContext) WithContext(ctx context.Context) publicwasm.Module { // Note: When there are multiple errors, the error returned is the last one. func (m *ModuleContext) Close() (err error) { err = m.store.CloseModule(m.module.Name) - if err2 := m.System.Close(); err2 != nil { + if sys := m.sys; sys == nil { // ex from ModuleBuilder + return + } else if err2 := m.sys.Close(); err2 != nil { err = err2 } return diff --git a/internal/wasm/store.go b/internal/wasm/store.go index bd6228ca56..b91a0176f0 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -256,16 +256,13 @@ func NewStore(engine Engine, enabledFeatures Features) *Store { // Instantiate uses name instead of the Module.NameSection ModuleName as it allows instantiating the same module under // different names safely and concurrently. // -// * ctx: the default context used for function calls +// * ctx: the default context used for function calls. +// * name: the name of the module. +// * sys: the system context, which will be closed (SysContext.Close) on ModuleContext.Close. // // Note: Module.Validate must be called prior to instantiation. -func (s *Store) Instantiate(ctx context.Context, module *Module, name string) (*ModuleContext, error) { - sys, err := NewSystemContext() // TODO: from config in #394 - if err != nil { // fail early - return nil, err - } - - if err = s.requireModuleName(name); err != nil { +func (s *Store) Instantiate(ctx context.Context, module *Module, name string, sys *SysContext) (*ModuleContext, error) { + if err := s.requireModuleName(name); err != nil { return nil, err } @@ -318,8 +315,7 @@ func (s *Store) Instantiate(ctx context.Context, module *Module, name string) (* m.applyData(module.DataSection) // Build the default context for calls to this module. - m.Ctx = NewModuleContext(ctx, s, m) - m.Ctx.System = sys + m.Ctx = NewModuleContext(ctx, s, m, sys) // Execute the start function. if module.StartSection != nil { diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index 84b5cbec60..4297e7b265 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -5,14 +5,12 @@ import ( "encoding/binary" "fmt" "math" - "os" "strconv" "sync" "testing" "github.com/stretchr/testify/require" - "github.com/tetratelabs/wazero/internal/cstring" "github.com/tetratelabs/wazero/wasm" ) @@ -76,7 +74,7 @@ func TestModuleInstance_Memory(t *testing.T) { t.Run(tc.name, func(t *testing.T) { s := newStore() - instance, err := s.Instantiate(context.Background(), tc.input, "test") + instance, err := s.Instantiate(context.Background(), tc.input, "test", nil) require.NoError(t, err) mem := instance.ExportedMemory("memory") @@ -93,7 +91,7 @@ func TestModuleContext_String(t *testing.T) { s := newStore() // Ensure paths that can create the host module can see the name. - m, err := s.Instantiate(context.Background(), &Module{}, "module") + m, err := s.Instantiate(context.Background(), &Module{}, "module", nil) require.NoError(t, err) require.Equal(t, "Module[module]", m.String()) require.Equal(t, "Module[module]", s.Module(m.module.Name).String()) @@ -106,7 +104,8 @@ func TestStore_Instantiate(t *testing.T) { type key string ctx := context.WithValue(context.Background(), key("a"), "b") // arbitrary non-default context - mod, err := s.Instantiate(ctx, m, "") + sys := &SysContext{} + mod, err := s.Instantiate(ctx, m, "", sys) require.NoError(t, err) defer mod.Close() @@ -115,12 +114,7 @@ func TestStore_Instantiate(t *testing.T) { require.Equal(t, s.modules[""], mod.module) require.Equal(t, s.modules[""].Memory, mod.memory) require.Equal(t, s, mod.store) - require.Equal(t, cstring.EmptyNullTerminatedStrings, mod.System.Args) - require.Equal(t, cstring.EmptyNullTerminatedStrings, mod.System.Environ) - require.Equal(t, os.Stdin, mod.System.Stdin) - require.Equal(t, os.Stdout, mod.System.Stdout) - require.Equal(t, os.Stderr, mod.System.Stderr) - require.Empty(t, mod.System.OpenedFiles) + require.Equal(t, sys, mod.sys) }) } @@ -137,7 +131,7 @@ func TestStore_CloseModule(t *testing.T) { initializer: func(t *testing.T, s *Store) { m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(wasm.Module) {}}) require.NoError(t, err) - _, err = s.Instantiate(context.Background(), m, importedModuleName) + _, err = s.Instantiate(context.Background(), m, importedModuleName, nil) require.NoError(t, err) }, }, @@ -149,7 +143,7 @@ func TestStore_CloseModule(t *testing.T) { FunctionSection: []uint32{0}, CodeSection: []*Code{{Body: []byte{OpcodeEnd}}}, ExportSection: map[string]*Export{"fn": {Type: ExternTypeFunc, Index: 0, Name: "fn"}}, - }, importedModuleName) + }, importedModuleName, nil) require.NoError(t, err) }, }, @@ -165,7 +159,7 @@ func TestStore_CloseModule(t *testing.T) { MemorySection: &Memory{Min: 1}, GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}}}, TableSection: &Table{Min: 10}, - }, importingModuleName) + }, importingModuleName, nil) require.NoError(t, err) _, ok := s.modules[importedModuleName] @@ -199,7 +193,7 @@ func TestStore_concurrent(t *testing.T) { var wg sync.WaitGroup s := newStore() - _, err = s.Instantiate(context.Background(), m, importedModuleName) + _, err = s.Instantiate(context.Background(), m, importedModuleName, nil) require.NoError(t, err) hm, ok := s.modules[importedModuleName] @@ -222,7 +216,7 @@ func TestStore_concurrent(t *testing.T) { for i := 0; i < goroutines; i++ { go func(i int) { defer wg.Done() - _, err := s.Instantiate(context.Background(), importingModule, strconv.Itoa(i)) + _, err := s.Instantiate(context.Background(), importingModule, strconv.Itoa(i), nil) require.NoError(t, err) }(i) } @@ -254,17 +248,17 @@ func TestStore_Instantiate_Errors(t *testing.T) { t.Run("Fails if module name already in use", func(t *testing.T) { s := newStore() - _, err = s.Instantiate(context.Background(), m, importedModuleName) + _, err = s.Instantiate(context.Background(), m, importedModuleName, nil) require.NoError(t, err) // Trying to register it again should fail - _, err = s.Instantiate(context.Background(), m, importedModuleName) + _, err = s.Instantiate(context.Background(), m, importedModuleName, nil) require.EqualError(t, err, "module imported has already been instantiated") }) t.Run("fail resolve import", func(t *testing.T) { s := newStore() - _, err = s.Instantiate(context.Background(), m, importedModuleName) + _, err = s.Instantiate(context.Background(), m, importedModuleName, nil) require.NoError(t, err) hm := s.modules[importedModuleName] @@ -278,14 +272,14 @@ func TestStore_Instantiate_Errors(t *testing.T) { // But the second one tries to import uninitialized-module -> {Type: ExternTypeFunc, Module: "non-exist", Name: "fn", DescFunc: 0}, }, - }, importingModuleName) + }, importingModuleName, nil) require.EqualError(t, err, "module[non-exist] not instantiated") }) t.Run("compilation failed", func(t *testing.T) { s := newStore() - _, err = s.Instantiate(context.Background(), m, importedModuleName) + _, err = s.Instantiate(context.Background(), m, importedModuleName, nil) require.NoError(t, err) hm := s.modules[importedModuleName] @@ -304,7 +298,7 @@ func TestStore_Instantiate_Errors(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}, }, - }, importingModuleName) + }, importingModuleName, nil) require.EqualError(t, err, "compilation failed: some compilation error") }) @@ -313,7 +307,7 @@ func TestStore_Instantiate_Errors(t *testing.T) { engine := s.engine.(*mockEngine) engine.callFailIndex = 1 - _, err = s.Instantiate(context.Background(), m, importedModuleName) + _, err = s.Instantiate(context.Background(), m, importedModuleName, nil) require.NoError(t, err) hm := s.modules[importedModuleName] @@ -328,7 +322,7 @@ func TestStore_Instantiate_Errors(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}, }, - }, importingModuleName) + }, importingModuleName, nil) require.EqualError(t, err, "start function[1] failed: call failed") }) } @@ -340,7 +334,7 @@ func TestStore_ExportImportedHostFunction(t *testing.T) { s := newStore() // Add the host module - _, err = s.Instantiate(context.Background(), m, m.NameSection.ModuleName) + _, err = s.Instantiate(context.Background(), m, m.NameSection.ModuleName, nil) require.NoError(t, err) t.Run("Module is the importing module", func(t *testing.T) { @@ -349,7 +343,7 @@ func TestStore_ExportImportedHostFunction(t *testing.T) { ImportSection: []*Import{{Type: ExternTypeFunc, Module: "host", Name: "host_fn", DescFunc: 0}}, MemorySection: &Memory{Min: 1}, ExportSection: map[string]*Export{"host.fn": {Type: ExternTypeFunc, Name: "host.fn", Index: 0}}, - }, "test") + }, "test", nil) require.NoError(t, err) mod, ok := s.modules["test"] @@ -400,7 +394,7 @@ func TestFunctionInstance_Call(t *testing.T) { store := NewStore(&mockEngine{shouldCompileFail: false, callFailIndex: -1}, Features20191205) // Add the host module - hm, err := store.Instantiate(storeCtx, m, "host") + hm, err := store.Instantiate(storeCtx, m, "host", nil) require.NoError(t, err) // Make a module to import the function @@ -414,7 +408,7 @@ func TestFunctionInstance_Call(t *testing.T) { }}, MemorySection: &Memory{Min: 1}, ExportSection: map[string]*Export{functionName: {Type: ExternTypeFunc, Name: functionName, Index: 0}}, - }, "test") + }, "test", nil) require.NoError(t, err) // This fails if the function wasn't invoked, or had an unexpected context. diff --git a/internal/wasm/sys.go b/internal/wasm/sys.go index d16aee0ff1..edd59966b3 100644 --- a/internal/wasm/sys.go +++ b/internal/wasm/sys.go @@ -1,13 +1,13 @@ package internalwasm import ( + "errors" "fmt" "io" "math" "os" - "strings" + "sync/atomic" - "github.com/tetratelabs/wazero/internal/cstring" "github.com/tetratelabs/wazero/wasi" ) @@ -20,102 +20,220 @@ type FileEntry struct { File wasi.File } -// SystemContext holds module-scoped system resources currently only used by internalwasi. -// -// TODO: Most fields are mutable so that WASI config can overwrite fields when starting a command module. This can be -// fixed in #394 by replacing wazero.WASIConfig with wazero.SystemConfig, defaulted by wazero.RuntimeConfig and -// overridable with InstantiateModule. -type SystemContext struct { - // WithArgs hold a possibly empty (cstring.EmptyNullTerminatedStrings) list of arguments similar to os.Args. - // - // TODO: document this better in #396 - Args *cstring.NullTerminatedStrings - - // WithArgs hold a possibly empty (cstring.EmptyNullTerminatedStrings) list of arguments key/value pairs, similar to - // os.Environ. - // - // TODO: document this better in #396 - Environ *cstring.NullTerminatedStrings - - // WithStdin defaults to os.Stdin. - // - // TODO: change default to read os.DevNull in #396 - Stdin io.Reader - - // WithStdout defaults to os.Stdout. - // - // TODO: change default to io.Discard in #396 - Stdout io.Writer - - // WithStderr defaults to os.Stderr. - // - // TODO: change default to io.Discard in #396 - Stderr io.Writer - - // OpenedFiles are a map of file descriptor numbers (starting at 3) to open files (or directories). +// SysContext holds module-scoped system resources currently only used by internalwasi. +type SysContext struct { + args, environ []string + argsSize, environSize uint32 + stdin io.Reader + stdout, stderr io.Writer + + // openedFiles is a map of file descriptor numbers (>=3) to open files (or directories) and defaults to empty. // TODO: This is unguarded, so not goroutine-safe! - OpenedFiles map[uint32]*FileEntry + openedFiles map[uint32]*FileEntry + + // lastFD is not meant to be read directly. Rather by nextFD. + lastFD uint32 +} + +// nextFD gets the next file descriptor number in a goroutine safe way (monotonically) or zero if we ran out. +// TODO: opendFiles is still not goroutine safe! +// TODO: This can return zero if we ran out of file descriptors. A future change can optimize by re-using an FD pool. +func (c *SysContext) nextFD() uint32 { + if c.lastFD == math.MaxUint32 { + return 0 + } + return atomic.AddUint32(&c.lastFD, 1) +} + +// Args is like os.Args and defaults to nil. +// +// Note: The count will never be more than math.MaxUint32. +// See wazero.SysConfig WithArgs +func (c *SysContext) Args() []string { + return c.args } -func NewSystemContext() (*SystemContext, error) { - return &SystemContext{ - Args: cstring.EmptyNullTerminatedStrings, - Environ: cstring.EmptyNullTerminatedStrings, - Stdin: os.Stdin, // TODO: stop this default #396 - Stdout: os.Stdout, - Stderr: os.Stderr, - OpenedFiles: map[uint32]*FileEntry{}, - }, nil // TODO: once we open stdin from os.DevNull, this may raise an error (albeit unlikely). +// ArgsSize is the size to encode Args as Null-terminated strings. +// +// Note: To get the size without null-terminators, subtract the length of Args from this value. +// See wazero.SysConfig WithArgs +// See https://en.wikipedia.org/wiki/Null-terminated_string +func (c *SysContext) ArgsSize() uint32 { + return c.argsSize } -// WithStdin the same as wazero.WASIConfig WithStdin -func (c *SystemContext) WithStdin(reader io.Reader) { - c.Stdin = reader +// Environ are "key=value" entries like os.Environ and default to nil. +// +// Note: The count will never be more than math.MaxUint32. +// See wazero.SysConfig WithEnviron +func (c *SysContext) Environ() []string { + return c.environ +} + +// EnvironSize is the size to encode Environ as Null-terminated strings. +// +// Note: To get the size without null-terminators, subtract the length of Environ from this value. +// See wazero.SysConfig WithEnviron +// See https://en.wikipedia.org/wiki/Null-terminated_string +func (c *SysContext) EnvironSize() uint32 { + return c.environSize } -// WithStdout the same as wazero.WASIConfig WithStdout -func (c *SystemContext) WithStdout(writer io.Writer) { - c.Stdout = writer +// Stdin is like exec.Cmd Stdin and defaults to a reader of os.DevNull. +// See wazero.SysConfig WithStdin +func (c *SysContext) Stdin() io.Reader { + return c.stdin } -// WithStderr the same as wazero.WASIConfig WithStderr -func (c *SystemContext) WithStderr(writer io.Writer) { - c.Stderr = writer +// Stdout is like exec.Cmd Stdout and defaults to io.Discard. +// See wazero.SysConfig WithStdout +func (c *SysContext) Stdout() io.Writer { + return c.stdout } -// WithArgs is the same as wazero.WASIConfig WithArgs -func (c *SystemContext) WithArgs(args ...string) error { - wasiStrings, err := cstring.NewNullTerminatedStrings(math.MaxUint32, "arg", args...) // TODO: this is crazy high even if spec allows it - if err != nil { - return err +// Stderr is like exec.Cmd Stderr and defaults to io.Discard. +// See wazero.SysConfig WithStderr +func (c *SysContext) Stderr() io.Writer { + return c.stderr +} + +// eofReader is safer than reading from os.DevNull as it can never overrun operating system file descriptors. +type eofReader struct{} + +// Read implements io.Reader +// Note: This doesn't use a pointer reference as it has no state and an empty struct doesn't allocate. +func (eofReader) Read([]byte) (int, error) { + return 0, io.EOF +} + +// DefaultSysContext returns SysContext with no values set. +// +// Note: This isn't a constant because SysContext.openedFiles is currently mutable even when empty. +// TODO: Make it an error to open or close files when no FS was assigned. +func DefaultSysContext() *SysContext { + if sys, err := NewSysContext(0, nil, nil, nil, nil, nil, nil); err != nil { + panic(fmt.Errorf("BUG: DefaultSysContext should never error: %w", err)) + } else { + return sys } - c.Args = wasiStrings - return nil } -// WithEnviron is the same as wazero.WASIConfig WithEnviron -func (c *SystemContext) WithEnviron(environ ...string) error { - for i, env := range environ { - if !strings.Contains(env, "=") { - return fmt.Errorf("environ[%d] is not joined with '='", i) - } +var _ = DefaultSysContext() // Force panic on bug. + +// NewSysContext is a factory function which helps avoid needing to know defaults or exporting all fields. +// Note: max is exposed for testing. max is only used for env/args validation. +func NewSysContext(max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, openedFiles map[uint32]*FileEntry) (sys *SysContext, err error) { + sys = &SysContext{args: args, environ: environ} + + if sys.argsSize, err = nullTerminatedByteCount(max, args); err != nil { + return nil, fmt.Errorf("args invalid: %w", err) + } + + if sys.environSize, err = nullTerminatedByteCount(max, environ); err != nil { + return nil, fmt.Errorf("environ invalid: %w", err) } - wasiStrings, err := cstring.NewNullTerminatedStrings(math.MaxUint32, "environ", environ...) // TODO: this is crazy high even if spec allows it - if err != nil { - return err + + if stdin == nil { + sys.stdin = eofReader{} + } else { + sys.stdin = stdin } - c.Environ = wasiStrings - return nil + + if stdout == nil { + sys.stdout = io.Discard + } else { + sys.stdout = stdout + } + + if stderr == nil { + sys.stderr = io.Discard + } else { + sys.stderr = stderr + } + + if openedFiles == nil { + sys.openedFiles = map[uint32]*FileEntry{} + sys.lastFD = 2 // STDERR + } else { + sys.openedFiles = openedFiles + sys.lastFD = 2 // STDERR + for fd := range openedFiles { + if fd > sys.lastFD { + sys.lastFD = fd + } + } + } + return } -// WithPreopen adds one element in wazero.WASIConfig WithPreopens -func (c *SystemContext) WithPreopen(dir string, fileSys wasi.FS) { - c.OpenedFiles[uint32(len(c.OpenedFiles))+3] = &FileEntry{Path: dir, FS: fileSys} +// nullTerminatedByteCount ensures the count or Nul-terminated length of the elements doesn't exceed max, and that no +// element includes the nul character. +func nullTerminatedByteCount(max uint32, elements []string) (uint32, error) { + count := uint32(len(elements)) + if count > max { + return 0, errors.New("exceeds maximum count") + } + + // The buffer size is the total size including null terminators. The null terminator count == value count, sum + // count with each value length. This works because in Go, the length of a string is the same as its byte count. + bufSize, maxSize := uint64(count), uint64(max) // uint64 to allow summing without overflow + for _, e := range elements { + // As this is null-terminated, We have to validate there are no null characters in the string. + for _, c := range e { + if c == 0 { + return 0, errors.New("contains NUL character") + } + } + + nextSize := bufSize + uint64(len(e)) + if nextSize > maxSize { + return 0, errors.New("exceeds maximum size") + } + bufSize = nextSize + + } + return uint32(bufSize), nil } // Close implements io.Closer -func (c *SystemContext) Close() (err error) { - // Note: WithStdin, WithStdout and WithStderr are not closed as we didn't open them. - // TODO: In #394, close open files +func (c *SysContext) Close() (err error) { + // stdin, stdout and stderr are only closed if we opened them. The only case we open is when stdin -> /dev/null + if f, ok := c.stdin.(*os.File); ok && f.Name() == os.DevNull { + _ = f.Close() // ignore error closing reader of /dev/null + } + // TODO: close openedFiles in #394 return } + +// CloseFile returns true if a file was opened and closed without error, or false if not. +func (c *SysContext) CloseFile(fd uint32) (bool, error) { + f, ok := c.openedFiles[fd] + if !ok { + return false, nil + } + delete(c.openedFiles, fd) + + if f.File == nil { // TODO: currently, this means it is a pre-opened filesystem, but this may change later. + return true, nil + } + if err := f.File.Close(); err != nil { + return false, err + } + return true, nil +} + +// OpenedFile returns a file and true if it was opened or nil and false, if not. +func (c *SysContext) OpenedFile(fd uint32) (*FileEntry, bool) { + f, ok := c.openedFiles[fd] + return f, ok +} + +// OpenFile returns the file descriptor of the new file or false if we ran out of file descriptors +func (c *SysContext) OpenFile(f *FileEntry) (uint32, bool) { + newFD := c.nextFD() + if newFD == 0 { + return 0, false + } + c.openedFiles[newFD] = f + return newFD, true +} diff --git a/internal/wasm/sys_test.go b/internal/wasm/sys_test.go index 8771c155c2..1f9710e4f9 100644 --- a/internal/wasm/sys_test.go +++ b/internal/wasm/sys_test.go @@ -1,89 +1,149 @@ package internalwasm import ( - "os" + "bytes" + "io" "testing" "github.com/stretchr/testify/require" - - "github.com/tetratelabs/wazero/internal/cstring" ) -func TestSystemContext_Defaults(t *testing.T) { - sys, err := NewSystemContext() - require.Nil(t, err) - - require.Equal(t, cstring.EmptyNullTerminatedStrings, sys.Args) - require.Equal(t, cstring.EmptyNullTerminatedStrings, sys.Environ) - require.Equal(t, os.Stdin, sys.Stdin) - require.Equal(t, os.Stdout, sys.Stdout) - require.Equal(t, os.Stderr, sys.Stderr) - require.Empty(t, sys.OpenedFiles) -} - -func TestSystemContext_WithArgs(t *testing.T) { - t.Run("valid", func(t *testing.T) { - sys, err := NewSystemContext() - require.Nil(t, err) +func TestDefaultSysContext(t *testing.T) { + sys, err := NewSysContext( + 0, // max + nil, // args + nil, // environ + nil, // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ) + require.NoError(t, err) - err = sys.WithArgs("a", "bc") - require.NoError(t, err) + require.Nil(t, sys.Args()) + require.Zero(t, sys.ArgsSize()) + require.Nil(t, sys.Environ()) + require.Zero(t, sys.EnvironSize()) + require.Equal(t, eofReader{}, sys.Stdin()) + require.Equal(t, io.Discard, sys.Stdout()) + require.Equal(t, io.Discard, sys.Stderr()) + require.Empty(t, sys.openedFiles) - require.Equal(t, &cstring.NullTerminatedStrings{ - NullTerminatedValues: [][]byte{ - {'a', 0}, - {'b', 'c', 0}, - }, - TotalBufSize: 5, - }, sys.Args) - }) - t.Run("error constructing args", func(t *testing.T) { - sys, err := NewSystemContext() - require.Nil(t, err) - - err = sys.WithArgs("\xff\xfe\xfd", "foo", "bar") - require.EqualError(t, err, "arg[0] is not a valid UTF-8 string") - }) + require.Equal(t, sys, DefaultSysContext()) } -func TestSystemContext_WithEnviron(t *testing.T) { - t.Run("valid", func(t *testing.T) { - sys, err := NewSystemContext() - require.Nil(t, err) +func TestNewSysContext_Args(t *testing.T) { + tests := []struct { + name string + args []string + maxSize uint32 + expectedSize uint32 + expectedErr string + }{ + { + name: "ok", + maxSize: 10, + args: []string{"a", "bc"}, + expectedSize: 5, + }, + { + name: "exceeds max count", + maxSize: 1, + args: []string{"a", "bc"}, + expectedErr: "args invalid: exceeds maximum count", + }, + { + name: "exceeds max size", + maxSize: 4, + args: []string{"a", "bc"}, + expectedErr: "args invalid: exceeds maximum size", + }, + { + name: "null character", + maxSize: 10, + args: []string{"a", string([]byte{'b', 0})}, + expectedErr: "args invalid: contains NUL character", + }, + } - err = sys.WithEnviron("a=b", "b=cd") - require.NoError(t, err) + for _, tt := range tests { + tc := tt - require.Equal(t, &cstring.NullTerminatedStrings{ - NullTerminatedValues: [][]byte{ - {'a', '=', 'b', 0}, - {'b', '=', 'c', 'd', 0}, - }, - TotalBufSize: 9, - }, sys.Environ) - }) + t.Run(tc.name, func(t *testing.T) { + sys, err := NewSysContext( + tc.maxSize, // max + tc.args, + nil, // environ + bytes.NewReader(make([]byte, 0)), // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ) + if tc.expectedErr == "" { + require.Nil(t, err) + require.Equal(t, tc.args, sys.Args()) + require.Equal(t, tc.expectedSize, sys.ArgsSize()) + } else { + require.EqualError(t, err, tc.expectedErr) + } + }) + } +} - errorTests := []struct { +func TestNewSysContext_Environ(t *testing.T) { + tests := []struct { name string - environ string - errorMessage string + environ []string + maxSize uint32 + expectedSize uint32 + expectedErr string }{ - {name: "error invalid utf-8", - environ: "non_utf8=\xff\xfe\xfd", - errorMessage: "environ[0] is not a valid UTF-8 string"}, - {name: "error not '='-joined pair", - environ: "no_equal_pair", - errorMessage: "environ[0] is not joined with '='"}, + { + name: "ok", + maxSize: 10, + environ: []string{"a=b", "c=de"}, + expectedSize: 9, + }, + { + name: "exceeds max count", + maxSize: 1, + environ: []string{"a=b", "c=de"}, + expectedErr: "environ invalid: exceeds maximum count", + }, + { + name: "exceeds max size", + maxSize: 4, + environ: []string{"a=b", "c=de"}, + expectedErr: "environ invalid: exceeds maximum size", + }, + { + name: "null character", + maxSize: 10, + environ: []string{"a=b", string(append([]byte("c=d"), 0))}, + expectedErr: "environ invalid: contains NUL character", + }, } - for _, tt := range errorTests { + + for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { - sys, err := NewSystemContext() - require.Nil(t, err) - - err = sys.WithEnviron(tc.environ) - require.EqualError(t, err, tc.errorMessage) + sys, err := NewSysContext( + tc.maxSize, // max + nil, // args + tc.environ, + bytes.NewReader(make([]byte, 0)), // stdin + nil, // stdout + nil, // stderr + nil, // openedFiles + ) + if tc.expectedErr == "" { + require.Nil(t, err) + require.Equal(t, tc.environ, sys.Environ()) + require.Equal(t, tc.expectedSize, sys.EnvironSize()) + } else { + require.EqualError(t, err, tc.expectedErr) + } }) } } diff --git a/tests/bench/bench_test.go b/tests/bench/bench_test.go index d85f24837b..8d767356f5 100644 --- a/tests/bench/bench_test.go +++ b/tests/bench/bench_test.go @@ -18,11 +18,13 @@ var caseWasm []byte func BenchmarkEngines(b *testing.B) { b.Run("interpreter", func(b *testing.B) { m := instantiateHostFunctionModuleWithEngine(b, wazero.NewRuntimeConfigInterpreter()) + defer m.Close() runAllBenches(b, m) }) if runtime.GOARCH == "amd64" { b.Run("jit", func(b *testing.B) { m := instantiateHostFunctionModuleWithEngine(b, wazero.NewRuntimeConfigJIT()) + defer m.Close() runAllBenches(b, m) }) } diff --git a/tests/bench/wasi_test.go b/tests/bench/wasi_test.go index 565ffb9463..8d14216b1a 100644 --- a/tests/bench/wasi_test.go +++ b/tests/bench/wasi_test.go @@ -1,7 +1,9 @@ package bench import ( + "bytes" "context" + "math" "testing" "github.com/stretchr/testify/require" @@ -25,10 +27,7 @@ var testMem = &wasm.MemoryInstance{ } func Test_EnvironGet(t *testing.T) { - sys, err := wasm.NewSystemContext() - require.NoError(t, err) - - err = sys.WithEnviron("a=b", "b=cd") + sys, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil) require.NoError(t, err) testCtx := newCtx(make([]byte, 20), sys) @@ -39,12 +38,7 @@ func Test_EnvironGet(t *testing.T) { } func Benchmark_EnvironGet(b *testing.B) { - sys, err := wasm.NewSystemContext() - if err != nil { - b.Fatal(err) - } - - err = sys.WithEnviron("a=b", "b=cd") + sys, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil) if err != nil { b.Fatal(err) } @@ -69,10 +63,12 @@ func Benchmark_EnvironGet(b *testing.B) { }) } -func newCtx(buf []byte, sys *wasm.SystemContext) *wasm.ModuleContext { - ret := wasm.NewModuleContext(context.Background(), nil, &wasm.ModuleInstance{ +func newCtx(buf []byte, sys *wasm.SysContext) *wasm.ModuleContext { + return wasm.NewModuleContext(context.Background(), nil, &wasm.ModuleInstance{ Memory: &wasm.MemoryInstance{Min: 1, Buffer: buf}, - }) - ret.System = sys - return ret + }, sys) +} + +func newSysContext(args, environ []string, openedFiles map[uint32]*wasm.FileEntry) (sys *wasm.SysContext, err error) { + return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, openedFiles) } diff --git a/tests/engine/adhoc_test.go b/tests/engine/adhoc_test.go index cbb10c9649..43d99818dc 100644 --- a/tests/engine/adhoc_test.go +++ b/tests/engine/adhoc_test.go @@ -59,6 +59,7 @@ func testHugeStack(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig) r := wazero.NewRuntimeWithConfig(newRuntimeConfig()) module, err := r.InstantiateModuleFromSource(hugestackWasm) require.NoError(t, err) + defer module.Close() fn := module.ExportedFunction("main") require.NotNil(t, fn) @@ -79,6 +80,7 @@ func testUnreachable(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig module, err := r.InstantiateModuleFromSource(unreachableWasm) require.NoError(t, err) + defer module.Close() _, err = module.ExportedFunction("main").Call(nil) exp := `wasm runtime error: panic in host function @@ -103,6 +105,7 @@ func testRecursiveEntry(t *testing.T, newRuntimeConfig func() *wazero.RuntimeCon module, err := r.InstantiateModuleFromSource(recursiveWasm) require.NoError(t, err) + defer module.Close() _, err = module.ExportedFunction("main").Call(nil, 1) require.NoError(t, err) @@ -135,6 +138,7 @@ func testImportedAndExportedFunc(t *testing.T, newRuntimeConfig func() *wazero.R (export "store_int" (func $store_int)) )`)) require.NoError(t, err) + defer module.Close() // Call store_int and ensure it didn't return an error code. results, err := module.ExportedFunction("store_int").Call(nil, 1, math.MaxUint64) @@ -211,6 +215,7 @@ func testHostFunctions(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConf (export "call->test.identity_f64" (func $call->test.identity_f64)) )`)) require.NoError(t, err) + defer m.Close() t.Run(fmt.Sprintf("host function with f32 param%s", k), func(t *testing.T) { name := "call->test.identity_f32" diff --git a/tests/spectest/spec_test.go b/tests/spectest/spec_test.go index 887198720a..9d1f89109a 100644 --- a/tests/spectest/spec_test.go +++ b/tests/spectest/spec_test.go @@ -267,7 +267,7 @@ func addSpectestModule(t *testing.T, store *wasm.Store) { }, } require.NoError(t, mod.Validate(wasm.Features20191205)) - _, err := store.Instantiate(context.Background(), mod, "spectest") + _, err := store.Instantiate(context.Background(), mod, "spectest", nil) require.NoError(t, err) } @@ -335,7 +335,7 @@ func runTest(t *testing.T, newEngine func() wasm.Engine) { } } moduleName = strings.TrimPrefix(moduleName, "$") - _, err = store.Instantiate(context.Background(), mod, moduleName) + _, err = store.Instantiate(context.Background(), mod, moduleName, nil) lastInstantiatedModuleName = moduleName require.NoError(t, err) case "register": @@ -458,7 +458,7 @@ func requireInstantiationError(t *testing.T, store *wasm.Store, buf []byte, msg return } - _, err = store.Instantiate(context.Background(), mod, t.Name()) + _, err = store.Instantiate(context.Background(), mod, t.Name(), nil) require.Error(t, err, msg) } diff --git a/wasi.go b/wasi.go index ef29c1af73..9b6fd3c308 100644 --- a/wasi.go +++ b/wasi.go @@ -2,7 +2,6 @@ package wazero import ( "fmt" - "io" internalwasi "github.com/tetratelabs/wazero/internal/wasi" internalwasm "github.com/tetratelabs/wazero/internal/wasm" @@ -23,49 +22,6 @@ func WASIMemFS() wasi.FS { } } -type WASIConfig struct { - stdin io.Reader - stdout io.Writer - stderr io.Writer - args []string - environ map[string]string - preopens map[string]wasi.FS -} - -func NewWASIConfig() *WASIConfig { - return &WASIConfig{} -} - -func (c *WASIConfig) WithStdin(stdin io.Reader) *WASIConfig { - c.stdin = stdin - return c -} - -func (c *WASIConfig) WithStdout(stdout io.Writer) *WASIConfig { - c.stdout = stdout - return c -} - -func (c *WASIConfig) WithStderr(stderr io.Writer) *WASIConfig { - c.stderr = stderr - return c -} - -func (c *WASIConfig) WithArgs(args ...string) *WASIConfig { - c.args = args - return c -} - -func (c *WASIConfig) WithEnviron(environ map[string]string) *WASIConfig { - c.environ = environ - return c -} - -func (c *WASIConfig) WithPreopens(preopens map[string]wasi.FS) *WASIConfig { - c.preopens = preopens - return c -} - // WASISnapshotPreview1 are functions importable as the module name wasi.ModuleSnapshotPreview1 func WASISnapshotPreview1() *Module { _, fns := internalwasi.SnapshotPreview1Functions() @@ -76,45 +32,6 @@ func WASISnapshotPreview1() *Module { return &Module{name: wasi.ModuleSnapshotPreview1, module: m} } -func newSystemContext(c *WASIConfig) (*internalwasm.SystemContext, error) { - sys, err := internalwasm.NewSystemContext() - if err != nil { - return nil, err - } - - if c.stdin != nil { - sys.WithStdin(c.stdin) - } - if c.stdout != nil { - sys.WithStdout(c.stdout) - } - if c.stderr != nil { - sys.WithStderr(c.stderr) - } - if len(c.args) > 0 { - err = sys.WithArgs(c.args...) - if err != nil { - panic(err) // better to panic vs have bother users about unlikely size > uint32 - } - } - if len(c.environ) > 0 { - environ := make([]string, 0, len(c.environ)) - for k, v := range c.environ { - environ = append(environ, fmt.Sprintf("%s=%s", k, v)) - } - err = sys.WithEnviron(environ...) - if err != nil { // this can't be due to lack of '=' as we did that above. - panic(err) // better to panic vs have bother users about unlikely size > uint32 - } - } - if len(c.preopens) > 0 { - for k, v := range c.preopens { - sys.WithPreopen(k, v) - } - } - return sys, nil -} - // StartWASICommandFromSource instantiates a module from the WebAssembly 1.0 (20191205) text or binary source or errs if // invalid. Once instantiated, this starts its WASI Command function ("_start"). // @@ -160,31 +77,33 @@ func StartWASICommandFromSource(r Runtime, source []byte) (wasm.Module, error) { // See StartWASICommandWithConfig // See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi func StartWASICommand(r Runtime, module *Module) (wasm.Module, error) { - return StartWASICommandWithConfig(r, module, nil) + return startWASICommandWithSysContext(r, module, internalwasm.DefaultSysContext()) } // StartWASICommandWithConfig is like StartWASICommand, except you can override configuration based on the importing // module. For example, you can use this to define different args depending on the importing module. // -// // Initialize base configuration: // r := wazero.NewRuntime() -// config := wazero.NewWASIConfig().WithStdout(buf) // wasi, _ := r.NewHostModule(wazero.WASISnapshotPreview1()) -// decoded, _ := r.CompileModule(source) +// mod, _ := r.CompileModule(source) +// +// // Initialize base configuration: +// sys := wazero.NewSysConfig().WithStdout(buf) // -// // Assign configuration only when ready to instantiate. -// module, _ := StartWASICommandWithConfig(r, decoded, config.WithArgs("rotate", "angle=90", "dir=cw")) +// // Assign different configuration on each instantiation +// module, _ := StartWASICommandWithConfig(r, mod.WithName("rotate"), sys.WithArgs("rotate", "angle=90", "dir=cw")) // +// Note: Config is copied during instantiation: Later changes to config do not affect the instantiated result. // See StartWASICommand -func StartWASICommandWithConfig(r Runtime, module *Module, config *WASIConfig) (mod wasm.Module, err error) { - // Until #394 we have to re-assign the system context manually. - var sys *internalwasm.SystemContext - if config != nil { - if sys, err = newSystemContext(config); err != nil { - return - } +func StartWASICommandWithConfig(r Runtime, module *Module, config *SysConfig) (mod wasm.Module, err error) { + var sys *internalwasm.SysContext + if sys, err = config.toSysContext(); err != nil { + return } + return startWASICommandWithSysContext(r, module, sys) +} +func startWASICommandWithSysContext(r Runtime, module *Module, sys *internalwasm.SysContext) (mod wasm.Module, err error) { if err = internalwasi.ValidateWASICommand(module.module, module.name); err != nil { return } @@ -194,15 +113,10 @@ func StartWASICommandWithConfig(r Runtime, module *Module, config *WASIConfig) ( return nil, fmt.Errorf("unsupported Runtime implementation: %s", r) } - if mod, err = internal.store.Instantiate(internal.ctx, module.module, module.name); err != nil { + if mod, err = internal.store.Instantiate(internal.ctx, module.module, module.name, sys); err != nil { return } - // Override as necessary - if sys != nil { - mod.(*internalwasm.ModuleContext).System = sys - } - start := mod.ExportedFunction(internalwasi.FunctionStart) if _, err = start.Call(mod.WithContext(internal.ctx)); err != nil { return nil, fmt.Errorf("module[%s] function[%s] failed: %w", module.name, internalwasi.FunctionStart, err) diff --git a/wasi_test.go b/wasi_test.go index f52154ce3a..f10b1dedbc 100644 --- a/wasi_test.go +++ b/wasi_test.go @@ -52,8 +52,8 @@ func TestStartWASICommandWithConfig(t *testing.T) { stdout := bytes.NewBuffer(nil) - // Configure WASI with baseline config - config := NewWASIConfig().WithStdout(stdout) + // Configure WASI to write stdout to a buffer, so that we can verify it later. + sys := NewSysConfig().WithStdout(stdout) wasi, err := r.InstantiateModule(WASISnapshotPreview1()) require.NoError(t, err) defer wasi.Close() @@ -63,7 +63,7 @@ func TestStartWASICommandWithConfig(t *testing.T) { // Re-use the same module many times. for _, tc := range []string{"a", "b", "c"} { - mod, err := StartWASICommandWithConfig(r, m.WithName(tc), config.WithArgs(tc)) + mod, err := StartWASICommandWithConfig(r, m.WithName(tc), sys.WithArgs(tc)) require.NoError(t, err) // Ensure the scoped configuration applied. As the args are null-terminated, we append zero (NUL). diff --git a/wasm.go b/wasm.go index 4fce90db41..fd41563919 100644 --- a/wasm.go +++ b/wasm.go @@ -71,8 +71,6 @@ type Runtime interface { // // Note: The last value of RuntimeConfig.WithContext is used for any start function. InstantiateModule(module *Module) (wasm.Module, error) - - // TODO: RemoveModule } func NewRuntime() Runtime { @@ -146,5 +144,5 @@ func (r *runtime) InstantiateModuleFromSource(source []byte) (wasm.Module, error // InstantiateModule implements Runtime.InstantiateModule func (r *runtime) InstantiateModule(module *Module) (wasm.Module, error) { - return r.store.Instantiate(r.ctx, module.module, module.name) + return r.store.Instantiate(r.ctx, module.module, module.name, internalwasm.DefaultSysContext()) }