From b98a11e9c3962dc4fa70e65f6ee6b18443218910 Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Thu, 21 Jul 2022 15:49:55 +0800 Subject: [PATCH] Refactors host function tests to stub with wasm (#710) This refactors host functions with no-op or constant returns to be implemented with wasm instead of the host function bridge. This allows better performance. This also breaks up and makes WASI tests consistent, in a way that shows parameter name drifts easier. Signed-off-by: Adrian Cole --- assemblyscript/assemblyscript.go | 134 +- assemblyscript/assemblyscript_test.go | 372 +--- emscripten/emscripten.go | 12 +- emscripten/emscripten_test.go | 5 +- experimental/log_listener_test.go | 4 +- internal/engine/compiler/engine_test.go | 14 +- internal/engine/interpreter/interpreter.go | 9 +- .../engine/interpreter/interpreter_test.go | 91 +- internal/sys/sys_test.go | 10 + internal/testing/enginetest/enginetest.go | 75 +- internal/wasm/gofunc.go | 21 +- internal/wasm/gofunc_test.go | 13 +- internal/wasm/host.go | 51 +- internal/wasm/instruction.go | 2 +- wasi_snapshot_preview1/args.go | 92 + wasi_snapshot_preview1/args_test.go | 191 ++ wasi_snapshot_preview1/clock.go | 86 +- wasi_snapshot_preview1/clock_test.go | 216 +- wasi_snapshot_preview1/environ.go | 95 + wasi_snapshot_preview1/environ_test.go | 164 ++ wasi_snapshot_preview1/example_test.go | 2 +- wasi_snapshot_preview1/fs.go | 678 ++++++ wasi_snapshot_preview1/fs_test.go | 1076 +++++++++ wasi_snapshot_preview1/poll.go | 32 +- wasi_snapshot_preview1/poll_test.go | 124 +- wasi_snapshot_preview1/proc.go | 36 +- wasi_snapshot_preview1/proc_test.go | 73 +- wasi_snapshot_preview1/random.go | 52 + wasi_snapshot_preview1/random_test.go | 114 + wasi_snapshot_preview1/sched.go | 9 + wasi_snapshot_preview1/sched_test.go | 16 + wasi_snapshot_preview1/sock.go | 25 + wasi_snapshot_preview1/sock_test.go | 34 + wasi_snapshot_preview1/wasi.go | 1201 +--------- wasi_snapshot_preview1/wasi_bench_test.go | 70 +- wasi_snapshot_preview1/wasi_test.go | 1982 +---------------- 36 files changed, 3342 insertions(+), 3839 deletions(-) create mode 100644 wasi_snapshot_preview1/args.go create mode 100644 wasi_snapshot_preview1/args_test.go create mode 100644 wasi_snapshot_preview1/environ.go create mode 100644 wasi_snapshot_preview1/environ_test.go create mode 100644 wasi_snapshot_preview1/fs.go create mode 100644 wasi_snapshot_preview1/fs_test.go create mode 100644 wasi_snapshot_preview1/random.go create mode 100644 wasi_snapshot_preview1/random_test.go create mode 100644 wasi_snapshot_preview1/sched.go create mode 100644 wasi_snapshot_preview1/sched_test.go create mode 100644 wasi_snapshot_preview1/sock.go create mode 100644 wasi_snapshot_preview1/sock_test.go diff --git a/assemblyscript/assemblyscript.go b/assemblyscript/assemblyscript.go index ae69027a..70797fc5 100644 --- a/assemblyscript/assemblyscript.go +++ b/assemblyscript/assemblyscript.go @@ -18,7 +18,9 @@ // these functions. // // See wasi_snapshot_preview1.Instantiate and -// https://www.assemblyscript.org/concepts.html#special-imports +// * https://www.assemblyscript.org/concepts.html#special-imports +// * https://www.assemblyscript.org/concepts.html#targeting-wasi +// * https://www.assemblyscript.org/compiler.html#compiler-options package assemblyscript import ( @@ -114,24 +116,26 @@ func (e *functionExporter) WithTraceToStderr() FunctionExporter { // ExportFunctions implements FunctionExporter.ExportFunctions func (e *functionExporter) ExportFunctions(builder wazero.ModuleBuilder) { - env := &assemblyscript{abortMessageDisabled: e.abortMessageDisabled, traceMode: e.traceMode} - builder.ExportFunction("abort", env.abort, "~lib/builtins/abort", + var abortFn fnAbort + if e.abortMessageDisabled { + abortFn = abort + } else { + abortFn = abortWithMessage + } + var traceFn interface{} + switch e.traceMode { + case traceDisabled: + traceFn = traceNoop + case traceStdout: + traceFn = traceToStdout + case traceStderr: + traceFn = traceToStderr + } + builder.ExportFunction("abort", abortFn, "~lib/builtins/abort", "message", "fileName", "lineNumber", "columnNumber") - builder.ExportFunction("trace", env.trace, "~lib/builtins/trace", + builder.ExportFunction("trace", traceFn, "~lib/builtins/trace", "message", "nArgs", "arg0", "arg1", "arg2", "arg3", "arg4") - builder.ExportFunction("seed", env.seed, "~lib/builtins/seed") -} - -// assemblyScript includes "Special imports" only used In AssemblyScript when a -// user didn't add `import "wasi"` to their entry file. -// -// See https://www.assemblyscript.org/concepts.html#special-imports -// See https://www.assemblyscript.org/concepts.html#targeting-wasi -// See https://www.assemblyscript.org/compiler.html#compiler-options -// See https://github.com/AssemblyScript/assemblyscript/issues/1562 -type assemblyscript struct { - abortMessageDisabled bool - traceMode traceMode + builder.ExportFunction("seed", seed, "~lib/builtins/seed") } // abort is called on unrecoverable errors. This is typically present in Wasm @@ -146,27 +150,25 @@ type assemblyscript struct { // (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) // // See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L18 -func (a *assemblyscript) abort( - ctx context.Context, - mod api.Module, - message uint32, - fileName uint32, - lineNumber uint32, - columnNumber uint32, -) { - if !a.abortMessageDisabled { - sysCtx := mod.(*wasm.CallContext).Sys - msg, err := readAssemblyScriptString(ctx, mod, message) - if err != nil { - return - } - fn, err := readAssemblyScriptString(ctx, mod, fileName) - if err != nil { - return - } - _, _ = fmt.Fprintf(sysCtx.Stderr(), "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber) - } +type fnAbort func( + ctx context.Context, mod api.Module, message, fileName, lineNumber, columnNumber uint32, +) +// abortWithMessage implements fnAbort +func abortWithMessage( + ctx context.Context, mod api.Module, message, fileName, lineNumber, columnNumber uint32, +) { + sysCtx := mod.(*wasm.CallContext).Sys + msg := requireAssemblyScriptString(ctx, mod, "message", message) + fn := requireAssemblyScriptString(ctx, mod, "fileName", fileName) + _, _ = fmt.Fprintf(sysCtx.Stderr(), "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber) + abort(ctx, mod, message, fileName, lineNumber, columnNumber) +} + +// abortWithMessage implements fnAbort ignoring the message. +func abort( + ctx context.Context, mod api.Module, message, fileName, lineNumber, columnNumber uint32, +) { // AssemblyScript expects the exit code to be 255 // See https://github.com/AssemblyScript/assemblyscript/blob/v0.20.13/tests/compiler/wasi/abort.js#L14 exitCode := uint32(255) @@ -178,7 +180,27 @@ func (a *assemblyscript) abort( panic(sys.NewExitError(mod.Name(), exitCode)) } -// trace implements the same named function in AssemblyScript. Ex. +// traceNoop implements trace in wasm to avoid host call overhead on no-op. +var traceNoop = &wasm.Func{ + Type: wasm.MustFunctionType(traceToStdout), + Code: &wasm.Code{Body: []byte{wasm.OpcodeEnd}}, +} + +// traceToStdout implements trace to the configured Stdout. +func traceToStdout( + ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64, +) { + traceTo(ctx, mod, message, nArgs, arg0, arg1, arg2, arg3, arg4, mod.(*wasm.CallContext).Sys.Stdout()) +} + +// traceToStdout implements trace to the configured Stderr. +func traceToStderr( + ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64, +) { + traceTo(ctx, mod, message, nArgs, arg0, arg1, arg2, arg3, arg4, mod.(*wasm.CallContext).Sys.Stderr()) +} + +// traceTo implements the function "trace" in AssemblyScript. Ex. // trace('Hello World!') // // Here's the import in a user's module that ends up using this, in WebAssembly @@ -186,26 +208,13 @@ func (a *assemblyscript) abort( // (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64))) // // See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L61 -func (a *assemblyscript) trace( +func traceTo( ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64, + writer io.Writer, ) { - var writer io.Writer - switch a.traceMode { - case traceDisabled: - return - case traceStdout: - writer = mod.(*wasm.CallContext).Sys.Stdout() - case traceStderr: - writer = mod.(*wasm.CallContext).Sys.Stderr() - } - - msg, err := readAssemblyScriptString(ctx, mod, message) - if err != nil { - panic(err) - } var ret strings.Builder ret.WriteString("trace: ") - ret.WriteString(msg) + ret.WriteString(requireAssemblyScriptString(ctx, mod, "message", message)) if nArgs >= 1 { ret.WriteString(" ") ret.WriteString(formatFloat(arg0)) @@ -227,8 +236,7 @@ func (a *assemblyscript) trace( ret.WriteString(formatFloat(arg4)) } ret.WriteByte('\n') - _, err = writer.Write([]byte(ret.String())) - if err != nil { + if _, err := writer.Write([]byte(ret.String())); err != nil { panic(err) } } @@ -245,7 +253,7 @@ func formatFloat(f float64) string { // (import "env" "seed" (func $~lib/builtins/seed (result f64))) // // See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L111 -func (a *assemblyscript) seed(mod api.Module) float64 { +func seed(mod api.Module) float64 { randSource := mod.(*wasm.CallContext).Sys.RandSource() v, err := ieee754.DecodeFloat64(randSource) if err != nil { @@ -254,21 +262,21 @@ func (a *assemblyscript) seed(mod api.Module) float64 { return v } -// readAssemblyScriptString reads a UTF-16 string created by AssemblyScript. -func readAssemblyScriptString(ctx context.Context, mod api.Module, offset uint32) (string, error) { +// requireAssemblyScriptString reads a UTF-16 string created by AssemblyScript. +func requireAssemblyScriptString(ctx context.Context, mod api.Module, fieldName string, offset uint32) string { // Length is four bytes before pointer. byteCount, ok := mod.Memory().ReadUint32Le(ctx, offset-4) if !ok { - return "", fmt.Errorf("Memory.ReadUint32Le(%d) out of range", offset-4) + panic(fmt.Errorf("out of memory reading %s", fieldName)) } if byteCount%2 != 0 { - return "", fmt.Errorf("read an odd number of bytes for utf-16 string: %d", byteCount) + panic(fmt.Errorf("invalid UTF-16 reading %s", fieldName)) } buf, ok := mod.Memory().Read(ctx, offset, byteCount) if !ok { - return "", fmt.Errorf("Memory.Read(%d, %d) out of range", offset, byteCount) + panic(fmt.Errorf("out of memory reading %s", fieldName)) } - return decodeUTF16(buf), nil + return decodeUTF16(buf) } func decodeUTF16(b []byte) string { diff --git a/assemblyscript/assemblyscript_test.go b/assemblyscript/assemblyscript_test.go index 945a71be..e45d677f 100644 --- a/assemblyscript/assemblyscript_test.go +++ b/assemblyscript/assemblyscript_test.go @@ -19,40 +19,29 @@ import ( "github.com/tetratelabs/wazero/sys" ) -var abortWat = `(module - (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) - (memory 1 1) - (export "abort" (func 0)) -)` - -var seedWat = `(module - (import "env" "seed" (func $~lib/builtins/seed (result f64))) - (memory 1 1) - (export "seed" (func 0)) -)` - -var traceWat = `(module - (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64))) - (memory 1 1) - (export "trace" (func 0)) -)` - // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") func TestAbort(t *testing.T) { + var stderr bytes.Buffer + mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(&stderr)) + defer r.Close(testCtx) + tests := []struct { name string + abortFn fnAbort exporter FunctionExporter expected string }{ { name: "enabled", + abortFn: abortWithMessage, exporter: NewFunctionExporter(), expected: "message at filename:1:2\n", }, { name: "disabled", + abortFn: abort, exporter: NewFunctionExporter().WithAbortMessageDisabled(), expected: "", }, @@ -62,42 +51,26 @@ func TestAbort(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - var out, log bytes.Buffer + defer stderr.Reset() - // Set context to one that has an experimental listener - ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log)) + messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename")) - r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter()) - defer r.Close(ctx) - - envBuilder := r.NewModuleBuilder("env") - tc.exporter.ExportFunctions(envBuilder) - _, err := envBuilder.Instantiate(ctx, r) - require.NoError(t, err) - - abortWasm, err := watzero.Wat2Wasm(abortWat) - require.NoError(t, err) - - code, err := r.CompileModule(ctx, abortWasm, wazero.NewCompileConfig()) - require.NoError(t, err) - - mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStderr(&out)) - require.NoError(t, err) - - messageOff, filenameOff := writeAbortMessageAndFileName(ctx, t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename")) - - _, err = mod.ExportedFunction("abort").Call(ctx, uint64(messageOff), uint64(filenameOff), 1, 2) + err := require.CapturePanic(func() { + tc.abortFn(testCtx, mod, messageOff, filenameOff, 1, 2) + }) require.Error(t, err) require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode()) - require.Equal(t, tc.expected, out.String()) - require.Equal(t, `==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2) -`, log.String()) + require.Equal(t, tc.expected, stderr.String()) }) } } func TestAbort_Error(t *testing.T) { + var stderr bytes.Buffer + mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(&stderr)) + defer r.Close(testCtx) + tests := []struct { name string messageUTF16 []byte @@ -126,109 +99,25 @@ func TestAbort_Error(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - var out, log bytes.Buffer + defer stderr.Reset() - // Set context to one that has an experimental listener - ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log)) + messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16) - r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter()) - defer r.Close(ctx) - - _, err := Instantiate(ctx, r) - require.NoError(t, err) - - abortWasm, err := watzero.Wat2Wasm(abortWat) - require.NoError(t, err) - - compiled, err := r.CompileModule(ctx, abortWasm, wazero.NewCompileConfig()) - require.NoError(t, err) - - exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(&out) - mod, err := r.InstantiateModule(ctx, compiled, exporter) - require.NoError(t, err) - - messageOff, filenameOff := writeAbortMessageAndFileName(ctx, t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16) - - _, err = mod.ExportedFunction("abort").Call(ctx, uint64(messageOff), uint64(filenameOff), 1, 2) - require.NoError(t, err) - require.Equal(t, "", out.String()) // nothing output if strings fail to read. - require.Equal(t, tc.expectedLog, log.String()) + // Since abort panics, any opcodes afterwards cannot be reached. + _ = require.CapturePanic(func() { + abortWithMessage(testCtx, mod, messageOff, filenameOff, 1, 2) + }) + require.Equal(t, "", stderr.String()) // nothing output if strings fail to read. }) } } -var unreachableAfterAbort = `(module - (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) - (func $main - i32.const 0 - i32.const 0 - i32.const 0 - i32.const 0 - call $~lib/builtins/abort - unreachable ;; If abort doesn't panic, this code is reached. - ) - (start $main) -)` - -// TestAbort_UnreachableAfter ensures code that follows an abort isn't invoked. -func TestAbort_UnreachableAfter(t *testing.T) { - var log bytes.Buffer - - // Set context to one that has an experimental listener - ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log)) - - r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter()) - defer r.Close(ctx) - - envBuilder := r.NewModuleBuilder("env") - // Disable the abort message as we are passing invalid memory offsets. - NewFunctionExporter().WithAbortMessageDisabled().ExportFunctions(envBuilder) - _, err := envBuilder.Instantiate(ctx, r) - require.NoError(t, err) - - abortWasm, err := watzero.Wat2Wasm(unreachableAfterAbort) - require.NoError(t, err) - - _, err = r.InstantiateModuleFromBinary(ctx, abortWasm) - require.Error(t, err) - require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode()) - require.Equal(t, `--> .main() - ==> env.~lib/builtins/abort(message=0,fileName=0,lineNumber=0,columnNumber=0) -`, log.String()) -} - func TestSeed(t *testing.T) { - var log bytes.Buffer + mod, r := requireModule(t, wazero.NewModuleConfig(). + WithRandSource(bytes.NewReader([]byte{0, 1, 2, 3, 4, 5, 6, 7}))) + defer r.Close(testCtx) - // Set context to one that has an experimental listener - ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log)) - - r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter()) - defer r.Close(ctx) - - seed := []byte{0, 1, 2, 3, 4, 5, 6, 7} - - _, err := Instantiate(ctx, r) - require.NoError(t, err) - - seedWasm, err := watzero.Wat2Wasm(seedWat) - require.NoError(t, err) - - code, err := r.CompileModule(ctx, seedWasm, wazero.NewCompileConfig()) - require.NoError(t, err) - - mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithRandSource(bytes.NewReader(seed))) - require.NoError(t, err) - - seedFn := mod.ExportedFunction("seed") - - _, err = seedFn.Call(ctx) - require.NoError(t, err) - - // If this test doesn't break, the seed is deterministic. - require.Equal(t, `==> env.~lib/builtins/seed() -<== (7.949928895127363e-275) -`, log.String()) + require.Equal(t, 7.949928895127363e-275, seed(mod)) } func TestSeed_error(t *testing.T) { @@ -238,18 +127,14 @@ func TestSeed_error(t *testing.T) { expectedErr string }{ { - name: "not 8 bytes", - source: bytes.NewReader([]byte{0, 1}), - expectedErr: `error reading random seed: unexpected EOF (recovered by wazero) -wasm stack trace: - env.~lib/builtins/seed() f64`, + name: "not 8 bytes", + source: bytes.NewReader([]byte{0, 1}), + expectedErr: `error reading random seed: unexpected EOF`, }, { - name: "error reading", - source: iotest.ErrReader(errors.New("ice cream")), - expectedErr: `error reading random seed: ice cream (recovered by wazero) -wasm stack trace: - env.~lib/builtins/seed() f64`, + name: "error reading", + source: iotest.ErrReader(errors.New("ice cream")), + expectedErr: `error reading random seed: ice cream`, }, } @@ -257,36 +142,17 @@ wasm stack trace: tc := tt t.Run(tc.name, func(t *testing.T) { - var log bytes.Buffer + mod, r := requireModule(t, wazero.NewModuleConfig().WithRandSource(tc.source)) + defer r.Close(testCtx) - // Set context to one that has an experimental listener - ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log)) - - r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter()) - defer r.Close(ctx) - - _, err := Instantiate(ctx, r) - require.NoError(t, err) - - seedWasm, err := watzero.Wat2Wasm(seedWat) - require.NoError(t, err) - - compiled, err := r.CompileModule(ctx, seedWasm, wazero.NewCompileConfig()) - require.NoError(t, err) - - exporter := wazero.NewModuleConfig().WithName(t.Name()).WithRandSource(tc.source) - mod, err := r.InstantiateModule(ctx, compiled, exporter) - require.NoError(t, err) - - _, err = mod.ExportedFunction("seed").Call(ctx) + err := require.CapturePanic(func() { seed(mod) }) require.EqualError(t, err, tc.expectedErr) - require.Equal(t, `==> env.~lib/builtins/seed() -`, log.String()) }) } } -func TestTrace(t *testing.T) { +// TestFunctionExporter_Trace ensures the trace output is according to configuration. +func TestFunctionExporter_Trace(t *testing.T) { noArgs := []uint64{4, 0, 0, 0, 0, 0, 0} noArgsLog := `==> env.~lib/builtins/trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0) <== () @@ -298,11 +164,12 @@ func TestTrace(t *testing.T) { expected, expectedLog string }{ { - name: "disabled", - exporter: NewFunctionExporter(), - params: noArgs, - expected: "", - expectedLog: noArgsLog, + name: "disabled", + exporter: NewFunctionExporter(), + params: noArgs, + expected: "", + // expect no host call since it is disabled. ==> is host and --> is wasm. + expectedLog: strings.ReplaceAll(noArgsLog, "==", "--"), }, { name: "ToStderr", @@ -359,10 +226,10 @@ func TestTrace(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - var out, log bytes.Buffer + var stderr, functionLog bytes.Buffer // Set context to one that has an experimental listener - ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log)) + ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&functionLog)) r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter()) defer r.Close(ctx) @@ -372,7 +239,11 @@ func TestTrace(t *testing.T) { _, err := envBuilder.Instantiate(ctx, r) require.NoError(t, err) - traceWasm, err := watzero.Wat2Wasm(traceWat) + traceWasm, err := watzero.Wat2Wasm(`(module + (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64))) + (memory 1 1) + (export "trace" (func 0)) +)`) require.NoError(t, err) code, err := r.CompileModule(ctx, traceWasm, wazero.NewCompileConfig()) @@ -380,9 +251,9 @@ func TestTrace(t *testing.T) { config := wazero.NewModuleConfig() if strings.Contains("ToStderr", tc.name) { - config = config.WithStderr(&out) + config = config.WithStderr(&stderr) } else { - config = config.WithStdout(&out) + config = config.WithStdout(&stderr) } mod, err := r.InstantiateModule(ctx, code, config) @@ -396,8 +267,8 @@ func TestTrace(t *testing.T) { _, err = mod.ExportedFunction("trace").Call(ctx, tc.params...) require.NoError(t, err) - require.Equal(t, tc.expected, out.String()) - require.Equal(t, tc.expectedLog, log.String()) + require.Equal(t, tc.expected, stderr.String()) + require.Equal(t, tc.expectedLog, functionLog.String()) }) } } @@ -406,24 +277,20 @@ func TestTrace_error(t *testing.T) { tests := []struct { name string message []byte - out io.Writer + stderr io.Writer expectedErr string }{ { - name: "not 8 bytes", - message: encodeUTF16("hello")[:5], - out: &bytes.Buffer{}, - expectedErr: `read an odd number of bytes for utf-16 string: 5 (recovered by wazero) -wasm stack trace: - env.~lib/builtins/trace(i32,i32,f64,f64,f64,f64,f64)`, + name: "not 8 bytes", + message: encodeUTF16("hello")[:5], + stderr: &bytes.Buffer{}, + expectedErr: `invalid UTF-16 reading message`, }, { - name: "error writing", - message: encodeUTF16("hello"), - out: &errWriter{err: errors.New("ice cream")}, - expectedErr: `ice cream (recovered by wazero) -wasm stack trace: - env.~lib/builtins/trace(i32,i32,f64,f64,f64,f64,f64)`, + name: "error writing", + message: encodeUTF16("hello"), + stderr: &errWriter{err: errors.New("ice cream")}, + expectedErr: `ice cream`, }, } @@ -431,43 +298,27 @@ wasm stack trace: tc := tt t.Run(tc.name, func(t *testing.T) { - var log bytes.Buffer + mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(tc.stderr)) + defer r.Close(testCtx) - // Set context to one that has an experimental listener - ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log)) - - r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter()) - defer r.Close(ctx) - - envBuilder := r.NewModuleBuilder("env") - NewFunctionExporter().WithTraceToStdout().ExportFunctions(envBuilder) - _, err := envBuilder.Instantiate(ctx, r) - require.NoError(t, err) - - traceWasm, err := watzero.Wat2Wasm(traceWat) - require.NoError(t, err) - - compiled, err := r.CompileModule(ctx, traceWasm, wazero.NewCompileConfig()) - require.NoError(t, err) - - exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(tc.out) - mod, err := r.InstantiateModule(ctx, compiled, exporter) - require.NoError(t, err) - - ok := mod.Memory().WriteUint32Le(ctx, 0, uint32(len(tc.message))) + ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(tc.message))) require.True(t, ok) - ok = mod.Memory().Write(ctx, uint32(4), tc.message) + ok = mod.Memory().Write(testCtx, uint32(4), tc.message) require.True(t, ok) - _, err = mod.ExportedFunction("trace").Call(ctx, 4, 0, 0, 0, 0, 0, 0) + err := require.CapturePanic(func() { + traceToStderr(testCtx, mod, 4, 0, 0, 0, 0, 0, 0) + }) require.EqualError(t, err, tc.expectedErr) - require.Equal(t, `==> env.~lib/builtins/trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0) -`, log.String()) }) } } -func Test_readAssemblyScriptString(t *testing.T) { +func Test_requireAssemblyScriptString(t *testing.T) { + var stderr bytes.Buffer + mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(&stderr)) + defer r.Close(testCtx) + tests := []struct { name string memory func(context.Context, api.Memory) @@ -476,42 +327,42 @@ func Test_readAssemblyScriptString(t *testing.T) { }{ { name: "success", - memory: func(ctx context.Context, memory api.Memory) { - memory.WriteUint32Le(ctx, 0, 10) + memory: func(testCtx context.Context, memory api.Memory) { + memory.WriteUint32Le(testCtx, 0, 10) b := encodeUTF16("hello") - memory.Write(ctx, 4, b) + memory.Write(testCtx, 4, b) }, offset: 4, expected: "hello", }, { name: "can't read size", - memory: func(ctx context.Context, memory api.Memory) { + memory: func(testCtx context.Context, memory api.Memory) { b := encodeUTF16("hello") - memory.Write(ctx, 0, b) + memory.Write(testCtx, 0, b) }, offset: 0, // will attempt to read size from offset -4 - expectedErr: "Memory.ReadUint32Le(4294967292) out of range", + expectedErr: "out of memory reading message", }, { name: "odd size", - memory: func(ctx context.Context, memory api.Memory) { - memory.WriteUint32Le(ctx, 0, 9) + memory: func(testCtx context.Context, memory api.Memory) { + memory.WriteUint32Le(testCtx, 0, 9) b := encodeUTF16("hello") - memory.Write(ctx, 4, b) + memory.Write(testCtx, 4, b) }, offset: 4, - expectedErr: "read an odd number of bytes for utf-16 string: 9", + expectedErr: "invalid UTF-16 reading message", }, { name: "can't read string", - memory: func(ctx context.Context, memory api.Memory) { - memory.WriteUint32Le(ctx, 0, 10_000_000) // set size to too large value + memory: func(testCtx context.Context, memory api.Memory) { + memory.WriteUint32Le(testCtx, 0, 10_000_000) // set size to too large value b := encodeUTF16("hello") - memory.Write(ctx, 4, b) + memory.Write(testCtx, 4, b) }, offset: 4, - expectedErr: "Memory.Read(4, 10000000) out of range", + expectedErr: "out of memory reading message", }, } @@ -519,41 +370,35 @@ func Test_readAssemblyScriptString(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - r := wazero.NewRuntime() - defer r.Close(testCtx) - - mod, err := r.NewModuleBuilder("mod"). - ExportMemory("memory", 1). - Instantiate(testCtx, r) - require.NoError(t, err) - tc.memory(testCtx, mod.Memory()) - s, err := readAssemblyScriptString(testCtx, mod, uint32(tc.offset)) if tc.expectedErr != "" { + err := require.CapturePanic(func() { + _ = requireAssemblyScriptString(testCtx, mod, "message", uint32(tc.offset)) + }) require.EqualError(t, err, tc.expectedErr) } else { - require.NoError(t, err) + s := requireAssemblyScriptString(testCtx, mod, "message", uint32(tc.offset)) require.Equal(t, tc.expected, s) } }) } } -func writeAbortMessageAndFileName(ctx context.Context, t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (int, int) { - off := 0 - ok := mem.WriteUint32Le(ctx, uint32(off), uint32(len(messageUTF16))) +func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (uint32, uint32) { + off := uint32(0) + ok := mem.WriteUint32Le(testCtx, off, uint32(len(messageUTF16))) require.True(t, ok) off += 4 messageOff := off - ok = mem.Write(ctx, uint32(off), messageUTF16) + ok = mem.Write(testCtx, off, messageUTF16) require.True(t, ok) - off += len(messageUTF16) - ok = mem.WriteUint32Le(ctx, uint32(off), uint32(len(fileNameUTF16))) + off += uint32(len(messageUTF16)) + ok = mem.WriteUint32Le(testCtx, off, uint32(len(fileNameUTF16))) require.True(t, ok) off += 4 filenameOff := off - ok = mem.Write(ctx, uint32(off), fileNameUTF16) + ok = mem.Write(testCtx, off, fileNameUTF16) require.True(t, ok) return messageOff, filenameOff } @@ -575,3 +420,16 @@ type errWriter struct { func (w *errWriter) Write([]byte) (int, error) { return 0, w.err } + +func requireModule(t *testing.T, config wazero.ModuleConfig) (api.Module, api.Closer) { + r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter()) + + compiled, err := r.NewModuleBuilder(t.Name()). + ExportMemoryWithMax("memory", 1, 1). + Compile(testCtx, wazero.NewCompileConfig()) + require.NoError(t, err) + + mod, err := r.InstantiateModule(testCtx, compiled, config) + require.NoError(t, err) + return mod, r +} diff --git a/emscripten/emscripten.go b/emscripten/emscripten.go index 8344b2f1..7ba6db47 100644 --- a/emscripten/emscripten.go +++ b/emscripten/emscripten.go @@ -17,6 +17,7 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasm" ) // Instantiate instantiates the "env" module used by Emscripten into the @@ -50,14 +51,10 @@ type functionExporter struct{} // ExportFunctions implements FunctionExporter.ExportFunctions func (e *functionExporter) ExportFunctions(builder wazero.ModuleBuilder) { - env := &emscripten{} - builder.ExportFunction("emscripten_notify_memory_growth", env.emscriptenNotifyMemoryGrowth, + builder.ExportFunction("emscripten_notify_memory_growth", emscriptenNotifyMemoryGrowth, "emscripten_notify_memory_growth", "memory_index") } -// emscripten implements common imports used by standalone wasm. -type emscripten struct{} - // emscriptenNotifyMemoryGrowth is called when wasm is compiled with // `-s ALLOW_MEMORY_GROWTH` and a "memory.grow" instruction succeeded. // The memoryIndex parameter will be zero until "multi-memory" is implemented. @@ -73,4 +70,7 @@ type emscripten struct{} // // See https://github.com/emscripten-core/emscripten/blob/3.1.16/system/lib/standalone/standalone.c#L118 // and https://emscripten.org/docs/api_reference/emscripten.h.html#abi-functions -func (a *emscripten) emscriptenNotifyMemoryGrowth(uint32) {} +var emscriptenNotifyMemoryGrowth = &wasm.Func{ + Type: &wasm.FunctionType{Params: []wasm.ValueType{wasm.ValueTypeI32}}, + Code: &wasm.Code{Body: []byte{wasm.OpcodeEnd}}, +} diff --git a/emscripten/emscripten_test.go b/emscripten/emscripten_test.go index b7c22244..4d026298 100644 --- a/emscripten/emscripten_test.go +++ b/emscripten/emscripten_test.go @@ -20,6 +20,7 @@ var growWasm []byte // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") +// TestGrow is an integration test until we have an Emscripten example. func TestGrow(t *testing.T) { var log bytes.Buffer @@ -40,6 +41,6 @@ func TestGrow(t *testing.T) { require.Error(t, err) require.Zero(t, err.(*sys.ExitError).ExitCode()) - // We expect the memory no-op memory growth hook to be invoked. - require.Contains(t, log.String(), "==> env.emscripten_notify_memory_growth(memory_index=0)") + // We expect the memory no-op memory growth hook to be invoked as wasm. + require.Contains(t, log.String(), "--> env.emscripten_notify_memory_growth(memory_index=0)") } diff --git a/experimental/log_listener_test.go b/experimental/log_listener_test.go index abf287ba..c7961b51 100644 --- a/experimental/log_listener_test.go +++ b/experimental/log_listener_test.go @@ -261,8 +261,8 @@ func Test_loggingListener(t *testing.T) { }, } - out := bytes.NewBuffer(nil) - lf := experimental.NewLoggingListenerFactory(out) + var out bytes.Buffer + lf := experimental.NewLoggingListenerFactory(&out) fnV := reflect.ValueOf(func() {}) for _, tt := range tests { tc := tt diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go index 0a5ce5b2..14008132 100644 --- a/internal/engine/compiler/engine_test.go +++ b/internal/engine/compiler/engine_test.go @@ -7,6 +7,7 @@ import ( "testing" "unsafe" + "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/enginetest" "github.com/tetratelabs/wazero/internal/testing/require" @@ -22,12 +23,17 @@ var et = &engineTester{} // engineTester implements enginetest.EngineTester. type engineTester struct{} -// NewEngine implements enginetest.EngineTester NewEngine. +// ListenerFactory implements the same method as documented on enginetest.EngineTester. +func (e *engineTester) ListenerFactory() experimental.FunctionListenerFactory { + return nil +} + +// NewEngine implements the same method as documented on enginetest.EngineTester. func (e *engineTester) NewEngine(enabledFeatures wasm.Features) wasm.Engine { return newEngine(enabledFeatures) } -// InitTables implements enginetest.EngineTester InitTables. +// InitTables implements the same method as documented on enginetest.EngineTester. func (e engineTester) InitTables(me wasm.ModuleEngine, tableIndexToLen map[wasm.Index]int, tableInits []wasm.TableInitEntry) [][]wasm.Reference { references := make([][]wasm.Reference, len(tableIndexToLen)) for tableIndex, l := range tableIndexToLen { @@ -44,7 +50,7 @@ func (e engineTester) InitTables(me wasm.ModuleEngine, tableIndexToLen map[wasm. return references } -// CompiledFunctionPointerValue implements enginetest.EngineTester CompiledFunctionPointerValue. +// CompiledFunctionPointerValue implements the same method as documented on enginetest.EngineTester. func (e engineTester) CompiledFunctionPointerValue(me wasm.ModuleEngine, funcIndex wasm.Index) uint64 { internal := me.(*moduleEngine) return uint64(uintptr(unsafe.Pointer(internal.functions[funcIndex]))) @@ -55,7 +61,7 @@ func TestCompiler_Engine_NewModuleEngine(t *testing.T) { enginetest.RunTestEngine_NewModuleEngine(t, et) } -func TestInterpreter_Engine_InitializeFuncrefGlobals(t *testing.T) { +func TestCompiler_Engine_InitializeFuncrefGlobals(t *testing.T) { enginetest.RunTestEngine_InitializeFuncrefGlobals(t, et) } diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go index a06cb13e..0f67fc9b 100644 --- a/internal/engine/interpreter/interpreter.go +++ b/internal/engine/interpreter/interpreter.go @@ -111,18 +111,13 @@ func (ce *callEngine) popValue() (v uint64) { return } -// peekValues peeks api.ValueType values from the stack and returns them in reverse order. +// peekValues peeks api.ValueType values from the stack and returns them. func (ce *callEngine) peekValues(count int) []uint64 { if count == 0 { return nil } stackLen := len(ce.stack) - peeked := ce.stack[stackLen-count : stackLen] - values := make([]uint64, 0, count) - for i := count - 1; i >= 0; i-- { - values = append(values, peeked[i]) - } - return values + return ce.stack[stackLen-count : stackLen] } func (ce *callEngine) drop(r *wazeroir.InclusiveRange) { diff --git a/internal/engine/interpreter/interpreter_test.go b/internal/engine/interpreter/interpreter_test.go index af96869a..7d531438 100644 --- a/internal/engine/interpreter/interpreter_test.go +++ b/internal/engine/interpreter/interpreter_test.go @@ -1,6 +1,7 @@ package interpreter import ( + "bytes" "context" "fmt" "math" @@ -8,6 +9,7 @@ import ( "testing" "unsafe" + "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/buildoptions" "github.com/tetratelabs/wazero/internal/testing/enginetest" "github.com/tetratelabs/wazero/internal/testing/require" @@ -24,7 +26,7 @@ func TestInterpreter_peekValues(t *testing.T) { ce.stack = []uint64{5, 4, 3, 2, 1} require.Nil(t, ce.peekValues(0)) - require.Equal(t, []uint64{1, 2}, ce.peekValues(2)) + require.Equal(t, []uint64{2, 1}, ce.peekValues(2)) } func TestInterpreter_CallEngine_PushFrame(t *testing.T) { @@ -62,10 +64,16 @@ func TestInterpreter_CallEngine_PushFrame_StackOverflow(t *testing.T) { // et is used for tests defined in the enginetest package. var et = &engineTester{} +var functionLog bytes.Buffer +var listenerFactory = experimental.NewLoggingListenerFactory(&functionLog) // engineTester implements enginetest.EngineTester. type engineTester struct{} +func (e engineTester) ListenerFactory() experimental.FunctionListenerFactory { + return listenerFactory +} + // NewEngine implements enginetest.EngineTester NewEngine. func (e engineTester) NewEngine(enabledFeatures wasm.Features) wasm.Engine { return NewEngine(enabledFeatures) @@ -107,15 +115,96 @@ func TestInterpreter_Engine_NewModuleEngine_InitTable(t *testing.T) { } func TestInterpreter_ModuleEngine_Call(t *testing.T) { + defer functionLog.Reset() enginetest.RunTestModuleEngine_Call(t, et) + require.Equal(t, `--> .$0(1,2) +<-- (1,2) +`, functionLog.String()) } func TestInterpreter_ModuleEngine_Call_HostFn(t *testing.T) { + defer functionLog.Reset() enginetest.RunTestModuleEngine_Call_HostFn(t, et) + require.Equal(t, `==> .$0(3) +<== (3) +--> imported.wasm_div_by(1) +<-- (1) +==> host.host_div_by(1) +<== (1) +--> imported.call->host_div_by(1) + ==> host.host_div_by(1) + <== (1) +<-- (1) +--> importing.call_import->call->host_div_by(1) + --> imported.call->host_div_by(1) + ==> host.host_div_by(1) + <== (1) + <-- (1) +<-- (1) +`, functionLog.String()) } func TestInterpreter_ModuleEngine_Call_Errors(t *testing.T) { + defer functionLog.Reset() enginetest.RunTestModuleEngine_Call_Errors(t, et) + + // TODO: Currently, the listener doesn't get notified on errors as they are + // implemented with panic. This means the end hooks aren't make resulting + // in dangling logs like this: + // ==> host.host_div_by(4294967295) + // instead of seeing a return like + // <== DivByZero + require.Equal(t, `==> host.host_div_by(1) +<== (1) +==> host.host_div_by(1) +<== (1) +--> imported.wasm_div_by(1) +<-- (1) +--> imported.wasm_div_by(1) +<-- (1) +--> imported.wasm_div_by(0) +--> imported.wasm_div_by(1) +<-- (1) +==> host.host_div_by(4294967295) +==> host.host_div_by(1) +<== (1) +==> host.host_div_by(0) +==> host.host_div_by(1) +<== (1) +--> imported.call->host_div_by(4294967295) + ==> host.host_div_by(4294967295) +--> imported.call->host_div_by(1) + ==> host.host_div_by(1) + <== (1) +<-- (1) +--> importing.call_import->call->host_div_by(0) + --> imported.call->host_div_by(0) + ==> host.host_div_by(0) +--> importing.call_import->call->host_div_by(1) + --> imported.call->host_div_by(1) + ==> host.host_div_by(1) + <== (1) + <-- (1) +<-- (1) +--> importing.call_import->call->host_div_by(4294967295) + --> imported.call->host_div_by(4294967295) + ==> host.host_div_by(4294967295) +--> importing.call_import->call->host_div_by(1) + --> imported.call->host_div_by(1) + ==> host.host_div_by(1) + <== (1) + <-- (1) +<-- (1) +--> importing.call_import->call->host_div_by(0) + --> imported.call->host_div_by(0) + ==> host.host_div_by(0) +--> importing.call_import->call->host_div_by(1) + --> imported.call->host_div_by(1) + ==> host.host_div_by(1) + <== (1) + <-- (1) +<-- (1) +`, functionLog.String()) } func TestInterpreter_ModuleEngine_Memory(t *testing.T) { diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go index c10bbf85..b4fc8b5c 100644 --- a/internal/sys/sys_test.go +++ b/internal/sys/sys_test.go @@ -14,6 +14,16 @@ import ( "github.com/tetratelabs/wazero/sys" ) +func TestContext_FS(t *testing.T) { + sysCtx := DefaultContext(testfs.FS{}) + + require.Equal(t, NewFSContext(testfs.FS{}), sysCtx.FS(testCtx)) + + // can override to something else + fsc := NewFSContext(testfs.FS{"foo": &testfs.File{}}) + require.Equal(t, fsc, sysCtx.FS(context.WithValue(testCtx, FSKey{}, fsc))) +} + func TestDefaultSysContext(t *testing.T) { sysCtx, err := NewContext( 0, // max diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go index 8e1c7464..8e9bf062 100644 --- a/internal/testing/enginetest/enginetest.go +++ b/internal/testing/enginetest/enginetest.go @@ -24,6 +24,7 @@ import ( "testing" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -37,9 +38,13 @@ var ( type EngineTester interface { NewEngine(enabledFeatures wasm.Features) wasm.Engine + + ListenerFactory() experimental.FunctionListenerFactory + // InitTables returns expected table contents ([]wasm.Reference) per table. InitTables(me wasm.ModuleEngine, tableIndexToLen map[wasm.Index]int, tableInits []wasm.TableInitEntry) [][]wasm.Reference + // CompiledFunctionPointerValue returns the opaque compiledFunction's pointer for the `funcIndex`. CompiledFunctionPointerValue(tme wasm.ModuleEngine, funcIndex wasm.Index) uint64 } @@ -81,7 +86,7 @@ func RunTestEngine_InitializeFuncrefGlobals(t *testing.T, et EngineTester) { // To use the function, we first need to add it to a module. instance := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} - fns := instance.BuildFunctions(m, nil) + fns := instance.BuildFunctions(m, buildListeners(et.ListenerFactory(), m)) me, err := e.NewModuleEngine(t.Name(), m, nil, fns, nil, nil) require.NoError(t, err) @@ -106,14 +111,24 @@ func RunTestEngine_InitializeFuncrefGlobals(t *testing.T, et EngineTester) { } func RunTestModuleEngine_Call(t *testing.T, et EngineTester) { - e := et.NewEngine(wasm.Features20191205) + e := et.NewEngine(wasm.Features20220419) - // Define a basic function which defines one parameter. This is used to test results when incorrect arity is used. + // Define a basic function which defines two parameters and two results. + // This is used to test results when incorrect arity is used. i64 := wasm.ValueTypeI64 m := &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}, ParamNumInUint64: 1, ResultNumInUint64: 1}}, + TypeSection: []*wasm.FunctionType{ + { + Params: []wasm.ValueType{i64, i64}, + Results: []wasm.ValueType{i64, i64}, + ParamNumInUint64: 2, + ResultNumInUint64: 2, + }, + }, FunctionSection: []uint32{0}, - CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}, LocalTypes: []wasm.ValueType{wasm.ValueTypeI64}}}, + CodeSection: []*wasm.Code{ + {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeEnd}}, + }, } m.BuildFunctionDefinitions() err := e.CompileModule(testCtx, m) @@ -121,7 +136,7 @@ func RunTestModuleEngine_Call(t *testing.T, et EngineTester) { // To use the function, we first need to add it to a module. module := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} - module.Functions = module.BuildFunctions(m, nil) + module.Functions = module.BuildFunctions(m, buildListeners(et.ListenerFactory(), m)) // Compile the module me, err := e.NewModuleEngine(module.Name, m, nil, module.Functions, nil, nil) @@ -130,18 +145,18 @@ func RunTestModuleEngine_Call(t *testing.T, et EngineTester) { // Ensure the base case doesn't fail: A single parameter should work as that matches the function signature. fn := module.Functions[0] - results, err := me.Call(testCtx, module.CallCtx, fn, 3) + results, err := me.Call(testCtx, module.CallCtx, fn, 1, 2) require.NoError(t, err) - require.Equal(t, uint64(3), results[0]) + require.Equal(t, []uint64{1, 2}, results) t.Run("errs when not enough parameters", func(t *testing.T) { _, err := me.Call(testCtx, module.CallCtx, fn) - require.EqualError(t, err, "expected 1 params, but passed 0") + require.EqualError(t, err, "expected 2 params, but passed 0") }) t.Run("errs when too many parameters", func(t *testing.T) { - _, err := me.Call(testCtx, module.CallCtx, fn, 1, 2) - require.EqualError(t, err, "expected 1 params, but passed 2") + _, err := me.Call(testCtx, module.CallCtx, fn, 1, 2, 3) + require.EqualError(t, err, "expected 2 params, but passed 3") }) } @@ -186,7 +201,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { require.NoError(t, err) module := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} - fns := module.BuildFunctions(m, nil) + fns := module.BuildFunctions(m, buildListeners(et.ListenerFactory(), m)) var func1, func2 = uint32(2), uint32(1) tableInits := []wasm.TableInitEntry{ @@ -221,7 +236,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { require.NoError(t, err) imported := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} - importedFunctions := imported.BuildFunctions(importedModule, nil) + importedFunctions := imported.BuildFunctions(importedModule, buildListeners(et.ListenerFactory(), importedModule)) // Imported functions are compiled before the importing module is instantiated. importedMe, err := e.NewModuleEngine(t.Name(), importedModule, nil, importedFunctions, nil, nil) @@ -245,7 +260,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { } importing := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} - fns := importing.BuildFunctions(importingModule, nil) + fns := importing.BuildFunctions(importingModule, buildListeners(et.ListenerFactory(), importingModule)) importingMe, err := e.NewModuleEngine(t.Name(), importingModule, importedFunctions, fns, tables, tableInits) require.NoError(t, err) @@ -272,7 +287,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { err := e.CompileModule(testCtx, importedModule) require.NoError(t, err) imported := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} - importedFunctions := imported.BuildFunctions(importedModule, nil) + importedFunctions := imported.BuildFunctions(importedModule, buildListeners(et.ListenerFactory(), importedModule)) // Imported functions are compiled before the importing module is instantiated. importedMe, err := e.NewModuleEngine(t.Name(), importedModule, nil, importedFunctions, nil, nil) @@ -292,7 +307,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { require.NoError(t, err) importing := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} - fns := importing.BuildFunctions(importingModule, nil) + fns := importing.BuildFunctions(importingModule, buildListeners(et.ListenerFactory(), importingModule)) var func1, func2 = uint32(0), uint32(4) tableInits := []wasm.TableInitEntry{ @@ -341,7 +356,7 @@ func runTestModuleEngine_Call_HostFn_ModuleContext(t *testing.T, et EngineTester _, ns := wasm.NewStore(features, e) modCtx := wasm.NewCallContext(ns, module, nil) - fns := module.BuildFunctions(m, nil) + fns := module.BuildFunctions(m, buildListeners(et.ListenerFactory(), m)) me, err := e.NewModuleEngine(t.Name(), m, nil, fns, nil, nil) require.NoError(t, err) @@ -359,7 +374,7 @@ func RunTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester) { e := et.NewEngine(wasm.Features20191205) - host, imported, importing, close := setupCallTests(t, e) + host, imported, importing, close := setupCallTests(t, e, et.ListenerFactory()) defer close() // Ensure the base case doesn't fail: A single parameter should work as that matches the function signature. @@ -405,7 +420,7 @@ func RunTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester) { func RunTestModuleEngine_Call_Errors(t *testing.T, et EngineTester) { e := et.NewEngine(wasm.Features20191205) - host, imported, importing, close := setupCallTests(t, e) + host, imported, importing, close := setupCallTests(t, e, et.ListenerFactory()) defer close() tests := []struct { @@ -591,7 +606,7 @@ func RunTestModuleEngine_Memory(t *testing.T, et EngineTester) { var memory api.Memory = module.Memory // To use functions, we need to instantiate them (associate them with a ModuleInstance). - module.Functions = module.BuildFunctions(m, nil) + module.Functions = module.BuildFunctions(m, buildListeners(et.ListenerFactory(), m)) module.BuildExports(m.ExportSection) grow, init := module.Functions[0], module.Functions[1] @@ -666,7 +681,7 @@ func divBy(d uint32) uint32 { return 1 / d // go panics if d == 0 } -func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, *wasm.ModuleInstance, *wasm.ModuleInstance, func()) { +func setupCallTests(t *testing.T, e wasm.Engine, fnlf experimental.FunctionListenerFactory) (*wasm.ModuleInstance, *wasm.ModuleInstance, *wasm.ModuleInstance, func()) { i32 := wasm.ValueTypeI32 ft := &wasm.FunctionType{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1} @@ -686,7 +701,7 @@ func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, *wasm.Mo err := e.CompileModule(testCtx, hostModule) require.NoError(t, err) host := &wasm.ModuleInstance{Name: hostModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}} - host.Functions = host.BuildFunctions(hostModule, nil) + host.Functions = host.BuildFunctions(hostModule, buildListeners(fnlf, hostModule)) host.BuildExports(hostModule.ExportSection) hostFn := host.Exports[hostFnName].Function @@ -721,7 +736,7 @@ func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, *wasm.Mo require.NoError(t, err) imported := &wasm.ModuleInstance{Name: importedModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}} - importedFunctions := imported.BuildFunctions(importedModule, nil) + importedFunctions := imported.BuildFunctions(importedModule, buildListeners(fnlf, importedModule)) imported.Functions = append([]*wasm.FunctionInstance{hostFn}, importedFunctions...) imported.BuildExports(importedModule.ExportSection) callHostFn := imported.Exports[callHostFnName].Function @@ -754,7 +769,7 @@ func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, *wasm.Mo // Add the exported function. importing := &wasm.ModuleInstance{Name: importingModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}} - importingFunctions := importing.BuildFunctions(importingModule, nil) + importingFunctions := importing.BuildFunctions(importingModule, buildListeners(fnlf, importingModule)) importing.Functions = append([]*wasm.FunctionInstance{callHostFn}, importingFunctions...) importing.BuildExports(importingModule.ExportSection) @@ -782,3 +797,15 @@ func linkModuleToEngine(module *wasm.ModuleInstance, me wasm.ModuleEngine) { // callEngineModuleContextModuleInstanceAddressOffset module.CallCtx = wasm.NewCallContext(nil, module, nil) } + +func buildListeners(factory experimental.FunctionListenerFactory, m *wasm.Module) []experimental.FunctionListener { + if factory == nil || len(m.FunctionSection) == 0 { + return nil + } + listeners := make([]experimental.FunctionListener, len(m.FunctionSection)) + importCount := m.ImportFuncCount() + for i := 0; i < len(listeners); i++ { + listeners[i] = factory.NewListener(m.FunctionDefinitionSection[uint32(i)+importCount]) + } + return listeners +} diff --git a/internal/wasm/gofunc.go b/internal/wasm/gofunc.go index 98747342..60cef086 100644 --- a/internal/wasm/gofunc.go +++ b/internal/wasm/gofunc.go @@ -148,8 +148,19 @@ func newModuleVal(m api.Module) reflect.Value { return val } +// MustFunctionType returns the function type corresponding to the function +// signature or panics if invalid. +func MustFunctionType(fn interface{}) *FunctionType { + fnV := reflect.ValueOf(fn) + _, ft, err := getFunctionType(&fnV) + if err != nil { + panic(err) + } + return ft +} + // getFunctionType returns the function type corresponding to the function signature or errs if invalid. -func getFunctionType(fn *reflect.Value, enabledFeatures Features) (fk FunctionKind, ft *FunctionType, err error) { +func getFunctionType(fn *reflect.Value) (fk FunctionKind, ft *FunctionType, err error) { p := fn.Type() if fn.Kind() != reflect.Func { @@ -169,14 +180,6 @@ func getFunctionType(fn *reflect.Value, enabledFeatures Features) (fk FunctionKi rCount := p.NumOut() - if rCount > 1 { - // Guard >1.0 feature multi-value - if err = enabledFeatures.Require(FeatureMultiValue); err != nil { - err = fmt.Errorf("multiple result types invalid as %v", err) - return - } - } - ft = &FunctionType{Params: make([]ValueType, p.NumIn()-pOffset), Results: make([]ValueType, rCount)} ft.CacheNumInUint64() diff --git a/internal/wasm/gofunc_test.go b/internal/wasm/gofunc_test.go index a090783a..57c215bb 100644 --- a/internal/wasm/gofunc_test.go +++ b/internal/wasm/gofunc_test.go @@ -87,7 +87,7 @@ func TestGetFunctionType(t *testing.T) { t.Run(tc.name, func(t *testing.T) { rVal := reflect.ValueOf(tc.inputFunc) - fk, ft, err := getFunctionType(&rVal, Features20191205|FeatureMultiValue) + fk, ft, err := getFunctionType(&rVal) require.NoError(t, err) require.Equal(t, tc.expectedKind, fk) require.Equal(t, tc.expectedType, ft) @@ -122,11 +122,6 @@ func TestGetFunctionTypeErrors(t *testing.T) { input: func() error { return nil }, expectedErr: "result[0] is an error, which is unsupported", }, - { - name: "multiple results - multi-value not enabled", - input: func() (uint64, uint32) { return 0, 0 }, - expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled", - }, { name: "multiple context types", input: func(api.Module, context.Context) error { return nil }, @@ -149,7 +144,7 @@ func TestGetFunctionTypeErrors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { rVal := reflect.ValueOf(tc.input) - _, _, err := getFunctionType(&rVal, Features20191205) + _, _, err := getFunctionType(&rVal) require.EqualError(t, err, tc.expectedErr) }) } @@ -254,7 +249,7 @@ func TestPopGoFuncParams(t *testing.T) { t.Run(tc.name, func(t *testing.T) { goFunc := reflect.ValueOf(tc.inputFunc) - fk, _, err := getFunctionType(&goFunc, Features20220419) + fk, _, err := getFunctionType(&goFunc) require.NoError(t, err) vals := PopGoFuncParams(&FunctionInstance{Kind: fk, GoFunc: &goFunc}, (&stack{stackVals}).pop) @@ -407,7 +402,7 @@ func TestCallGoFunc(t *testing.T) { t.Run(tc.name, func(t *testing.T) { goFunc := reflect.ValueOf(tc.inputFunc) - fk, _, err := getFunctionType(&goFunc, Features20220419) + fk, _, err := getFunctionType(&goFunc) require.NoError(t, err) results := CallGoFunc(testCtx, callCtx, &FunctionInstance{Kind: fk, GoFunc: &goFunc}, tc.inputParams) diff --git a/internal/wasm/host.go b/internal/wasm/host.go index 5d0094ca..4f74ec21 100644 --- a/internal/wasm/host.go +++ b/internal/wasm/host.go @@ -9,6 +9,16 @@ import ( "github.com/tetratelabs/wazero/internal/wasmdebug" ) +// Func is a function with an inlined type, typically used for NewHostModule. +type Func struct { + // Type is the equivalent function in the SectionIDType. + // This will resolve to an existing or new element. + Type *FunctionType + + // Code is the equivalent function in the SectionIDCode. + Code *Code +} + // NewHostModule is defined internally for use in WASI tests and to keep the code size in the root directory small. func NewHostModule( moduleName string, @@ -79,7 +89,7 @@ func addFuncs( nameToGoFunc map[string]interface{}, funcToNames map[string][]string, enabledFeatures Features, -) error { +) (err error) { funcCount := uint32(len(nameToGoFunc)) funcNames := make([]string, 0, funcCount) if m.NameSection == nil { @@ -100,19 +110,36 @@ func addFuncs( for idx := Index(0); idx < funcCount; idx++ { exportName := funcNames[idx] debugName := wasmdebug.FuncName(moduleName, exportName, idx) - fn := reflect.ValueOf(nameToGoFunc[exportName]) - _, functionType, err := getFunctionType(&fn, enabledFeatures) - if err != nil { - return fmt.Errorf("func[%s] %w", debugName, err) - } - names := funcToNames[exportName] - namesLen := len(names) - if namesLen > 1 && namesLen-1 != len(functionType.Params) { - return fmt.Errorf("func[%s] has %d params, but %d param names", debugName, namesLen-1, len(functionType.Params)) + + gf := nameToGoFunc[exportName] + var ft *FunctionType + if hf, ok := gf.(*Func); ok { + ft = hf.Type + m.CodeSection = append(m.CodeSection, hf.Code) + } else { + fn := reflect.ValueOf(gf) + _, ft, err = getFunctionType(&fn) + if err != nil { + return fmt.Errorf("func[%s] %w", debugName, err) + } + m.CodeSection = append(m.CodeSection, &Code{GoFunc: &fn}) + } + + m.FunctionSection = append(m.FunctionSection, m.maybeAddType(ft)) + + names := funcToNames[exportName] + namesLen := len(names) + if namesLen > 1 && namesLen-1 != len(ft.Params) { + return fmt.Errorf("func[%s] has %d params, but %d param names", debugName, namesLen-1, len(ft.Params)) + } + if len(ft.Results) > 1 { + // Guard >1.0 feature multi-value + if err = enabledFeatures.Require(FeatureMultiValue); err != nil { + err = fmt.Errorf("func[%s] multiple result types invalid as %v", debugName, err) + return + } } - m.FunctionSection = append(m.FunctionSection, m.maybeAddType(functionType)) - m.CodeSection = append(m.CodeSection, &Code{GoFunc: &fn}) m.ExportSection = append(m.ExportSection, &Export{Type: ExternTypeFunc, Name: exportName, Index: idx}) if namesLen > 0 { m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: names[0]}) diff --git a/internal/wasm/instruction.go b/internal/wasm/instruction.go index 857633ff..e92fc706 100644 --- a/internal/wasm/instruction.go +++ b/internal/wasm/instruction.go @@ -248,7 +248,7 @@ const ( // OpcodeRefFunc pushes a funcref value whose index equals the immediate to this opcode. // This is defined in the reference-types proposal, but necessary for FeatureBulkMemoryOperations as well. // - // Currently only supported in the constant expression in element segments. + // Currently, this is only supported in the constant expression in element segments. OpcodeRefFunc = 0xd2 // Below are toggled with FeatureSignExtensionOps diff --git a/wasi_snapshot_preview1/args.go b/wasi_snapshot_preview1/args.go new file mode 100644 index 00000000..e0f6928b --- /dev/null +++ b/wasi_snapshot_preview1/args.go @@ -0,0 +1,92 @@ +package wasi_snapshot_preview1 + +import ( + "context" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasm" +) + +const ( + functionArgsGet = "args_get" + functionArgsSizesGet = "args_sizes_get" +) + +// argsGet is the WASI function named functionArgsGet that reads command-line +// argument data. +// +// Parameters +// +// * argv: offset to begin writing argument offsets in uint32 little-endian +// encoding to api.Memory +// * argsSizesGet result argc * 4 bytes are written to this offset +// * argvBuf: offset to write the null terminated arguments to api.Memory +// * argsSizesGet result argv_buf_size bytes are written to this offset +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoFault: there is not enough memory to write results +// +// For example, if argsSizesGet wrote argc=2 and argvBufSize=5 for arguments: +// "a" and "bc" parameters argv=7 and argvBuf=1, this function writes the below +// to api.Memory: +// +// argvBufSize uint32le uint32le +// +----------------+ +--------+ +--------+ +// | | | | | | +// []byte{?, 'a', 0, 'b', 'c', 0, ?, 1, 0, 0, 0, 3, 0, 0, 0, ?} +// argvBuf --^ ^ ^ +// argv --| | +// offset that begins "a" --+ | +// offset that begins "bc" --+ +// +// See argsSizesGet +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_get +// See https://en.wikipedia.org/wiki/Null-terminated_string +func argsGet(ctx context.Context, mod api.Module, argv, argvBuf uint32) Errno { + sysCtx := mod.(*wasm.CallContext).Sys + return writeOffsetsAndNullTerminatedValues(ctx, mod.Memory(), sysCtx.Args(), argv, argvBuf) +} + +// argsSizesGet is the WASI function named functionArgsSizesGet that reads +// command-line argument sizes. +// +// Parameters +// +// * resultArgc: offset to write the argument count to api.Memory +// * resultArgvBufSize: offset to write the null-terminated argument length to +// api.Memory +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoFault: there is not enough memory to write results +// +// For example, if args are "a", "bc" and parameters resultArgc=1 and +// resultArgvBufSize=6, this function writes the below to api.Memory: +// +// uint32le uint32le +// +--------+ +--------+ +// | | | | +// []byte{?, 2, 0, 0, 0, ?, 5, 0, 0, 0, ?} +// resultArgc --^ ^ +// 2 args --+ | +// resultArgvBufSize --| +// len([]byte{'a',0,'b',c',0}) --+ +// +// See argsGet +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_sizes_get +// See https://en.wikipedia.org/wiki/Null-terminated_string +func argsSizesGet(ctx context.Context, mod api.Module, resultArgc, resultArgvBufSize uint32) Errno { + sysCtx := mod.(*wasm.CallContext).Sys + mem := mod.Memory() + + if !mem.WriteUint32Le(ctx, resultArgc, uint32(len(sysCtx.Args()))) { + return ErrnoFault + } + if !mem.WriteUint32Le(ctx, resultArgvBufSize, sysCtx.ArgsSize()) { + return ErrnoFault + } + return ErrnoSuccess +} diff --git a/wasi_snapshot_preview1/args_test.go b/wasi_snapshot_preview1/args_test.go new file mode 100644 index 00000000..dd02b68a --- /dev/null +++ b/wasi_snapshot_preview1/args_test.go @@ -0,0 +1,191 @@ +package wasi_snapshot_preview1 + +import ( + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +func Test_argsGet(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig().WithArgs("a", "bc")) + defer r.Close(testCtx) + + argv := uint32(7) // arbitrary offset + argvBuf := uint32(1) // arbitrary offset + expectedMemory := []byte{ + '?', // argvBuf is after this + 'a', 0, 'b', 'c', 0, // null terminated "a", "bc" + '?', // argv is after this + 1, 0, 0, 0, // little endian-encoded offset of "a" + 3, 0, 0, 0, // little endian-encoded offset of "bc" + '?', // stopped after encoding + } + + maskMemory(t, testCtx, mod, len(expectedMemory)) + + // Invoke argsGet and check the memory side effects. + requireErrno(t, ErrnoSuccess, mod, functionArgsGet, uint64(argv), uint64(argvBuf)) + require.Equal(t, ` +==> wasi_snapshot_preview1.args_get(argv=7,argv_buf=1) +<== ESUCCESS +`, "\n"+log.String()) + + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) + require.True(t, ok) + require.Equal(t, expectedMemory, actual) +} + +func Test_argsGet_Errors(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig().WithArgs("a", "bc")) + defer r.Close(testCtx) + + memorySize := mod.Memory().Size(testCtx) + validAddress := uint32(0) // arbitrary valid address as arguments to args_get. We chose 0 here. + + tests := []struct { + name string + argv, argvBuf uint32 + expectedLog string + }{ + { + name: "out-of-memory argv", + argv: memorySize, + argvBuf: validAddress, + expectedLog: ` +==> wasi_snapshot_preview1.args_get(argv=65536,argv_buf=0) +<== EFAULT +`, + }, + { + name: "out-of-memory argvBuf", + argv: validAddress, + argvBuf: memorySize, + expectedLog: ` +==> wasi_snapshot_preview1.args_get(argv=0,argv_buf=65536) +<== EFAULT +`, + }, + { + name: "argv exceeds the maximum valid address by 1", + // 4*argCount is the size of the result of the pointers to args, 4 is the size of uint32 + argv: memorySize - 4*2 + 1, + argvBuf: validAddress, + expectedLog: ` +==> wasi_snapshot_preview1.args_get(argv=65529,argv_buf=0) +<== EFAULT +`, + }, + { + name: "argvBuf exceeds the maximum valid address by 1", + argv: validAddress, + // "a", "bc" size = size of "a0bc0" = 5 + argvBuf: memorySize - 5 + 1, + expectedLog: ` +==> wasi_snapshot_preview1.args_get(argv=0,argv_buf=65532) +<== EFAULT +`, + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + requireErrno(t, ErrnoFault, mod, functionArgsGet, uint64(tc.argv), uint64(tc.argvBuf)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) + }) + } +} + +func Test_argsSizesGet(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig().WithArgs("a", "bc")) + defer r.Close(testCtx) + + resultArgc := uint32(1) // arbitrary offset + resultArgvBufSize := uint32(6) // arbitrary offset + expectedMemory := []byte{ + '?', // resultArgc is after this + 0x2, 0x0, 0x0, 0x0, // little endian-encoded arg count + '?', // resultArgvBufSize is after this + 0x5, 0x0, 0x0, 0x0, // little endian-encoded size of null terminated strings + '?', // stopped after encoding + } + + maskMemory(t, testCtx, mod, len(expectedMemory)) + + // Invoke argsSizesGet and check the memory side effects. + requireErrno(t, ErrnoSuccess, mod, functionArgsSizesGet, uint64(resultArgc), uint64(resultArgvBufSize)) + require.Equal(t, ` +==> wasi_snapshot_preview1.args_sizes_get(result.argc=1,result.argv_buf_size=6) +<== ESUCCESS +`, "\n"+log.String()) + + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) + require.True(t, ok) + require.Equal(t, expectedMemory, actual) +} + +func Test_argsSizesGet_Errors(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig().WithArgs("a", "bc")) + defer r.Close(testCtx) + + memorySize := mod.Memory().Size(testCtx) + validAddress := uint32(0) // arbitrary valid address as arguments to args_sizes_get. We chose 0 here. + + tests := []struct { + name string + argc, argvBufSize uint32 + expectedLog string + }{ + { + name: "out-of-memory argc", + argc: memorySize, + argvBufSize: validAddress, + expectedLog: ` +==> wasi_snapshot_preview1.args_sizes_get(result.argc=65536,result.argv_buf_size=0) +<== EFAULT +`, + }, + { + name: "out-of-memory argvBufSize", + argc: validAddress, + argvBufSize: memorySize, + expectedLog: ` +==> wasi_snapshot_preview1.args_sizes_get(result.argc=0,result.argv_buf_size=65536) +<== EFAULT +`, + }, + { + name: "argc exceeds the maximum valid address by 1", + argc: memorySize - 4 + 1, // 4 is the size of uint32, the type of the count of args + argvBufSize: validAddress, + expectedLog: ` +==> wasi_snapshot_preview1.args_sizes_get(result.argc=65533,result.argv_buf_size=0) +<== EFAULT +`, + }, + { + name: "argvBufSize exceeds the maximum valid size by 1", + argc: validAddress, + argvBufSize: memorySize - 4 + 1, // 4 is count of bytes to encode uint32le + expectedLog: ` +==> wasi_snapshot_preview1.args_sizes_get(result.argc=0,result.argv_buf_size=65533) +<== EFAULT +`, + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + requireErrno(t, ErrnoFault, mod, functionArgsSizesGet, uint64(tc.argc), uint64(tc.argvBufSize)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) + }) + } +} diff --git a/wasi_snapshot_preview1/clock.go b/wasi_snapshot_preview1/clock.go index a2071e0d..7d01f6d2 100644 --- a/wasi_snapshot_preview1/clock.go +++ b/wasi_snapshot_preview1/clock.go @@ -9,42 +9,40 @@ import ( ) const ( - // functionClockResGet returns the resolution of a clock. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_res_getid-clockid---errno-timestamp - functionClockResGet = "clock_res_get" - - // importClockResGet is the WebAssembly 1.0 Text format import of functionClockResGet. - importClockResGet = `(import "wasi_snapshot_preview1" "clock_res_get" - (func $wasi.clock_res_get (param $id i32) (param $result.resolution i32) (result (;errno;) i32)))` - - // functionClockTimeGet returns the time value of a clock. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_time_getid-clockid-precision-timestamp---errno-timestamp + functionClockResGet = "clock_res_get" functionClockTimeGet = "clock_time_get" - - // importClockTimeGet is the WebAssembly 1.0 Text format import of functionClockTimeGet. - importClockTimeGet = `(import "wasi_snapshot_preview1" "clock_time_get" - (func $wasi.clock_time_get (param $id i32) (param $precision i64) (param $result.timestamp i32) (result (;errno;) i32)))` ) // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clockid-enumu32 const ( - // clockIDRealtime is the clock ID named "realtime" associated with sys.Walltime + // clockIDRealtime is the name ID named "realtime" like sys.Walltime clockIDRealtime = iota - // clockIDMonotonic is the clock ID named "monotonic" with sys.Nanotime + // clockIDMonotonic is the name ID named "monotonic" like sys.Nanotime clockIDMonotonic - // clockIDProcessCputime is the unsupported clock ID named "process_cputime_id" + // clockIDProcessCputime is the unsupported "process_cputime_id" clockIDProcessCputime - // clockIDThreadCputime is the unsupported clock ID named "thread_cputime_id" + // clockIDThreadCputime is the unsupported "thread_cputime_id" clockIDThreadCputime ) -// ClockResGet is the WASI function named functionClockResGet that returns the resolution of time values returned by ClockTimeGet. +// clockResGet is the WASI function named functionClockResGet that returns the +// resolution of time values returned by clockTimeGet. // -// * id - The clock id for which to return the time. -// * resultResolution - the offset to write the resolution to mod.Memory -// * the resolution is an uint64 little-endian encoding. +// Parameters // -// For example, if the resolution is 100ns, this function writes the below to `mod.Memory`: +// * id: clock ID to use +// * resultResolution: offset to write the resolution to api.Memory +// * the resolution is an uint64 little-endian encoding +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoNotsup: the clock ID is not supported. +// * ErrnoInval: the clock ID is invalid. +// * ErrnoFault: there is not enough memory to write results +// +// For example, if the resolution is 100ns, this function writes the below to +// api.Memory: // // uint64le // +-------------------------------------+ @@ -52,11 +50,10 @@ const ( // []byte{?, 0x64, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ?} // resultResolution --^ // -// Note: importClockResGet shows this signature in the WebAssembly 1.0 Text Format. // Note: This is similar to `clock_getres` in POSIX. // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_res_getid-clockid---errno-timestamp // See https://linux.die.net/man/3/clock_getres -func (a *wasi) ClockResGet(ctx context.Context, mod api.Module, id uint32, resultResolution uint32) Errno { +func clockResGet(ctx context.Context, mod api.Module, id uint32, resultResolution uint32) Errno { sysCtx := mod.(*wasm.CallContext).Sys var resolution uint64 // ns @@ -66,8 +63,9 @@ func (a *wasi) ClockResGet(ctx context.Context, mod api.Module, id uint32, resul case clockIDMonotonic: resolution = uint64(sysCtx.NanotimeResolution()) case clockIDProcessCputime, clockIDThreadCputime: - // Similar to many other runtimes, we only support realtime and monotonic clocks. Other types - // are slated to be removed from the next version of WASI. + // Similar to many other runtimes, we only support realtime and + // monotonic clocks. Other types are slated to be removed from the next + // version of WASI. return ErrnoNotsup default: return ErrnoInval @@ -78,15 +76,27 @@ func (a *wasi) ClockResGet(ctx context.Context, mod api.Module, id uint32, resul return ErrnoSuccess } -// ClockTimeGet is the WASI function named functionClockTimeGet that returns the time value of a clock (time.Now). +// clockTimeGet is the WASI function named functionClockTimeGet that returns +// the time value of a name (time.Now). // -// * id - The clock id for which to return the time. -// * precision - The maximum lag (exclusive) that the returned time value may have, compared to its actual value. -// * resultTimestamp - the offset to write the timestamp to mod.Memory -// * the timestamp is epoch nanoseconds encoded as a uint64 little-endian encoding. +// Parameters // -// For example, if time.Now returned exactly midnight UTC 2022-01-01 (1640995200000000000), and -// parameters resultTimestamp=1, this function writes the below to `mod.Memory`: +// * id: clock ID to use +// * precision: maximum lag (exclusive) that the returned time value may have, +// compared to its actual value +// * resultTimestamp: offset to write the timestamp to api.Memory +// * the timestamp is epoch nanos encoded as a little-endian uint64 +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoNotsup: the clock ID is not supported. +// * ErrnoInval: the clock ID is invalid. +// * ErrnoFault: there is not enough memory to write results +// +// For example, if time.Now returned exactly midnight UTC 2022-01-01 +// (1640995200000000000), and parameters resultTimestamp=1, this function +// writes the below to api.Memory: // // uint64le // +------------------------------------------+ @@ -94,11 +104,10 @@ func (a *wasi) ClockResGet(ctx context.Context, mod api.Module, id uint32, resul // []byte{?, 0x0, 0x0, 0x1f, 0xa6, 0x70, 0xfc, 0xc5, 0x16, ?} // resultTimestamp --^ // -// Note: importClockTimeGet shows this signature in the WebAssembly 1.0 Text Format. // Note: This is similar to `clock_gettime` in POSIX. // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_time_getid-clockid-precision-timestamp---errno-timestamp // See https://linux.die.net/man/3/clock_gettime -func (a *wasi) ClockTimeGet(ctx context.Context, mod api.Module, id uint32, precision uint64, resultTimestamp uint32) Errno { +func clockTimeGet(ctx context.Context, mod api.Module, id uint32, precision uint64, resultTimestamp uint32) Errno { // TODO: precision is currently ignored. sysCtx := mod.(*wasm.CallContext).Sys @@ -110,8 +119,9 @@ func (a *wasi) ClockTimeGet(ctx context.Context, mod api.Module, id uint32, prec case clockIDMonotonic: val = uint64(sysCtx.Nanotime(ctx)) case clockIDProcessCputime, clockIDThreadCputime: - // Similar to many other runtimes, we only support realtime and monotonic clocks. Other types - // are slated to be removed from the next version of WASI. + // Similar to many other runtimes, we only support realtime and + // monotonic clocks. Other types are slated to be removed from the next + // version of WASI. return ErrnoNotsup default: return ErrnoInval diff --git a/wasi_snapshot_preview1/clock_test.go b/wasi_snapshot_preview1/clock_test.go index f09aac10..c5928995 100644 --- a/wasi_snapshot_preview1/clock_test.go +++ b/wasi_snapshot_preview1/clock_test.go @@ -2,19 +2,15 @@ package wasi_snapshot_preview1 import ( _ "embed" - "fmt" "testing" + "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/wasm" ) -// Test_ClockResGet only tests it is stubbed for GrainLang per #271 -func Test_ClockResGet(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionClockResGet, importClockResGet, nil) - defer mod.Close(testCtx) - - resultResolution := uint32(1) // arbitrary offset +func Test_clockResGet(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig()) + defer r.Close(testCtx) expectedMemoryMicro := []byte{ '?', // resultResolution is after this @@ -30,56 +26,41 @@ func Test_ClockResGet(t *testing.T) { tests := []struct { name string - clockID uint64 + clockID uint32 expectedMemory []byte - invocation func(clockID uint64) Errno + expectedLog string }{ { - name: "wasi.ClockResGet", - clockID: 0, + name: "Realtime", + clockID: clockIDRealtime, expectedMemory: expectedMemoryMicro, - invocation: func(clockID uint64) Errno { - return a.ClockResGet(testCtx, mod, uint32(clockID), resultResolution) - }, + expectedLog: ` +==> wasi_snapshot_preview1.clock_res_get(id=0,result.resolution=1) +<== ESUCCESS +`, }, { - name: "wasi.ClockResGet", - clockID: 1, + name: "Monotonic", + clockID: clockIDMonotonic, expectedMemory: expectedMemoryNano, - invocation: func(clockID uint64) Errno { - return a.ClockResGet(testCtx, mod, uint32(clockID), resultResolution) - }, - }, - { - name: functionClockResGet, - clockID: 0, - expectedMemory: expectedMemoryMicro, - invocation: func(clockID uint64) Errno { - results, err := fn.Call(testCtx, clockID, uint64(resultResolution)) - require.NoError(t, err) - return Errno(results[0]) // results[0] is the errno - }, - }, - { - name: functionClockResGet, - clockID: 1, - expectedMemory: expectedMemoryNano, - invocation: func(clockID uint64) Errno { - results, err := fn.Call(testCtx, clockID, uint64(resultResolution)) - require.NoError(t, err) - return Errno(results[0]) // results[0] is the errno - }, + expectedLog: ` +==> wasi_snapshot_preview1.clock_res_get(id=1,result.resolution=1) +<== ESUCCESS +`, }, } for _, tt := range tests { tc := tt - t.Run(fmt.Sprintf("%v/clockID=%v", tc.name, tc.clockID), func(t *testing.T) { + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + maskMemory(t, testCtx, mod, len(tc.expectedMemory)) - errno := tc.invocation(tc.clockID) - require.Equal(t, ErrnoSuccess, errno, ErrnoName(errno)) + resultResolution := uint32(1) // arbitrary offset + requireErrno(t, ErrnoSuccess, mod, functionClockResGet, uint64(tc.clockID), uint64(resultResolution)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(tc.expectedMemory))) require.True(t, ok) @@ -88,30 +69,42 @@ func Test_ClockResGet(t *testing.T) { } } -func Test_ClockResGet_Unsupported(t *testing.T) { - resultResolution := uint32(1) // arbitrary offset - mod, fn := instantiateModule(testCtx, t, functionClockResGet, importClockResGet, nil) - defer mod.Close(testCtx) +func Test_clockResGet_Unsupported(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig()) + defer r.Close(testCtx) tests := []struct { name string - clockID uint64 + clockID uint32 expectedErrno Errno + expectedLog string }{ { name: "process cputime", clockID: 2, expectedErrno: ErrnoNotsup, + expectedLog: ` +==> wasi_snapshot_preview1.clock_res_get(id=2,result.resolution=1) +<== ENOTSUP +`, }, { name: "thread cputime", clockID: 3, expectedErrno: ErrnoNotsup, + expectedLog: ` +==> wasi_snapshot_preview1.clock_res_get(id=3,result.resolution=1) +<== ENOTSUP +`, }, { name: "undefined", clockID: 100, expectedErrno: ErrnoInval, + expectedLog: ` +==> wasi_snapshot_preview1.clock_res_get(id=100,result.resolution=1) +<== EINVAL +`, }, } @@ -119,100 +112,80 @@ func Test_ClockResGet_Unsupported(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - results, err := fn.Call(testCtx, tc.clockID, uint64(resultResolution)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) + defer log.Reset() + + resultResolution := uint32(1) // arbitrary offset + requireErrno(t, tc.expectedErrno, mod, functionClockResGet, uint64(tc.clockID), uint64(resultResolution)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } } -func Test_ClockTimeGet(t *testing.T) { - resultTimestamp := uint32(1) // arbitrary offset +func Test_clockTimeGet(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig()) + defer r.Close(testCtx) - mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil) - defer mod.Close(testCtx) - - clocks := []struct { - clock string - id uint32 + tests := []struct { + name string + clockID uint32 expectedMemory []byte + expectedLog string }{ { - clock: "Realtime", - id: clockIDRealtime, + name: "Realtime", + clockID: clockIDRealtime, expectedMemory: []byte{ '?', // resultTimestamp is after this 0x0, 0x0, 0x1f, 0xa6, 0x70, 0xfc, 0xc5, 0x16, // little endian-encoded epochNanos '?', // stopped after encoding }, + expectedLog: ` +==> wasi_snapshot_preview1.clock_time_get(id=0,precision=0,result.timestamp=1) +<== ESUCCESS +`, }, { - clock: "Monotonic", - id: clockIDMonotonic, + name: "Monotonic", + clockID: clockIDMonotonic, expectedMemory: []byte{ '?', // resultTimestamp is after this 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // fake nanotime starts at zero '?', // stopped after encoding }, + expectedLog: ` +==> wasi_snapshot_preview1.clock_time_get(id=1,precision=0,result.timestamp=1) +<== ESUCCESS +`, }, } - for _, c := range clocks { - cc := c - t.Run(cc.clock, func(t *testing.T) { - tests := []struct { - name string - invocation func() Errno - }{ - { - name: "wasi.ClockTimeGet", - invocation: func() Errno { - return a.ClockTimeGet(testCtx, mod, cc.id, 0 /* TODO: precision */, resultTimestamp) - }, - }, - { - name: functionClockTimeGet, - invocation: func() Errno { - results, err := fn.Call(testCtx, uint64(cc.id), 0 /* TODO: precision */, uint64(resultTimestamp)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - return errno - }, - }, - } + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - // Reset the fake clock - sysCtx, err := newSysContext(nil, nil, nil) - require.NoError(t, err) - mod.(*wasm.CallContext).Sys = sysCtx + maskMemory(t, testCtx, mod, len(tc.expectedMemory)) - maskMemory(t, testCtx, mod, len(cc.expectedMemory)) + resultTimestamp := uint32(1) // arbitrary offset + requireErrno(t, ErrnoSuccess, mod, functionClockTimeGet, uint64(tc.clockID), 0 /* TODO: precision */, uint64(resultTimestamp)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) - errno := tc.invocation() - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(cc.expectedMemory))) - require.True(t, ok) - require.Equal(t, cc.expectedMemory, actual) - }) - } + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(tc.expectedMemory))) + require.True(t, ok) + require.Equal(t, tc.expectedMemory, actual) }) } } -func Test_ClockTimeGet_Unsupported(t *testing.T) { - resultTimestamp := uint32(1) // arbitrary offset - mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil) - defer mod.Close(testCtx) +func Test_clockTimeGet_Unsupported(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig()) + defer r.Close(testCtx) tests := []struct { name string - clockID uint64 + clockID uint32 expectedErrno Errno + expectedLog string }{ { name: "process cputime", @@ -235,24 +208,26 @@ func Test_ClockTimeGet_Unsupported(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - results, err := fn.Call(testCtx, tc.clockID, 0 /* TODO: precision */, uint64(resultTimestamp)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno + defer log.Reset() + + resultTimestamp := uint32(1) // arbitrary offset + errno := clockTimeGet(testCtx, mod, tc.clockID, 0 /* TODO: precision */, resultTimestamp) require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) + require.Equal(t, tc.expectedLog, log.String()) }) } } -func Test_ClockTimeGet_Errors(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil) - defer mod.Close(testCtx) +func Test_clockTimeGet_Errors(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig()) + defer r.Close(testCtx) memorySize := mod.Memory().Size(testCtx) tests := []struct { - name string - resultTimestamp uint32 - argvBufSize uint32 + name string + resultTimestamp, argvBufSize uint32 + expectedLog string }{ { name: "resultTimestamp out-of-memory", @@ -269,10 +244,11 @@ func Test_ClockTimeGet_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - results, err := fn.Call(testCtx, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(tc.resultTimestamp)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno + defer log.Reset() + + errno := clockTimeGet(testCtx, mod, 0 /* TODO: id */, 0 /* TODO: precision */, tc.resultTimestamp) require.Equal(t, ErrnoFault, errno, ErrnoName(errno)) + require.Equal(t, tc.expectedLog, log.String()) }) } } diff --git a/wasi_snapshot_preview1/environ.go b/wasi_snapshot_preview1/environ.go new file mode 100644 index 00000000..027adcc3 --- /dev/null +++ b/wasi_snapshot_preview1/environ.go @@ -0,0 +1,95 @@ +package wasi_snapshot_preview1 + +import ( + "context" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasm" +) + +const ( + functionEnvironGet = "environ_get" + functionEnvironSizesGet = "environ_sizes_get" +) + +// environGet is the WASI function named functionEnvironGet that reads +// environment variables. +// +// Parameters +// +// * environ: offset to begin writing environment offsets in uint32 +// little-endian encoding to api.Memory +// * environSizesGet result environc * 4 bytes are written to this offset +// * environBuf: offset to write the null-terminated variables to api.Memory +// * the format is like os.Environ: null-terminated "key=val" entries +// * environSizesGet result environBufSize bytes are written to this offset +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoFault: there is not enough memory to write results +// +// For example, if environSizesGet wrote environc=2 and environBufSize=9 for +// environment variables: "a=b", "b=cd" and parameters environ=11 and +// environBuf=1, this function writes the below to api.Memory: +// +// environBufSize uint32le uint32le +// +------------------------------------+ +--------+ +--------+ +// | | | | | | +// []byte{?, 'a', '=', 'b', 0, 'b', '=', 'c', 'd', 0, ?, 1, 0, 0, 0, 5, 0, 0, 0, ?} +// environBuf --^ ^ ^ +// environ offset for "a=b" --+ | +// environ offset for "b=cd" --+ +// +// See environSizesGet +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_get +// See https://en.wikipedia.org/wiki/Null-terminated_string +func environGet(ctx context.Context, mod api.Module, environ uint32, environBuf uint32) Errno { + sysCtx := mod.(*wasm.CallContext).Sys + return writeOffsetsAndNullTerminatedValues(ctx, mod.Memory(), sysCtx.Environ(), environ, environBuf) +} + +// environSizesGet is the WASI function named functionEnvironSizesGet that +// reads environment variable sizes. +// +// Parameters +// +// * resultEnvironc: offset to write the count of environment variables to +// api.Memory +// * resultEnvironBufSize: offset to write the null-terminated environment +// variable length to api.Memory +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoFault: there is not enough memory to write results +// +// For example, if environ are "a=b","b=cd" and parameters resultEnvironc=1 and +// resultEnvironBufSize=6, this function writes the below to api.Memory: +// +// uint32le uint32le +// +--------+ +--------+ +// | | | | +// []byte{?, 2, 0, 0, 0, ?, 9, 0, 0, 0, ?} +// resultEnvironc --^ ^ +// 2 variables --+ | +// resultEnvironBufSize --| +// len([]byte{'a','=','b',0, | +// 'b','=','c','d',0}) --+ +// +// See environGet +// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_sizes_get +// and https://en.wikipedia.org/wiki/Null-terminated_string +func environSizesGet(ctx context.Context, mod api.Module, resultEnvironc uint32, resultEnvironBufSize uint32) Errno { + sysCtx := mod.(*wasm.CallContext).Sys + mem := mod.Memory() + + if !mem.WriteUint32Le(ctx, resultEnvironc, uint32(len(sysCtx.Environ()))) { + return ErrnoFault + } + if !mem.WriteUint32Le(ctx, resultEnvironBufSize, sysCtx.EnvironSize()) { + return ErrnoFault + } + + return ErrnoSuccess +} diff --git a/wasi_snapshot_preview1/environ_test.go b/wasi_snapshot_preview1/environ_test.go new file mode 100644 index 00000000..708c93ef --- /dev/null +++ b/wasi_snapshot_preview1/environ_test.go @@ -0,0 +1,164 @@ +package wasi_snapshot_preview1 + +import ( + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +func Test_environGet(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig(). + WithEnv("a", "b").WithEnv("b", "cd")) + defer r.Close(testCtx) + + resultEnviron := uint32(11) // arbitrary offset + resultEnvironBuf := uint32(1) // arbitrary offset + expectedMemory := []byte{ + '?', // environBuf is after this + 'a', '=', 'b', 0, // null terminated "a=b", + 'b', '=', 'c', 'd', 0, // null terminated "b=cd" + '?', // environ is after this + 1, 0, 0, 0, // little endian-encoded offset of "a=b" + 5, 0, 0, 0, // little endian-encoded offset of "b=cd" + '?', // stopped after encoding + } + + maskMemory(t, testCtx, mod, len(expectedMemory)) + + // Invoke environGet and check the memory side effects. + requireErrno(t, ErrnoSuccess, mod, functionEnvironGet, uint64(resultEnviron), uint64(resultEnvironBuf)) + require.Equal(t, ` +==> wasi_snapshot_preview1.environ_get(environ=11,environ_buf=1) +<== ESUCCESS +`, "\n"+log.String()) + + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) + require.True(t, ok) + require.Equal(t, expectedMemory, actual) +} + +func Test_environGet_Errors(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig(). + WithEnv("a", "bc").WithEnv("b", "cd")) + defer r.Close(testCtx) + + memorySize := mod.Memory().Size(testCtx) + validAddress := uint32(0) // arbitrary valid address as arguments to environ_get. We chose 0 here. + + tests := []struct { + name string + environ, environBuf uint32 + expectedLog string + }{ + { + name: "out-of-memory environPtr", + environ: memorySize, + environBuf: validAddress, + }, + { + name: "out-of-memory environBufPtr", + environ: validAddress, + environBuf: memorySize, + }, + { + name: "environPtr exceeds the maximum valid address by 1", + // 4*envCount is the expected length for environPtr, 4 is the size of uint32 + environ: memorySize - 4*2 + 1, + environBuf: validAddress, + }, + { + name: "environBufPtr exceeds the maximum valid address by 1", + environ: validAddress, + // "a=bc", "b=cd" size = size of "a=bc0b=cd0" = 10 + environBuf: memorySize - 10 + 1, + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + errno := environGet(testCtx, mod, tc.environ, tc.environBuf) + require.Equal(t, ErrnoFault, errno, ErrnoName(errno)) + require.Equal(t, tc.expectedLog, log.String()) + }) + } +} + +func Test_environSizesGet(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig(). + WithEnv("a", "b").WithEnv("b", "cd")) + defer r.Close(testCtx) + + resultEnvironc := uint32(1) // arbitrary offset + resultEnvironBufSize := uint32(6) // arbitrary offset + expectedMemory := []byte{ + '?', // resultEnvironc is after this + 0x2, 0x0, 0x0, 0x0, // little endian-encoded environment variable count + '?', // resultEnvironBufSize is after this + 0x9, 0x0, 0x0, 0x0, // little endian-encoded size of null terminated strings + '?', // stopped after encoding + } + + maskMemory(t, testCtx, mod, len(expectedMemory)) + + // Invoke environSizesGet and check the memory side effects. + requireErrno(t, ErrnoSuccess, mod, functionEnvironSizesGet, uint64(resultEnvironc), uint64(resultEnvironBufSize)) + require.Equal(t, ` +==> wasi_snapshot_preview1.environ_sizes_get(result.environc=1,result.environBufSize=6) +<== ESUCCESS +`, "\n"+log.String()) + + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) + require.True(t, ok) + require.Equal(t, expectedMemory, actual) +} + +func Test_environSizesGet_Errors(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig(). + WithEnv("a", "b").WithEnv("b", "cd")) + defer r.Close(testCtx) + + memorySize := mod.Memory().Size(testCtx) + validAddress := uint32(0) // arbitrary valid address as arguments to environ_sizes_get. We chose 0 here. + + tests := []struct { + name string + environc, environBufSize uint32 + expectedLog string + }{ + { + name: "out-of-memory environCountPtr", + environc: memorySize, + environBufSize: validAddress, + }, + { + name: "out-of-memory environBufSizePtr", + environc: validAddress, + environBufSize: memorySize, + }, + { + name: "environCountPtr exceeds the maximum valid address by 1", + environc: memorySize - 4 + 1, // 4 is the size of uint32, the type of the count of environ + environBufSize: validAddress, + }, + { + name: "environBufSizePtr exceeds the maximum valid size by 1", + environc: validAddress, + environBufSize: memorySize - 4 + 1, // 4 is count of bytes to encode uint32le + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + errno := environSizesGet(testCtx, mod, tc.environc, tc.environBufSize) + require.Equal(t, ErrnoFault, errno, ErrnoName(errno)) + require.Equal(t, tc.expectedLog, log.String()) + }) + } +} diff --git a/wasi_snapshot_preview1/example_test.go b/wasi_snapshot_preview1/example_test.go index 417fbb87..e0666da6 100644 --- a/wasi_snapshot_preview1/example_test.go +++ b/wasi_snapshot_preview1/example_test.go @@ -44,7 +44,7 @@ func Example() { // Override default configuration (which discards stdout). mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout).WithName("wasi-demo")) if mod != nil { - defer mod.Close(ctx) + defer r.Close(ctx) } // Note: Most compilers do not exit the module after running "_start", unless diff --git a/wasi_snapshot_preview1/fs.go b/wasi_snapshot_preview1/fs.go new file mode 100644 index 00000000..39b09ee5 --- /dev/null +++ b/wasi_snapshot_preview1/fs.go @@ -0,0 +1,678 @@ +package wasi_snapshot_preview1 + +import ( + "context" + "errors" + "io" + "io/fs" + + "github.com/tetratelabs/wazero/api" + internalsys "github.com/tetratelabs/wazero/internal/sys" + "github.com/tetratelabs/wazero/internal/wasm" +) + +const ( + functionFdAdvise = "fd_advise" + functionFdAllocate = "fd_allocate" + functionFdClose = "fd_close" + functionFdDatasync = "fd_datasync" + functionFdFdstatGet = "fd_fdstat_get" + functionFdFdstatSetFlags = "fd_fdstat_set_flags" + functionFdFdstatSetRights = "fd_fdstat_set_rights" + functionFdFilestatGet = "fd_filestat_get" + functionFdFilestatSetSize = "fd_filestat_set_size" + functionFdFilestatSetTimes = "fd_filestat_set_times" + functionFdPread = "fd_pread" + functionFdPrestatGet = "fd_prestat_get" + functionFdPrestatDirName = "fd_prestat_dir_name" + functionFdPwrite = "fd_pwrite" + functionFdRead = "fd_read" + functionFdReaddir = "fd_readdir" + functionFdRenumber = "fd_renumber" + functionFdSeek = "fd_seek" + functionFdSync = "fd_sync" + functionFdTell = "fd_tell" + functionFdWrite = "fd_write" + + functionPathCreateDirectory = "path_create_directory" + functionPathFilestatGet = "path_filestat_get" + functionPathFilestatSetTimes = "path_filestat_set_times" + functionPathLink = "path_link" + functionPathOpen = "path_open" + functionPathReadlink = "path_readlink" + functionPathRemoveDirectory = "path_remove_directory" + functionPathRename = "path_rename" + functionPathSymlink = "path_symlink" + functionPathUnlinkFile = "path_unlink_file" +) + +// fdAdvise is the WASI function named functionFdAdvise which provides file +// advisory information on a file descriptor. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_advisefd-fd-offset-filesize-len-filesize-advice-advice---errno +var fdAdvise = stubFunction(i32, i64, i64, i32) // stubbed for GrainLang per #271. + +// fdAllocate is the WASI function named functionFdAllocate which forces the +// allocation of space in a file. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_allocatefd-fd-offset-filesize-len-filesize---errno +var fdAllocate = stubFunction(i32, i64, i64) // stubbed for GrainLang per #271. + +// fdClose is the WASI function named functionFdClose which closes a file +// descriptor. +// +// Parameters +// +// * fd: file descriptor to close +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoBadf: the fd was not open. +// +// Note: This is similar to `close` in POSIX. +// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_close +// and https://linux.die.net/man/3/close +func fdClose(ctx context.Context, mod api.Module, fd uint32) Errno { + sysCtx := mod.(*wasm.CallContext).Sys + if ok := sysCtx.FS(ctx).CloseFile(ctx, fd); !ok { + return ErrnoBadf + } + + return ErrnoSuccess +} + +// fdDatasync is the WASI function named functionFdDatasync which synchronizes +// the data of a file to disk. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_datasyncfd-fd---errno +var fdDatasync = stubFunction(i32) // stubbed for GrainLang per #271. + +// fdFdstatGet is the WASI function named functionFdFdstatGet which returns the +// attributes of a file descriptor. +// +// Parameters +// +// * fd: file descriptor to get the fdstat attributes data +// * resultFdstat: offset to write the result fdstat data +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoBadf: `fd` is invalid +// * ErrnoFault: `resultFdstat` points to an offset out of memory +// +// fdstat byte layout is 24-byte size, with the following fields: +// * fs_filetype 1 byte, to indicate the file type +// * fs_flags 2 bytes, to indicate the file descriptor flag +// * 5 pad bytes +// * fs_right_base 8 bytes, to indicate the current rights of the fd +// * fs_right_inheriting 8 bytes, to indicate the maximum rights of the fd +// +// For example, with a file corresponding with `fd` was a directory (=3) opened +// with `fd_read` right (=1) and no fs_flags (=0), parameter resultFdstat=1, +// this function writes the below to api.Memory: +// +// uint16le padding uint64le uint64le +// uint8 --+ +--+ +-----------+ +--------------------+ +--------------------+ +// | | | | | | | | | +// []byte{?, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0} +// resultFdstat --^ ^-- fs_flags ^-- fs_right_base ^-- fs_right_inheriting +// | +// +-- fs_filetype +// +// Note: fdFdstatGet returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as +// well as additional fields. +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fdstat +// and https://linux.die.net/man/3/fsync +func fdFdstatGet(ctx context.Context, mod api.Module, fd uint32, resultStat uint32) Errno { + sysCtx := mod.(*wasm.CallContext).Sys + if _, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok { + return ErrnoBadf + } + // TODO: actually write the fdstat! + return ErrnoSuccess +} + +// fdFdstatSetFlags is the WASI function named functionFdFdstatSetFlags which +// adjusts the flags associated with a file descriptor. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_fdstat_set_flagsfd-fd-flags-fdflags---errnoand is stubbed for GrainLang per #271 +var fdFdstatSetFlags = stubFunction(i32, i32) // stubbed for GrainLang per #271. + +// fdFdstatSetRights is the WASI function named functionFdFdstatSetRights which +// adjusts the rights associated with a file descriptor. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_fdstat_set_rightsfd-fd-fs_rights_base-rights-fs_rights_inheriting-rights---errno +// +// Note: This will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 +var fdFdstatSetRights = stubFunction(i32, i64, i64) // stubbed for GrainLang per #271. + +// fdFilestatGet is the WASI function named functionFdFilestatGet which returns +// the attributes of an open file. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_getfd-fd---errno-filestat +var fdFilestatGet = stubFunction(i32, i32) // stubbed for GrainLang per #271. + +// fdFilestatSetSize is the WASI function named functionFdFilestatSetSize which +// adjusts the size of an open file. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_set_sizefd-fd-size-filesize---errno +var fdFilestatSetSize = stubFunction(i32, i64) // stubbed for GrainLang per #271. + +// fdFilestatSetTimes is the WASI function named functionFdFilestatSetTimes +// which adjusts the times of an open file. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_set_timesfd-fd-atim-timestamp-mtim-timestamp-fst_flags-fstflags---errno +var fdFilestatSetTimes = stubFunction(i32, i64, i64, i32) // stubbed for GrainLang per #271. + +// fdPread is the WASI function named functionFdPread which reads from a file +// descriptor, without using and updating the file descriptor's offset. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_preadfd-fd-iovs-iovec_array-offset-filesize---errno-size +var fdPread = stubFunction(i32, i32, i32, i64, i32) // stubbed for GrainLang per #271. + +// fdPrestatGet is the WASI function named functionFdPrestatGet which returns +// the prestat data of a file descriptor. +// +// Parameters +// +// * fd: file descriptor to get the prestat +// * resultPrestat: offset to write the result prestat data +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoBadf: `fd` is invalid or the `fd` is not a pre-opened directory +// * ErrnoFault: `resultPrestat` points to an offset out of memory +// +// prestat byte layout is 8 bytes, beginning with an 8-bit tag and 3 pad bytes. +// The only valid tag is `prestat_dir`, which is tag zero. This simplifies the +// byte layout to 4 empty bytes followed by the uint32le encoded path length. +// +// For example, the directory name corresponding with `fd` was "/tmp" and +// parameter resultPrestat=1, this function writes the below to api.Memory: +// +// padding uint32le +// uint8 --+ +-----+ +--------+ +// | | | | | +// []byte{?, 0, 0, 0, 0, 4, 0, 0, 0, ?} +// resultPrestat --^ ^ +// tag --+ | +// +-- size in bytes of the string "/tmp" +// +// See fdPrestatDirName and +// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#prestat +func fdPrestatGet(ctx context.Context, mod api.Module, fd uint32, resultPrestat uint32) Errno { + sysCtx := mod.(*wasm.CallContext).Sys + entry, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd) + if !ok { + return ErrnoBadf + } + + // Zero-value 8-bit tag, and 3-byte zero-value paddings, which is uint32le(0) in short. + if !mod.Memory().WriteUint32Le(ctx, resultPrestat, uint32(0)) { + return ErrnoFault + } + // Write the length of the directory name at offset 4. + if !mod.Memory().WriteUint32Le(ctx, resultPrestat+4, uint32(len(entry.Path))) { + return ErrnoFault + } + + return ErrnoSuccess +} + +// fdPrestatDirName is the WASI function named functionFdPrestatDirName which +// returns the path of the pre-opened directory of a file descriptor. +// +// Parameters +// +// * fd: file descriptor to get the path of the pre-opened directory +// * path: offset in api.Memory to write the result path +// * pathLen: count of bytes to write to `path` +// * This should match the uint32le fdPrestatGet writes to offset +// `resultPrestat`+4 +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoBadf: `fd` is invalid +// * ErrnoFault: `path` points to an offset out of memory +// * ErrnoNametoolong: `pathLen` is longer than the actual length of the result +// +// For example, the directory name corresponding with `fd` was "/tmp" and +// parameters path=1 pathLen=4 (correct), this function will write the below to +// api.Memory: +// +// pathLen +// +--------------+ +// | | +// []byte{?, '/', 't', 'm', 'p', ?} +// path --^ +// +// See fdPrestatGet +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name +func fdPrestatDirName(ctx context.Context, mod api.Module, fd uint32, pathPtr uint32, pathLen uint32) Errno { + sysCtx := mod.(*wasm.CallContext).Sys + f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd) + if !ok { + return ErrnoBadf + } + + // Some runtimes may have another semantics. See /RATIONALE.md + if uint32(len(f.Path)) < pathLen { + return ErrnoNametoolong + } + + // TODO: fdPrestatDirName may have to return ErrnoNotdir if the type of the prestat data of `fd` is not a PrestatDir. + if !mod.Memory().Write(ctx, pathPtr, []byte(f.Path)[:pathLen]) { + return ErrnoFault + } + return ErrnoSuccess +} + +// fdPwrite is the WASI function named functionFdPwrite which writes to a file +// descriptor, without using and updating the file descriptor's offset. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_pwritefd-fd-iovs-ciovec_array-offset-filesize---errno-size +var fdPwrite = stubFunction(i32, i32, i32, i64, i32) // stubbed for GrainLang per #271. + +// fdRead is the WASI function named functionFdRead which reads from a file +// descriptor. +// +// Parameters +// +// * fd: an opened file descriptor to read data from +// * iovs: offset in api.Memory to read offset, size pairs representing where +// to write file data +// * Both offset and length are encoded as uint32le +// * iovsCount: count of memory offset, size pairs to read sequentially +// starting at iovs +// * resultSize: offset in api.Memory to write the number of bytes read +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoBadf: `fd` is invalid +// * ErrnoFault: `iovs` or `resultSize` point to an offset out of memory +// * ErrnoIo: a file system error +// +// For example, this function needs to first read `iovs` to determine where +// to write contents. If parameters iovs=1 iovsCount=2, this function reads two +// offset/length pairs from api.Memory: +// +// iovs[0] iovs[1] +// +---------------------+ +--------------------+ +// | uint32le uint32le| |uint32le uint32le| +// +---------+ +--------+ +--------+ +--------+ +// | | | | | | | | +// []byte{?, 18, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 2, 0, 0, 0, ?... } +// iovs --^ ^ ^ ^ +// | | | | +// offset --+ length --+ offset --+ length --+ +// +// If the contents of the `fd` parameter was "wazero" (6 bytes) and parameter +// resultSize=26, this function writes the below to api.Memory: +// +// iovs[0].length iovs[1].length +// +--------------+ +----+ uint32le +// | | | | +--------+ +// []byte{ 0..16, ?, 'w', 'a', 'z', 'e', ?, 'r', 'o', ?, 6, 0, 0, 0 } +// iovs[0].offset --^ ^ ^ +// iovs[1].offset --+ | +// resultSize --+ +// +// Note: This is similar to `readv` in POSIX. https://linux.die.net/man/3/readv +// +// See fdWrite +// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_read +func fdRead(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno { + sysCtx := mod.(*wasm.CallContext).Sys + reader := internalsys.FdReader(ctx, sysCtx, fd) + if reader == nil { + return ErrnoBadf + } + + var nread uint32 + for i := uint32(0); i < iovsCount; i++ { + iovPtr := iovs + i*8 + offset, ok := mod.Memory().ReadUint32Le(ctx, iovPtr) + if !ok { + return ErrnoFault + } + l, ok := mod.Memory().ReadUint32Le(ctx, iovPtr+4) + if !ok { + return ErrnoFault + } + b, ok := mod.Memory().Read(ctx, offset, l) + if !ok { + return ErrnoFault + } + n, err := reader.Read(b) // Note: n <= l + nread += uint32(n) + if errors.Is(err, io.EOF) { + break + } else if err != nil { + return ErrnoIo + } + } + if !mod.Memory().WriteUint32Le(ctx, resultSize, nread) { + return ErrnoFault + } + return ErrnoSuccess +} + +// fdReaddir is the WASI function named functionFdReaddir which reads directory +// entries from a directory. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_readdirfd-fd-buf-pointeru8-buf_len-size-cookie-dircookie---errno-size +var fdReaddir = stubFunction(i32, i32, i32, i64, i32) // stubbed for GrainLang per #271. + +// fdRenumber is the WASI function named functionFdRenumber which atomically +// replaces a file descriptor by renumbering another file descriptor. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno +var fdRenumber = stubFunction(i32, i32) // stubbed for GrainLang per #271. + +// fdSeek is the WASI function named functionFdSeek which moves the offset of a +// file descriptor. +// +// Parameters +// +// * fd: file descriptor to move the offset of +// * offset: signed int64, which is encoded as uint64, input argument to +// `whence`, which results in a new offset +// * whence: operator that creates the new offset, given `offset` bytes +// * If io.SeekStart, new offset == `offset`. +// * If io.SeekCurrent, new offset == existing offset + `offset`. +// * If io.SeekEnd, new offset == file size of `fd` + `offset`. +// * resultNewoffset: offset in api.Memory to write the new offset to, +// relative to start of the file +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoBadf: `fd` is invalid +// * ErrnoFault: `resultNewoffset` points to an offset out of memory +// * ErrnoInval: `whence` is an invalid value +// * ErrnoIo: a file system error +// +// For example, if fd 3 is a file with offset 0, and parameters fd=3, offset=4, +// whence=0 (=io.SeekStart), resultNewOffset=1, this function writes the below +// to api.Memory: +// +// uint64le +// +--------------------+ +// | | +// []byte{?, 4, 0, 0, 0, 0, 0, 0, 0, ? } +// resultNewoffset --^ +// +// Note: This is similar to `lseek` in POSIX. https://linux.die.net/man/3/lseek +// +// See io.Seeker +// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_seek +func fdSeek(ctx context.Context, mod api.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) Errno { + sysCtx := mod.(*wasm.CallContext).Sys + var seeker io.Seeker + // Check to see if the file descriptor is available + if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok || f.File == nil { + return ErrnoBadf + // fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it. + } else if seeker, ok = f.File.(io.Seeker); !ok { + return ErrnoBadf + } + + if whence > io.SeekEnd /* exceeds the largest valid whence */ { + return ErrnoInval + } + newOffset, err := seeker.Seek(int64(offset), int(whence)) + if err != nil { + return ErrnoIo + } + + if !mod.Memory().WriteUint32Le(ctx, resultNewoffset, uint32(newOffset)) { + return ErrnoFault + } + + return ErrnoSuccess +} + +// fdSync is the WASI function named functionFdSync which synchronizes the data +// and metadata of a file to disk. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_syncfd-fd---errno +var fdSync = stubFunction(i32) // stubbed for GrainLang per #271. + +// fdTell is the WASI function named functionFdTell which returns the current +// offset of a file descriptor. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_tellfd-fd---errno-filesize +var fdTell = stubFunction(i32, i32) // stubbed for GrainLang per #271. + +// fdWrite is the WASI function named functionFdWrite which writes to a file +// descriptor. +// +// Parameters +// +// * fd: an opened file descriptor to write data to +// * iovs: offset in api.Memory to read offset, size pairs representing the +// data to write to `fd` +// * Both offset and length are encoded as uint32le. +// * iovsCount: count of memory offset, size pairs to read sequentially +// starting at iovs +// * resultSize: offset in api.Memory to write the number of bytes written +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoBadf: `fd` is invalid +// * ErrnoFault: `iovs` or `resultSize` point to an offset out of memory +// * ErrnoIo: a file system error +// +// For example, this function needs to first read `iovs` to determine what to +// write to `fd`. If parameters iovs=1 iovsCount=2, this function reads two +// offset/length pairs from api.Memory: +// +// iovs[0] iovs[1] +// +---------------------+ +--------------------+ +// | uint32le uint32le| |uint32le uint32le| +// +---------+ +--------+ +--------+ +--------+ +// | | | | | | | | +// []byte{?, 18, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 2, 0, 0, 0, ?... } +// iovs --^ ^ ^ ^ +// | | | | +// offset --+ length --+ offset --+ length --+ +// +// This function reads those chunks api.Memory into the `fd` sequentially. +// +// iovs[0].length iovs[1].length +// +--------------+ +----+ +// | | | | +// []byte{ 0..16, ?, 'w', 'a', 'z', 'e', ?, 'r', 'o', ? } +// iovs[0].offset --^ ^ +// iovs[1].offset --+ +// +// Since "wazero" was written, if parameter resultSize=26, this function writes +// the below to api.Memory: +// +// uint32le +// +--------+ +// | | +// []byte{ 0..24, ?, 6, 0, 0, 0', ? } +// resultSize --^ +// +// Note: This is similar to `writev` in POSIX. https://linux.die.net/man/3/writev +// +// See fdRead +// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#ciovec +// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write +func fdWrite(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno { + sysCtx := mod.(*wasm.CallContext).Sys + writer := internalsys.FdWriter(ctx, sysCtx, fd) + if writer == nil { + return ErrnoBadf + } + + var nwritten uint32 + for i := uint32(0); i < iovsCount; i++ { + iovPtr := iovs + i*8 + offset, ok := mod.Memory().ReadUint32Le(ctx, iovPtr) + if !ok { + return ErrnoFault + } + // Note: emscripten has been known to write zero length iovec. However, + // it is not common in other compilers, so we don't optimize for it. + l, ok := mod.Memory().ReadUint32Le(ctx, iovPtr+4) + if !ok { + return ErrnoFault + } + b, ok := mod.Memory().Read(ctx, offset, l) + if !ok { + return ErrnoFault + } + n, err := writer.Write(b) + if err != nil { + return ErrnoIo + } + nwritten += uint32(n) + } + if !mod.Memory().WriteUint32Le(ctx, resultSize, nwritten) { + return ErrnoFault + } + return ErrnoSuccess +} + +// pathCreateDirectory is the WASI function named functionPathCreateDirectory +// which creates a directory. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_create_directoryfd-fd-path-string---errno +var pathCreateDirectory = stubFunction(i32, i32, i32) // stubbed for GrainLang per #271. + +// pathFilestatGet is the WASI function named functionPathFilestatGet which +// returns the attributes of a file or directory. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_filestat_getfd-fd-flags-lookupflags-path-string---errno-filestat +var pathFilestatGet = stubFunction(i32, i32, i32, i32, i32) // stubbed for GrainLang per #271. + +// pathFilestatSetTimes is the WASI function named functionPathFilestatSetTimes +// which adjusts the timestamps of a file or directory. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_filestat_set_timesfd-fd-flags-lookupflags-path-string-atim-timestamp-mtim-timestamp-fst_flags-fstflags---errno +var pathFilestatSetTimes = stubFunction(i32, i32, i32, i32, i64, i64, i32) // stubbed for GrainLang per #271. + +// pathLink is the WASI function named functionPathLink which adjusts the +// timestamps of a file or directory. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#path_link +var pathLink = stubFunction(i32, i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271. + +// pathOpen is the WASI function named functionPathOpen which opens a file or +// directory. This returns ErrnoBadf if the fd is invalid. +// +// Parameters +// +// * fd: file descriptor of a directory that `path` is relative to +// * dirflags: flags to indicate how to resolve `path` +// * path: offset in api.Memory to read the path string from +// * pathLen: length of `path` +// * oFlags: open flags to indicate the method by which to open the file +// * fsRightsBase: rights of the newly created file descriptor for `path` +// * fsRightsInheriting: rights of the file descriptors derived from the newly +// created file descriptor for `path` +// * fdFlags: file descriptor flags +// * resultOpenedFd: offset in api.Memory to write the newly created file +// descriptor to. +// * The result FD value is guaranteed to be less than 2**31 +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoBadf: `fd` is invalid +// * ErrnoFault: `resultOpenedFd` points to an offset out of memory +// * ErrnoNoent: `path` does not exist. +// * ErrnoExist: `path` exists, while `oFlags` requires that it must not. +// * ErrnoNotdir: `path` is not a directory, while `oFlags` requires it. +// * ErrnoIo: a file system error +// +// For example, this function needs to first read `path` to determine the file +// to open. If parameters `path` = 1, `pathLen` = 6, and the path is "wazero", +// pathOpen reads the path from api.Memory: +// +// pathLen +// +------------------------+ +// | | +// []byte{ ?, 'w', 'a', 'z', 'e', 'r', 'o', ?... } +// path --^ +// +// Then, if parameters resultOpenedFd = 8, and this function opened a new file +// descriptor 5 with the given flags, this function writes the below to +// api.Memory: +// +// uint32le +// +--------+ +// | | +// []byte{ 0..6, ?, 5, 0, 0, 0, ?} +// resultOpenedFd --^ +// +// Notes +// * This is similar to `openat` in POSIX. https://linux.die.net/man/3/openat +// * The returned file descriptor is not guaranteed to be the lowest-number +// * Rights will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 +// +// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#path_open +func pathOpen(ctx context.Context, mod api.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase, + fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno Errno) { + sysCtx := mod.(*wasm.CallContext).Sys + fsc := sysCtx.FS(ctx) + if _, ok := fsc.OpenedFile(ctx, fd); !ok { + return ErrnoBadf + } + + b, ok := mod.Memory().Read(ctx, pathPtr, pathLen) + if !ok { + return ErrnoFault + } + + if newFD, err := fsc.OpenFile(ctx, string(b)); err != nil { + switch { + case errors.Is(err, fs.ErrNotExist): + return ErrnoNoent + case errors.Is(err, fs.ErrExist): + return ErrnoExist + default: + return ErrnoIo + } + } else if !mod.Memory().WriteUint32Le(ctx, resultOpenedFd, newFD) { + _ = fsc.CloseFile(ctx, newFD) + return ErrnoFault + } + return ErrnoSuccess +} + +// pathReadlink is the WASI function named functionPathReadlink that reads the +// contents of a symbolic link. +// +// See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_readlinkfd-fd-path-string-buf-pointeru8-buf_len-size---errno-size +var pathReadlink = stubFunction(i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271. + +// pathRemoveDirectory is the WASI function named functionPathRemoveDirectory +// which removes a directory. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_remove_directoryfd-fd-path-string---errno +var pathRemoveDirectory = stubFunction(i32, i32, i32) // stubbed for GrainLang per #271. + +// pathRename is the WASI function named functionPathRename which renames a +// file or directory. +var pathRename = stubFunction(i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271. + +// pathSymlink is the WASI function named functionPathSymlink which creates a +// symbolic link. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#path_symlink +var pathSymlink = stubFunction(i32, i32, i32, i32, i32) // stubbed for GrainLang per #271. + +// pathUnlinkFile is the WASI function named functionPathUnlinkFile which +// unlinks a file. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_unlink_filefd-fd-path-string---errno +var pathUnlinkFile = stubFunction(i32, i32, i32) // stubbed for GrainLang per #271. diff --git a/wasi_snapshot_preview1/fs_test.go b/wasi_snapshot_preview1/fs_test.go new file mode 100644 index 00000000..a4b7e01d --- /dev/null +++ b/wasi_snapshot_preview1/fs_test.go @@ -0,0 +1,1076 @@ +package wasi_snapshot_preview1 + +import ( + "bytes" + "io" + "io/fs" + "math" + "os" + "path" + "testing" + "testing/fstest" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// Test_fdAdvise only tests it is stubbed for GrainLang per #271 +func Test_fdAdvise(t *testing.T) { + log := requireErrnoNosys(t, functionFdAdvise, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_advise(fd=0,offset=0,len=0,result.advice=0) +<-- ENOSYS +`, log) +} + +// Test_fdAllocate only tests it is stubbed for GrainLang per #271 +func Test_fdAllocate(t *testing.T) { + log := requireErrnoNosys(t, functionFdAllocate, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_allocate(fd=0,offset=0,len=0) +<-- ENOSYS +`, log) +} + +func Test_fdClose(t *testing.T) { + // fd_close needs to close an open file descriptor. Open two files so that we can tell which is closed. + path1, path2 := "a", "b" + testFS := fstest.MapFS{path1: {Data: make([]byte, 0)}, path2: {Data: make([]byte, 0)}} + + mod, r, log := requireModule(t, wazero.NewModuleConfig().WithFS(testFS)) + defer r.Close(testCtx) + + // open both paths without using WASI + fsc := mod.(*wasm.CallContext).Sys.FS(testCtx) + + fdToClose, err := fsc.OpenFile(testCtx, path1) + require.NoError(t, err) + + fdToKeep, err := fsc.OpenFile(testCtx, path2) + require.NoError(t, err) + + // Close + requireErrno(t, ErrnoSuccess, mod, functionFdClose, uint64(fdToClose)) + require.Equal(t, ` +==> wasi_snapshot_preview1.fd_close(fd=4) +<== ESUCCESS +`, "\n"+log.String()) + + // Verify fdToClose is closed and removed from the opened FDs. + _, ok := fsc.OpenedFile(testCtx, fdToClose) + require.False(t, ok) + + // Verify fdToKeep is not closed + _, ok = fsc.OpenedFile(testCtx, fdToKeep) + require.True(t, ok) + + log.Reset() + t.Run("ErrnoBadF for an invalid FD", func(t *testing.T) { + requireErrno(t, ErrnoBadf, mod, functionFdClose, uint64(42)) // 42 is an arbitrary invalid FD + require.Equal(t, ` +==> wasi_snapshot_preview1.fd_close(fd=42) +<== EBADF +`, "\n"+log.String()) + }) +} + +// Test_fdDatasync only tests it is stubbed for GrainLang per #271 +func Test_fdDatasync(t *testing.T) { + log := requireErrnoNosys(t, functionFdDatasync, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_datasync(fd=0) +<-- ENOSYS +`, log) +} + +func Test_fdFdstatGet(t *testing.T) { + file, dir := "a", "b" + testFS := fstest.MapFS{file: {Data: make([]byte, 0)}, dir: {Mode: fs.ModeDir}} + + mod, r, log := requireModule(t, wazero.NewModuleConfig().WithFS(testFS)) + defer r.Close(testCtx) + memorySize := mod.Memory().Size(testCtx) + + // open both paths without using WASI + fsc := mod.(*wasm.CallContext).Sys.FS(testCtx) + + fileFd, err := fsc.OpenFile(testCtx, file) + require.NoError(t, err) + + dirFd, err := fsc.OpenFile(testCtx, dir) + require.NoError(t, err) + + tests := []struct { + name string + fd, resultStat uint32 + // TODO: expectedMem + expectedErrno Errno + expectedLog string + }{ + { + name: "file", + fd: fileFd, + // TODO: expectedMem for a file + }, + { + name: "dir", + fd: dirFd, + // TODO: expectedMem for a dir + }, + { + name: "bad FD", + fd: math.MaxUint32, + expectedErrno: ErrnoBadf, + }, + { + name: "resultStat exceeds the maximum valid address by 1", + fd: dirFd, + resultStat: memorySize - 24 + 1, + // TODO: ErrnoFault + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + errno := fdFdstatGet(testCtx, mod, tc.fd, tc.resultStat) + require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) + require.Equal(t, tc.expectedLog, log.String()) + }) + } +} + +// Test_fdFdstatSetFlags only tests it is stubbed for GrainLang per #271 +func Test_fdFdstatSetFlags(t *testing.T) { + log := requireErrnoNosys(t, functionFdFdstatSetFlags, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_fdstat_set_flags(fd=0,flags=0) +<-- ENOSYS +`, log) +} + +// Test_fdFdstatSetRights only tests it is stubbed for GrainLang per #271 +func Test_fdFdstatSetRights(t *testing.T) { + log := requireErrnoNosys(t, functionFdFdstatSetRights, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_fdstat_set_rights(fd=0,fs_rights_base=0,fs_rights_inheriting=0) +<-- ENOSYS +`, log) +} + +// Test_fdFilestatGet only tests it is stubbed for GrainLang per #271 +func Test_fdFilestatGet(t *testing.T) { + log := requireErrnoNosys(t, functionFdFilestatGet, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_filestat_get(fd=0,result.buf=0) +<-- ENOSYS +`, log) +} + +// Test_fdFilestatSetSize only tests it is stubbed for GrainLang per #271 +func Test_fdFilestatSetSize(t *testing.T) { + log := requireErrnoNosys(t, functionFdFilestatSetSize, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_filestat_set_size(fd=0,size=0) +<-- ENOSYS +`, log) +} + +// Test_fdFilestatSetTimes only tests it is stubbed for GrainLang per #271 +func Test_fdFilestatSetTimes(t *testing.T) { + log := requireErrnoNosys(t, functionFdFilestatSetTimes, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_filestat_set_times(fd=0,atim=0,mtim=0,fst_flags=0) +<-- ENOSYS +`, log) +} + +// Test_fdPread only tests it is stubbed for GrainLang per #271 +func Test_fdPread(t *testing.T) { + log := requireErrnoNosys(t, functionFdPread, 0, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_pread(fd=0,iovs=0,iovs_len=0,offset=0,result.nread=0) +<-- ENOSYS +`, log) +} + +func Test_fdPrestatGet(t *testing.T) { + pathName := "/tmp" + mod, fd, log, r := requireOpenFile(t, pathName, nil) + defer r.Close(testCtx) + + resultPrestat := uint32(1) // arbitrary offset + expectedMemory := []byte{ + '?', // resultPrestat after this + 0, // 8-bit tag indicating `prestat_dir`, the only available tag + 0, 0, 0, // 3-byte padding + // the result path length field after this + byte(len(pathName)), 0, 0, 0, // = in little endian encoding + '?', + } + + maskMemory(t, testCtx, mod, len(expectedMemory)) + + requireErrno(t, ErrnoSuccess, mod, functionFdPrestatGet, uint64(fd), uint64(resultPrestat)) + require.Equal(t, ` +==> wasi_snapshot_preview1.fd_prestat_get(fd=4,result.prestat=1) +<== ESUCCESS +`, "\n"+log.String()) + + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) + require.True(t, ok) + require.Equal(t, expectedMemory, actual) +} + +func Test_fdPrestatGet_Errors(t *testing.T) { + pathName := "/tmp" + mod, fd, log, r := requireOpenFile(t, pathName, nil) + defer r.Close(testCtx) + + memorySize := mod.Memory().Size(testCtx) + tests := []struct { + name string + fd uint32 + resultPrestat uint32 + expectedErrno Errno + }{ + { + name: "invalid FD", + fd: 42, // arbitrary invalid FD + resultPrestat: 0, // valid offset + expectedErrno: ErrnoBadf, + }, + { + name: "out-of-memory resultPrestat", + fd: fd, + resultPrestat: memorySize, + expectedErrno: ErrnoFault, + }, + // TODO: non pre-opened file == api.ErrnoBadf + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + errno := fdPrestatGet(testCtx, mod, tc.fd, tc.resultPrestat) + require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) + }) + } +} + +func Test_fdPrestatDirName(t *testing.T) { + pathName := "/tmp" + mod, fd, log, r := requireOpenFile(t, pathName, nil) + defer r.Close(testCtx) + + path := uint32(1) // arbitrary offset + pathLen := uint32(3) // shorter than len("/tmp") to test the path is written for the length of pathLen + expectedMemory := []byte{ + '?', + '/', 't', 'm', + '?', '?', '?', + } + + maskMemory(t, testCtx, mod, len(expectedMemory)) + + requireErrno(t, ErrnoSuccess, mod, functionFdPrestatDirName, uint64(fd), uint64(path), uint64(pathLen)) + require.Equal(t, ` +==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=4,path=1,path_len=3) +<== ESUCCESS +`, "\n"+log.String()) + + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) + require.True(t, ok) + require.Equal(t, expectedMemory, actual) +} + +func Test_fdPrestatDirName_Errors(t *testing.T) { + pathName := "/tmp" + mod, fd, log, r := requireOpenFile(t, pathName, nil) + defer r.Close(testCtx) + + memorySize := mod.Memory().Size(testCtx) + 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 + fd uint32 + path uint32 + pathLen uint32 + expectedErrno Errno + }{ + { + name: "out-of-memory path", + fd: fd, + path: memorySize, + pathLen: pathLen, + expectedErrno: ErrnoFault, + }, + { + name: "path exceeds the maximum valid address by 1", + fd: fd, + path: memorySize - pathLen + 1, + pathLen: pathLen, + expectedErrno: ErrnoFault, + }, + { + name: "pathLen exceeds the length of the dir name", + fd: fd, + path: validAddress, + pathLen: pathLen + 1, + expectedErrno: ErrnoNametoolong, + }, + { + name: "invalid fd", + fd: 42, // arbitrary invalid fd + path: validAddress, + pathLen: pathLen, + expectedErrno: ErrnoBadf, + }, + // TODO: non pre-opened file == ErrnoBadf + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + errno := fdPrestatDirName(testCtx, mod, tc.fd, tc.path, tc.pathLen) + require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) + }) + } +} + +// Test_fdPwrite only tests it is stubbed for GrainLang per #271 +func Test_fdPwrite(t *testing.T) { + log := requireErrnoNosys(t, functionFdPwrite, 0, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_pwrite(fd=0,iovs=0,iovs_len=0,offset=0,result.nwritten=0) +<-- ENOSYS +`, log) +} + +func Test_fdRead(t *testing.T) { + mod, fd, log, r := requireOpenFile(t, "/test_path", []byte("wazero")) + defer r.Close(testCtx) + + iovs := uint32(1) // arbitrary offset + initialMemory := []byte{ + '?', // `iovs` is after this + 18, 0, 0, 0, // = iovs[0].offset + 4, 0, 0, 0, // = iovs[0].length + 23, 0, 0, 0, // = iovs[1].offset + 2, 0, 0, 0, // = iovs[1].length + '?', + } + iovsCount := uint32(2) // The count of iovs + resultSize := uint32(26) // arbitrary offset + expectedMemory := append( + initialMemory, + 'w', 'a', 'z', 'e', // iovs[0].length bytes + '?', // iovs[1].offset is after this + 'r', 'o', // iovs[1].length bytes + '?', // resultSize is after this + 6, 0, 0, 0, // sum(iovs[...].length) == length of "wazero" + '?', + ) + + maskMemory(t, testCtx, mod, len(expectedMemory)) + + ok := mod.Memory().Write(testCtx, 0, initialMemory) + require.True(t, ok) + + requireErrno(t, ErrnoSuccess, mod, functionFdRead, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize)) + require.Equal(t, ` +==> wasi_snapshot_preview1.fd_read(fd=4,iovs=1,iovs_len=2,result.size=26) +<== ESUCCESS +`, "\n"+log.String()) + + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) + require.True(t, ok) + require.Equal(t, expectedMemory, actual) +} + +func Test_fdRead_Errors(t *testing.T) { + mod, fd, log, r := requireOpenFile(t, "/test_path", []byte("wazero")) + defer r.Close(testCtx) + + tests := []struct { + name string + fd, iovs, iovsCount, resultSize uint32 + memory []byte + expectedErrno Errno + }{ + { + name: "invalid fd", + fd: 42, // arbitrary invalid fd + expectedErrno: ErrnoBadf, + }, + { + name: "out-of-memory reading iovs[0].offset", + fd: fd, + iovs: 1, + memory: []byte{'?'}, + expectedErrno: ErrnoFault, + }, + { + name: "out-of-memory reading iovs[0].length", + fd: fd, + iovs: 1, iovsCount: 1, + memory: []byte{ + '?', // `iovs` is after this + 9, 0, 0, 0, // = iovs[0].offset + }, + expectedErrno: ErrnoFault, + }, + { + name: "iovs[0].offset is outside memory", + fd: fd, + iovs: 1, iovsCount: 1, + memory: []byte{ + '?', // `iovs` is after this + 0, 0, 0x1, 0, // = iovs[0].offset on the second page + 1, 0, 0, 0, // = iovs[0].length + }, + expectedErrno: ErrnoFault, + }, + { + name: "length to read exceeds memory by 1", + fd: fd, + iovs: 1, iovsCount: 1, + memory: []byte{ + '?', // `iovs` is after this + 9, 0, 0, 0, // = iovs[0].offset + 0, 0, 0x1, 0, // = iovs[0].length on the second page + '?', + }, + expectedErrno: ErrnoFault, + }, + { + name: "resultSize offset is outside memory", + fd: fd, + iovs: 1, iovsCount: 1, + resultSize: 10, // 1 past memory + memory: []byte{ + '?', // `iovs` is after this + 9, 0, 0, 0, // = iovs[0].offset + 1, 0, 0, 0, // = iovs[0].length + '?', + }, + expectedErrno: ErrnoFault, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + offset := uint32(wasm.MemoryPagesToBytesNum(testMemoryPageSize) - uint64(len(tc.memory))) + + memoryWriteOK := mod.Memory().Write(testCtx, offset, tc.memory) + require.True(t, memoryWriteOK) + + errno := fdRead(testCtx, mod, tc.fd, tc.iovs+offset, tc.iovsCount+offset, tc.resultSize+offset) + require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) + }) + } +} + +// Test_fdReaddir only tests it is stubbed for GrainLang per #271 +func Test_fdReaddir(t *testing.T) { + log := requireErrnoNosys(t, functionFdReaddir, 0, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_readdir(fd=0,buf=0,buf_len=0,cookie=0,result.bufused=0) +<-- ENOSYS +`, log) +} + +// Test_fdRenumber only tests it is stubbed for GrainLang per #271 +func Test_fdRenumber(t *testing.T) { + log := requireErrnoNosys(t, functionFdRenumber, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_renumber(fd=0,to=0) +<-- ENOSYS +`, log) +} + +func Test_fdSeek(t *testing.T) { + mod, fd, log, r := requireOpenFile(t, "/test_path", []byte("wazero")) + defer r.Close(testCtx) + + resultNewoffset := uint32(1) // arbitrary offset in api.Memory for the new offset value + + tests := []struct { + name string + offset int64 + whence int + expectedOffset int64 + expectedMemory []byte + expectedLog string + }{ + { + name: "SeekStart", + offset: 4, // arbitrary offset + whence: io.SeekStart, + expectedOffset: 4, // = offset + expectedMemory: []byte{ + '?', // resultNewoffset is after this + 4, 0, 0, 0, // = expectedOffset + '?', + }, + expectedLog: ` +==> wasi_snapshot_preview1.fd_seek(fd=4,offset=4,whence=0,result.newoffset=1) +<== ESUCCESS +`, + }, + { + name: "SeekCurrent", + offset: 1, // arbitrary offset + whence: io.SeekCurrent, + expectedOffset: 2, // = 1 (the initial offset of the test file) + 1 (offset) + expectedMemory: []byte{ + '?', // resultNewoffset is after this + 2, 0, 0, 0, // = expectedOffset + '?', + }, + expectedLog: ` +==> wasi_snapshot_preview1.fd_seek(fd=4,offset=1,whence=1,result.newoffset=1) +<== ESUCCESS +`, + }, + { + name: "SeekEnd", + offset: -1, // arbitrary offset, note that offset can be negative + whence: io.SeekEnd, + expectedOffset: 5, // = 6 (the size of the test file with content "wazero") + -1 (offset) + expectedMemory: []byte{ + '?', // resultNewoffset is after this + 5, 0, 0, 0, // = expectedOffset + '?', + }, + expectedLog: ` +==> wasi_snapshot_preview1.fd_seek(fd=4,offset=18446744073709551615,whence=2,result.newoffset=1) +<== ESUCCESS +`, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + maskMemory(t, testCtx, mod, len(tc.expectedMemory)) + + // Since we initialized this file, we know it is a seeker (because it is a MapFile) + fsc := mod.(*wasm.CallContext).Sys.FS(testCtx) + f, ok := fsc.OpenedFile(testCtx, fd) + require.True(t, ok) + seeker := f.File.(io.Seeker) + + // set the initial offset of the file to 1 + offset, err := seeker.Seek(1, io.SeekStart) + require.NoError(t, err) + require.Equal(t, int64(1), offset) + + requireErrno(t, ErrnoSuccess, mod, functionFdSeek, uint64(fd), uint64(tc.offset), uint64(tc.whence), uint64(resultNewoffset)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) + + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(tc.expectedMemory))) + require.True(t, ok) + require.Equal(t, tc.expectedMemory, actual) + + offset, err = seeker.Seek(0, io.SeekCurrent) + require.NoError(t, err) + require.Equal(t, tc.expectedOffset, offset) // test that the offset of file is actually updated. + }) + } +} + +func Test_fdSeek_Errors(t *testing.T) { + mod, fd, log, r := requireOpenFile(t, "/test_path", []byte("wazero")) + defer r.Close(testCtx) + + memorySize := mod.Memory().Size(testCtx) + + tests := []struct { + name string + fd uint32 + offset uint64 + whence, resultNewoffset uint32 + expectedErrno Errno + expectedLog string + }{ + { + name: "invalid fd", + fd: 42, // arbitrary invalid fd + expectedErrno: ErrnoBadf, + expectedLog: ` +==> wasi_snapshot_preview1.fd_seek(fd=42,offset=0,whence=0,result.newoffset=0) +<== EBADF +`, + }, + { + name: "invalid whence", + fd: fd, + whence: 3, // invalid whence, the largest whence io.SeekEnd(2) + 1 + expectedErrno: ErrnoInval, + expectedLog: ` +==> wasi_snapshot_preview1.fd_seek(fd=4,offset=0,whence=3,result.newoffset=0) +<== EINVAL +`, + }, + { + name: "out-of-memory writing resultNewoffset", + fd: fd, + resultNewoffset: memorySize, + expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.fd_seek(fd=4,offset=0,whence=0,result.newoffset=65536) +<== EFAULT +`, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + requireErrno(t, tc.expectedErrno, mod, functionFdSeek, uint64(tc.fd), tc.offset, uint64(tc.whence), uint64(tc.resultNewoffset)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) + }) + } +} + +// Test_fdSync only tests it is stubbed for GrainLang per #271 +func Test_fdSync(t *testing.T) { + log := requireErrnoNosys(t, functionFdSync, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_sync(fd=0) +<-- ENOSYS +`, log) +} + +// Test_fdTell only tests it is stubbed for GrainLang per #271 +func Test_fdTell(t *testing.T) { + log := requireErrnoNosys(t, functionFdTell, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.fd_tell(fd=0,result.offset=0) +<-- ENOSYS +`, log) +} + +func Test_fdWrite(t *testing.T) { + tmpDir := t.TempDir() // open before loop to ensure no locking problems. + pathName := "test_path" + mod, fd, log, r := requireOpenWritableFile(t, tmpDir, pathName) + defer r.Close(testCtx) + + iovs := uint32(1) // arbitrary offset + initialMemory := []byte{ + '?', // `iovs` is after this + 18, 0, 0, 0, // = iovs[0].offset + 4, 0, 0, 0, // = iovs[0].length + 23, 0, 0, 0, // = iovs[1].offset + 2, 0, 0, 0, // = iovs[1].length + '?', // iovs[0].offset is after this + 'w', 'a', 'z', 'e', // iovs[0].length bytes + '?', // iovs[1].offset is after this + 'r', 'o', // iovs[1].length bytes + '?', + } + iovsCount := uint32(2) // The count of iovs + resultSize := uint32(26) // arbitrary offset + expectedMemory := append( + initialMemory, + 6, 0, 0, 0, // sum(iovs[...].length) == length of "wazero" + '?', + ) + + maskMemory(t, testCtx, mod, len(expectedMemory)) + ok := mod.Memory().Write(testCtx, 0, initialMemory) + require.True(t, ok) + + requireErrno(t, ErrnoSuccess, mod, functionFdWrite, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize)) + require.Equal(t, ` +==> wasi_snapshot_preview1.fd_write(fd=4,iovs=1,iovs_len=2,result.size=26) +<== ESUCCESS +`, "\n"+log.String()) + + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) + require.True(t, ok) + require.Equal(t, expectedMemory, actual) + + // Since we initialized this file, we know we can read it by path + buf, err := os.ReadFile(path.Join(tmpDir, pathName)) + require.NoError(t, err) + + require.Equal(t, []byte("wazero"), buf) // verify the file was actually written +} + +func Test_fdWrite_Errors(t *testing.T) { + tmpDir := t.TempDir() // open before loop to ensure no locking problems. + pathName := "test_path" + mod, fd, log, r := requireOpenWritableFile(t, tmpDir, pathName) + defer r.Close(testCtx) + + // Setup valid test memory + iovs, iovsCount := uint32(0), uint32(1) + memory := []byte{ + 8, 0, 0, 0, // = iovs[0].offset (where the data "hi" begins) + 2, 0, 0, 0, // = iovs[0].length (how many bytes are in "hi") + 'h', 'i', // iovs[0].length bytes + } + + tests := []struct { + name string + fd, resultSize uint32 + memory []byte + expectedErrno Errno + expectedLog string + }{ + { + name: "invalid fd", + fd: 42, // arbitrary invalid fd + expectedErrno: ErrnoBadf, + expectedLog: ` +==> wasi_snapshot_preview1.fd_write(fd=42,iovs=0,iovs_len=1,result.size=0) +<== EBADF +`, + }, + { + name: "out-of-memory reading iovs[0].offset", + fd: fd, + memory: []byte{}, + expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.fd_write(fd=4,iovs=0,iovs_len=1,result.size=0) +<== EFAULT +`, + }, + { + name: "out-of-memory reading iovs[0].length", + fd: fd, + memory: memory[0:4], // iovs[0].offset was 4 bytes and iovs[0].length next, but not enough mod.Memory()! + expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.fd_write(fd=4,iovs=0,iovs_len=1,result.size=0) +<== EFAULT +`, + }, + { + name: "iovs[0].offset is outside memory", + fd: fd, + memory: memory[0:8], // iovs[0].offset (where to read "hi") is outside memory. + expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.fd_write(fd=4,iovs=0,iovs_len=1,result.size=0) +<== EFAULT +`, + }, + { + name: "length to read exceeds memory by 1", + fd: fd, + memory: memory[0:9], // iovs[0].offset (where to read "hi") is in memory, but truncated. + expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.fd_write(fd=4,iovs=0,iovs_len=1,result.size=0) +<== EFAULT +`, + }, + { + name: "resultSize offset is outside memory", + fd: fd, + memory: memory, + resultSize: uint32(len(memory)), // read was ok, but there wasn't enough memory to write the result. + expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.fd_write(fd=4,iovs=0,iovs_len=1,result.size=10) +<== EFAULT +`, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + mod.Memory().(*wasm.MemoryInstance).Buffer = tc.memory + + requireErrno(t, tc.expectedErrno, mod, functionFdWrite, uint64(tc.fd), uint64(iovs), uint64(iovsCount), + uint64(tc.resultSize)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) + }) + } +} + +// Test_pathCreateDirectory only tests it is stubbed for GrainLang per #271 +func Test_pathCreateDirectory(t *testing.T) { + log := requireErrnoNosys(t, functionPathCreateDirectory, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.path_create_directory(fd=0,path=0,path_len=0) +<-- ENOSYS +`, log) +} + +// Test_pathFilestatGet only tests it is stubbed for GrainLang per #271 +func Test_pathFilestatGet(t *testing.T) { + log := requireErrnoNosys(t, functionPathFilestatGet, 0, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.path_filestat_get(fd=0,flags=0,path=0,path_len=0,result.buf=0) +<-- ENOSYS +`, log) +} + +// Test_pathFilestatSetTimes only tests it is stubbed for GrainLang per #271 +func Test_pathFilestatSetTimes(t *testing.T) { + log := requireErrnoNosys(t, functionPathFilestatSetTimes, 0, 0, 0, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.path_filestat_set_times(fd=0,flags=0,path=0,path_len=0,atim=0,mtim=0,fst_flags=0) +<-- ENOSYS +`, log) +} + +// Test_pathLink only tests it is stubbed for GrainLang per #271 +func Test_pathLink(t *testing.T) { + log := requireErrnoNosys(t, functionPathLink, 0, 0, 0, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.path_link(old_fd=0,old_flags=0,old_path=0,old_path_len=0,new_fd=0,new_path=0,new_path_len=0) +<-- ENOSYS +`, log) +} + +func Test_pathOpen(t *testing.T) { + rootFD := uint32(3) // after 0, 1, and 2, that are stdin/out/err + expectedFD := rootFD + 1 + // Setup the initial memory to include the path name starting at an offset. + pathName := "wazero" + initialMemory := append([]byte{'?'}, pathName...) + + expectedMemory := append( + initialMemory, + '?', // `resultOpenedFd` is after this + byte(expectedFD), 0, 0, 0, + '?', + ) + + dirflags := uint32(0) + pathPtr := uint32(1) + pathLen := uint32(len(pathName)) + oflags := uint32(0) + // rights are ignored per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 + fsRightsBase := uint64(1) + fsRightsInheriting := uint64(2) + fdflags := uint32(0) + resultOpenedFd := uint32(len(initialMemory) + 1) + + testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}} + mod, r, log := requireModule(t, wazero.NewModuleConfig().WithFS(testFS)) + defer r.Close(testCtx) + + maskMemory(t, testCtx, mod, len(expectedMemory)) + ok := mod.Memory().Write(testCtx, 0, initialMemory) + require.True(t, ok) + + requireErrno(t, ErrnoSuccess, mod, functionPathOpen, uint64(rootFD), uint64(dirflags), uint64(pathPtr), + uint64(pathLen), uint64(oflags), fsRightsBase, fsRightsInheriting, uint64(fdflags), uint64(resultOpenedFd)) + require.Equal(t, ` +==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=1,path_len=6,oflags=0,fs_rights_base=1,fs_rights_inheriting=2,fdflags=0,result.opened_fd=8) +<== ESUCCESS +`, "\n"+log.String()) + + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) + require.True(t, ok) + require.Equal(t, expectedMemory, actual) + + // verify the file was actually opened + fsc := mod.(*wasm.CallContext).Sys.FS(testCtx) + f, ok := fsc.OpenedFile(testCtx, expectedFD) + require.True(t, ok) + require.Equal(t, pathName, f.Path) +} + +func Test_pathOpen_Errors(t *testing.T) { + validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err + pathName := "wazero" + testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}} + mod, r, log := requireModule(t, wazero.NewModuleConfig().WithFS(testFS)) + defer r.Close(testCtx) + + validPath := uint32(0) // arbitrary offset + validPathLen := uint32(6) // the length of "wazero" + mod.Memory().Write(testCtx, validPath, []byte(pathName)) + + tests := []struct { + name string + fd, path, pathLen, oflags, resultOpenedFd uint32 + expectedErrno Errno + expectedLog string + }{ + { + name: "invalid fd", + fd: 42, // arbitrary invalid fd + expectedErrno: ErrnoBadf, + expectedLog: ` +==> wasi_snapshot_preview1.path_open(fd=42,dirflags=0,path=0,path_len=0,oflags=0,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=0) +<== EBADF +`, + }, + { + name: "out-of-memory reading path", + fd: validFD, + path: mod.Memory().Size(testCtx), + pathLen: validPathLen, + expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=65536,path_len=6,oflags=0,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=0) +<== EFAULT +`, + }, + { + name: "out-of-memory reading pathLen", + fd: validFD, + path: validPath, + pathLen: mod.Memory().Size(testCtx) + 1, // path is in the valid memory range, but pathLen is out-of-memory for path + expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=0,path_len=65537,oflags=0,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=0) +<== EFAULT +`, + }, + { + name: "no such file exists", + fd: validFD, + path: validPath, + pathLen: validPathLen - 1, // this make the path "wazer", which doesn't exit + expectedErrno: ErrnoNoent, + expectedLog: ` +==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=0,path_len=5,oflags=0,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=0) +<== ENOENT +`, + }, + { + name: "out-of-memory writing resultOpenedFd", + fd: validFD, + path: validPath, + pathLen: validPathLen, + resultOpenedFd: mod.Memory().Size(testCtx), // path and pathLen correctly point to the right path, but where to write the opened FD is outside memory. + expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=0,path_len=6,oflags=0,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=65536) +<== EFAULT +`, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + requireErrno(t, tc.expectedErrno, mod, functionPathOpen, uint64(tc.fd), uint64(0), uint64(tc.path), + uint64(tc.pathLen), uint64(tc.oflags), 0, 0, 0, uint64(tc.resultOpenedFd)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) + }) + } +} + +// Test_pathReadlink only tests it is stubbed for GrainLang per #271 +func Test_pathReadlink(t *testing.T) { + log := requireErrnoNosys(t, functionPathReadlink, 0, 0, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.path_readlink(fd=0,path=0,path_len=0,buf=0,buf_len=0,result.bufused=0) +<-- ENOSYS +`, log) +} + +// Test_pathRemoveDirectory only tests it is stubbed for GrainLang per #271 +func Test_pathRemoveDirectory(t *testing.T) { + log := requireErrnoNosys(t, functionPathRemoveDirectory, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.path_remove_directory(fd=0,path=0,path_len=0) +<-- ENOSYS +`, log) +} + +// Test_pathRename only tests it is stubbed for GrainLang per #271 +func Test_pathRename(t *testing.T) { + log := requireErrnoNosys(t, functionPathRename, 0, 0, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.path_rename(fd=0,old_path=0,old_path_len=0,new_fd=0,new_path=0,new_path_len=0) +<-- ENOSYS +`, log) +} + +// Test_pathSymlink only tests it is stubbed for GrainLang per #271 +func Test_pathSymlink(t *testing.T) { + log := requireErrnoNosys(t, functionPathSymlink, 0, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.path_symlink(old_path=0,old_path_len=0,fd=0,new_path=0,new_path_len=0) +<-- ENOSYS +`, log) +} + +// Test_pathUnlinkFile only tests it is stubbed for GrainLang per #271 +func Test_pathUnlinkFile(t *testing.T) { + log := requireErrnoNosys(t, functionPathUnlinkFile, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.path_unlink_file(fd=0,path=0,path_len=0) +<-- ENOSYS +`, log) +} + +func requireOpenFile(t *testing.T, pathName string, data []byte) (api.Module, uint32, *bytes.Buffer, api.Closer) { + mapFile := &fstest.MapFile{Data: data} + if data == nil { + mapFile.Mode = os.ModeDir + } + testFS := fstest.MapFS{pathName[1:]: mapFile} // strip the leading slash + mod, r, log := requireModule(t, wazero.NewModuleConfig().WithFS(testFS)) + fsc := mod.(*wasm.CallContext).Sys.FS(testCtx) + fd, err := fsc.OpenFile(testCtx, pathName) + require.NoError(t, err) + return mod, fd, log, r +} + +// requireOpenWritableFile is temporary until we add the ability to open files for writing. +func requireOpenWritableFile(t *testing.T, tmpDir string, pathName string) (api.Module, uint32, *bytes.Buffer, api.Closer) { + writeable, testFS := createWriteableFile(t, tmpDir, pathName, []byte{}) + mod, r, log := requireModule(t, wazero.NewModuleConfig().WithFS(testFS)) + fsc := mod.(*wasm.CallContext).Sys.FS(testCtx) + fd, err := fsc.OpenFile(testCtx, pathName) + require.NoError(t, err) + + // Swap the read-only file with a writeable one until #390 + f, ok := fsc.OpenedFile(testCtx, fd) + require.True(t, ok) + f.File.Close() + f.File = writeable + + return mod, fd, log, r +} + +// createWriteableFile uses real files when io.Writer tests are needed. +func createWriteableFile(t *testing.T, tmpDir string, pathName string, data []byte) (fs.File, fs.FS) { + require.NotNil(t, data) + absolutePath := path.Join(tmpDir, pathName) + require.NoError(t, os.WriteFile(absolutePath, data, 0o600)) + + // open the file for writing in a custom way until #390 + f, err := os.OpenFile(absolutePath, os.O_RDWR, 0o600) + require.NoError(t, err) + return f, os.DirFS(tmpDir) +} diff --git a/wasi_snapshot_preview1/poll.go b/wasi_snapshot_preview1/poll.go index 68622f8f..d05b6093 100644 --- a/wasi_snapshot_preview1/poll.go +++ b/wasi_snapshot_preview1/poll.go @@ -9,9 +9,11 @@ import ( "github.com/tetratelabs/wazero/internal/wasm" ) +const functionPollOneoff = "poll_oneoff" + // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-eventtype-enumu8 const ( - // eventTypeClock is the timeout event named "clock". + // eventTypeClock is the timeout event named "name". eventTypeClock = iota // eventTypeFdRead is the data available event named "fd_read". eventTypeFdRead @@ -19,22 +21,23 @@ const ( eventTypeFdWrite ) -// PollOneoff is the WASI function named functionPollOneoff that concurrently +// pollOneoff is the WASI function named functionPollOneoff that concurrently // polls for the occurrence of a set of events. // // Parameters // -// * in - pointer to the subscriptions (48 bytes each) -// * out - pointer to the resulting events (32 bytes each) -// * nsubscriptions - count of subscriptions, zero returns ErrnoInval. -// * resultNevents - count of events. +// * in: pointer to the subscriptions (48 bytes each) +// * out: pointer to the resulting events (32 bytes each) +// * nsubscriptions: count of subscriptions, zero returns ErrnoInval. +// * resultNevents: count of events. // // Result (Errno) // // The return value is ErrnoSuccess except the following error conditions: -// * ErrnoInval - If the parameters are invalid -// * ErrnoNotsup - If a parameters is valid, but not yet supported. -// * ErrnoFault - if there is not enough memory to read the subscriptions or write results. +// * ErrnoInval: the parameters are invalid +// * ErrnoNotsup: a parameters is valid, but not yet supported. +// * ErrnoFault: there is not enough memory to read the subscriptions or +// write results. // // Notes // @@ -43,7 +46,7 @@ const ( // * This is similar to `poll` in POSIX. // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#poll_oneoff // See https://linux.die.net/man/3/poll -func (a *wasi) PollOneoff(ctx context.Context, mod api.Module, in, out, nsubscriptions, resultNevents uint32) Errno { +func pollOneoff(ctx context.Context, mod api.Module, in, out, nsubscriptions, resultNevents uint32) Errno { if nsubscriptions == 0 { return ErrnoInval } @@ -75,7 +78,7 @@ func (a *wasi) PollOneoff(ctx context.Context, mod api.Module, in, out, nsubscri eventType := inBuf[inOffset+8] // +8 past userdata switch eventType { case eventTypeClock: // handle later - // +8 past userdata +8 clock alignment + // +8 past userdata +8 name alignment errno = processClockEvent(ctx, mod, inBuf[inOffset+8+8:]) case eventTypeFdRead, eventTypeFdWrite: // +8 past userdata +4 FD alignment @@ -95,7 +98,7 @@ func (a *wasi) PollOneoff(ctx context.Context, mod api.Module, in, out, nsubscri return ErrnoSuccess } -// processClockEvent supports only relative clock events, as that's what's used +// processClockEvent supports only relative name events, as that's what's used // to implement sleep in various compilers including Rust, Zig and TinyGo. func processClockEvent(ctx context.Context, mod api.Module, inBuf []byte) Errno { _ /* ID */ = binary.LittleEndian.Uint32(inBuf[0:8]) // See below @@ -114,7 +117,7 @@ func processClockEvent(ctx context.Context, mod api.Module, inBuf []byte) Errno // https://linux.die.net/man/3/clock_settime says relative timers are // unaffected. Since this function only supports relative timeout, we can - // skip clock ID validation and use a single sleep function. + // skip name ID validation and use a single sleep function. sysCtx := mod.(*wasm.CallContext).Sys sysCtx.Nanosleep(ctx, int64(timeout)) @@ -127,7 +130,8 @@ func processFDEvent(ctx context.Context, mod api.Module, eventType byte, inBuf [ fd := binary.LittleEndian.Uint32(inBuf) sysCtx := mod.(*wasm.CallContext).Sys - // Choose the best error, which falls back to unsupported, until we support files. + // Choose the best error, which falls back to unsupported, until we support + // files. errno := ErrnoNotsup if eventType == eventTypeFdRead && internalsys.FdReader(ctx, sysCtx, fd) == nil { errno = ErrnoBadf diff --git a/wasi_snapshot_preview1/poll_test.go b/wasi_snapshot_preview1/poll_test.go index 12833342..6a0a5fbe 100644 --- a/wasi_snapshot_preview1/poll_test.go +++ b/wasi_snapshot_preview1/poll_test.go @@ -3,38 +3,31 @@ package wasi_snapshot_preview1 import ( "testing" + "github.com/tetratelabs/wazero" internalsys "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) -func Test_PollOneoff(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionPollOneoff, importPollOneoff, nil) - defer mod.Close(testCtx) +func Test_pollOneoff(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig()) + defer r.Close(testCtx) - tests := []struct { - name string - mem []byte - expectedMem []byte // at offset out - }{ - { - name: "monotonic relative", - mem: []byte{ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata - eventTypeClock, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // event type and padding - clockIDMonotonic, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // clockID - 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // timeout (ns) - 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // precision (ns) - 0x00, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // flags (relative) - '?', // stopped after encoding - }, - expectedMem: []byte{ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata - byte(ErrnoSuccess), 0x0, // errno is 16 bit - eventTypeClock, 0x0, 0x0, 0x0, // 4 bytes for type enum - '?', // stopped after encoding - }, - }, + mem := []byte{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata + eventTypeClock, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // event type and padding + clockIDMonotonic, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // clockID + 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // timeout (ns) + 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // precision (ns) + 0x00, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // flags (relative) + '?', // stopped after encoding + } + + expectedMem := []byte{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata + byte(ErrnoSuccess), 0x0, // errno is 16 bit + eventTypeClock, 0x0, 0x0, 0x0, // 4 bytes for type enum + '?', // stopped after encoding } in := uint32(0) // past in @@ -42,45 +35,28 @@ func Test_PollOneoff(t *testing.T) { nsubscriptions := uint32(1) resultNevents := uint32(512) // past out - requireExpectedMem := func(expectedMem []byte) { - outMem, ok := mod.Memory().Read(testCtx, out, uint32(len(expectedMem))) - require.True(t, ok) - require.Equal(t, expectedMem, outMem) + maskMemory(t, testCtx, mod, 1024) + mod.Memory().Write(testCtx, in, mem) - nevents, ok := mod.Memory().ReadUint32Le(testCtx, resultNevents) - require.True(t, ok) - require.Equal(t, nsubscriptions, nevents) - } + requireErrno(t, ErrnoSuccess, mod, functionPollOneoff, uint64(in), uint64(out), uint64(nsubscriptions), + uint64(resultNevents)) + require.Equal(t, ` +==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=1,result.nevents=512) +<== ESUCCESS +`, "\n"+log.String()) - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - t.Run("wasi.PollOneoff", func(t *testing.T) { - maskMemory(t, testCtx, mod, 1024) - mod.Memory().Write(testCtx, in, tc.mem) + outMem, ok := mod.Memory().Read(testCtx, out, uint32(len(expectedMem))) + require.True(t, ok) + require.Equal(t, expectedMem, outMem) - errno := a.PollOneoff(testCtx, mod, in, out, nsubscriptions, resultNevents) - require.Equal(t, ErrnoSuccess, errno, ErrnoName(errno)) - requireExpectedMem(tc.expectedMem) - }) - - t.Run(functionPollOneoff, func(t *testing.T) { - maskMemory(t, testCtx, mod, 1024) - mod.Memory().Write(testCtx, in, tc.mem) - - results, err := fn.Call(testCtx, uint64(in), uint64(out), uint64(nsubscriptions), uint64(resultNevents)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoSuccess, errno, ErrnoName(errno)) - requireExpectedMem(tc.expectedMem) - }) - }) - } + nevents, ok := mod.Memory().ReadUint32Le(testCtx, resultNevents) + require.True(t, ok) + require.Equal(t, nsubscriptions, nevents) } -func Test_PollOneoff_Errors(t *testing.T) { - mod, _ := instantiateModule(testCtx, t, functionPollOneoff, importPollOneoff, nil) - defer mod.Close(testCtx) +func Test_pollOneoff_Errors(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig()) + defer r.Close(testCtx) tests := []struct { name string @@ -88,6 +64,7 @@ func Test_PollOneoff_Errors(t *testing.T) { mem []byte // at offset in expectedErrno Errno expectedMem []byte // at offset out + expectedLog string }{ { name: "in out of range", @@ -96,6 +73,10 @@ func Test_PollOneoff_Errors(t *testing.T) { out: 128, // past in resultNevents: 512, //past out expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.poll_oneoff(in=65536,out=128,nsubscriptions=1,result.nevents=512) +<== EFAULT +`, }, { name: "out out of range", @@ -103,18 +84,30 @@ func Test_PollOneoff_Errors(t *testing.T) { resultNevents: 512, //past out nsubscriptions: 1, expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.poll_oneoff(in=0,out=65536,nsubscriptions=1,result.nevents=512) +<== EFAULT +`, }, { name: "resultNevents out of range", resultNevents: wasm.MemoryPageSize, nsubscriptions: 1, expectedErrno: ErrnoFault, + expectedLog: ` +==> wasi_snapshot_preview1.poll_oneoff(in=0,out=0,nsubscriptions=1,result.nevents=65536) +<== EFAULT +`, }, { name: "nsubscriptions zero", out: 128, // past in resultNevents: 512, //past out expectedErrno: ErrnoInval, + expectedLog: ` +==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=0,result.nevents=512) +<== EINVAL +`, }, { name: "unsupported eventTypeFdRead", @@ -134,20 +127,27 @@ func Test_PollOneoff_Errors(t *testing.T) { eventTypeFdRead, 0x0, 0x0, 0x0, // 4 bytes for type enum '?', // stopped after encoding }, + expectedLog: ` +==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=1,result.nevents=512) +<== ESUCCESS +`, }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + maskMemory(t, testCtx, mod, 1024) if tc.mem != nil { mod.Memory().Write(testCtx, tc.in, tc.mem) } - errno := a.PollOneoff(testCtx, mod, tc.in, tc.out, tc.nsubscriptions, tc.resultNevents) - require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) + requireErrno(t, tc.expectedErrno, mod, functionPollOneoff, uint64(tc.in), uint64(tc.out), + uint64(tc.nsubscriptions), uint64(tc.resultNevents)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) out, ok := mod.Memory().Read(testCtx, tc.out, uint32(len(tc.expectedMem))) require.True(t, ok) diff --git a/wasi_snapshot_preview1/proc.go b/wasi_snapshot_preview1/proc.go index ff857f42..a539547c 100644 --- a/wasi_snapshot_preview1/proc.go +++ b/wasi_snapshot_preview1/proc.go @@ -8,33 +8,20 @@ import ( ) const ( - // functionProcExit terminates the execution of the module with an exit code. - // See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#proc_exit - functionProcExit = "proc_exit" - - // importProcExit is the WebAssembly 1.0 Text format import of functionProcExit. - importProcExit = `(import "wasi_snapshot_preview1" "proc_exit" - (func $wasi.proc_exit (param $rval i32)))` - - // functionProcRaise sends a signal to the process of the calling thread. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-proc_raisesig-signal---errno + functionProcExit = "proc_exit" functionProcRaise = "proc_raise" - - // importProcRaise is the WebAssembly 1.0 Text format import of functionProcRaise. - importProcRaise = `(import "wasi_snapshot_preview1" "proc_raise" - (func $wasi.proc_raise (param $sig i32) (result (;errno;) i32)))` ) -// ProcExit is the WASI function that terminates the execution of the module with an exit code. -// An exit code of 0 indicates successful termination. The meanings of other values are not defined by WASI. +// procExit is the WASI function named functionProcExit that terminates the +// execution of the module with an exit code. The only successful exit code is +// zero. // -// * rval - The exit code. +// Parameters // -// In wazero, this calls api.Module CloseWithExitCode. +// * exitCode: exit code. // -// Note: importProcExit shows this signature in the WebAssembly 1.0 Text Format. // See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#proc_exit -func (a *wasi) ProcExit(ctx context.Context, mod api.Module, exitCode uint32) { +func procExit(ctx context.Context, mod api.Module, exitCode uint32) { // Ensure other callers see the exit code. _ = mod.CloseWithExitCode(ctx, exitCode) @@ -44,7 +31,8 @@ func (a *wasi) ProcExit(ctx context.Context, mod api.Module, exitCode uint32) { panic(sys.NewExitError(mod.Name(), exitCode)) } -// ProcRaise is the WASI function named functionProcRaise -func (a *wasi) ProcRaise(ctx context.Context, mod api.Module, sig uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} +// procRaise is the WASI function named functionProcRaise which sends a signal +// to the process of the calling thread. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-proc_raisesig-signal---errno +var procRaise = stubFunction(i32) // stubbed for GrainLang per #271. diff --git a/wasi_snapshot_preview1/proc_test.go b/wasi_snapshot_preview1/proc_test.go index c7d1376f..5462d619 100644 --- a/wasi_snapshot_preview1/proc_test.go +++ b/wasi_snapshot_preview1/proc_test.go @@ -5,14 +5,17 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/watzero" "github.com/tetratelabs/wazero/sys" ) -func Test_ProcExit(t *testing.T) { +func Test_procExit(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig()) + defer r.Close(testCtx) + tests := []struct { - name string - exitCode uint32 + name string + exitCode uint32 + expectedLog string }{ { name: "success (exitcode 0)", @@ -28,61 +31,21 @@ func Test_ProcExit(t *testing.T) { tc := tt t.Run(tc.name, func(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(testCtx, t, functionProcExit, importProcExit, nil) - defer mod.Close(testCtx) + defer log.Reset() - // When ProcExit is called, CallEngine.Call returns immediately, - // returning the exit code as the error. - _, err := fn.Call(testCtx, uint64(tc.exitCode)) + // Since procExit panics, any opcodes afterwards cannot be reached. + err := require.CapturePanic(func() { procExit(testCtx, mod, tc.exitCode) }) require.Equal(t, tc.exitCode, err.(*sys.ExitError).ExitCode()) + require.Equal(t, tc.expectedLog, log.String()) }) } } -var unreachableAfterExit = `(module - (import "wasi_snapshot_preview1" "proc_exit" - (func $wasi.proc_exit (param $rval i32))) - (func $main - i32.const 0 - call $wasi.proc_exit - unreachable ;; If abort doesn't panic, this code is reached. - ) - (start $main) -)` - -// Test_ProcExit_StopsExecution ensures code that follows a proc_exit isn't invoked. -func Test_ProcExit_StopsExecution(t *testing.T) { - r := wazero.NewRuntime() - defer r.Close(testCtx) - - _, err := NewBuilder(r).Instantiate(testCtx, r) - require.NoError(t, err) - - exitWasm, err := watzero.Wat2Wasm(unreachableAfterExit) - require.NoError(t, err) - - _, err = r.InstantiateModuleFromBinary(testCtx, exitWasm) - require.Error(t, err) - require.Equal(t, uint32(0), err.(*sys.ExitError).ExitCode()) -} - -// Test_ProcRaise only tests it is stubbed for GrainLang per #271 -func Test_ProcRaise(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionProcRaise, importProcRaise, nil) - defer mod.Close(testCtx) - - t.Run("wasi.ProcRaise", func(t *testing.T) { - errno := a.ProcRaise(testCtx, mod, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionProcRaise, func(t *testing.T) { - results, err := fn.Call(testCtx, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) +// Test_procRaise only tests it is stubbed for GrainLang per #271 +func Test_procRaise(t *testing.T) { + log := requireErrnoNosys(t, functionProcRaise, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.proc_raise(sig=0) +<-- ENOSYS +`, log) } diff --git a/wasi_snapshot_preview1/random.go b/wasi_snapshot_preview1/random.go new file mode 100644 index 00000000..e00eec9e --- /dev/null +++ b/wasi_snapshot_preview1/random.go @@ -0,0 +1,52 @@ +package wasi_snapshot_preview1 + +import ( + "context" + "io" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasm" +) + +const functionRandomGet = "random_get" + +// randomGet is the WASI function named functionRandomGet which writes random +// data to a buffer. +// +// Parameters +// +// * buf: api.Memory offset to write random values +// * bufLen: size of random data in bytes +// +// Result (Errno) +// +// The return value is ErrnoSuccess except the following error conditions: +// * ErrnoFault: `buf` or `bufLen` point to an offset out of memory +// * ErrnoIo: a file system error +// +// For example, if underlying random source was seeded like +// `rand.NewSource(42)`, we expect api.Memory to contain: +// +// bufLen (5) +// +--------------------------+ +// | | +// []byte{?, 0x53, 0x8c, 0x7f, 0x96, 0xb1, ?} +// buf --^ +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-bufLen-size---errno +func randomGet(ctx context.Context, mod api.Module, buf uint32, bufLen uint32) (errno Errno) { + sysCtx := mod.(*wasm.CallContext).Sys + randSource := sysCtx.RandSource() + + randomBytes, ok := mod.Memory().Read(ctx, buf, bufLen) + if !ok { // out-of-range + return ErrnoFault + } + + // We can ignore the returned n as it only != byteCount on error + if _, err := io.ReadAtLeast(randSource, randomBytes, int(bufLen)); err != nil { + return ErrnoIo + } + + return ErrnoSuccess +} diff --git a/wasi_snapshot_preview1/random_test.go b/wasi_snapshot_preview1/random_test.go new file mode 100644 index 00000000..ff20967a --- /dev/null +++ b/wasi_snapshot_preview1/random_test.go @@ -0,0 +1,114 @@ +package wasi_snapshot_preview1 + +import ( + "bytes" + "errors" + "io" + "testing" + "testing/iotest" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +func Test_randomGet(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig(). + WithRandSource(deterministicRandomSource())) + defer r.Close(testCtx) + + expectedMemory := []byte{ + '?', // `offset` is after this + 0x53, 0x8c, 0x7f, 0x96, 0xb1, // random data from seed value of 42 + '?', // stopped after encoding + } + + length := uint32(5) // arbitrary length, + offset := uint32(1) // offset, + + maskMemory(t, testCtx, mod, len(expectedMemory)) + + // Invoke randomGet and check the memory side effects! + requireErrno(t, ErrnoSuccess, mod, functionRandomGet, uint64(offset), uint64(length)) + require.Equal(t, ` +==> wasi_snapshot_preview1.random_get(buf=1,buf_len=5) +<== ESUCCESS +`, "\n"+log.String()) + + actual, ok := mod.Memory().Read(testCtx, 0, offset+length+1) + require.True(t, ok) + require.Equal(t, expectedMemory, actual) +} + +func Test_randomGet_Errors(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig(). + WithRandSource(deterministicRandomSource())) + defer r.Close(testCtx) + + memorySize := mod.Memory().Size(testCtx) + + tests := []struct { + name string + offset, length uint32 + expectedLog string + }{ + { + name: "out-of-memory", + offset: memorySize, + length: 1, + expectedLog: ` +==> wasi_snapshot_preview1.random_get(buf=65536,buf_len=1) +<== EFAULT +`, + }, + { + name: "random length exceeds maximum valid address by 1", + offset: 0, // arbitrary valid offset + length: memorySize + 1, + expectedLog: ` +==> wasi_snapshot_preview1.random_get(buf=0,buf_len=65537) +<== EFAULT +`, + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + defer log.Reset() + + requireErrno(t, ErrnoFault, mod, functionRandomGet, uint64(tc.offset), uint64(tc.length)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) + }) + } +} + +func Test_randomGet_SourceError(t *testing.T) { + tests := []struct { + name string + randSource io.Reader + expectedLog string + }{ + { + name: "error", + randSource: iotest.ErrReader(errors.New("RandSource error")), + }, + { + name: "incomplete", + randSource: bytes.NewReader([]byte{1, 2}), + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + mod, r, log := requireModule(t, wazero.NewModuleConfig(). + WithRandSource(tc.randSource)) + defer r.Close(testCtx) + + errno := randomGet(testCtx, mod, uint32(1), uint32(5)) // arbitrary offset and length + require.Equal(t, ErrnoIo, errno, ErrnoName(errno)) + require.Equal(t, tc.expectedLog, log.String()) + }) + } +} diff --git a/wasi_snapshot_preview1/sched.go b/wasi_snapshot_preview1/sched.go new file mode 100644 index 00000000..3f6f0409 --- /dev/null +++ b/wasi_snapshot_preview1/sched.go @@ -0,0 +1,9 @@ +package wasi_snapshot_preview1 + +const functionSchedYield = "sched_yield" + +// schedYield is the WASI function named functionSchedYield which temporarily +// yields execution of the calling thread. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sched_yield---errno +var schedYield = stubFunction() // stubbed for GrainLang per #271. diff --git a/wasi_snapshot_preview1/sched_test.go b/wasi_snapshot_preview1/sched_test.go new file mode 100644 index 00000000..9f85ddf1 --- /dev/null +++ b/wasi_snapshot_preview1/sched_test.go @@ -0,0 +1,16 @@ +package wasi_snapshot_preview1 + +import ( + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" +) + +// Test_schedYield only tests it is stubbed for GrainLang per #271 +func Test_schedYield(t *testing.T) { + log := requireErrnoNosys(t, functionSchedYield) + require.Equal(t, ` +--> wasi_snapshot_preview1.sched_yield() +<-- ENOSYS +`, log) +} diff --git a/wasi_snapshot_preview1/sock.go b/wasi_snapshot_preview1/sock.go new file mode 100644 index 00000000..6344517b --- /dev/null +++ b/wasi_snapshot_preview1/sock.go @@ -0,0 +1,25 @@ +package wasi_snapshot_preview1 + +const ( + functionSockRecv = "sock_recv" + functionSockSend = "sock_send" + functionSockShutdown = "sock_shutdown" +) + +// sockRecv is the WASI function named functionSockRecv which receives a +// message from a socket. +// +// See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_recvfd-fd-ri_data-iovec_array-ri_flags-riflags---errno-size-roflags +var sockRecv = stubFunction(i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271. + +// sockSend is the WASI function named functionSockSend which sends a message +// on a socket. +// +// See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_sendfd-fd-si_data-ciovec_array-si_flags-siflags---errno-size +var sockSend = stubFunction(i32, i32, i32, i32, i32) // stubbed for GrainLang per #271. + +// sockShutdown is the WASI function named functionSockShutdown which shuts +// down socket send and receive channels. +// +// See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_shutdownfd-fd-how-sdflags---errno +var sockShutdown = stubFunction(i32, i32) // stubbed for GrainLang per #271. diff --git a/wasi_snapshot_preview1/sock_test.go b/wasi_snapshot_preview1/sock_test.go new file mode 100644 index 00000000..325d64a7 --- /dev/null +++ b/wasi_snapshot_preview1/sock_test.go @@ -0,0 +1,34 @@ +package wasi_snapshot_preview1 + +import ( + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" +) + +// Test_sockRecv only tests it is stubbed for GrainLang per #271 +func Test_sockRecv(t *testing.T) { + log := requireErrnoNosys(t, functionSockRecv, 0, 0, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.sock_recv(fd=0,ri_data=0,ri_data_count=0,ri_flags=0,result.ro_datalen=0,result.ro_flags=0) +<-- ENOSYS +`, log) +} + +// Test_sockSend only tests it is stubbed for GrainLang per #271 +func Test_sockSend(t *testing.T) { + log := requireErrnoNosys(t, functionSockSend, 0, 0, 0, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.sock_send(fd=0,si_data=0,si_data_count=0,si_flags=0,result.so_datalen=0) +<-- ENOSYS +`, log) +} + +// Test_sockShutdown only tests it is stubbed for GrainLang per #271 +func Test_sockShutdown(t *testing.T) { + log := requireErrnoNosys(t, functionSockShutdown, 0, 0) + require.Equal(t, ` +--> wasi_snapshot_preview1.sock_shutdown(fd=0,how=0) +<-- ENOSYS +`, log) +} diff --git a/wasi_snapshot_preview1/wasi.go b/wasi_snapshot_preview1/wasi.go index d8f57744..5153441a 100644 --- a/wasi_snapshot_preview1/wasi.go +++ b/wasi_snapshot_preview1/wasi.go @@ -17,13 +17,9 @@ package wasi_snapshot_preview1 import ( "context" - "errors" - "io" - "io/fs" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" - internalsys "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -31,6 +27,7 @@ import ( // // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md const ModuleName = "wasi_snapshot_preview1" +const i32, i64 = wasm.ValueTypeI32, wasm.ValueTypeI64 // Instantiate instantiates the ModuleName module into the runtime default // namespace. @@ -67,9 +64,9 @@ type builder struct{ r wazero.Runtime } // moduleBuilder returns a new wazero.ModuleBuilder for ModuleName func (b *builder) moduleBuilder() wazero.ModuleBuilder { - builder := b.r.NewModuleBuilder(ModuleName) - exportFunctions(builder) - return builder + ret := b.r.NewModuleBuilder(ModuleName) + exportFunctions(ret) + return ret } // Compile implements Builder.Compile @@ -82,338 +79,6 @@ func (b *builder) Instantiate(ctx context.Context, ns wazero.Namespace) (api.Clo return b.moduleBuilder().Instantiate(ctx, ns) } -const ( - // functionArgsGet reads command-line argument data. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-args_getargv-pointerpointeru8-argv_buf-pointeru8---errno - functionArgsGet = "args_get" - - // importArgsGet is the WebAssembly 1.0 Text format import of functionArgsGet. - importArgsGet = `(import "wasi_snapshot_preview1" "args_get" - (func $wasi.args_get (param $argv i32) (param $argv_buf i32) (result (;errno;) i32)))` - - // functionArgsSizesGet returns command-line argument data sizes. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-args_sizes_get---errno-size-size - functionArgsSizesGet = "args_sizes_get" - - // importArgsSizesGet is the WebAssembly 1.0 Text format import of functionArgsSizesGet. - importArgsSizesGet = `(import "wasi_snapshot_preview1" "args_sizes_get" - (func $wasi.args_sizes_get (param $result.argc i32) (param $result.argv_buf_size i32) (result (;errno;) i32)))` - - // functionEnvironGet reads environment variable data. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-environ_getenviron-pointerpointeru8-environ_buf-pointeru8---errno - functionEnvironGet = "environ_get" - - // importEnvironGet is the WebAssembly 1.0 Text format import of functionEnvironGet. - importEnvironGet = `(import "wasi_snapshot_preview1" "environ_get" - (func $wasi.environ_get (param $environ i32) (param $environ_buf i32) (result (;errno;) i32)))` - - // functionEnvironSizesGet returns environment variable data sizes. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-environ_sizes_get---errno-size-size - functionEnvironSizesGet = "environ_sizes_get" - - // importEnvironSizesGet is the WebAssembly 1.0 Text format import of functionEnvironSizesGet. - importEnvironSizesGet = `(import "wasi_snapshot_preview1" "environ_sizes_get" - (func $wasi.environ_sizes_get (param $result.environc i32) (param $result.environBufSize i32) (result (;errno;) i32)))` - - // functionFdAdvise provides file advisory information on a file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_advisefd-fd-offset-filesize-len-filesize-advice-advice---errno - functionFdAdvise = "fd_advise" - - // importFdAdvise is the WebAssembly 1.0 Text format import of functionFdAdvise. - importFdAdvise = `(import "wasi_snapshot_preview1" "fd_advise" - (func $wasi.fd_advise (param $fd i32) (param $offset i64) (param $len i64) (param $result.advice i32) (result (;errno;) i32)))` - - // functionFdAllocate forces the allocation of space in a file. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_allocatefd-fd-offset-filesize-len-filesize---errno - functionFdAllocate = "fd_allocate" - - // importFdAllocate is the WebAssembly 1.0 Text format import of functionFdAllocate. - importFdAllocate = `(import "wasi_snapshot_preview1" "fd_allocate" - (func $wasi.fd_allocate (param $fd i32) (param $offset i64) (param $len i64) (result (;errno;) i32)))` - - // functionFdClose closes a file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_close - functionFdClose = "fd_close" - - // importFdClose is the WebAssembly 1.0 Text format import of functionFdClose. - importFdClose = `(import "wasi_snapshot_preview1" "fd_close" - (func $wasi.fd_close (param $fd i32) (result (;errno;) i32)))` - - // functionFdDatasync synchronizes the data of a file to disk. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_close - functionFdDatasync = "fd_datasync" - - // importFdDatasync is the WebAssembly 1.0 Text format import of functionFdDatasync. - importFdDatasync = `(import "wasi_snapshot_preview1" "fd_datasync" - (func $wasi.fd_datasync (param $fd i32) (result (;errno;) i32)))` - - // functionFdFdstatGet gets the attributes of a file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_fdstat_getfd-fd---errno-fdstat - functionFdFdstatGet = "fd_fdstat_get" - - // importFdFdstatGet is the WebAssembly 1.0 Text format import of functionFdFdstatGet. - _ = /* importFdFdstatGet */ `(import "wasi_snapshot_preview1" "fd_fdstat_get" - (func $wasi.fd_fdstat_get (param $fd i32) (param $result.stat i32) (result (;errno;) i32)))` //nolint - - // functionFdFdstatSetFlags adjusts the flags associated with a file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_fdstat_set_flagsfd-fd-flags-fdflags---errno - functionFdFdstatSetFlags = "fd_fdstat_set_flags" - - // importFdFdstatSetFlags is the WebAssembly 1.0 Text format import of functionFdFdstatSetFlags. - importFdFdstatSetFlags = `(import "wasi_snapshot_preview1" "fd_fdstat_set_flags" - (func $wasi.fd_fdstat_set_flags (param $fd i32) (param $flags i32) (result (;errno;) i32)))` - - // functionFdFdstatSetRights adjusts the rights associated with a file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_fdstat_set_rightsfd-fd-fs_rights_base-rights-fs_rights_inheriting-rights---errno - functionFdFdstatSetRights = "fd_fdstat_set_rights" - - // importFdFdstatSetRights is the WebAssembly 1.0 Text format import of functionFdFdstatSetRights. - importFdFdstatSetRights = `(import "wasi_snapshot_preview1" "fd_fdstat_set_rights" - (func $wasi.fd_fdstat_set_rights (param $fd i32) (param $fs_rights_base i64) (param $fs_rights_inheriting i64) (result (;errno;) i32)))` - - // functionFdFilestatGet returns the attributes of an open file. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_getfd-fd---errno-filestat - functionFdFilestatGet = "fd_filestat_get" - - // importFdFilestatGet is the WebAssembly 1.0 Text format import of functionFdFilestatGet. - importFdFilestatGet = `(import "wasi_snapshot_preview1" "fd_filestat_get" - (func $wasi.fd_filestat_get (param $fd i32) (param $result.buf i32) (result (;errno;) i32)))` - - // functionFdFilestatSetSize adjusts the size of an open file. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_set_sizefd-fd-size-filesize---errno - functionFdFilestatSetSize = "fd_filestat_set_size" - - // importFdFilestatSetSize is the WebAssembly 1.0 Text format import of functionFdFilestatSetSize. - importFdFilestatSetSize = `(import "wasi_snapshot_preview1" "fd_filestat_set_size" - (func $wasi.fd_filestat_set_size (param $fd i32) (param $size i64) (result (;errno;) i32)))` - - // functionFdFilestatSetTimes adjusts the times of an open file. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_set_timesfd-fd-atim-timestamp-mtim-timestamp-fst_flags-fstflags---errno - functionFdFilestatSetTimes = "fd_filestat_set_times" - - // importFdFilestatSetTimes is the WebAssembly 1.0 Text format import of functionFdFilestatSetTimes. - importFdFilestatSetTimes = `(import "wasi_snapshot_preview1" "fd_filestat_set_times" - (func $wasi.fd_filestat_set_times (param $fd i32) (param $atim i64) (param $mtim i64) (param $fst_flags i32) (result (;errno;) i32)))` - - // functionFdPread reads from a file descriptor, without using and updating the file descriptor's offset. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_preadfd-fd-iovs-iovec_array-offset-filesize---errno-size - functionFdPread = "fd_pread" - - // importFdPread is the WebAssembly 1.0 Text format import of functionFdPread. - importFdPread = `(import "wasi_snapshot_preview1" "fd_pread" - (func $wasi.fd_pread (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $offset i64) (param $result.nread i32) (result (;errno;) i32)))` - - // functionFdPrestatGet returns the prestat data of a file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_get - functionFdPrestatGet = "fd_prestat_get" - - // importFdPrestatGet is the WebAssembly 1.0 Text format import of functionFdPrestatGet. - importFdPrestatGet = `(import "wasi_snapshot_preview1" "fd_prestat_get" - (func $wasi.fd_prestat_get (param $fd i32) (param $result.prestat i32) (result (;errno;) i32)))` - - // functionFdPrestatDirName returns the path of the pre-opened directory of a file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name - functionFdPrestatDirName = "fd_prestat_dir_name" - - // importFdPrestatDirName is the WebAssembly 1.0 Text format import of functionFdPrestatDirName. - importFdPrestatDirName = `(import "wasi_snapshot_preview1" "fd_prestat_dir_name" - (func $wasi.fd_prestat_dir_name (param $fd i32) (param $path i32) (param $path_len i32) (result (;errno;) i32)))` - - // functionFdPwrite writes to a file descriptor, without using and updating the file descriptor's offset. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_pwritefd-fd-iovs-ciovec_array-offset-filesize---errno-size - functionFdPwrite = "fd_pwrite" - - // importFdPwrite is the WebAssembly 1.0 Text format import of functionFdPwrite. - importFdPwrite = `(import "wasi_snapshot_preview1" "fd_pwrite" - (func $wasi.fd_pwrite (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $offset i64) (param $result.nwritten i32) (result (;errno;) i32)))` - - // functionFdRead read bytes from a file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_read - functionFdRead = "fd_read" - - // importFdRead is the WebAssembly 1.0 Text format import of functionFdRead. - importFdRead = `(import "wasi_snapshot_preview1" "fd_read" - (func $wasi.fd_read (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32)))` - - // functionFdReaddir reads directory entries from a directory. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_readdirfd-fd-buf-pointeru8-buf_len-size-cookie-dircookie---errno-size - functionFdReaddir = "fd_readdir" - - // importFdReaddir is the WebAssembly 1.0 Text format import of functionFdReaddir. - importFdReaddir = `(import "wasi_snapshot_preview1" "fd_readdir" - (func $wasi.fd_readdir (param $fd i32) (param $buf i32) (param $buf_len i32) (param $cookie i64) (param $result.bufused i32) (result (;errno;) i32)))` - - // functionFdRenumber atomically replaces a file descriptor by renumbering another file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno - functionFdRenumber = "fd_renumber" - - // importFdRenumber is the WebAssembly 1.0 Text format import of functionFdRenumber. - importFdRenumber = `(import "wasi_snapshot_preview1" "fd_renumber" - (func $wasi.fd_renumber (param $fd i32) (param $to i32) (result (;errno;) i32)))` - - // functionFdSeek moves the offset of a file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_seekfd-fd-offset-filedelta-whence-whence---errno-filesize - functionFdSeek = "fd_seek" - - // importFdSeek is the WebAssembly 1.0 Text format import of functionFdSeek. - importFdSeek = `(import "wasi_snapshot_preview1" "fd_seek" - (func $wasi.fd_seek (param $fd i32) (param $offset i64) (param $whence i32) (param $result.newoffset i32) (result (;errno;) i32)))` - - // functionFdSync synchronizes the data and metadata of a file to disk. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_syncfd-fd---errno - functionFdSync = "fd_sync" - - // importFdSync is the WebAssembly 1.0 Text format import of functionFdSync. - importFdSync = `(import "wasi_snapshot_preview1" "fd_sync" - (func $wasi.fd_sync (param $fd i32) (result (;errno;) i32)))` - - // functionFdTell returns the current offset of a file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_tellfd-fd---errno-filesize - functionFdTell = "fd_tell" - - // importFdTell is the WebAssembly 1.0 Text format import of functionFdTell. - importFdTell = `(import "wasi_snapshot_preview1" "fd_tell" - (func $wasi.fd_tell (param $fd i32) (param $result.offset i32) (result (;errno;) i32)))` - - // functionFdWrite write bytes to a file descriptor. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write - functionFdWrite = "fd_write" - - // importFdWrite is the WebAssembly 1.0 Text format import of functionFdWrite. - importFdWrite = `(import "wasi_snapshot_preview1" "fd_write" - (func $wasi.fd_write (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32)))` - - // functionPathCreateDirectory creates a directory. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_create_directoryfd-fd-path-string---errno - functionPathCreateDirectory = "path_create_directory" - - // importPathCreateDirectory is the WebAssembly 1.0 Text format import of functionPathCreateDirectory. - importPathCreateDirectory = `(import "wasi_snapshot_preview1" "path_create_directory" - (func $wasi.path_create_directory (param $fd i32) (param $path i32) (param $path_len i32) (result (;errno;) i32)))` - - // functionPathFilestatGet returns the attributes of a file or directory. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_filestat_getfd-fd-flags-lookupflags-path-string---errno-filestat - functionPathFilestatGet = "path_filestat_get" - - // importPathFilestatGet is the WebAssembly 1.0 Text format import of functionPathFilestatGet. - importPathFilestatGet = `(import "wasi_snapshot_preview1" "path_filestat_get" - (func $wasi.path_filestat_get (param $fd i32) (param $flags i32) (param $path i32) (param $path_len i32) (param $result.buf i32) (result (;errno;) i32)))` - - // functionPathFilestatSetTimes adjusts the timestamps of a file or directory. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_filestat_set_timesfd-fd-flags-lookupflags-path-string-atim-timestamp-mtim-timestamp-fst_flags-fstflags---errno - functionPathFilestatSetTimes = "path_filestat_set_times" - - // importPathFilestatSetTimes is the WebAssembly 1.0 Text format import of functionPathFilestatSetTimes. - importPathFilestatSetTimes = `(import "wasi_snapshot_preview1" "path_filestat_set_times" - (func $wasi.path_filestat_set_times (param $fd i32) (param $flags i32) (param $path i32) (param $path_len i32) (param $atim i64) (param $mtim i64) (param $fst_flags i32) (result (;errno;) i32)))` - - // functionPathLink adjusts the timestamps of a file or directory. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#path_link - functionPathLink = "path_link" - - // importPathLink is the WebAssembly 1.0 Text format import of functionPathLink. - importPathLink = `(import "wasi_snapshot_preview1" "path_link" - (func $wasi.path_link (param $old_fd i32) (param $old_flags i32) (param $old_path i32) (param $old_path_len i32) (param $new_fd i32) (param $new_path i32) (param $new_path_len i32) (result (;errno;) i32)))` - - // functionPathOpen opens a file or directory. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_openfd-fd-dirflags-lookupflags-path-string-oflags-oflags-fs_rights_base-rights-fs_rights_inheriting-rights-fdflags-fdflags---errno-fd - functionPathOpen = "path_open" - - // importPathOpen is the WebAssembly 1.0 Text format import of functionPathOpen. - importPathOpen = `(import "wasi_snapshot_preview1" "path_open" - (func $wasi.path_open (param $fd i32) (param $dirflags i32) (param $path i32) (param $path_len i32) (param $oflags i32) (param $fs_rights_base i64) (param $fs_rights_inheriting i64) (param $fdflags i32) (param $result.opened_fd i32) (result (;errno;) i32)))` - - // functionPathReadlink reads the contents of a symbolic link. - // See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_readlinkfd-fd-path-string-buf-pointeru8-buf_len-size---errno-size - functionPathReadlink = "path_readlink" - - // importPathReadlink is the WebAssembly 1.0 Text format import of functionPathReadlink. - importPathReadlink = `(import "wasi_snapshot_preview1" "path_readlink" - (func $wasi.path_readlink (param $fd i32) (param $path i32) (param $path_len i32) (param $buf i32) (param $buf_len i32) (param $result.bufused i32) (result (;errno;) i32)))` - - // functionPathRemoveDirectory removes a directory. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_remove_directoryfd-fd-path-string---errno - functionPathRemoveDirectory = "path_remove_directory" - - // importPathRemoveDirectory is the WebAssembly 1.0 Text format import of functionPathRemoveDirectory. - importPathRemoveDirectory = `(import "wasi_snapshot_preview1" "path_remove_directory" - (func $wasi.path_remove_directory (param $fd i32) (param $path i32) (param $path_len i32) (result (;errno;) i32)))` - - // functionPathRename renames a file or directory. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_renamefd-fd-old_path-string-new_fd-fd-new_path-string---errno - functionPathRename = "path_rename" - - // importPathRename is the WebAssembly 1.0 Text format import of functionPathRename. - importPathRename = `(import "wasi_snapshot_preview1" "path_rename" - (func $wasi.path_rename (param $fd i32) (param $old_path i32) (param $old_path_len i32) (param $new_fd i32) (param $new_path i32) (param $new_path_len i32) (result (;errno;) i32)))` - - // functionPathSymlink creates a symbolic link. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#path_symlink - functionPathSymlink = "path_symlink" - - // importPathSymlink is the WebAssembly 1.0 Text format import of functionPathSymlink. - importPathSymlink = `(import "wasi_snapshot_preview1" "path_symlink" - (func $wasi.path_symlink (param $old_path i32) (param $old_path_len i32) (param $fd i32) (param $new_path i32) (param $new_path_len i32) (result (;errno;) i32)))` - - // functionPathUnlinkFile unlinks a file. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_unlink_filefd-fd-path-string---errno - functionPathUnlinkFile = "path_unlink_file" - - // importPathUnlinkFile is the WebAssembly 1.0 Text format import of functionPathUnlinkFile. - importPathUnlinkFile = `(import "wasi_snapshot_preview1" "path_unlink_file" - (func $wasi.path_unlink_file (param $fd i32) (param $path i32) (param $path_len i32) (result (;errno;) i32)))` - - // functionPollOneoff concurrently polls for the occurrence of a set of events. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-poll_oneoffin-constpointersubscription-out-pointerevent-nsubscriptions-size---errno-size - functionPollOneoff = "poll_oneoff" - - // importPollOneoff is the WebAssembly 1.0 Text format import of functionPollOneoff. - importPollOneoff = `(import "wasi_snapshot_preview1" "poll_oneoff" - (func $wasi.poll_oneoff (param $in i32) (param $out i32) (param $nsubscriptions i32) (param $result.nevents i32) (result (;errno;) i32)))` - - // functionSchedYield temporarily yields execution of the calling thread. - // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sched_yield---errno - functionSchedYield = "sched_yield" - - // importSchedYield is the WebAssembly 1.0 Text format import of functionSchedYield. - importSchedYield = `(import "wasi_snapshot_preview1" "sched_yield" - (func $wasi.sched_yield (result (;errno;) i32)))` - - // functionRandomGet writes random data in buffer. - // See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-buf_len-size---errno - functionRandomGet = "random_get" - - // importRandomGet is the WebAssembly 1.0 Text format import of functionRandomGet. - importRandomGet = `(import "wasi_snapshot_preview1" "random_get" - (func $wasi.random_get (param $buf i32) (param $buf_len i32) (result (;errno;) i32)))` - - // functionSockRecv receives a message from a socket. - // See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_recvfd-fd-ri_data-iovec_array-ri_flags-riflags---errno-size-roflags - functionSockRecv = "sock_recv" - - // importSockRecv is the WebAssembly 1.0 Text format import of functionSockRecv. - importSockRecv = `(import "wasi_snapshot_preview1" "sock_recv" - (func $wasi.sock_recv (param $fd i32) (param $ri_data i32) (param $ri_data_count i32) (param $ri_flags i32) (param $result.ro_datalen i32) (param $result.ro_flags i32) (result (;errno;) i32)))` - - // functionSockSend sends a message on a socket. - // See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_sendfd-fd-si_data-ciovec_array-si_flags-siflags---errno-size - functionSockSend = "sock_send" - - // importSockSend is the WebAssembly 1.0 Text format import of functionSockSend. - importSockSend = `(import "wasi_snapshot_preview1" "sock_send" - (func $wasi.sock_send (param $fd i32) (param $si_data i32) (param $si_data_count i32) (param $si_flags i32) (param $result.so_datalen i32) (result (;errno;) i32)))` - - // functionSockShutdown shuts down socket send and receive channels. - // See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_shutdownfd-fd-how-sdflags---errno - functionSockShutdown = "sock_shutdown" - - // importSockShutdown is the WebAssembly 1.0 Text format import of functionSockShutdown. - importSockShutdown = `(import "wasi_snapshot_preview1" "sock_shutdown" - (func $wasi.sock_shutdown (param $fd i32) (param $how i32) (result (;errno;) i32)))` -) - -// wasi includes all host functions to export for WASI version "wasi_snapshot_preview1". -// // ## Translation notes // ### String // WebAssembly 1.0 has no string type, so any string input parameter expands to two uint32 parameters: offset @@ -440,857 +105,104 @@ const ( // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md // See https://github.com/WebAssembly/WASI/issues/215 // See https://wwa.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0. -type wasi struct{} // exportFunctions adds all go functions that implement wasi. // These should be exported in the module named ModuleName. func exportFunctions(builder wazero.ModuleBuilder) { - a := &wasi{} - // Note: these are ordered per spec for consistency even if the resulting map can't guarantee that. + // Note:se are ordered per spec for consistency even if the resulting map can't guarantee that. // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#functions - builder.ExportFunction(functionArgsGet, a.ArgsGet, + builder.ExportFunction(functionArgsGet, argsGet, functionArgsGet, "argv", "argv_buf") - builder.ExportFunction(functionArgsSizesGet, a.ArgsSizesGet, + builder.ExportFunction(functionArgsSizesGet, argsSizesGet, functionArgsSizesGet, "result.argc", "result.argv_buf_size") - builder.ExportFunction(functionEnvironGet, a.EnvironGet, + builder.ExportFunction(functionEnvironGet, environGet, functionEnvironGet, "environ", "environ_buf") - builder.ExportFunction(functionEnvironSizesGet, a.EnvironSizesGet, + builder.ExportFunction(functionEnvironSizesGet, environSizesGet, functionEnvironSizesGet, "result.environc", "result.environBufSize") - builder.ExportFunction(functionClockResGet, a.ClockResGet, + builder.ExportFunction(functionClockResGet, clockResGet, functionClockResGet, "id", "result.resolution") - builder.ExportFunction(functionClockTimeGet, a.ClockTimeGet, + builder.ExportFunction(functionClockTimeGet, clockTimeGet, functionClockTimeGet, "id", "precision", "result.timestamp") - builder.ExportFunction(functionFdAdvise, a.FdAdvise, + builder.ExportFunction(functionFdAdvise, fdAdvise, functionFdAdvise, "fd", "offset", "len", "result.advice") - builder.ExportFunction(functionFdAllocate, a.FdAllocate, + builder.ExportFunction(functionFdAllocate, fdAllocate, functionFdAllocate, "fd", "offset", "len") - builder.ExportFunction(functionFdClose, a.FdClose, + builder.ExportFunction(functionFdClose, fdClose, functionFdClose, "fd") - builder.ExportFunction(functionFdDatasync, a.FdDatasync, + builder.ExportFunction(functionFdDatasync, fdDatasync, functionFdDatasync, "fd") - builder.ExportFunction(functionFdFdstatGet, a.FdFdstatGet, + builder.ExportFunction(functionFdFdstatGet, fdFdstatGet, functionFdFdstatGet, "fd", "result.stat") - builder.ExportFunction(functionFdFdstatSetFlags, a.FdFdstatSetFlags, + builder.ExportFunction(functionFdFdstatSetFlags, fdFdstatSetFlags, functionFdFdstatSetFlags, "fd", "flags") - builder.ExportFunction(functionFdFdstatSetRights, a.FdFdstatSetRights, + builder.ExportFunction(functionFdFdstatSetRights, fdFdstatSetRights, functionFdFdstatSetRights, "fd", "fs_rights_base", "fs_rights_inheriting") - builder.ExportFunction(functionFdFilestatGet, a.FdFilestatGet, + builder.ExportFunction(functionFdFilestatGet, fdFilestatGet, functionFdFilestatGet, "fd", "result.buf") - builder.ExportFunction(functionFdFilestatSetSize, a.FdFilestatSetSize, + builder.ExportFunction(functionFdFilestatSetSize, fdFilestatSetSize, functionFdFilestatSetSize, "fd", "size") - builder.ExportFunction(functionFdFilestatSetTimes, a.FdFilestatSetTimes, + builder.ExportFunction(functionFdFilestatSetTimes, fdFilestatSetTimes, functionFdFilestatSetTimes, "fd", "atim", "mtim", "fst_flags") - builder.ExportFunction(functionFdPread, a.FdPread, + builder.ExportFunction(functionFdPread, fdPread, functionFdPread, "fd", "iovs", "iovs_len", "offset", "result.nread") - builder.ExportFunction(functionFdPrestatGet, a.FdPrestatGet, + builder.ExportFunction(functionFdPrestatGet, fdPrestatGet, functionFdPrestatGet, "fd", "result.prestat") - builder.ExportFunction(functionFdPrestatDirName, a.FdPrestatDirName, + builder.ExportFunction(functionFdPrestatDirName, fdPrestatDirName, functionFdPrestatDirName, "fd", "path", "path_len") - builder.ExportFunction(functionFdPwrite, a.FdPwrite, + builder.ExportFunction(functionFdPwrite, fdPwrite, functionFdPwrite, "fd", "iovs", "iovs_len", "offset", "result.nwritten") - builder.ExportFunction(functionFdRead, a.FdRead, + builder.ExportFunction(functionFdRead, fdRead, functionFdRead, "fd", "iovs", "iovs_len", "result.size") - builder.ExportFunction(functionFdReaddir, a.FdReaddir, + builder.ExportFunction(functionFdReaddir, fdReaddir, functionFdReaddir, "fd", "buf", "buf_len", "cookie", "result.bufused") - builder.ExportFunction(functionFdRenumber, a.FdRenumber, + builder.ExportFunction(functionFdRenumber, fdRenumber, functionFdRenumber, "fd", "to") - builder.ExportFunction(functionFdSeek, a.FdSeek, + builder.ExportFunction(functionFdSeek, fdSeek, functionFdSeek, "fd", "offset", "whence", "result.newoffset") - builder.ExportFunction(functionFdSync, a.FdSync, + builder.ExportFunction(functionFdSync, fdSync, functionFdSync, "fd") - builder.ExportFunction(functionFdTell, a.FdTell, + builder.ExportFunction(functionFdTell, fdTell, functionFdTell, "fd", "result.offset") - builder.ExportFunction(functionFdWrite, a.FdWrite, + builder.ExportFunction(functionFdWrite, fdWrite, functionFdWrite, "fd", "iovs", "iovs_len", "result.size") - builder.ExportFunction(functionPathCreateDirectory, a.PathCreateDirectory, + builder.ExportFunction(functionPathCreateDirectory, pathCreateDirectory, functionPathCreateDirectory, "fd", "path", "path_len") - builder.ExportFunction(functionPathFilestatGet, a.PathFilestatGet, + builder.ExportFunction(functionPathFilestatGet, pathFilestatGet, functionPathFilestatGet, "fd", "flags", "path", "path_len", "result.buf") - builder.ExportFunction(functionPathFilestatSetTimes, a.PathFilestatSetTimes, + builder.ExportFunction(functionPathFilestatSetTimes, pathFilestatSetTimes, functionPathFilestatSetTimes, "fd", "flags", "path", "path_len", "atim", "mtim", "fst_flags") - builder.ExportFunction(functionPathLink, a.PathLink, + builder.ExportFunction(functionPathLink, pathLink, functionPathLink, "old_fd", "old_flags", "old_path", "old_path_len", "new_fd", "new_path", "new_path_len") - builder.ExportFunction(functionPathOpen, a.PathOpen, + builder.ExportFunction(functionPathOpen, pathOpen, functionPathOpen, "fd", "dirflags", "path", "path_len", "oflags", "fs_rights_base", "fs_rights_inheriting", "fdflags", "result.opened_fd") - builder.ExportFunction(functionPathReadlink, a.PathReadlink, + builder.ExportFunction(functionPathReadlink, pathReadlink, functionPathReadlink, "fd", "path", "path_len", "buf", "buf_len", "result.bufused") - builder.ExportFunction(functionPathRemoveDirectory, a.PathRemoveDirectory, + builder.ExportFunction(functionPathRemoveDirectory, pathRemoveDirectory, functionPathRemoveDirectory, "fd", "path", "path_len") - builder.ExportFunction(functionPathRename, a.PathRename, + builder.ExportFunction(functionPathRename, pathRename, functionPathRename, "fd", "old_path", "old_path_len", "new_fd", "new_path", "new_path_len") - builder.ExportFunction(functionPathSymlink, a.PathSymlink, + builder.ExportFunction(functionPathSymlink, pathSymlink, functionPathSymlink, "old_path", "old_path_len", "fd", "new_path", "new_path_len") - builder.ExportFunction(functionPathUnlinkFile, a.PathUnlinkFile, + builder.ExportFunction(functionPathUnlinkFile, pathUnlinkFile, functionPathUnlinkFile, "fd", "path", "path_len") - builder.ExportFunction(functionPollOneoff, a.PollOneoff, + builder.ExportFunction(functionPollOneoff, pollOneoff, functionPollOneoff, "in", "out", "nsubscriptions", "result.nevents") - builder.ExportFunction(functionProcExit, a.ProcExit, + builder.ExportFunction(functionProcExit, procExit, functionProcExit, "rval") - builder.ExportFunction(functionProcRaise, a.ProcRaise, + builder.ExportFunction(functionProcRaise, procRaise, functionProcRaise, "sig") - builder.ExportFunction(functionSchedYield, a.SchedYield, + builder.ExportFunction(functionSchedYield, schedYield, functionSchedYield) - builder.ExportFunction(functionRandomGet, a.RandomGet, + builder.ExportFunction(functionRandomGet, randomGet, functionRandomGet, "buf", "buf_len") - builder.ExportFunction(functionSockRecv, a.SockRecv, + builder.ExportFunction(functionSockRecv, sockRecv, functionSockRecv, "fd", "ri_data", "ri_data_count", "ri_flags", "result.ro_datalen", "result.ro_flags") - builder.ExportFunction(functionSockSend, a.SockSend, + builder.ExportFunction(functionSockSend, sockSend, functionSockSend, "fd", "si_data", "si_data_count", "si_flags", "result.so_datalen") - builder.ExportFunction(functionSockShutdown, a.SockShutdown, + builder.ExportFunction(functionSockShutdown, sockShutdown, functionSockShutdown, "fd", "how") } -// ArgsGet is the WASI function that reads command-line argument data (WithArgs). -// -// There are two parameters. Both are offsets in api.Module Memory. If either are invalid due to -// memory constraints, this returns ErrnoFault. -// -// * argv - is the offset to begin writing argument offsets in uint32 little-endian encoding. -// * ArgsSizesGet result argc * 4 bytes are written to this offset -// * argvBuf - is the offset to write the null terminated arguments to mod.Memory -// * ArgsSizesGet result argv_buf_size bytes are written to this offset -// -// For example, if ArgsSizesGet wrote argc=2 and argvBufSize=5 for arguments: "a" and "bc" -// parameters argv=7 and argvBuf=1, this function writes the below to `mod.Memory`: -// -// argvBufSize uint32le uint32le -// +----------------+ +--------+ +--------+ -// | | | | | | -// []byte{?, 'a', 0, 'b', 'c', 0, ?, 1, 0, 0, 0, 3, 0, 0, 0, ?} -// argvBuf --^ ^ ^ -// argv --| | -// offset that begins "a" --+ | -// offset that begins "bc" --+ -// -// Note: importArgsGet shows this signature in the WebAssembly 1.0 Text Format. -// See ArgsSizesGet -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_get -// See https://en.wikipedia.org/wiki/Null-terminated_string -func (a *wasi) ArgsGet(ctx context.Context, mod api.Module, argv, argvBuf uint32) Errno { - sysCtx := mod.(*wasm.CallContext).Sys - return writeOffsetsAndNullTerminatedValues(ctx, mod.Memory(), sysCtx.Args(), argv, argvBuf) -} - -// ArgsSizesGet is the WASI function named functionArgsSizesGet that reads command-line argument data (WithArgs) -// sizes. -// -// There are two result parameters: these are offsets in the api.Module Memory to write -// corresponding sizes in uint32 little-endian encoding. If either are invalid due to memory constraints, this -// returns ErrnoFault. -// -// * resultArgc - is the offset to write the argument count to mod.Memory -// * resultArgvBufSize - is the offset to write the null-terminated argument length to mod.Memory -// -// For example, if WithArgs are []string{"a","bc"} and -// parameters resultArgc=1 and resultArgvBufSize=6, this function writes the below to `mod.Memory`: -// -// uint32le uint32le -// +--------+ +--------+ -// | | | | -// []byte{?, 2, 0, 0, 0, ?, 5, 0, 0, 0, ?} -// resultArgc --^ ^ -// 2 args --+ | -// resultArgvBufSize --| -// len([]byte{'a',0,'b',c',0}) --+ -// -// Note: importArgsSizesGet shows this signature in the WebAssembly 1.0 Text Format. -// See ArgsGet -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_sizes_get -// See https://en.wikipedia.org/wiki/Null-terminated_string -func (a *wasi) ArgsSizesGet(ctx context.Context, mod api.Module, resultArgc, resultArgvBufSize uint32) Errno { - sysCtx := mod.(*wasm.CallContext).Sys - mem := mod.Memory() - - if !mem.WriteUint32Le(ctx, resultArgc, uint32(len(sysCtx.Args()))) { - return ErrnoFault - } - if !mem.WriteUint32Le(ctx, resultArgvBufSize, sysCtx.ArgsSize()) { - return ErrnoFault - } - return ErrnoSuccess -} - -// EnvironGet is the WASI function named functionEnvironGet that reads environment variables. (WithEnviron) -// -// There are two parameters. Both are offsets in api.Module Memory. If either are invalid due to -// memory constraints, this returns ErrnoFault. -// -// * environ - is the offset to begin writing environment variables offsets in uint32 little-endian encoding. -// * EnvironSizesGet result environc * 4 bytes are written to this offset -// * environBuf - is the offset to write the environment variables to mod.Memory -// * the format is the same as os.Environ, null terminated "key=val" entries -// * EnvironSizesGet result environBufSize bytes are written to this offset -// -// For example, if EnvironSizesGet wrote environc=2 and environBufSize=9 for environment variables: "a=b", "b=cd" -// and parameters environ=11 and environBuf=1, this function writes the below to `mod.Memory`: -// -// environBufSize uint32le uint32le -// +------------------------------------+ +--------+ +--------+ -// | | | | | | -// []byte{?, 'a', '=', 'b', 0, 'b', '=', 'c', 'd', 0, ?, 1, 0, 0, 0, 5, 0, 0, 0, ?} -// environBuf --^ ^ ^ -// environ offset for "a=b" --+ | -// environ offset for "b=cd" --+ -// -// Note: importEnvironGet shows this signature in the WebAssembly 1.0 Text Format. -// See EnvironSizesGet -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_get -// See https://en.wikipedia.org/wiki/Null-terminated_string -func (a *wasi) EnvironGet(ctx context.Context, mod api.Module, environ uint32, environBuf uint32) Errno { - sysCtx := mod.(*wasm.CallContext).Sys - return writeOffsetsAndNullTerminatedValues(ctx, mod.Memory(), sysCtx.Environ(), environ, environBuf) -} - -// EnvironSizesGet is the WASI function named functionEnvironSizesGet that reads environment variable -// (WithEnviron) sizes. -// -// There are two result parameters: these are offsets in the wasiapi.Module Memory to write -// corresponding sizes in uint32 little-endian encoding. If either are invalid due to memory constraints, this -// returns ErrnoFault. -// -// * resultEnvironc - is the offset to write the environment variable count to mod.Memory -// * resultEnvironBufSize - is the offset to write the null-terminated environment variable length to mod.Memory -// -// For example, if WithEnviron is []string{"a=b","b=cd"} and -// parameters resultEnvironc=1 and resultEnvironBufSize=6, this function writes the below to `mod.Memory`: -// -// uint32le uint32le -// +--------+ +--------+ -// | | | | -// []byte{?, 2, 0, 0, 0, ?, 9, 0, 0, 0, ?} -// resultEnvironc --^ ^ -// 2 variables --+ | -// resultEnvironBufSize --| -// len([]byte{'a','=','b',0, | -// 'b','=','c','d',0}) --+ -// -// Note: importEnvironGet shows this signature in the WebAssembly 1.0 Text Format. -// See EnvironGet -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_sizes_get -// See https://en.wikipedia.org/wiki/Null-terminated_string -func (a *wasi) EnvironSizesGet(ctx context.Context, mod api.Module, resultEnvironc uint32, resultEnvironBufSize uint32) Errno { - sysCtx := mod.(*wasm.CallContext).Sys - mem := mod.Memory() - - if !mem.WriteUint32Le(ctx, resultEnvironc, uint32(len(sysCtx.Environ()))) { - return ErrnoFault - } - if !mem.WriteUint32Le(ctx, resultEnvironBufSize, sysCtx.EnvironSize()) { - return ErrnoFault - } - - return ErrnoSuccess -} - -// FdAdvise is the WASI function named functionFdAdvise and is stubbed for GrainLang per #271 -func (a *wasi) FdAdvise(ctx context.Context, mod api.Module, fd uint32, offset, len uint64, resultAdvice uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdAllocate is the WASI function named functionFdAllocate and is stubbed for GrainLang per #271 -func (a *wasi) FdAllocate(ctx context.Context, mod api.Module, fd uint32, offset, len uint64) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdClose is the WASI function to close a file descriptor. This returns ErrnoBadf if the fd is invalid. -// -// * fd - the file descriptor to close -// -// Note: importFdClose shows this signature in the WebAssembly 1.0 Text Format. -// Note: This is similar to `close` in POSIX. -// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_close -// See https://linux.die.net/man/3/close -func (a *wasi) FdClose(ctx context.Context, mod api.Module, fd uint32) Errno { - sysCtx := mod.(*wasm.CallContext).Sys - if ok := sysCtx.FS(ctx).CloseFile(ctx, fd); !ok { - return ErrnoBadf - } - - return ErrnoSuccess -} - -// FdDatasync is the WASI function named functionFdDatasync and is stubbed for GrainLang per #271 -func (a *wasi) FdDatasync(ctx context.Context, mod api.Module, fd uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdFdstatGet is the WASI function to return the attributes of a file descriptor. -// -// * fd - the file descriptor to get the fdstat attributes data -// * resultFdstat - the offset to write the result fdstat data -// -// The Errno returned is ErrnoSuccess except the following error conditions: -// * ErrnoBadf - if `fd` is invalid -// * ErrnoFault - if `resultFdstat` contains an invalid offset due to the memory constraint -// -// fdstat byte layout is 24-byte size, which as the following elements in order -// * fs_filetype 1 byte, to indicate the file type -// * fs_flags 2 bytes, to indicate the file descriptor flag -// * 5 pad bytes -// * fs_right_base 8 bytes, to indicate the current rights of the fd -// * fs_right_inheriting 8 bytes, to indicate the maximum rights of the fd -// -// For example, with a file corresponding with `fd` was a directory (=3) opened with `fd_read` right (=1) and no fs_flags (=0), -// parameter resultFdstat=1, this function writes the below to `mod.Memory`: -// -// uint16le padding uint64le uint64le -// uint8 --+ +--+ +-----------+ +--------------------+ +--------------------+ -// | | | | | | | | | -// []byte{?, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0} -// resultFdstat --^ ^-- fs_flags ^-- fs_right_base ^-- fs_right_inheriting -// | -// +-- fs_filetype -// -// Note: importFdFdstatGet shows this signature in the WebAssembly 1.0 Text Format. -// Note: FdFdstatGet returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as well as additional fields. -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fdstat -// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_fdstat_get -// See https://linux.die.net/man/3/fsync -func (a *wasi) FdFdstatGet(ctx context.Context, mod api.Module, fd uint32, resultStat uint32) Errno { - sysCtx := mod.(*wasm.CallContext).Sys - if _, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok { - return ErrnoBadf - } - return ErrnoSuccess -} - -// FdPrestatGet is the WASI function to return the prestat data of a file descriptor. -// -// * fd - the file descriptor to get the prestat -// * resultPrestat - the offset to write the result prestat data -// -// The Errno returned is ErrnoSuccess except the following error conditions: -// * ErrnoBadf - if `fd` is invalid or the `fd` is not a pre-opened directory. -// * ErrnoFault - if `resultPrestat` is an invalid offset due to the memory constraint -// -// prestat byte layout is 8 bytes, beginning with an 8-bit tag and 3 pad bytes. The only valid tag is `prestat_dir`, -// which is tag zero. This simplifies the byte layout to 4 empty bytes followed by the uint32le encoded path length. -// -// For example, the directory name corresponding with `fd` was "/tmp" and -// parameter resultPrestat=1, this function writes the below to `mod.Memory`: -// -// padding uint32le -// uint8 --+ +-----+ +--------+ -// | | | | | -// []byte{?, 0, 0, 0, 0, 4, 0, 0, 0, ?} -// resultPrestat --^ ^ -// tag --+ | -// +-- size in bytes of the string "/tmp" -// -// Note: importFdPrestatGet shows this signature in the WebAssembly 1.0 Text Format. -// See FdPrestatDirName -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#prestat -// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_prestat_get -func (a *wasi) FdPrestatGet(ctx context.Context, mod api.Module, fd uint32, resultPrestat uint32) Errno { - sysCtx := mod.(*wasm.CallContext).Sys - entry, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd) - if !ok { - return ErrnoBadf - } - - // Zero-value 8-bit tag, and 3-byte zero-value paddings, which is uint32le(0) in short. - if !mod.Memory().WriteUint32Le(ctx, resultPrestat, uint32(0)) { - return ErrnoFault - } - // Write the length of the directory name at offset 4. - if !mod.Memory().WriteUint32Le(ctx, resultPrestat+4, uint32(len(entry.Path))) { - return ErrnoFault - } - - return ErrnoSuccess -} - -// FdFdstatSetFlags is the WASI function named functionFdFdstatSetFlags and is stubbed for GrainLang per #271 -func (a *wasi) FdFdstatSetFlags(ctx context.Context, mod api.Module, fd uint32, flags uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdFdstatSetRights implements wasi.FdFdstatSetRights -// Note: This will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 -func (a *wasi) FdFdstatSetRights(ctx context.Context, mod api.Module, fd uint32, fsRightsBase, fsRightsInheriting uint64) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdFilestatGet is the WASI function named functionFdFilestatGet -func (a *wasi) FdFilestatGet(ctx context.Context, mod api.Module, fd uint32, resultBuf uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdFilestatSetSize is the WASI function named functionFdFilestatSetSize -func (a *wasi) FdFilestatSetSize(ctx context.Context, mod api.Module, fd uint32, size uint64) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdFilestatSetTimes is the WASI function named functionFdFilestatSetTimes -func (a *wasi) FdFilestatSetTimes(ctx context.Context, mod api.Module, fd uint32, atim, mtim uint64, fstFlags uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdPread is the WASI function named functionFdPread -func (a *wasi) FdPread(ctx context.Context, mod api.Module, fd, iovs, iovsCount uint32, offset uint64, resultNread uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdPrestatDirName is the WASI function to return the path of the pre-opened directory of a file descriptor. -// -// * fd - the file descriptor to get the path of the pre-opened directory -// * path - the offset in `mod.Memory` to write the result path -// * pathLen - the count of bytes to write to `path` -// * This should match the uint32le FdPrestatGet writes to offset `resultPrestat`+4 -// -// The Errno returned is ErrnoSuccess except the following error conditions: -// * ErrnoBadf - if `fd` is invalid -// * ErrnoFault - if `path` is an invalid offset due to the memory constraint -// * ErrnoNametoolong - if `pathLen` is longer than the actual length of the result path -// -// For example, the directory name corresponding with `fd` was "/tmp" and -// parameters path=1 pathLen=4 (correct), this function will write the below to `mod.Memory`: -// -// pathLen -// +--------------+ -// | | -// []byte{?, '/', 't', 'm', 'p', ?} -// path --^ -// -// Note: importFdPrestatDirName shows this signature in the WebAssembly 1.0 Text Format. -// See FdPrestatGet -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name -func (a *wasi) FdPrestatDirName(ctx context.Context, mod api.Module, fd uint32, pathPtr uint32, pathLen uint32) Errno { - sysCtx := mod.(*wasm.CallContext).Sys - f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd) - if !ok { - return ErrnoBadf - } - - // Some runtimes may have another semantics. See /RATIONALE.md - if uint32(len(f.Path)) < pathLen { - return ErrnoNametoolong - } - - // TODO: FdPrestatDirName may have to return ErrnoNotdir if the type of the prestat data of `fd` is not a PrestatDir. - if !mod.Memory().Write(ctx, pathPtr, []byte(f.Path)[:pathLen]) { - return ErrnoFault - } - return ErrnoSuccess -} - -// FdPwrite is the WASI function named functionFdPwrite -func (a *wasi) FdPwrite(ctx context.Context, mod api.Module, fd, iovs, iovsCount uint32, offset uint64, resultNwritten uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdRead is the WASI function to read from a file descriptor. -// -// * fd - an opened file descriptor to read data from -// * iovs - the offset in `mod.Memory` to read offset, size pairs representing where to write file data. -// * Both offset and length are encoded as uint32le. -// * iovsCount - the count of memory offset, size pairs to read sequentially starting at iovs. -// * resultSize - the offset in `mod.Memory` to write the number of bytes read -// -// The Errno returned is ErrnoSuccess except the following error conditions: -// * ErrnoBadf - if `fd` is invalid -// * ErrnoFault - if `iovs` or `resultSize` contain an invalid offset due to the memory constraint -// * ErrnoIo - if an IO related error happens during the operation -// -// For example, this function needs to first read `iovs` to determine where to write contents. If -// parameters iovs=1 iovsCount=2, this function reads two offset/length pairs from `mod.Memory`: -// -// iovs[0] iovs[1] -// +---------------------+ +--------------------+ -// | uint32le uint32le| |uint32le uint32le| -// +---------+ +--------+ +--------+ +--------+ -// | | | | | | | | -// []byte{?, 18, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 2, 0, 0, 0, ?... } -// iovs --^ ^ ^ ^ -// | | | | -// offset --+ length --+ offset --+ length --+ -// -// If the contents of the `fd` parameter was "wazero" (6 bytes) and -// parameter resultSize=26, this function writes the below to `mod.Memory`: -// -// iovs[0].length iovs[1].length -// +--------------+ +----+ uint32le -// | | | | +--------+ -// []byte{ 0..16, ?, 'w', 'a', 'z', 'e', ?, 'r', 'o', ?, 6, 0, 0, 0 } -// iovs[0].offset --^ ^ ^ -// iovs[1].offset --+ | -// resultSize --+ -// -// Note: importFdRead shows this signature in the WebAssembly 1.0 Text Format. -// Note: This is similar to `readv` in POSIX. -// See FdWrite -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_read -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#iovec -// See https://linux.die.net/man/3/readv -func (a *wasi) FdRead(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno { - sysCtx := mod.(*wasm.CallContext).Sys - reader := internalsys.FdReader(ctx, sysCtx, fd) - if reader == nil { - return ErrnoBadf - } - - var nread uint32 - for i := uint32(0); i < iovsCount; i++ { - iovPtr := iovs + i*8 - offset, ok := mod.Memory().ReadUint32Le(ctx, iovPtr) - if !ok { - return ErrnoFault - } - l, ok := mod.Memory().ReadUint32Le(ctx, iovPtr+4) - if !ok { - return ErrnoFault - } - b, ok := mod.Memory().Read(ctx, offset, l) - if !ok { - return ErrnoFault - } - n, err := reader.Read(b) // Note: n <= l - nread += uint32(n) - if errors.Is(err, io.EOF) { - break - } else if err != nil { - return ErrnoIo - } - } - if !mod.Memory().WriteUint32Le(ctx, resultSize, nread) { - return ErrnoFault - } - return ErrnoSuccess -} - -// FdReaddir is the WASI function named functionFdReaddir -func (a *wasi) FdReaddir(ctx context.Context, mod api.Module, fd, buf, bufLen uint32, cookie uint64, resultBufused uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdRenumber is the WASI function named functionFdRenumber -func (a *wasi) FdRenumber(ctx context.Context, mod api.Module, fd, to uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdSeek is the WASI function to move the offset of a file descriptor. -// -// * fd: the file descriptor to move the offset of -// * offset: the signed int64, which is encoded as uint64, input argument to `whence`, which results in a new offset -// * whence: the operator that creates the new offset, given `offset` bytes -// * If io.SeekStart, new offset == `offset`. -// * If io.SeekCurrent, new offset == existing offset + `offset`. -// * If io.SeekEnd, new offset == file size of `fd` + `offset`. -// * resultNewoffset: the offset in `mod.Memory` to write the new offset to, relative to start of the file -// -// The Errno returned is ErrnoSuccess except the following error conditions: -// * ErrnoBadf - if `fd` is invalid -// * ErrnoFault - if `resultNewoffset` is an invalid offset in `mod.Memory` due to the memory constraint -// * ErrnoInval - if `whence` is an invalid value -// * ErrnoIo - if other error happens during the operation of the underying file system -// -// For example, if fd 3 is a file with offset 0, and -// parameters fd=3, offset=4, whence=0 (=io.SeekStart), resultNewOffset=1, -// this function writes the below to `mod.Memory`: -// -// uint64le -// +--------------------+ -// | | -// []byte{?, 4, 0, 0, 0, 0, 0, 0, 0, ? } -// resultNewoffset --^ -// -// See io.Seeker -// Note: importFdSeek shows this signature in the WebAssembly 1.0 Text Format. -// Note: This is similar to `lseek` in POSIX. -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_seek -// See https://linux.die.net/man/3/lseek -func (a *wasi) FdSeek(ctx context.Context, mod api.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) Errno { - sysCtx := mod.(*wasm.CallContext).Sys - var seeker io.Seeker - // Check to see if the file descriptor is available - if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok || f.File == nil { - return ErrnoBadf - // fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it. - } else if seeker, ok = f.File.(io.Seeker); !ok { - return ErrnoBadf - } - - if whence > io.SeekEnd /* exceeds the largest valid whence */ { - return ErrnoInval - } - newOffset, err := seeker.Seek(int64(offset), int(whence)) - if err != nil { - return ErrnoIo - } - - if !mod.Memory().WriteUint32Le(ctx, resultNewoffset, uint32(newOffset)) { - return ErrnoFault - } - - return ErrnoSuccess -} - -// FdSync is the WASI function named functionFdSync -func (a *wasi) FdSync(ctx context.Context, mod api.Module, fd uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdTell is the WASI function named functionFdTell -func (a *wasi) FdTell(ctx context.Context, mod api.Module, fd, resultOffset uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// FdWrite is the WASI function to write to a file descriptor. -// -// * fd - an opened file descriptor to write data to -// * iovs - the offset in `mod.Memory` to read offset, size pairs representing the data to write to `fd` -// * Both offset and length are encoded as uint32le. -// * iovsCount - the count of memory offset, size pairs to read sequentially starting at iovs. -// * resultSize - the offset in `mod.Memory` to write the number of bytes written -// -// The Errno returned is ErrnoSuccess except the following error conditions: -// * ErrnoBadf - if `fd` is invalid -// * ErrnoFault - if `iovs` or `resultSize` contain an invalid offset due to the memory constraint -// * ErrnoIo - if an IO related error happens during the operation -// -// For example, this function needs to first read `iovs` to determine what to write to `fd`. If -// parameters iovs=1 iovsCount=2, this function reads two offset/length pairs from `mod.Memory`: -// -// iovs[0] iovs[1] -// +---------------------+ +--------------------+ -// | uint32le uint32le| |uint32le uint32le| -// +---------+ +--------+ +--------+ +--------+ -// | | | | | | | | -// []byte{?, 18, 0, 0, 0, 4, 0, 0, 0, 23, 0, 0, 0, 2, 0, 0, 0, ?... } -// iovs --^ ^ ^ ^ -// | | | | -// offset --+ length --+ offset --+ length --+ -// -// This function reads those chunks `mod.Memory` into the `fd` sequentially. -// -// iovs[0].length iovs[1].length -// +--------------+ +----+ -// | | | | -// []byte{ 0..16, ?, 'w', 'a', 'z', 'e', ?, 'r', 'o', ? } -// iovs[0].offset --^ ^ -// iovs[1].offset --+ -// -// Since "wazero" was written, if parameter resultSize=26, this function writes the below to `mod.Memory`: -// -// uint32le -// +--------+ -// | | -// []byte{ 0..24, ?, 6, 0, 0, 0', ? } -// resultSize --^ -// -// Note: importFdWrite shows this signature in the WebAssembly 1.0 Text Format. -// Note: This is similar to `writev` in POSIX. -// See FdRead -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#ciovec -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write -// See https://linux.die.net/man/3/writev -func (a *wasi) FdWrite(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno { - sysCtx := mod.(*wasm.CallContext).Sys - writer := internalsys.FdWriter(ctx, sysCtx, fd) - if writer == nil { - return ErrnoBadf - } - - var nwritten uint32 - for i := uint32(0); i < iovsCount; i++ { - iovPtr := iovs + i*8 - offset, ok := mod.Memory().ReadUint32Le(ctx, iovPtr) - if !ok { - return ErrnoFault - } - // Note: emscripten has been known to write zero length iovec. However, - // it is not common in other compilers, so we don't optimize for it. - l, ok := mod.Memory().ReadUint32Le(ctx, iovPtr+4) - if !ok { - return ErrnoFault - } - b, ok := mod.Memory().Read(ctx, offset, l) - if !ok { - return ErrnoFault - } - n, err := writer.Write(b) - if err != nil { - return ErrnoIo - } - nwritten += uint32(n) - } - if !mod.Memory().WriteUint32Le(ctx, resultSize, nwritten) { - return ErrnoFault - } - return ErrnoSuccess -} - -// PathCreateDirectory is the WASI function named functionPathCreateDirectory -func (a *wasi) PathCreateDirectory(ctx context.Context, mod api.Module, fd, path, pathLen uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// PathFilestatGet is the WASI function named functionPathFilestatGet -func (a *wasi) PathFilestatGet(ctx context.Context, mod api.Module, fd, flags, path, pathLen, resultBuf uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// PathFilestatSetTimes is the WASI function named functionPathFilestatSetTimes -func (a *wasi) PathFilestatSetTimes(ctx context.Context, mod api.Module, fd, flags, path, pathLen uint32, atim, mtime uint64, fstFlags uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// PathLink is the WASI function named functionPathLink -func (a *wasi) PathLink(ctx context.Context, mod api.Module, oldFd, oldFlags, oldPath, oldPathLen, newFd, newPath, newPathLen uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// PathOpen is the WASI function to open a file or directory. This returns ErrnoBadf if the fd is invalid. -// -// * fd - the file descriptor of a directory that `path` is relative to -// * dirflags - flags to indicate how to resolve `path` -// * path - the offset in `mod.Memory` to read the path string from -// * pathLen - the length of `path` -// * oFlags - the open flags to indicate the method by which to open the file -// * fsRightsBase - the rights of the newly created file descriptor for `path` -// * fsRightsInheriting - the rights of the file descriptors derived from the newly created file descriptor for `path` -// * fdFlags - the file descriptor flags -// * resultOpenedFd - the offset in `mod.Memory` to write the newly created file descriptor to. -// * The result FD value is guaranteed to be less than 2**31 -// -// The Errno returned is ErrnoSuccess except the following error conditions: -// * ErrnoBadf - if `fd` is invalid -// * ErrnoFault - if `resultOpenedFd` contains an invalid offset due to the memory constraint -// * ErrnoNoent - if `path` does not exist. -// * ErrnoExist - if `path` exists, while `oFlags` requires that it must not. -// * ErrnoNotdir - if `path` is not a directory, while `oFlags` requires that it must be. -// * ErrnoIo - if other error happens during the operation of the underying file system. -// -// For example, this function needs to first read `path` to determine the file to open. -// If parameters `path` = 1, `pathLen` = 6, and the path is "wazero", PathOpen reads the path from `mod.Memory`: -// -// pathLen -// +------------------------+ -// | | -// []byte{ ?, 'w', 'a', 'z', 'e', 'r', 'o', ?... } -// path --^ -// -// Then, if parameters resultOpenedFd = 8, and this function opened a new file descriptor 5 with the given flags, -// this function writes the below to `mod.Memory`: -// -// uint32le -// +--------+ -// | | -// []byte{ 0..6, ?, 5, 0, 0, 0, ?} -// resultOpenedFd --^ -// -// Note: importPathOpen shows this signature in the WebAssembly 1.0 Text Format. -// Note: This is similar to `openat` in POSIX. -// Note: The returned file descriptor is not guaranteed to be the lowest-numbered file -// Note: Rights will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 -// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#path_open -// See https://linux.die.net/man/3/openat -func (a *wasi) PathOpen(ctx context.Context, mod api.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase, - fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno Errno) { - sysCtx := mod.(*wasm.CallContext).Sys - fsc := sysCtx.FS(ctx) - if _, ok := fsc.OpenedFile(ctx, fd); !ok { - return ErrnoBadf - } - - b, ok := mod.Memory().Read(ctx, pathPtr, pathLen) - if !ok { - return ErrnoFault - } - - if newFD, err := fsc.OpenFile(ctx, string(b)); err != nil { - switch { - case errors.Is(err, fs.ErrNotExist): - return ErrnoNoent - case errors.Is(err, fs.ErrExist): - return ErrnoExist - default: - return ErrnoIo - } - } else if !mod.Memory().WriteUint32Le(ctx, resultOpenedFd, newFD) { - _ = fsc.CloseFile(ctx, newFD) - return ErrnoFault - } - return ErrnoSuccess -} - -// PathReadlink is the WASI function named functionPathReadlink -func (a *wasi) PathReadlink(ctx context.Context, mod api.Module, fd, path, pathLen, buf, bufLen, resultBufused uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// PathRemoveDirectory is the WASI function named functionPathRemoveDirectory -func (a *wasi) PathRemoveDirectory(ctx context.Context, mod api.Module, fd, path, pathLen uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// PathRename is the WASI function named functionPathRename -func (a *wasi) PathRename(ctx context.Context, mod api.Module, fd, oldPath, oldPathLen, newFd, newPath, newPathLen uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// PathSymlink is the WASI function named functionPathSymlink -func (a *wasi) PathSymlink(ctx context.Context, mod api.Module, oldPath, oldPathLen, fd, newPath, newPathLen uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// PathUnlinkFile is the WASI function named functionPathUnlinkFile -func (a *wasi) PathUnlinkFile(ctx context.Context, mod api.Module, fd, path, pathLen uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// SchedYield is the WASI function named functionSchedYield -func (a *wasi) SchedYield(mod api.Module) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// RandomGet is the WASI function named functionRandomGet that write random data in buffer (rand.Read(ctx, )). -// -// * buf - is the mod.Memory offset to write random values -// * bufLen - size of random data in bytes -// -// For example, if underlying random source was seeded like `rand.NewSource(42)`, we expect `mod.Memory` to contain: -// -// bufLen (5) -// +--------------------------+ -// | | -// []byte{?, 0x53, 0x8c, 0x7f, 0x96, 0xb1, ?} -// buf --^ -// -// Note: importRandomGet shows this signature in the WebAssembly 1.0 Text Format. -// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-bufLen-size---errno -func (a *wasi) RandomGet(ctx context.Context, mod api.Module, buf uint32, bufLen uint32) (errno Errno) { - sysCtx := mod.(*wasm.CallContext).Sys - randSource := sysCtx.RandSource() - - randomBytes, ok := mod.Memory().Read(ctx, buf, bufLen) - if !ok { // out-of-range - return ErrnoFault - } - - // We can ignore the returned n as it only != byteCount on error - if _, err := io.ReadAtLeast(randSource, randomBytes, int(bufLen)); err != nil { - return ErrnoIo - } - - return ErrnoSuccess -} - -// SockRecv is the WASI function named functionSockRecv -func (a *wasi) SockRecv(ctx context.Context, mod api.Module, fd, riData, riDataCount, riFlags, resultRoDataLen, resultRoFlags uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// SockSend is the WASI function named functionSockSend -func (a *wasi) SockSend(ctx context.Context, mod api.Module, fd, siData, siDataCount, siFlags, resultSoDataLen uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - -// SockShutdown is the WASI function named functionSockShutdown -func (a *wasi) SockShutdown(ctx context.Context, mod api.Module, fd, how uint32) Errno { - return ErrnoNosys // stubbed for GrainLang per #271 -} - func writeOffsetsAndNullTerminatedValues(ctx context.Context, mem api.Memory, values []string, offsets, bytes uint32) Errno { for _, value := range values { // Write current offset and advance it. @@ -1312,3 +224,16 @@ func writeOffsetsAndNullTerminatedValues(ctx context.Context, mem api.Memory, va return ErrnoSuccess } + +// stubFunction returns a function for the given params which returns ErrnoNosys. +func stubFunction(params ...wasm.ValueType) *wasm.Func { + return &wasm.Func{ + Type: &wasm.FunctionType{ + Params: params, + Results: []wasm.ValueType{wasm.ValueTypeI32}, + ParamNumInUint64: len(params), + ResultNumInUint64: 1, + }, + Code: &wasm.Code{Body: []byte{wasm.OpcodeI32Const, byte(ErrnoNosys), wasm.OpcodeEnd}}, + } +} diff --git a/wasi_snapshot_preview1/wasi_bench_test.go b/wasi_snapshot_preview1/wasi_bench_test.go index e4fd409e..eb4b1859 100644 --- a/wasi_snapshot_preview1/wasi_bench_test.go +++ b/wasi_snapshot_preview1/wasi_bench_test.go @@ -3,53 +3,55 @@ package wasi_snapshot_preview1 import ( "testing" - "github.com/tetratelabs/wazero/internal/sys" + "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/wasm" ) -var testMem = &wasm.MemoryInstance{ - Min: 1, - Buffer: []byte{ - 0, // environBuf is after this - 'a', '=', 'b', 0, // null terminated "a=b", - 'b', '=', 'c', 'd', 0, // null terminated "b=cd" - 0, // environ is after this - 1, 0, 0, 0, // little endian-encoded offset of "a=b" - 5, 0, 0, 0, // little endian-encoded offset of "b=cd" - 0, - }, +var testMem = []byte{ + 0, // environBuf is after this + 'a', '=', 'b', 0, // null terminated "a=b", + 'b', '=', 'c', 'd', 0, // null terminated "b=cd" + 0, // environ is after this + 1, 0, 0, 0, // little endian-encoded offset of "a=b" + 5, 0, 0, 0, // little endian-encoded offset of "b=cd" + 0, } func Test_Benchmark_EnvironGet(t *testing.T) { - sysCtx, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil) - require.NoError(t, err) + mod, r, log := requireModule(t, wazero.NewModuleConfig(). + WithEnv("a", "b").WithEnv("b", "cd")) + defer r.Close(testCtx) - mod := newModule(make([]byte, 20), sysCtx) - environGet := (&wasi{}).EnvironGet + // Invoke environGet and check the memory side effects. + requireErrno(t, ErrnoSuccess, mod, functionEnvironGet, uint64(11), uint64(1)) + require.Equal(t, ` +==> wasi_snapshot_preview1.environ_get(environ=11,environ_buf=1) +<== ESUCCESS +`, "\n"+log.String()) - require.Equal(t, ErrnoSuccess, environGet(testCtx, mod, 11, 1)) - require.Equal(t, mod.Memory(), testMem) + mem, ok := mod.Memory().Read(testCtx, 0, uint32(len(testMem))) + require.True(t, ok) + require.Equal(t, testMem, mem) } func Benchmark_EnvironGet(b *testing.B) { - sysCtx, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil) + r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter()) + + compiled, err := r.NewModuleBuilder(b.Name()). + ExportMemoryWithMax("memory", 1, 1). + Compile(testCtx, wazero.NewCompileConfig()) if err != nil { b.Fatal(err) } - mod := newModule([]byte{ - 0, // environBuf is after this - 'a', '=', 'b', 0, // null terminated "a=b", - 'b', '=', 'c', 'd', 0, // null terminated "b=cd" - 0, // environ is after this - 1, 0, 0, 0, // little endian-encoded offset of "a=b" - 5, 0, 0, 0, // little endian-encoded offset of "b=cd" - 0, - }, sysCtx) + mod, err := r.InstantiateModule(testCtx, compiled, wazero.NewModuleConfig(). + WithEnv("a", "bc").WithEnv("b", "cd")) + if err != nil { + b.Fatal(err) + } + defer r.Close(testCtx) - environGet := (&wasi{}).EnvironGet - b.Run("EnvironGet", func(b *testing.B) { + b.Run("environGet", func(b *testing.B) { for i := 0; i < b.N; i++ { if environGet(testCtx, mod, 0, 4) != ErrnoSuccess { b.Fatal() @@ -57,9 +59,3 @@ func Benchmark_EnvironGet(b *testing.B) { } }) } - -func newModule(buf []byte, sys *sys.Context) *wasm.CallContext { - return wasm.NewCallContext(nil, &wasm.ModuleInstance{ - Memory: &wasm.MemoryInstance{Min: 1, Buffer: buf}, - }, sys) -} diff --git a/wasi_snapshot_preview1/wasi_test.go b/wasi_snapshot_preview1/wasi_test.go index 01917b27..e8a1ba2b 100644 --- a/wasi_snapshot_preview1/wasi_test.go +++ b/wasi_snapshot_preview1/wasi_test.go @@ -4,25 +4,14 @@ import ( "bytes" "context" _ "embed" - "errors" - "fmt" "io" - "io/fs" - "math" "math/rand" - "os" - "path" "testing" - "testing/fstest" - "testing/iotest" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/experimental" - internalsys "github.com/tetratelabs/wazero/internal/sys" + . "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/wasm" - "github.com/tetratelabs/wazero/internal/watzero" ) const seed = int64(42) // fixed seed value @@ -34,1872 +23,6 @@ var deterministicRandomSource = func() io.Reader { // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") -var a = &wasi{} - -func Test_ArgsGet(t *testing.T) { - sysCtx, err := newSysContext([]string{"a", "bc"}, nil, nil) - require.NoError(t, err) - - argv := uint32(7) // arbitrary offset - argvBuf := uint32(1) // arbitrary offset - expectedMemory := []byte{ - '?', // argvBuf is after this - 'a', 0, 'b', 'c', 0, // null terminated "a", "bc" - '?', // argv is after this - 1, 0, 0, 0, // little endian-encoded offset of "a" - 3, 0, 0, 0, // little endian-encoded offset of "bc" - '?', // stopped after encoding - } - - mod, fn := instantiateModule(testCtx, t, functionArgsGet, importArgsGet, sysCtx) - defer mod.Close(testCtx) - - t.Run("wasi.ArgsGet", func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - // Invoke ArgsGet directly and check the memory side effects. - errno := a.ArgsGet(testCtx, mod, argv, argvBuf) - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) - - t.Run(functionArgsGet, func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - results, err := fn.Call(testCtx, uint64(argv), uint64(argvBuf)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) -} - -func Test_ArgsGet_Errors(t *testing.T) { - sysCtx, err := newSysContext([]string{"a", "bc"}, nil, nil) - require.NoError(t, err) - - mod, _ := instantiateModule(testCtx, t, functionArgsGet, importArgsGet, sysCtx) - defer mod.Close(testCtx) - - memorySize := mod.Memory().Size(testCtx) - validAddress := uint32(0) // arbitrary valid address as arguments to args_get. We chose 0 here. - - tests := []struct { - name string - argv uint32 - argvBuf uint32 - }{ - { - name: "out-of-memory argv", - argv: memorySize, - argvBuf: validAddress, - }, - { - name: "out-of-memory argvBuf", - argv: validAddress, - argvBuf: memorySize, - }, - { - name: "argv exceeds the maximum valid address by 1", - // 4*argCount is the size of the result of the pointers to args, 4 is the size of uint32 - argv: memorySize - 4*2 + 1, - argvBuf: validAddress, - }, - { - name: "argvBuf exceeds the maximum valid address by 1", - argv: validAddress, - // "a", "bc" size = size of "a0bc0" = 5 - argvBuf: memorySize - 5 + 1, - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - errno := a.ArgsGet(testCtx, mod, tc.argv, tc.argvBuf) - require.NoError(t, err) - require.Equal(t, ErrnoFault, errno, ErrnoName(errno)) - }) - } -} - -func Test_ArgsSizesGet(t *testing.T) { - sysCtx, err := newSysContext([]string{"a", "bc"}, nil, nil) - require.NoError(t, err) - - resultArgc := uint32(1) // arbitrary offset - resultArgvBufSize := uint32(6) // arbitrary offset - expectedMemory := []byte{ - '?', // resultArgc is after this - 0x2, 0x0, 0x0, 0x0, // little endian-encoded arg count - '?', // resultArgvBufSize is after this - 0x5, 0x0, 0x0, 0x0, // little endian-encoded size of null terminated strings - '?', // stopped after encoding - } - - mod, fn := instantiateModule(testCtx, t, functionArgsSizesGet, importArgsSizesGet, sysCtx) - defer mod.Close(testCtx) - - t.Run("wasi.ArgsSizesGet", func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - // Invoke ArgsSizesGet directly and check the memory side effects. - errno := a.ArgsSizesGet(testCtx, mod, resultArgc, resultArgvBufSize) - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) - - t.Run(functionArgsSizesGet, func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - results, err := fn.Call(testCtx, uint64(resultArgc), uint64(resultArgvBufSize)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) -} - -func Test_ArgsSizesGet_Errors(t *testing.T) { - sysCtx, err := newSysContext([]string{"a", "bc"}, nil, nil) - require.NoError(t, err) - - mod, _ := instantiateModule(testCtx, t, functionArgsSizesGet, importArgsSizesGet, sysCtx) - defer mod.Close(testCtx) - - memorySize := mod.Memory().Size(testCtx) - validAddress := uint32(0) // arbitrary valid address as arguments to args_sizes_get. We chose 0 here. - - tests := []struct { - name string - argc uint32 - argvBufSize uint32 - }{ - { - name: "out-of-memory argc", - argc: memorySize, - argvBufSize: validAddress, - }, - { - name: "out-of-memory argvBufSize", - argc: validAddress, - argvBufSize: memorySize, - }, - { - name: "argc exceeds the maximum valid address by 1", - argc: memorySize - 4 + 1, // 4 is the size of uint32, the type of the count of args - argvBufSize: validAddress, - }, - { - name: "argvBufSize exceeds the maximum valid size by 1", - argc: validAddress, - argvBufSize: memorySize - 4 + 1, // 4 is count of bytes to encode uint32le - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - errno := a.ArgsSizesGet(testCtx, mod, tc.argc, tc.argvBufSize) - require.Equal(t, ErrnoFault, errno, ErrnoName(errno)) - }) - } -} - -func Test_EnvironGet(t *testing.T) { - sysCtx, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil) - require.NoError(t, err) - - resultEnviron := uint32(11) // arbitrary offset - resultEnvironBuf := uint32(1) // arbitrary offset - expectedMemory := []byte{ - '?', // environBuf is after this - 'a', '=', 'b', 0, // null terminated "a=b", - 'b', '=', 'c', 'd', 0, // null terminated "b=cd" - '?', // environ is after this - 1, 0, 0, 0, // little endian-encoded offset of "a=b" - 5, 0, 0, 0, // little endian-encoded offset of "b=cd" - '?', // stopped after encoding - } - - mod, fn := instantiateModule(testCtx, t, functionEnvironGet, importEnvironGet, sysCtx) - defer mod.Close(testCtx) - - t.Run("wasi.EnvironGet", func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - // Invoke EnvironGet directly and check the memory side effects. - errno := a.EnvironGet(testCtx, mod, resultEnviron, resultEnvironBuf) - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) - - t.Run(functionEnvironGet, func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - results, err := fn.Call(testCtx, uint64(resultEnviron), uint64(resultEnvironBuf)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) -} - -func Test_EnvironGet_Errors(t *testing.T) { - sysCtx, err := newSysContext(nil, []string{"a=bc", "b=cd"}, nil) - require.NoError(t, err) - - mod, _ := instantiateModule(testCtx, t, functionEnvironGet, importEnvironGet, sysCtx) - defer mod.Close(testCtx) - - memorySize := mod.Memory().Size(testCtx) - validAddress := uint32(0) // arbitrary valid address as arguments to environ_get. We chose 0 here. - - tests := []struct { - name string - environ uint32 - environBuf uint32 - }{ - { - name: "out-of-memory environPtr", - environ: memorySize, - environBuf: validAddress, - }, - { - name: "out-of-memory environBufPtr", - environ: validAddress, - environBuf: memorySize, - }, - { - name: "environPtr exceeds the maximum valid address by 1", - // 4*envCount is the expected length for environPtr, 4 is the size of uint32 - environ: memorySize - 4*2 + 1, - environBuf: validAddress, - }, - { - name: "environBufPtr exceeds the maximum valid address by 1", - environ: validAddress, - // "a=bc", "b=cd" size = size of "a=bc0b=cd0" = 10 - environBuf: memorySize - 10 + 1, - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - errno := a.EnvironGet(testCtx, mod, tc.environ, tc.environBuf) - require.Equal(t, ErrnoFault, errno, ErrnoName(errno)) - }) - } -} - -func Test_EnvironSizesGet(t *testing.T) { - sysCtx, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil) - require.NoError(t, err) - - resultEnvironc := uint32(1) // arbitrary offset - resultEnvironBufSize := uint32(6) // arbitrary offset - expectedMemory := []byte{ - '?', // resultEnvironc is after this - 0x2, 0x0, 0x0, 0x0, // little endian-encoded environment variable count - '?', // resultEnvironBufSize is after this - 0x9, 0x0, 0x0, 0x0, // little endian-encoded size of null terminated strings - '?', // stopped after encoding - } - - mod, fn := instantiateModule(testCtx, t, functionEnvironSizesGet, importEnvironSizesGet, sysCtx) - defer mod.Close(testCtx) - - t.Run("wasi.EnvironSizesGet", func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - // Invoke EnvironSizesGet directly and check the memory side effects. - errno := a.EnvironSizesGet(testCtx, mod, resultEnvironc, resultEnvironBufSize) - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) - - t.Run(functionEnvironSizesGet, func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - results, err := fn.Call(testCtx, uint64(resultEnvironc), uint64(resultEnvironBufSize)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) -} - -func Test_EnvironSizesGet_Errors(t *testing.T) { - sysCtx, err := newSysContext(nil, []string{"a=b", "b=cd"}, nil) - require.NoError(t, err) - - mod, _ := instantiateModule(testCtx, t, functionEnvironSizesGet, importEnvironSizesGet, sysCtx) - defer mod.Close(testCtx) - - memorySize := mod.Memory().Size(testCtx) - validAddress := uint32(0) // arbitrary valid address as arguments to environ_sizes_get. We chose 0 here. - - tests := []struct { - name string - environc uint32 - environBufSize uint32 - }{ - { - name: "out-of-memory environCountPtr", - environc: memorySize, - environBufSize: validAddress, - }, - { - name: "out-of-memory environBufSizePtr", - environc: validAddress, - environBufSize: memorySize, - }, - { - name: "environCountPtr exceeds the maximum valid address by 1", - environc: memorySize - 4 + 1, // 4 is the size of uint32, the type of the count of environ - environBufSize: validAddress, - }, - { - name: "environBufSizePtr exceeds the maximum valid size by 1", - environc: validAddress, - environBufSize: memorySize - 4 + 1, // 4 is count of bytes to encode uint32le - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - errno := a.EnvironSizesGet(testCtx, mod, tc.environc, tc.environBufSize) - require.Equal(t, ErrnoFault, errno, ErrnoName(errno)) - }) - } -} - -// Test_FdAdvise only tests it is stubbed for GrainLang per #271 -func Test_FdAdvise(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdAdvise, importFdAdvise, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdAdvise", func(t *testing.T) { - errno := a.FdAdvise(testCtx, mod, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdAdvise, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_FdAllocate only tests it is stubbed for GrainLang per #271 -func Test_FdAllocate(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdAllocate, importFdAllocate, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdAllocate", func(t *testing.T) { - errno := a.FdAllocate(testCtx, mod, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdAllocate, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -func Test_FdClose(t *testing.T) { - fdToClose := uint32(4) // arbitrary fd - fdToKeep := uint32(5) // another arbitrary fd - - setupFD := func() (api.Module, api.Function, *wasi) { - // fd_close needs to close an open file descriptor. Open two files so that we can tell which is closed. - path1, path2 := "a", "b" - - testFS := fstest.MapFS{path1: {Data: make([]byte, 0)}, path2: {Data: make([]byte, 0)}} - sysCtx, err := newSysContext(nil, nil, testFS) - require.NoError(t, err) - fsc := sysCtx.FS(testCtx) - - fd, err := fsc.OpenFile(testCtx, path1) - require.NoError(t, err) - require.Equal(t, fdToClose, fd) - - fd, err = fsc.OpenFile(testCtx, path2) - require.NoError(t, err) - require.Equal(t, fdToKeep, fd) - - mod, fn := instantiateModule(testCtx, t, functionFdClose, importFdClose, sysCtx) - return mod, fn, a - } - - verify := func(mod api.Module) { - // Verify fdToClose is closed and removed from the opened FDs. - fsc := mod.(*wasm.CallContext).Sys.FS(testCtx) - _, ok := fsc.OpenedFile(testCtx, fdToClose) - require.False(t, ok) - - // Verify fdToKeep is not closed - _, ok = fsc.OpenedFile(testCtx, fdToKeep) - require.True(t, ok) - } - - t.Run("wasi.FdClose", func(t *testing.T) { - mod, _, api := setupFD() - defer mod.Close(testCtx) - - errno := api.FdClose(testCtx, mod, fdToClose) - require.Zero(t, errno, ErrnoName(errno)) - - verify(mod) - }) - t.Run(functionFdClose, func(t *testing.T) { - mod, fn, _ := setupFD() - defer mod.Close(testCtx) - - results, err := fn.Call(testCtx, uint64(fdToClose)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Zero(t, errno, ErrnoName(errno)) - - verify(mod) - }) - t.Run("ErrnoBadF for an invalid FD", func(t *testing.T) { - mod, _, api := setupFD() - defer mod.Close(testCtx) - - errno := api.FdClose(testCtx, mod, 42) // 42 is an arbitrary invalid FD - require.Equal(t, ErrnoBadf, errno) - }) -} - -// Test_FdDatasync only tests it is stubbed for GrainLang per #271 -func Test_FdDatasync(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdDatasync, importFdDatasync, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdDatasync", func(t *testing.T) { - errno := a.FdDatasync(testCtx, mod, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdDatasync, func(t *testing.T) { - results, err := fn.Call(testCtx, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// TODO: Test_FdFdstatGet Test_FdFdstatGet_Errors - -// Test_FdFdstatSetFlags only tests it is stubbed for GrainLang per #271 -func Test_FdFdstatSetFlags(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdFdstatSetFlags, importFdFdstatSetFlags, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdFdstatSetFlags", func(t *testing.T) { - errno := a.FdFdstatSetFlags(testCtx, mod, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdFdstatSetFlags, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_FdFdstatSetRights only tests it is stubbed for GrainLang per #271 -func Test_FdFdstatSetRights(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdFdstatSetRights, importFdFdstatSetRights, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdFdstatSetRights", func(t *testing.T) { - errno := a.FdFdstatSetRights(testCtx, mod, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdFdstatSetRights, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_FdFilestatGet only tests it is stubbed for GrainLang per #271 -func Test_FdFilestatGet(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdFilestatGet, importFdFilestatGet, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdFilestatGet", func(t *testing.T) { - errno := a.FdFilestatGet(testCtx, mod, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdFilestatGet, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_FdFilestatSetSize only tests it is stubbed for GrainLang per #271 -func Test_FdFilestatSetSize(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdFilestatSetSize, importFdFilestatSetSize, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdFilestatSetSize", func(t *testing.T) { - errno := a.FdFilestatSetSize(testCtx, mod, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdFilestatSetSize, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_FdFilestatSetTimes only tests it is stubbed for GrainLang per #271 -func Test_FdFilestatSetTimes(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdFilestatSetTimes, importFdFilestatSetTimes, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdFilestatSetTimes", func(t *testing.T) { - errno := a.FdFilestatSetTimes(testCtx, mod, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdFilestatSetTimes, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_FdPread only tests it is stubbed for GrainLang per #271 -func Test_FdPread(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdPread, importFdPread, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdPread", func(t *testing.T) { - errno := a.FdPread(testCtx, mod, 0, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdPread, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -func Test_FdPrestatGet(t *testing.T) { - pathName := "/tmp" - sysCtx, fd := requireOpenDir(t, pathName) - - mod, fn := instantiateModule(testCtx, t, functionFdPrestatGet, importFdPrestatGet, sysCtx) - defer mod.Close(testCtx) - - resultPrestat := uint32(1) // arbitrary offset - expectedMemory := []byte{ - '?', // resultPrestat after this - 0, // 8-bit tag indicating `prestat_dir`, the only available tag - 0, 0, 0, // 3-byte padding - // the result path length field after this - byte(len(pathName)), 0, 0, 0, // = in little endian encoding - '?', - } - - t.Run("wasi.FdPrestatGet", func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - errno := a.FdPrestatGet(testCtx, mod, fd, resultPrestat) - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) - - t.Run(functionFdPrestatDirName, func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - results, err := fn.Call(testCtx, uint64(fd), uint64(resultPrestat)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) -} - -func requireOpenDir(t *testing.T, pathName string) (*internalsys.Context, uint32) { - testFS := fstest.MapFS{pathName[1:]: {Mode: fs.ModeDir}} - sysCtx, err := newSysContext(nil, nil, testFS) - require.NoError(t, err) - fsc := sysCtx.FS(testCtx) - fd, err := fsc.OpenFile(testCtx, pathName) - require.NoError(t, err) - return sysCtx, fd -} - -func Test_FdPrestatGet_Errors(t *testing.T) { - validAddress := uint32(0) // Arbitrary valid address as arguments to fd_prestat_get. We chose 0 here. - - pathName := "/tmp" - sysCtx, fd := requireOpenDir(t, pathName) - - mod, _ := instantiateModule(testCtx, t, functionFdPrestatGet, importFdPrestatGet, sysCtx) - defer mod.Close(testCtx) - - memorySize := mod.Memory().Size(testCtx) - - tests := []struct { - name string - fd uint32 - resultPrestat uint32 - expectedErrno Errno - }{ - { - name: "invalid FD", - fd: 42, // arbitrary invalid FD - resultPrestat: validAddress, - expectedErrno: ErrnoBadf, - }, - { - name: "out-of-memory resultPrestat", - fd: fd, - resultPrestat: memorySize, - expectedErrno: ErrnoFault, - }, - // TODO: non pre-opened file == api.ErrnoBadf - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - errno := a.FdPrestatGet(testCtx, mod, tc.fd, tc.resultPrestat) - require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) - }) - } -} - -func Test_FdPrestatDirName(t *testing.T) { - pathName := "/tmp" - sysCtx, fd := requireOpenDir(t, pathName) - - mod, fn := instantiateModule(testCtx, t, functionFdPrestatDirName, importFdPrestatDirName, sysCtx) - defer mod.Close(testCtx) - - path := uint32(1) // arbitrary offset - pathLen := uint32(3) // shorter than len("/tmp") to test the path is written for the length of pathLen - expectedMemory := []byte{ - '?', - '/', 't', 'm', - '?', '?', '?', - } - - t.Run("wasi.FdPrestatDirName", func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - errno := a.FdPrestatDirName(testCtx, mod, fd, path, pathLen) - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) - - t.Run(functionFdPrestatDirName, func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - results, err := fn.Call(testCtx, uint64(fd), uint64(path), uint64(pathLen)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) -} - -func Test_FdPrestatDirName_Errors(t *testing.T) { - pathName := "/tmp" - sysCtx, fd := requireOpenDir(t, pathName) - - mod, _ := instantiateModule(testCtx, t, functionFdPrestatDirName, importFdPrestatDirName, sysCtx) - defer mod.Close(testCtx) - - memorySize := mod.Memory().Size(testCtx) - 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 - fd uint32 - path uint32 - pathLen uint32 - expectedErrno Errno - }{ - { - name: "out-of-memory path", - fd: fd, - path: memorySize, - pathLen: pathLen, - expectedErrno: ErrnoFault, - }, - { - name: "path exceeds the maximum valid address by 1", - fd: fd, - path: memorySize - pathLen + 1, - pathLen: pathLen, - expectedErrno: ErrnoFault, - }, - { - name: "pathLen exceeds the length of the dir name", - fd: fd, - path: validAddress, - pathLen: pathLen + 1, - expectedErrno: ErrnoNametoolong, - }, - { - name: "invalid fd", - fd: 42, // arbitrary invalid fd - path: validAddress, - pathLen: pathLen, - expectedErrno: ErrnoBadf, - }, - // TODO: non pre-opened file == ErrnoBadf - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - errno := a.FdPrestatDirName(testCtx, mod, tc.fd, tc.path, tc.pathLen) - require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) - }) - } -} - -// Test_FdPwrite only tests it is stubbed for GrainLang per #271 -func Test_FdPwrite(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdPwrite, importFdPwrite, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdPwrite", func(t *testing.T) { - errno := a.FdPwrite(testCtx, mod, 0, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdPwrite, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -func Test_FdRead(t *testing.T) { - var fd uint32 - iovs := uint32(1) // arbitrary offset - initialMemory := []byte{ - '?', // `iovs` is after this - 18, 0, 0, 0, // = iovs[0].offset - 4, 0, 0, 0, // = iovs[0].length - 23, 0, 0, 0, // = iovs[1].offset - 2, 0, 0, 0, // = iovs[1].length - '?', - } - iovsCount := uint32(2) // The count of iovs - resultSize := uint32(26) // arbitrary offset - expectedMemory := append( - initialMemory, - 'w', 'a', 'z', 'e', // iovs[0].length bytes - '?', // iovs[1].offset is after this - 'r', 'o', // iovs[1].length bytes - '?', // resultSize is after this - 6, 0, 0, 0, // sum(iovs[...].length) == length of "wazero" - '?', - ) - - // Test_FdRead uses a matrix because setting up test files is complicated and has to be clean each time. - type fdReadFn func(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno - tests := []struct { - name string - fdRead func(api.Module, api.Function) fdReadFn - }{ - {"wasi.FdRead", func(_ api.Module, _ api.Function) fdReadFn { - return a.FdRead - }}, - {functionFdRead, func(mod api.Module, fn api.Function) fdReadFn { - return func(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno { - results, err := fn.Call(testCtx, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize)) - require.NoError(t, err) - return Errno(results[0]) - } - }}, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - // Create a fresh file to read the contents from - _, testFS := createFile(t, "test_path", []byte("wazero")) - sysCtx, err := newSysContext(nil, nil, testFS) - require.NoError(t, err) - fsc := sysCtx.FS(testCtx) - - fd, err = fsc.OpenFile(testCtx, "test_path") - require.NoError(t, err) - - mod, fn := instantiateModule(testCtx, t, functionFdRead, importFdRead, sysCtx) - defer mod.Close(testCtx) - - maskMemory(t, testCtx, mod, len(expectedMemory)) - - ok := mod.Memory().Write(testCtx, 0, initialMemory) - require.True(t, ok) - - errno := tc.fdRead(mod, fn)(testCtx, mod, fd, iovs, iovsCount, resultSize) - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) - } -} - -func Test_FdRead_Errors(t *testing.T) { - _, testFS := createFile(t, "test_path", []byte{}) // file with empty contents - sysCtx, err := newSysContext(nil, nil, testFS) - require.NoError(t, err) - fsc := sysCtx.FS(testCtx) - - validFD, err := fsc.OpenFile(testCtx, "test_path") - require.NoError(t, err) - - mod, _ := instantiateModule(testCtx, t, functionFdRead, importFdRead, sysCtx) - defer mod.Close(testCtx) - - tests := []struct { - name string - fd, iovs, iovsCount, resultSize uint32 - memory []byte - expectedErrno Errno - }{ - { - name: "invalid fd", - fd: 42, // arbitrary invalid fd - expectedErrno: ErrnoBadf, - }, - { - name: "out-of-memory reading iovs[0].offset", - fd: validFD, - iovs: 1, - memory: []byte{'?'}, - expectedErrno: ErrnoFault, - }, - { - name: "out-of-memory reading iovs[0].length", - fd: validFD, - iovs: 1, iovsCount: 1, - memory: []byte{ - '?', // `iovs` is after this - 9, 0, 0, 0, // = iovs[0].offset - }, - expectedErrno: ErrnoFault, - }, - { - name: "iovs[0].offset is outside memory", - fd: validFD, - iovs: 1, iovsCount: 1, - memory: []byte{ - '?', // `iovs` is after this - 0, 0, 0x1, 0, // = iovs[0].offset on the second page - 1, 0, 0, 0, // = iovs[0].length - }, - expectedErrno: ErrnoFault, - }, - { - name: "length to read exceeds memory by 1", - fd: validFD, - iovs: 1, iovsCount: 1, - memory: []byte{ - '?', // `iovs` is after this - 9, 0, 0, 0, // = iovs[0].offset - 0, 0, 0x1, 0, // = iovs[0].length on the second page - '?', - }, - expectedErrno: ErrnoFault, - }, - { - name: "resultSize offset is outside memory", - fd: validFD, - iovs: 1, iovsCount: 1, - resultSize: 10, // 1 past memory - memory: []byte{ - '?', // `iovs` is after this - 9, 0, 0, 0, // = iovs[0].offset - 1, 0, 0, 0, // = iovs[0].length - '?', - }, - expectedErrno: ErrnoFault, - }, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - offset := uint32(wasm.MemoryPagesToBytesNum(testMemoryPageSize) - uint64(len(tc.memory))) - - memoryWriteOK := mod.Memory().Write(testCtx, offset, tc.memory) - require.True(t, memoryWriteOK) - - errno := a.FdRead(testCtx, mod, tc.fd, tc.iovs+offset, tc.iovsCount+offset, tc.resultSize+offset) - require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) - }) - } -} - -// Test_FdReaddir only tests it is stubbed for GrainLang per #271 -func Test_FdReaddir(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdReaddir, importFdReaddir, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdReaddir", func(t *testing.T) { - errno := a.FdReaddir(testCtx, mod, 0, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdReaddir, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_FdRenumber only tests it is stubbed for GrainLang per #271 -func Test_FdRenumber(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdRenumber, importFdRenumber, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdRenumber", func(t *testing.T) { - errno := a.FdRenumber(testCtx, mod, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdRenumber, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -func Test_FdSeek(t *testing.T) { - resultNewoffset := uint32(1) // arbitrary offset in `ctx.Memory` for the new offset value - _, testFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents - sysCtx, err := newSysContext(nil, nil, testFS) - require.NoError(t, err) - - fsCtx := sysCtx.FS(testCtx) - fd, err := fsCtx.OpenFile(testCtx, "test_path") - require.NoError(t, err) - - mod, fn := instantiateModule(testCtx, t, functionFdSeek, importFdSeek, sysCtx) - defer mod.Close(testCtx) - - // Test_FdSeek uses a matrix because setting up test files is complicated and has to be clean each time. - type fdSeekFn func(ctx context.Context, mod api.Module, fd uint32, offset uint64, whence, resultNewOffset uint32) Errno - seekFns := []struct { - name string - fdSeek func() fdSeekFn - }{ - {"wasi.FdSeek", func() fdSeekFn { - return a.FdSeek - }}, - {functionFdSeek, func() fdSeekFn { - return func(ctx context.Context, mod api.Module, fd uint32, offset uint64, whence, resultNewoffset uint32) Errno { - results, err := fn.Call(ctx, uint64(fd), offset, uint64(whence), uint64(resultNewoffset)) - require.NoError(t, err) - return Errno(results[0]) - } - }}, - } - - tests := []struct { - name string - offset int64 - whence int - expectedOffset int64 - expectedMemory []byte - }{ - { - name: "SeekStart", - offset: 4, // arbitrary offset - whence: io.SeekStart, - expectedOffset: 4, // = offset - expectedMemory: []byte{ - '?', // resultNewoffset is after this - 4, 0, 0, 0, // = expectedOffset - '?', - }, - }, - { - name: "SeekCurrent", - offset: 1, // arbitrary offset - whence: io.SeekCurrent, - expectedOffset: 2, // = 1 (the initial offset of the test file) + 1 (offset) - expectedMemory: []byte{ - '?', // resultNewoffset is after this - 2, 0, 0, 0, // = expectedOffset - '?', - }, - }, - { - name: "SeekEnd", - offset: -1, // arbitrary offset, note that offset can be negative - whence: io.SeekEnd, - expectedOffset: 5, // = 6 (the size of the test file with content "wazero") + -1 (offset) - expectedMemory: []byte{ - '?', // resultNewoffset is after this - 5, 0, 0, 0, // = expectedOffset - '?', - }, - }, - } - - for _, seekFn := range seekFns { - sf := seekFn - t.Run(sf.name, func(t *testing.T) { - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - maskMemory(t, testCtx, mod, len(tc.expectedMemory)) - - // Since we initialized this file, we know it is a seeker (because it is a MapFile) - f, ok := fsCtx.OpenedFile(testCtx, fd) - require.True(t, ok) - seeker := f.File.(io.Seeker) - - // set the initial offset of the file to 1 - offset, err := seeker.Seek(1, io.SeekStart) - require.NoError(t, err) - require.Equal(t, int64(1), offset) - - errno := sf.fdSeek()(testCtx, mod, fd, uint64(tc.offset), uint32(tc.whence), resultNewoffset) - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(tc.expectedMemory))) - require.True(t, ok) - require.Equal(t, tc.expectedMemory, actual) - - offset, err = seeker.Seek(0, io.SeekCurrent) - require.NoError(t, err) - require.Equal(t, tc.expectedOffset, offset) // test that the offset of file is actually updated. - }) - } - }) - } -} - -func Test_FdSeek_Errors(t *testing.T) { - _, testFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents - sysCtx, err := newSysContext(nil, nil, testFS) - require.NoError(t, err) - - fsCtx := sysCtx.FS(testCtx) - validFD, err := fsCtx.OpenFile(testCtx, "test_path") - require.NoError(t, err) - - mod, _ := instantiateModule(testCtx, t, functionFdSeek, importFdSeek, sysCtx) - defer mod.Close(testCtx) - - memorySize := mod.Memory().Size(testCtx) - - tests := []struct { - name string - fd uint32 - offset uint64 - whence, resultNewoffset uint32 - expectedErrno Errno - }{ - { - name: "invalid fd", - fd: 42, // arbitrary invalid fd - expectedErrno: ErrnoBadf, - }, - { - name: "invalid whence", - fd: validFD, - whence: 3, // invalid whence, the largest whence io.SeekEnd(2) + 1 - expectedErrno: ErrnoInval, - }, - { - name: "out-of-memory writing resultNewoffset", - fd: validFD, - resultNewoffset: memorySize, - expectedErrno: ErrnoFault, - }, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - errno := a.FdSeek(testCtx, mod, tc.fd, tc.offset, tc.whence, tc.resultNewoffset) - require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) - }) - } -} - -// Test_FdSync only tests it is stubbed for GrainLang per #271 -func Test_FdSync(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdSync, importFdSync, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdSync", func(t *testing.T) { - errno := a.FdSync(testCtx, mod, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdSync, func(t *testing.T) { - results, err := fn.Call(testCtx, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_FdTell only tests it is stubbed for GrainLang per #271 -func Test_FdTell(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionFdTell, importFdTell, nil) - defer mod.Close(testCtx) - - t.Run("wasi.FdTell", func(t *testing.T) { - errno := a.FdTell(testCtx, mod, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionFdTell, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -func Test_FdWrite(t *testing.T) { - fd := uint32(4) - iovs := uint32(1) // arbitrary offset - initialMemory := []byte{ - '?', // `iovs` is after this - 18, 0, 0, 0, // = iovs[0].offset - 4, 0, 0, 0, // = iovs[0].length - 23, 0, 0, 0, // = iovs[1].offset - 2, 0, 0, 0, // = iovs[1].length - '?', // iovs[0].offset is after this - 'w', 'a', 'z', 'e', // iovs[0].length bytes - '?', // iovs[1].offset is after this - 'r', 'o', // iovs[1].length bytes - '?', - } - iovsCount := uint32(2) // The count of iovs - resultSize := uint32(26) // arbitrary offset - expectedMemory := append( - initialMemory, - 6, 0, 0, 0, // sum(iovs[...].length) == length of "wazero" - '?', - ) - - // Test_FdWrite uses a matrix because setting up test files is complicated and has to be clean each time. - type fdWriteFn func(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno - tests := []struct { - name string - fdWrite func(api.Module, api.Function) fdWriteFn - }{ - {"wasi.FdWrite", func(_ api.Module, _ api.Function) fdWriteFn { - return a.FdWrite - }}, - {functionFdWrite, func(mod api.Module, fn api.Function) fdWriteFn { - return func(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno { - results, err := fn.Call(ctx, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize)) - require.NoError(t, err) - return Errno(results[0]) - } - }}, - } - - tmpDir := t.TempDir() // open before loop to ensure no locking problems. - - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - // Create a fresh file to write the contents to - pathName := "test_path" - sysCtx := newContextWithWritableFile(t, tmpDir, pathName) - - mod, fn := instantiateModule(testCtx, t, functionFdWrite, importFdWrite, sysCtx) - defer mod.Close(testCtx) - - maskMemory(t, testCtx, mod, len(expectedMemory)) - ok := mod.Memory().Write(testCtx, 0, initialMemory) - require.True(t, ok) - - errno := tc.fdWrite(mod, fn)(testCtx, mod, fd, iovs, iovsCount, resultSize) - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - - // Since we initialized this file, we know we can read it by path - buf, err := os.ReadFile(path.Join(tmpDir, pathName)) - require.NoError(t, err) - - require.Equal(t, []byte("wazero"), buf) // verify the file was actually written - }) - } -} - -func Test_FdWrite_Errors(t *testing.T) { - tmpDir := t.TempDir() // open before loop to ensure no locking problems. - pathName := "test_path" - validFD := uint32(4) - sysCtx := newContextWithWritableFile(t, tmpDir, pathName) - - mod, _ := instantiateModule(testCtx, t, functionFdWrite, importFdWrite, sysCtx) - defer mod.Close(testCtx) - - // Setup valid test memory - iovs, iovsCount := uint32(0), uint32(1) - memory := []byte{ - 8, 0, 0, 0, // = iovs[0].offset (where the data "hi" begins) - 2, 0, 0, 0, // = iovs[0].length (how many bytes are in "hi") - 'h', 'i', // iovs[0].length bytes - } - - tests := []struct { - name string - fd, resultSize uint32 - memory []byte - expectedErrno Errno - }{ - { - name: "invalid fd", - fd: 42, // arbitrary invalid fd - expectedErrno: ErrnoBadf, - }, - { - name: "out-of-memory reading iovs[0].offset", - fd: validFD, - memory: []byte{}, - expectedErrno: ErrnoFault, - }, - { - name: "out-of-memory reading iovs[0].length", - fd: validFD, - memory: memory[0:4], // iovs[0].offset was 4 bytes and iovs[0].length next, but not enough mod.Memory()! - expectedErrno: ErrnoFault, - }, - { - name: "iovs[0].offset is outside memory", - fd: validFD, - memory: memory[0:8], // iovs[0].offset (where to read "hi") is outside memory. - expectedErrno: ErrnoFault, - }, - { - name: "length to read exceeds memory by 1", - fd: validFD, - memory: memory[0:9], // iovs[0].offset (where to read "hi") is in memory, but truncated. - expectedErrno: ErrnoFault, - }, - { - name: "resultSize offset is outside memory", - fd: validFD, - memory: memory, - resultSize: uint32(len(memory)), // read was ok, but there wasn't enough memory to write the result. - expectedErrno: ErrnoFault, - }, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - mod.Memory().(*wasm.MemoryInstance).Buffer = tc.memory - - errno := a.FdWrite(testCtx, mod, tc.fd, iovs, iovsCount, tc.resultSize) - require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) - }) - } -} - -// Test_PathCreateDirectory only tests it is stubbed for GrainLang per #271 -func Test_PathCreateDirectory(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionPathCreateDirectory, importPathCreateDirectory, nil) - defer mod.Close(testCtx) - - t.Run("wasi.PathCreateDirectory", func(t *testing.T) { - errno := a.PathCreateDirectory(testCtx, mod, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionPathCreateDirectory, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_PathFilestatGet only tests it is stubbed for GrainLang per #271 -func Test_PathFilestatGet(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionPathFilestatGet, importPathFilestatGet, nil) - defer mod.Close(testCtx) - - t.Run("wasi.PathFilestatGet", func(t *testing.T) { - errno := a.PathFilestatGet(testCtx, mod, 0, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionPathFilestatGet, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_PathFilestatSetTimes only tests it is stubbed for GrainLang per #271 -func Test_PathFilestatSetTimes(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionPathFilestatSetTimes, importPathFilestatSetTimes, nil) - defer mod.Close(testCtx) - - t.Run("wasi.PathFilestatSetTimes", func(t *testing.T) { - errno := a.PathFilestatSetTimes(testCtx, mod, 0, 0, 0, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionPathFilestatSetTimes, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_PathLink only tests it is stubbed for GrainLang per #271 -func Test_PathLink(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionPathLink, importPathLink, nil) - defer mod.Close(testCtx) - - t.Run("wasi.PathLink", func(t *testing.T) { - errno := a.PathLink(testCtx, mod, 0, 0, 0, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionPathLink, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -func Test_PathOpen(t *testing.T) { - type pathOpenArgs struct { - dirflags uint32 - pathPtr uint32 - pathLen uint32 - oflags uint32 - fsRightsBase uint64 - fsRightsInheriting uint64 - fdflags uint32 - resultOpenedFd uint32 - } - - rootFD := uint32(3) // after 0, 1, and 2, that are stdin/out/err - expectedFD := rootFD + 1 - - setup := func(pathName string) (api.Module, api.Function, pathOpenArgs, []byte) { - // Setup the initial memory to include the path name starting at an offset. - initialMemory := append([]byte{'?'}, pathName...) - - expectedMemory := append( - initialMemory, - '?', // `resultOpenedFd` is after this - byte(expectedFD), 0, 0, 0, - '?', - ) - - args := pathOpenArgs{ - dirflags: 0, - pathPtr: 1, - pathLen: uint32(len(pathName)), - oflags: 0, - fsRightsBase: 1, // rights are ignored per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 - fsRightsInheriting: 2, - fdflags: 0, - resultOpenedFd: uint32(len(initialMemory) + 1), - } - - testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}} - sysCtx, err := newSysContext(nil, nil, testFS) - require.NoError(t, err) - - mod, fn := instantiateModule(testCtx, t, functionPathOpen, importPathOpen, sysCtx) - maskMemory(t, testCtx, mod, len(expectedMemory)) - ok := mod.Memory().Write(testCtx, 0, initialMemory) - require.True(t, ok) - return mod, fn, args, expectedMemory - } - - verify := func(ctx context.Context, errno Errno, mod api.Module, pathName string, expectedMemory []byte) { - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - - // verify the file was actually opened - fsc := mod.(*wasm.CallContext).Sys.FS(ctx) - f, ok := fsc.OpenedFile(testCtx, expectedFD) - require.True(t, ok) - require.Equal(t, pathName, f.Path) - } - - t.Run("wasi.PathOpen", func(t *testing.T) { - pathName := "wazero" - - mod, _, args, expectedMemory := setup(pathName) - errno := a.PathOpen(testCtx, mod, rootFD, args.dirflags, args.pathPtr, args.pathLen, args.oflags, - args.fsRightsBase, args.fsRightsInheriting, args.fdflags, args.resultOpenedFd) - verify(testCtx, errno, mod, pathName, expectedMemory) - }) - - t.Run(functionPathOpen, func(t *testing.T) { - pathName := "wazero" - - mod, fn, args, expectedMemory := setup(pathName) - results, err := fn.Call(testCtx, uint64(rootFD), uint64(args.dirflags), uint64(args.pathPtr), uint64(args.pathLen), - uint64(args.oflags), args.fsRightsBase, args.fsRightsInheriting, uint64(args.fdflags), uint64(args.resultOpenedFd)) - require.NoError(t, err) - errno := Errno(results[0]) - verify(testCtx, errno, mod, pathName, expectedMemory) - }) - - t.Run("wasi.PathOpen.WithFS", func(t *testing.T) { - pathName := "wazero" - - // The filesystem initialized in setup() is not used as it will be overridden. - mod, _, args, expectedMemory := setup(pathName) - - // Override fs.FS through context - expectedMemory[8] = byte(expectedFD) // replace expected memory with expected fd - testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}} - ctx, closer := experimental.WithFS(testCtx, testFS) - defer closer.Close(ctx) - - errno := a.PathOpen(ctx, mod, rootFD, args.dirflags, args.pathPtr, args.pathLen, args.oflags, - args.fsRightsBase, args.fsRightsInheriting, args.fdflags, args.resultOpenedFd) - require.Zero(t, errno, ErrnoName(errno)) - - verify(ctx, errno, mod, pathName, expectedMemory) - }) -} - -func Test_PathOpen_Errors(t *testing.T) { - validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err - pathName := "wazero" - testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}} - sysCtx, err := newSysContext(nil, nil, testFS) - require.NoError(t, err) - - mod, _ := instantiateModule(testCtx, t, functionPathOpen, importPathOpen, sysCtx) - defer mod.Close(testCtx) - - validPath := uint32(0) // arbitrary offset - validPathLen := uint32(6) // the length of "wazero" - mod.Memory().Write(testCtx, validPath, []byte(pathName)) - - tests := []struct { - name string - fd, path, pathLen, oflags, resultOpenedFd uint32 - expectedErrno Errno - }{ - { - name: "invalid fd", - fd: 42, // arbitrary invalid fd - expectedErrno: ErrnoBadf, - }, - { - name: "out-of-memory reading path", - fd: validFD, - path: mod.Memory().Size(testCtx), - pathLen: validPathLen, - expectedErrno: ErrnoFault, - }, - { - name: "out-of-memory reading pathLen", - fd: validFD, - path: validPath, - pathLen: mod.Memory().Size(testCtx) + 1, // path is in the valid memory range, but pathLen is out-of-memory for path - expectedErrno: ErrnoFault, - }, - { - name: "no such file exists", - fd: validFD, - path: validPath, - pathLen: validPathLen - 1, // this make the path "wazer", which doesn't exit - expectedErrno: ErrnoNoent, - }, - { - name: "out-of-memory writing resultOpenedFd", - fd: validFD, - path: validPath, - pathLen: validPathLen, - resultOpenedFd: mod.Memory().Size(testCtx), // path and pathLen correctly point to the right path, but where to write the opened FD is outside memory. - expectedErrno: ErrnoFault, - }, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - errno := a.PathOpen(testCtx, mod, tc.fd, 0, tc.path, tc.pathLen, tc.oflags, 0, 0, 0, tc.resultOpenedFd) - require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno)) - }) - } -} - -// Test_PathReadlink only tests it is stubbed for GrainLang per #271 -func Test_PathReadlink(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionPathReadlink, importPathReadlink, nil) - defer mod.Close(testCtx) - - t.Run("wasi.PathLink", func(t *testing.T) { - errno := a.PathReadlink(testCtx, mod, 0, 0, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionPathReadlink, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_PathRemoveDirectory only tests it is stubbed for GrainLang per #271 -func Test_PathRemoveDirectory(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionPathRemoveDirectory, importPathRemoveDirectory, nil) - defer mod.Close(testCtx) - - t.Run("wasi.PathRemoveDirectory", func(t *testing.T) { - errno := a.PathRemoveDirectory(testCtx, mod, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionPathRemoveDirectory, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_PathRename only tests it is stubbed for GrainLang per #271 -func Test_PathRename(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionPathRename, importPathRename, nil) - defer mod.Close(testCtx) - - t.Run("wasi.PathRename", func(t *testing.T) { - errno := a.PathRename(testCtx, mod, 0, 0, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionPathRename, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_PathSymlink only tests it is stubbed for GrainLang per #271 -func Test_PathSymlink(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionPathSymlink, importPathSymlink, nil) - defer mod.Close(testCtx) - - t.Run("wasi.PathSymlink", func(t *testing.T) { - errno := a.PathSymlink(testCtx, mod, 0, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionPathSymlink, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_PathUnlinkFile only tests it is stubbed for GrainLang per #271 -func Test_PathUnlinkFile(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionPathUnlinkFile, importPathUnlinkFile, nil) - defer mod.Close(testCtx) - - t.Run("wasi.PathUnlinkFile", func(t *testing.T) { - errno := a.PathUnlinkFile(testCtx, mod, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionPathUnlinkFile, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_SchedYield only tests it is stubbed for GrainLang per #271 -func Test_SchedYield(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionSchedYield, importSchedYield, nil) - defer mod.Close(testCtx) - - t.Run("wasi.SchedYield", func(t *testing.T) { - errno := a.SchedYield(mod) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionSchedYield, func(t *testing.T) { - results, err := fn.Call(testCtx) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -func Test_RandomGet(t *testing.T) { - expectedMemory := []byte{ - '?', // `offset` is after this - 0x53, 0x8c, 0x7f, 0x96, 0xb1, // random data from seed value of 42 - '?', // stopped after encoding - } - - length := uint32(5) // arbitrary length, - offset := uint32(1) // offset, - - t.Run("wasi.RandomGet", func(t *testing.T) { - mod, _ := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, nil) - defer mod.Close(testCtx) - - maskMemory(t, testCtx, mod, len(expectedMemory)) - - // Invoke RandomGet directly and check the memory side effects! - errno := a.RandomGet(testCtx, mod, offset, length) - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, offset+length+1) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) - - t.Run(functionRandomGet, func(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, nil) - defer mod.Close(testCtx) - - maskMemory(t, testCtx, mod, len(expectedMemory)) - - results, err := fn.Call(testCtx, uint64(offset), uint64(length)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, offset+length+1) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) -} - -func Test_RandomGet_Errors(t *testing.T) { - validAddress := uint32(0) // arbitrary valid address - - mod, _ := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, nil) - defer mod.Close(testCtx) - - memorySize := mod.Memory().Size(testCtx) - - tests := []struct { - name string - offset uint32 - length uint32 - }{ - { - name: "out-of-memory", - offset: memorySize, - length: 1, - }, - - { - name: "random length exceeds maximum valid address by 1", - offset: validAddress, - length: memorySize + 1, - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - errno := a.RandomGet(testCtx, mod, tc.offset, tc.length) - require.Equal(t, ErrnoFault, errno, ErrnoName(errno)) - }) - } -} - -func Test_RandomGet_SourceError(t *testing.T) { - tests := []struct { - name string - randSource io.Reader - }{ - { - name: "error", - randSource: iotest.ErrReader(errors.New("RandSource error")), - }, - { - name: "incomplete", - randSource: bytes.NewReader([]byte{1, 2}), - }, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - sysCtx, err := internalsys.NewContext( - math.MaxUint32, - nil, - nil, - new(bytes.Buffer), - nil, - nil, - tc.randSource, - nil, 0, - nil, 0, - nil, // nanosleep - nil, - ) - require.NoError(t, err) - - mod, _ := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, sysCtx) - defer mod.Close(testCtx) - - errno := a.RandomGet(testCtx, mod, uint32(1), uint32(5)) // arbitrary offset and length - require.Equal(t, ErrnoIo, errno, ErrnoName(errno)) - }) - } -} - -// Test_SockRecv only tests it is stubbed for GrainLang per #271 -func Test_SockRecv(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionSockRecv, importSockRecv, nil) - defer mod.Close(testCtx) - - t.Run("wasi.SockRecv", func(t *testing.T) { - errno := a.SockRecv(testCtx, mod, 0, 0, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionSockRecv, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_SockSend only tests it is stubbed for GrainLang per #271 -func Test_SockSend(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionSockSend, importSockSend, nil) - defer mod.Close(testCtx) - - t.Run("wasi.SockSend", func(t *testing.T) { - errno := a.SockSend(testCtx, mod, 0, 0, 0, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionSockSend, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0, 0, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - -// Test_SockShutdown only tests it is stubbed for GrainLang per #271 -func Test_SockShutdown(t *testing.T) { - mod, fn := instantiateModule(testCtx, t, functionSockShutdown, importSockShutdown, nil) - defer mod.Close(testCtx) - - t.Run("wasi.SockShutdown", func(t *testing.T) { - errno := a.SockShutdown(testCtx, mod, 0, 0) - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) - - t.Run(functionSockShutdown, func(t *testing.T) { - results, err := fn.Call(testCtx, 0, 0) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - require.Equal(t, ErrnoNosys, errno, ErrnoName(errno)) - }) -} - const testMemoryPageSize = 1 // maskMemory sets the first memory in the store to '?' * size, so tests can see what's written. @@ -1909,93 +32,46 @@ func maskMemory(t *testing.T, ctx context.Context, mod api.Module, size int) { } } -func instantiateModule(ctx context.Context, t *testing.T, wasiFunction, wasiImport string, sysCtx *internalsys.Context) (api.Module, api.Function) { +func requireModule(t *testing.T, config wazero.ModuleConfig) (api.Module, api.Closer, *bytes.Buffer) { + var log bytes.Buffer + + // Set context to one that has an experimental listener + ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log)) + r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter()) - _, err := Instantiate(testCtx, r) + compiled, err := (&builder{r}).moduleBuilder(). + ExportMemoryWithMax("memory", 1, 1). + Compile(ctx, wazero.NewCompileConfig()) require.NoError(t, err) - binary, err := watzero.Wat2Wasm(fmt.Sprintf(`(module - %[2]s - (memory 1 1) ;; just an arbitrary size big enough for tests - (export "memory" (memory 0)) - (export "%[1]s" (func $wasi.%[1]s)) -)`, wasiFunction, wasiImport)) + mod, err := r.InstantiateModule(ctx, compiled, config) require.NoError(t, err) - - compiled, err := r.CompileModule(ctx, binary, wazero.NewCompileConfig()) - require.NoError(t, err) - defer compiled.Close(ctx) - - mod, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig(). - WithName(t.Name()). - WithRandSource(deterministicRandomSource())) - require.NoError(t, err) - - if sysCtx != nil { - mod.(*wasm.CallContext).Sys = sysCtx - } - - fn := mod.ExportedFunction(wasiFunction) - require.NotNil(t, fn) - - return mod, fn + return mod, r, &log } -func newSysContext(args, environ []string, fs fs.FS) (sysCtx *internalsys.Context, err error) { - return internalsys.NewContext( - math.MaxUint32, - args, - environ, - new(bytes.Buffer), - nil, - nil, - deterministicRandomSource(), - nil, 0, - nil, 0, - nil, // nanosleep - fs, - ) -} +// requireErrnoNosys ensures a call of the given function returns errno. The log +// message returned can verify the output is wasm `-->` or a host `==>` +// function. +func requireErrnoNosys(t *testing.T, funcName string, params ...uint64) string { + var log bytes.Buffer -func createFile(t *testing.T, pathName string, data []byte) (fs.File, fs.FS) { - mapFile := &fstest.MapFile{Data: data} - if data == nil { - mapFile.Mode = os.ModeDir - } - mapFS := fstest.MapFS{pathName: mapFile} - f, err := mapFS.Open(pathName) - require.NoError(t, err) - return f, mapFS -} + // Set context to one that has an experimental listener + ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log)) -// newContextWithWritableFile is temporary until we add the ability to open files for writing. -func newContextWithWritableFile(t *testing.T, tmpDir string, pathName string) *internalsys.Context { - writeable, testFS := createWriteableFile(t, tmpDir, pathName, []byte{}) - sysCtx, err := newSysContext(nil, nil, testFS) + r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter()) + defer r.Close(ctx) + + mod, err := Instantiate(ctx, r) require.NoError(t, err) - fsc := sysCtx.FS(testCtx) - fd, err := fsc.OpenFile(testCtx, pathName) - require.NoError(t, err) - - // Swap the read-only file with a writeable one until #390 - f, ok := fsc.OpenedFile(testCtx, fd) - require.True(t, ok) - f.File.Close() - f.File = writeable - - return sysCtx + requireErrno(t, ErrnoNosys, mod, funcName, params...) + return "\n" + log.String() } -// createWriteableFile uses real files when io.Writer tests are needed. -func createWriteableFile(t *testing.T, tmpDir string, pathName string, data []byte) (fs.File, fs.FS) { - require.NotNil(t, data) - absolutePath := path.Join(tmpDir, pathName) - require.NoError(t, os.WriteFile(absolutePath, data, 0o600)) - - // open the file for writing in a custom way until #390 - f, err := os.OpenFile(absolutePath, os.O_RDWR, 0o600) +func requireErrno(t *testing.T, expectedErrno Errno, mod api.Closer, funcName string, params ...uint64) { + results, err := mod.(api.Module).ExportedFunction(funcName).Call(testCtx, params...) require.NoError(t, err) - return f, os.DirFS(tmpDir) + errno := Errno(results[0]) + require.Equal(t, expectedErrno, errno, ErrnoName(errno)) }