Adds sys.Walltime and sys.Nanotime for security and determinism (#616)

This adds two clock interfaces: sys.Walltime and sys.Nanotime to
allow implementations to override readings for purposes of security or
determinism.

The default values of both are a fake timestamp, to avoid the sandbox
break we formerly had by returning the real time. This is similar to how
we don't inherit OS Env values.
This commit is contained in:
Crypt Keeper
2022-06-04 15:14:31 +08:00
committed by GitHub
parent 94d1d31733
commit 507ce79080
43 changed files with 1102 additions and 428 deletions

View File

@@ -31,7 +31,7 @@ bench:
bench.check:
@go build ./internal/integration_test/bench/...
@# Don't use -test.benchmem as it isn't accurate when comparing against CGO libs
@for d in vs/wasmedge vs/wasmer vs/wasmtime ; do \
@for d in vs/time vs/wasmedge vs/wasmer vs/wasmtime ; do \
cd ./internal/integration_test/$$d ; \
go test -bench=. . -tags='wasmedge' $(ensureCompilerFastest) ; \
cd - ;\
@@ -131,7 +131,7 @@ golangci_lint_goarch ?= $(shell go env GOARCH)
.PHONY: lint
lint: $(golangci_lint_path)
@GOARCH=$(golangci_lint_goarch) $(golangci_lint_path) run --timeout 5m
@GOARCH=$(golangci_lint_goarch) CGO_ENABLED=0 $(golangci_lint_path) run --timeout 5m
.PHONY: format
format:

View File

@@ -396,7 +396,56 @@ See https://github.com/bytecodealliance/wasmtime/blob/2ca01ae9478f199337cf743a6a
Their semantics match when `pathLen` == the length of `path`, so in practice this difference won't matter match.
### ClockResGet
## sys.Walltime and Nanotime
The `sys` package has two function types, `Walltime` and `Nanotime` for real
and monotonic clock exports. The naming matches conventions used in Go.
```go
func time_now() (sec int64, nsec int32, mono int64) {
sec, nsec = walltime()
return sec, nsec, nanotime()
}
```
Splitting functions for wall and clock time allow implementations to choose
whether to implement the clock once (as in Go), or split them out.
Each can be configured with a `ClockResolution`, although is it usually
incorrect as detailed in a sub-heading below. The only reason for exposing this
is to satisfy WASI:
See https://github.com/WebAssembly/wasi-clocks
## Why default to fake time?
WebAssembly has an implicit design pattern of capabilities based security. By
defaulting to a fake time, we reduce the chance of timing attacks, at the cost
of requiring configuration to opt-into real clocks.
See https://gruss.cc/files/fantastictimers.pdf for an example attacks.
## Why not `time.Clock`?
wazero can't use `time.Clock` as a plugin for clock implementation as it is
only substitutable with build flags (`faketime`) and conflates wall and
monotonic time in the same call.
Go's `time.Clock` was added monotonic time after the fact. For portability with
prior APIs, a decision was made to combine readings into the same API call.
See https://go.googlesource.com/proposal/+/master/design/12914-monotonic.md
WebAssembly time imports do not have the same concern. In fact even Go's
imports for clocks split walltime from nanotime readings.
See https://github.com/golang/go/blob/252324e879e32f948d885f787decf8af06f82be9/misc/wasm/wasm_exec.js#L243-L255
Finally, Go's clock is not an interface. WebAssembly users who want determinism
or security need to be able to substitute an alternative clock implementation
from the host process one.
### `ClockResolution`
A clock's resolution is hardware and OS dependent so requires a system call to retrieve an accurate value.
Go does not provide a function for getting resolution, so without CGO we don't have an easy way to get an actual

View File

@@ -85,14 +85,17 @@ The [wasi_snapshot_preview1][13] tag of WASI is widely implemented, so wazero
bundles an implementation. That way, you don't have to write these functions.
For example, here's how you can allow WebAssembly modules to read
"/work/home/a.txt" as "/a.txt" or "./a.txt":
"/work/home/a.txt" as "/a.txt" or "./a.txt" as well the system clock:
```go
_, err := wasi_snapshot_preview1.Instantiate(ctx, r)
if err != nil {
log.Panicln(err)
}
config := wazero.NewModuleConfig().WithFS(os.DirFS("/work/home"))
config := wazero.NewModuleConfig().
WithFS(os.DirFS("/work/home")). // instead of no file system
WithSysWalltime().WithSysNanotime() // instead of fake time
module, err := r.InstantiateModule(ctx, compiled, config)
...
```

View File

@@ -27,6 +27,7 @@ import (
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/ieee754"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -155,7 +156,7 @@ func (a *assemblyscript) abort(
columnNumber uint32,
) {
if !a.abortMessageDisabled {
sys := sysCtx(mod)
sysCtx := sysCtx(mod)
msg, err := readAssemblyScriptString(ctx, mod, message)
if err != nil {
return
@@ -164,7 +165,7 @@ func (a *assemblyscript) abort(
if err != nil {
return
}
_, _ = fmt.Fprintf(sys.Stderr(), "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber)
_, _ = fmt.Fprintf(sysCtx.Stderr(), "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber)
}
_ = mod.CloseWithExitCode(ctx, 255)
}
@@ -269,7 +270,7 @@ func decodeUTF16(b []byte) string {
return string(utf16.Decode(u16s))
}
func sysCtx(m api.Module) *wasm.SysContext {
func sysCtx(m api.Module) *internalsys.Context {
if internal, ok := m.(*wasm.CallContext); !ok {
panic(fmt.Errorf("unsupported wasm.Module implementation: %v", m))
} else {

102
config.go
View File

@@ -6,12 +6,15 @@ import (
"io"
"io/fs"
"math"
"time"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/engine/compiler"
"github.com/tetratelabs/wazero/internal/engine/interpreter"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/platform"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/sys"
)
// RuntimeConfig controls runtime behavior, with the default implementation as NewRuntimeConfig
@@ -337,7 +340,7 @@ func (c *compileConfig) WithMemorySizer(memorySizer api.MemorySizer) CompileConf
//
// Ex.
// // Initialize base configuration:
// config := wazero.NewModuleConfig().WithStdout(buf)
// config := wazero.NewModuleConfig().WithStdout(buf).WithSysNanotime()
//
// // Assign different configuration on each instantiation
// module, _ := r.InstantiateModule(ctx, compiled, config.WithName("rotate").WithArgs("rotate", "angle=90", "dir=cw"))
@@ -442,6 +445,43 @@ type ModuleConfig interface {
// See https://linux.die.net/man/3/stdout
WithStdout(io.Writer) ModuleConfig
// WithWalltime configures the wall clock, sometimes referred to as the
// real time clock. Defaults to a constant fake result.
//
// Ex. To override with your own clock:
// moduleConfig = moduleConfig.
// WithWalltime(func(context.Context) (sec int64, nsec int32) {
// return clock.walltime()
// }, sys.ClockResolution(time.Microsecond.Nanoseconds()))
//
// Note: This does not default to time.Now as that violates sandboxing. Use
// WithSysWalltime for a usable implementation.
WithWalltime(sys.Walltime, sys.ClockResolution) ModuleConfig
// WithSysWalltime uses time.Now for sys.Walltime with a resolution of 1us
// (1000ns).
//
// See WithWalltime
WithSysWalltime() ModuleConfig
// WithNanotime configures the monotonic clock, used to measure elapsed
// time in nanoseconds. Defaults to a constant fake result.
//
// Ex. To override with your own clock:
// moduleConfig = moduleConfig.
// WithNanotime(func(context.Context) int64 {
// return clock.nanotime()
// }, sys.ClockResolution(time.Microsecond.Nanoseconds()))
//
// Note: This does not default to time.Since as that violates sandboxing.
// Use WithSysNanotime for a usable implementation.
WithNanotime(sys.Nanotime, sys.ClockResolution) ModuleConfig
// WithSysNanotime uses time.Now for sys.Nanotime with a resolution of 1us.
//
// See WithNanotime
WithSysNanotime() ModuleConfig
// WithRandSource configures a source of random bytes. Defaults to crypto/rand.Reader.
//
// This reader is most commonly used by the functions like "random_get" in "wasi_snapshot_preview1" or "seed" in
@@ -472,13 +512,16 @@ type moduleConfig struct {
stdout io.Writer
stderr io.Writer
randSource io.Reader
walltimeTime *sys.Walltime
walltimeResolution sys.ClockResolution
nanotimeTime *sys.Nanotime
nanotimeResolution sys.ClockResolution
args []string
// environ is pair-indexed to retain order similar to os.Environ.
environ []string
// environKeys allow overwriting of existing values.
environKeys map[string]int
fs *sys.FSConfig
fs *internalsys.FSConfig
}
// NewModuleConfig returns a ModuleConfig that can be used for configuring module instantiation.
@@ -487,7 +530,7 @@ func NewModuleConfig() ModuleConfig {
startFunctions: []string{"_start"},
environKeys: map[string]int{},
fs: sys.NewFSConfig(),
fs: internalsys.NewFSConfig(),
}
}
@@ -553,6 +596,36 @@ func (c *moduleConfig) WithStdout(stdout io.Writer) ModuleConfig {
return &ret
}
// WithWalltime implements ModuleConfig.WithWalltime
func (c *moduleConfig) WithWalltime(walltime sys.Walltime, resolution sys.ClockResolution) ModuleConfig {
ret := *c // copy
ret.walltimeTime = &walltime
ret.walltimeResolution = resolution
return &ret
}
// We choose arbitrary resolutions here because there's no perfect alternative. For example, according to the
// source in time.go, windows monotonic resolution can be 15ms. This chooses arbitrarily 1us for wall time and
// 1ns for monotonic. See RATIONALE.md for more context.
// WithSysWalltime implements ModuleConfig.WithSysWalltime
func (c *moduleConfig) WithSysWalltime() ModuleConfig {
return c.WithWalltime(platform.Walltime, sys.ClockResolution(time.Microsecond.Nanoseconds()))
}
// WithNanotime implements ModuleConfig.WithNanotime
func (c *moduleConfig) WithNanotime(nanotime sys.Nanotime, resolution sys.ClockResolution) ModuleConfig {
ret := *c // copy
ret.nanotimeTime = &nanotime
ret.nanotimeResolution = resolution
return &ret
}
// WithSysNanotime implements ModuleConfig.WithSysNanotime
func (c *moduleConfig) WithSysNanotime() ModuleConfig {
return c.WithNanotime(platform.Nanotime, sys.ClockResolution(1))
}
// WithRandSource implements ModuleConfig.WithRandSource
func (c *moduleConfig) WithRandSource(source io.Reader) ModuleConfig {
ret := *c // copy
@@ -567,8 +640,8 @@ func (c *moduleConfig) WithWorkDirFS(fs fs.FS) ModuleConfig {
return &ret
}
// toSysContext creates a baseline wasm.SysContext configured by ModuleConfig.
func (c *moduleConfig) toSysContext() (sys *wasm.SysContext, err error) {
// toSysContext creates a baseline wasm.Context configured by ModuleConfig.
func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
var environ []string // Intentionally doesn't pre-allocate to reduce logic to default to nil.
// Same validation as syscall.Setenv for Linux
for i := 0; i < len(c.environ); i += 2 {
@@ -578,7 +651,7 @@ func (c *moduleConfig) toSysContext() (sys *wasm.SysContext, err error) {
return
}
for j := 0; j < len(key); j++ {
if key[j] == '=' { // NUL enforced in NewSysContext
if key[j] == '=' { // NUL enforced in NewContext
err = errors.New("environ invalid: key contains '=' character")
return
}
@@ -591,5 +664,16 @@ func (c *moduleConfig) toSysContext() (sys *wasm.SysContext, err error) {
return nil, err
}
return wasm.NewSysContext(math.MaxUint32, c.args, environ, c.stdin, c.stdout, c.stderr, c.randSource, preopens)
return internalsys.NewContext(
math.MaxUint32,
c.args,
environ,
c.stdin,
c.stdout,
c.stderr,
c.randSource,
c.walltimeTime, c.walltimeResolution,
c.nanotimeTime, c.nanotimeResolution,
preopens,
)
}

View File

@@ -9,9 +9,10 @@ import (
"testing/fstest"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/sys"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/sys"
)
func TestRuntimeConfig(t *testing.T) {
@@ -299,7 +300,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
tests := []struct {
name string
input ModuleConfig
expected *wasm.SysContext
expected *internalsys.Context
}{
{
name: "empty",
@@ -312,6 +313,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
@@ -326,6 +329,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
@@ -340,6 +345,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
@@ -354,6 +361,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
@@ -368,6 +377,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
@@ -382,6 +393,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
@@ -396,6 +409,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
@@ -410,10 +425,11 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
{
name: "WithEnv twice",
input: NewModuleConfig().WithEnv("a", "b").WithEnv("c", "de"),
@@ -425,6 +441,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
@@ -439,7 +457,9 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
map[uint32]*sys.FileEntry{ // openedFiles
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS},
4: {Path: ".", FS: testFS},
},
@@ -456,7 +476,9 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
map[uint32]*sys.FileEntry{ // openedFiles
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS2},
4: {Path: ".", FS: testFS2},
},
@@ -473,7 +495,9 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
map[uint32]*sys.FileEntry{ // openedFiles
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: ".", FS: testFS},
},
),
@@ -489,7 +513,9 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
map[uint32]*sys.FileEntry{ // openedFiles
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS},
4: {Path: ".", FS: testFS2},
},
@@ -506,24 +532,171 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
map[uint32]*sys.FileEntry{ // openedFiles
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: ".", FS: testFS},
4: {Path: "/", FS: testFS2},
},
),
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sys, err := tc.input.(*moduleConfig).toSysContext()
sysCtx, err := tc.input.(*moduleConfig).toSysContext()
require.NoError(t, err)
require.Equal(t, tc.expected, sys)
require.Equal(t, tc.expected, sysCtx)
})
}
}
// TestModuleConfig_toSysContext_WithWalltime has to test differently because we can't
// compare function pointers when functions are passed by value.
func TestModuleConfig_toSysContext_WithWalltime(t *testing.T) {
tests := []struct {
name string
input ModuleConfig
expectedSec int64
expectedNsec int32
expectedResolution sys.ClockResolution
expectedErr string
}{
{
name: "ok",
input: NewModuleConfig().
WithWalltime(func(context.Context) (sec int64, nsec int32) {
return 1, 2
}, 3),
expectedSec: 1,
expectedNsec: 2,
expectedResolution: 3,
},
{
name: "overwrites",
input: NewModuleConfig().
WithWalltime(func(context.Context) (sec int64, nsec int32) {
return 3, 4
}, 5).
WithWalltime(func(context.Context) (sec int64, nsec int32) {
return 1, 2
}, 3),
expectedSec: 1,
expectedNsec: 2,
expectedResolution: 3,
},
{
name: "invalid resolution",
input: NewModuleConfig().
WithWalltime(func(context.Context) (sec int64, nsec int32) {
return 1, 2
}, 0),
expectedErr: "invalid Walltime resolution: 0",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := tc.input.(*moduleConfig).toSysContext()
if tc.expectedErr == "" {
require.Nil(t, err)
sec, nsec := sysCtx.Walltime(testCtx)
require.Equal(t, tc.expectedSec, sec)
require.Equal(t, tc.expectedNsec, nsec)
require.Equal(t, tc.expectedResolution, sysCtx.WalltimeResolution())
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
t.Run("context", func(t *testing.T) {
sysCtx, err := NewModuleConfig().
WithWalltime(func(ctx context.Context) (sec int64, nsec int32) {
require.Equal(t, testCtx, ctx)
return 1, 2
}, 3).(*moduleConfig).toSysContext()
require.NoError(t, err)
sec, nsec := sysCtx.Walltime(testCtx)
// If below pass, the context was correct!
require.Equal(t, int64(1), sec)
require.Equal(t, int32(2), nsec)
})
}
// TestModuleConfig_toSysContext_WithNanotime has to test differently because we can't
// compare function pointers when functions are passed by value.
func TestModuleConfig_toSysContext_WithNanotime(t *testing.T) {
tests := []struct {
name string
input ModuleConfig
expectedNanos int64
expectedResolution sys.ClockResolution
expectedErr string
}{
{
name: "ok",
input: NewModuleConfig().
WithNanotime(func(context.Context) int64 {
return 1
}, 2),
expectedNanos: 1,
expectedResolution: 2,
},
{
name: "overwrites",
input: NewModuleConfig().
WithNanotime(func(context.Context) int64 {
return 3
}, 4).
WithNanotime(func(context.Context) int64 {
return 1
}, 2),
expectedNanos: 1,
expectedResolution: 2,
},
{
name: "invalid resolution",
input: NewModuleConfig().
WithNanotime(func(context.Context) int64 {
return 1
}, 0),
expectedErr: "invalid Nanotime resolution: 0",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := tc.input.(*moduleConfig).toSysContext()
if tc.expectedErr == "" {
require.Nil(t, err)
nanos := sysCtx.Nanotime(testCtx)
require.Equal(t, tc.expectedNanos, nanos)
require.Equal(t, tc.expectedResolution, sysCtx.NanotimeResolution())
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
t.Run("context", func(t *testing.T) {
sysCtx, err := NewModuleConfig().
WithNanotime(func(ctx context.Context) int64 {
require.Equal(t, testCtx, ctx)
return 1
}, 2).(*moduleConfig).toSysContext()
require.NoError(t, err)
// If below pass, the context was correct!
require.Equal(t, int64(1), sysCtx.Nanotime(testCtx))
})
}
func TestModuleConfig_toSysContext_Errors(t *testing.T) {
tests := []struct {
name string
@@ -576,9 +749,30 @@ func TestModuleConfig_toSysContext_Errors(t *testing.T) {
}
}
// requireSysContext ensures wasm.NewSysContext doesn't return an error, which makes it usable in test matrices.
func requireSysContext(t *testing.T, max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randsource io.Reader, openedFiles map[uint32]*sys.FileEntry) *wasm.SysContext {
sysCtx, err := wasm.NewSysContext(max, args, environ, stdin, stdout, stderr, randsource, openedFiles)
// requireSysContext ensures wasm.NewContext doesn't return an error, which makes it usable in test matrices.
func requireSysContext(
t *testing.T,
max uint32,
args, environ []string,
stdin io.Reader,
stdout, stderr io.Writer,
randSource io.Reader,
walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
openedFiles map[uint32]*internalsys.FileEntry,
) *internalsys.Context {
sysCtx, err := internalsys.NewContext(
max,
args,
environ,
stdin,
stdout,
stderr,
randSource,
walltime, walltimeResolution,
nanotime, nanotimeResolution,
openedFiles,
)
require.NoError(t, err)
return sysCtx
}

View File

@@ -1,9 +1,13 @@
## AssemblyScript example
This example runs a WebAssembly program compiled using AssemblyScript, built with `npm install && npm run build`.
The program exports two functions, `hello_world` which executes simple integer math, and `goodbye_world`, which
throws an error that is logged using the AssemblyScript `abort` built-in function. Wazero is configured to export
functions used by WebAssembly for reporting errors and trace messages.
This example runs a WebAssembly program compiled using AssemblyScript, built
with `npm install && npm run build`.
AssemblyScript program exports two functions, `hello_world` which executes
simple math, and `goodbye_world`, which throws an error that is logged using
AssemblyScript `abort` built-in function.
This demo configures AssemblyScript imports for errors and trace messages.
Ex.
```bash

View File

@@ -25,8 +25,8 @@ func main() {
// Choose the context to use for function calls.
ctx := context.Background()
// Create a new WebAssembly Runtime. AssemblyScript enables certain wasm 2.0 features by default, so
// we go ahead and configure the runtime for wasm 2.0 compatibility.
// Create a new WebAssembly Runtime.
// Use WebAssembly 2.0 because AssemblyScript uses some >1.0 features.
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig().
WithWasmCore2())
defer r.Close(ctx) // This closes everything this Runtime created.
@@ -44,10 +44,12 @@ func main() {
log.Panicln(err)
}
// Instantiate a WebAssembly module that imports the "abort" and "trace" functions defined by
// assemblyscript.Instantiate and exports functions we'll use in this example. We override the
// default module config that discards stdout and stderr.
mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout).WithStderr(os.Stderr))
// Instantiate a WebAssembly module that imports the "abort" and "trace"
// functions defined by assemblyscript.Instantiate and exports functions
// we'll use in this example.
mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().
// Override the default module config that discards stdout and stderr.
WithStdout(os.Stdout).WithStderr(os.Stderr))
if err != nil {
log.Panicln(err)
}
@@ -63,16 +65,16 @@ func main() {
log.Panicln(err)
}
// Call hello_world, which returns the input value incremented by 3. It includes a call to trace()
// for detailed logging but the above assemblyscript.Instantiate does not enable it by default.
// Call hello_world, which returns the input value incremented by 3.
// While this calls trace(), our configuration didn't enable it.
results, err := helloWorld.Call(ctx, uint64(num))
if err != nil {
log.Panicln(err)
}
fmt.Printf("hello_world returned: %v", results[0])
// Call goodbye_world, which aborts with an error. assemblyscript.Instantiate configures abort
// to print to stderr.
// Call goodbye_world, which aborts with an error.
// assemblyscript.Instantiate was configured above to abort to stderr.
results, err = goodbyeWorld.Call(ctx)
if err == nil {
log.Panicln("goodbye_world did not fail")

View File

@@ -1,3 +1,4 @@
## WASI example
This example shows how to use I/O in your WebAssembly modules using WASI (WebAssembly System Interface).
This example shows how to use I/O in your WebAssembly modules using WASI
(WebAssembly System Interface).

View File

@@ -38,7 +38,7 @@ func main() {
log.Panicln(err)
}
// Combine the above into our baseline config, overriding defaults (which discard stdout and have no file system).
// Combine the above into our baseline config, overriding defaults (which discards stdout and has no file system).
config := wazero.NewModuleConfig().WithStdout(os.Stdout).WithFS(rooted)
// Instantiate WASI, which implements system I/O such as console output.

View File

@@ -1,12 +0,0 @@
package experimental
import (
"context"
"github.com/tetratelabs/wazero/internal/sys"
)
// WithTimeNowUnixNano allows you to control the value otherwise returned by time.Now().UnixNano()
func WithTimeNowUnixNano(ctx context.Context, timeUnixNano func() uint64) context.Context {
return context.WithValue(ctx, sys.TimeNowUnixNanoKey{}, timeUnixNano)
}

View File

@@ -1,58 +0,0 @@
package experimental_test
import (
"context"
_ "embed"
"fmt"
"log"
"time"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/wasi_snapshot_preview1"
)
const epochNanos = uint64(1640995200000000000) // midnight UTC 2022-01-01
// clockWasm was generated by the following:
// cd testdata; wat2wasm --debug-names clock.wat
//go:embed testdata/clock.wasm
var clockWasm []byte
// This is a basic example of overriding the clock via WithTimeNowUnixNano. The main goal is to show how it is configured.
func Example_withTimeNowUnixNano() {
ctx := context.Background()
r := wazero.NewRuntime()
defer r.Close(ctx) // This closes everything this Runtime created.
if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil {
log.Panicln(err)
}
// Instantiate a module that only re-exports a WASI function that uses the clock.
mod, err := r.InstantiateModuleFromBinary(ctx, clockWasm)
if err != nil {
log.Panicln(err)
}
// Call clock_time_get in context of an experimental clock function
ctx = experimental.WithTimeNowUnixNano(ctx, func() uint64 { return epochNanos })
results, err := mod.ExportedFunction("clock_time_get").Call(ctx, 0, 0, 0)
if err != nil {
log.Panicln(err)
}
if results[0] != 0 {
log.Panicf("received errno %d\n", results[0])
}
// Try to read the time WASI wrote to memory at offset zero.
if nanos, ok := mod.Memory().ReadUint64Le(ctx, 0); !ok {
log.Panicf("Memory.ReadUint64Le(0) out of range of memory size %d", mod.Memory().Size(ctx))
} else {
fmt.Println(time.UnixMicro(int64(nanos / 1000)).UTC())
}
// Output:
// 2022-01-01 00:00:00 +0000 UTC
}

View File

@@ -13,7 +13,7 @@ import (
)
func TestMain(m *testing.M) {
if !platform.IsSupported() {
if !platform.CompilerSupported() {
os.Exit(0)
}
os.Exit(m.Run())

View File

@@ -176,7 +176,7 @@ func TestCompiler_ModuleEngine_Memory(t *testing.T) {
// requireSupportedOSArch is duplicated also in the platform package to ensure no cyclic dependency.
func requireSupportedOSArch(t *testing.T) {
if !platform.IsSupported() {
if !platform.CompilerSupported() {
t.Skip()
}
}

View File

@@ -48,7 +48,7 @@ var tests = map[string]func(t *testing.T, r wazero.Runtime){
}
func TestEngineCompiler(t *testing.T) {
if !platform.IsSupported() {
if !platform.CompilerSupported() {
t.Skip()
}
runAllTests(t, tests, wazero.NewRuntimeConfigCompiler())

View File

@@ -19,7 +19,7 @@ var hammers = map[string]func(t *testing.T, r wazero.Runtime){
}
func TestEngineCompiler_hammer(t *testing.T) {
if !platform.IsSupported() {
if !platform.CompilerSupported() {
t.Skip()
}
runAllTests(t, hammers, wazero.NewRuntimeConfigCompiler())

View File

@@ -14,6 +14,7 @@ import (
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/u64"
"github.com/tetratelabs/wazero/internal/wasm"
@@ -344,7 +345,7 @@ func addSpectestModule(t *testing.T, s *wasm.Store, ns *wasm.Namespace) {
err = s.Engine.CompileModule(testCtx, mod)
require.NoError(t, err)
_, err = s.Instantiate(testCtx, ns, mod, mod.NameSection.ModuleName, wasm.DefaultSysContext(), nil)
_, err = s.Instantiate(testCtx, ns, mod, mod.NameSection.ModuleName, sys.DefaultContext(), nil)
require.NoError(t, err)
}

View File

@@ -21,7 +21,7 @@ var testcases embed.FS
const enabledFeatures = wasm.Features20220419
func TestCompiler(t *testing.T) {
if !platform.IsSupported() {
if !platform.CompilerSupported() {
t.Skip()
}

View File

@@ -0,0 +1,9 @@
module github.com/tetratelabs/wazero/internal/integration_test/vs/clock
go 1.17
require github.com/tetratelabs/wazero v0.0.0
require golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
replace github.com/tetratelabs/wazero => ../../../..

View File

@@ -0,0 +1,2 @@
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -0,0 +1,58 @@
// Package time benchmarks shows ways to implementing the platform clock more
// efficiently. As long as CGO is available, all platforms can use
// `runtime.nanotime` to more efficiently implement sys.Nanotime vs using
// time.Since or x/sys.
//
// While results would be more impressive, this doesn't show how to use
// `runtime.walltime` to avoid the double-performance vs using time.Now. The
// corresponding function only exists in darwin, so prevents this benchmark
// from running on other platforms.
package time
import (
"testing"
"time"
_ "unsafe" // for go:linkname
"golang.org/x/sys/unix"
)
//go:noescape
//go:linkname nanotime runtime.nanotime
func nanotime() int64
func BenchmarkClock(b *testing.B) {
b.Run("time.Now", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = time.Now()
}
})
b.Run("ClockGettime(CLOCK_REALTIME)", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var now unix.Timespec
if err := unix.ClockGettime(unix.CLOCK_REALTIME, &now); err != nil {
b.Fatal(err)
}
}
})
base := time.Now()
b.Run("time.Since", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = time.Since(base)
}
})
b.Run("runtime.nanotime", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = nanotime()
}
})
b.Run("ClockGettime(CLOCK_MONOTONIC)", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var tick unix.Timespec
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &tick); err != nil {
b.Fatal(err)
}
}
})
}

View File

@@ -11,7 +11,7 @@ import (
var testCode, _ = io.ReadAll(io.LimitReader(rand.Reader, 8*1024))
func Test_MmapCodeSegment(t *testing.T) {
if !IsSupported() {
if !CompilerSupported() {
t.Skip()
}
@@ -30,7 +30,7 @@ func Test_MmapCodeSegment(t *testing.T) {
}
func Test_MunmapCodeSegment(t *testing.T) {
if !IsSupported() {
if !CompilerSupported() {
t.Skip()
}

View File

@@ -9,8 +9,8 @@ import (
"runtime"
)
// IsSupported is exported for tests and includes constraints here and also the assembler.
func IsSupported() bool {
// CompilerSupported is exported for tests and includes constraints here and also the assembler.
func CompilerSupported() bool {
switch runtime.GOOS {
case "darwin", "windows", "linux":
default:

48
internal/platform/time.go Normal file
View File

@@ -0,0 +1,48 @@
package platform
import (
"context"
"time"
)
const FakeEpochNanos = int64(1640995200000000000) // midnight UTC 2022-01-01
// FakeWalltime implements sys.Walltime with FakeEpochNanos.
func FakeWalltime(context.Context) (sec int64, nsec int32) {
return FakeEpochNanos / 1e9, int32(FakeEpochNanos % 1e9)
}
// FakeNanotime implements sys.Nanotime with FakeEpochNanos.
func FakeNanotime(context.Context) int64 {
return FakeEpochNanos
}
// Walltime implements sys.Walltime with time.Now.
//
// Note: This is only notably less efficient than it could be is reading
// runtime.walltime(). time.Now defensively reads nanotime also, just in case
// time.Since is used. This doubles the performance impact. However, wall time
// is likely to be read less frequently than Nanotime. Also, doubling the cost
// matters less on fast platforms that can return both in <=100ns.
func Walltime(context.Context) (sec int64, nsec int32) {
t := time.Now()
return t.Unix(), int32(t.Nanosecond())
}
// nanoBase uses time.Now to ensure a monotonic clock reading on all platforms
// via time.Since.
var nanoBase = time.Now()
// nanotimePortable implements sys.Nanotime with time.Since.
//
// Note: This is less efficient than it could be is reading runtime.nanotime(),
// Just to do that requires CGO.
func nanotimePortable() int64 {
return time.Since(nanoBase).Nanoseconds()
}
// Nanotime implements sys.Nanotime with runtime.nanotime() if CGO is available
// and time.Since if not.
func Nanotime(context.Context) int64 {
return nanotime()
}

View File

@@ -0,0 +1,11 @@
//go:build cgo
package platform
import _ "unsafe" // for go:linkname
// nanotime uses runtime.nanotime as it is available on all platforms and
// benchmarks faster than using time.Since.
//go:noescape
//go:linkname nanotime runtime.nanotime
func nanotime() int64

View File

@@ -0,0 +1,7 @@
//go:build !cgo
package platform
func nanotime() int64 {
return nanotimePortable()
}

View File

@@ -0,0 +1,47 @@
package platform
import (
"context"
"testing"
"time"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/sys"
)
func Test_Walltime(t *testing.T) {
now := time.Now().Unix()
sec, nsec := Walltime(context.Background())
// Loose test that the second variant is close to now.
// The only thing that could flake this is a time adjustment during the test.
require.True(t, now == sec || now == sec-1)
// Verify bounds of nanosecond fraction as measuring it precisely won't work.
require.True(t, nsec >= 0)
require.True(t, nsec < int32(time.Second.Nanoseconds()))
}
func Test_Nanotime(t *testing.T) {
tests := []struct {
name string
nanotime sys.Nanotime
}{
{"Nanotime", Nanotime},
{"nanotimePortable", func(ctx context.Context) int64 {
return nanotimePortable()
}},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
delta := time.Since(nanoBase).Nanoseconds()
nanos := Nanotime(context.Background())
// It takes more than a nanosecond to make the two clock readings required
// to implement time.Now. Hence, delta will always be less than nanos.
require.True(t, delta <= nanos)
})
}
}

View File

@@ -1,30 +1,41 @@
package wasm
package sys
import (
"context"
"crypto/rand"
"errors"
"fmt"
"io"
"time"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/sys"
)
// SysContext holds module-scoped system resources currently only used by internalwasi.
type SysContext struct {
// Context holds module-scoped system resources currently only supported by
// built-in host functions.
type Context struct {
args, environ []string
argsSize, environSize uint32
stdin io.Reader
stdout, stderr io.Writer
// Note: Using function pointers here keeps them stable for tests.
walltime *sys.Walltime
walltimeResolution sys.ClockResolution
nanotime *sys.Nanotime
nanotimeResolution sys.ClockResolution
randSource io.Reader
fs *sys.FSContext
fs *FSContext
}
// Args is like os.Args and defaults to nil.
//
// Note: The count will never be more than math.MaxUint32.
// See wazero.ModuleConfig WithArgs
func (c *SysContext) Args() []string {
func (c *Context) Args() []string {
return c.args
}
@@ -33,7 +44,7 @@ func (c *SysContext) Args() []string {
// Note: To get the size without null-terminators, subtract the length of Args from this value.
// See wazero.ModuleConfig WithArgs
// See https://en.wikipedia.org/wiki/Null-terminated_string
func (c *SysContext) ArgsSize() uint32 {
func (c *Context) ArgsSize() uint32 {
return c.argsSize
}
@@ -41,7 +52,7 @@ func (c *SysContext) ArgsSize() uint32 {
//
// Note: The count will never be more than math.MaxUint32.
// See wazero.ModuleConfig WithEnv
func (c *SysContext) Environ() []string {
func (c *Context) Environ() []string {
return c.environ
}
@@ -50,36 +61,56 @@ func (c *SysContext) Environ() []string {
// Note: To get the size without null-terminators, subtract the length of Environ from this value.
// See wazero.ModuleConfig WithEnv
// See https://en.wikipedia.org/wiki/Null-terminated_string
func (c *SysContext) EnvironSize() uint32 {
func (c *Context) EnvironSize() uint32 {
return c.environSize
}
// Stdin is like exec.Cmd Stdin and defaults to a reader of os.DevNull.
// See wazero.ModuleConfig WithStdin
func (c *SysContext) Stdin() io.Reader {
func (c *Context) Stdin() io.Reader {
return c.stdin
}
// Stdout is like exec.Cmd Stdout and defaults to io.Discard.
// See wazero.ModuleConfig WithStdout
func (c *SysContext) Stdout() io.Writer {
func (c *Context) Stdout() io.Writer {
return c.stdout
}
// Stderr is like exec.Cmd Stderr and defaults to io.Discard.
// See wazero.ModuleConfig WithStderr
func (c *SysContext) Stderr() io.Writer {
func (c *Context) Stderr() io.Writer {
return c.stderr
}
// Walltime implements sys.Walltime.
func (c *Context) Walltime(ctx context.Context) (sec int64, nsec int32) {
return (*(c.walltime))(ctx)
}
// WalltimeResolution returns resolution of Walltime.
func (c *Context) WalltimeResolution() sys.ClockResolution {
return c.walltimeResolution
}
// Nanotime implements sys.Nanotime.
func (c *Context) Nanotime(ctx context.Context) int64 {
return (*(c.nanotime))(ctx)
}
// NanotimeResolution returns resolution of Nanotime.
func (c *Context) NanotimeResolution() sys.ClockResolution {
return c.nanotimeResolution
}
// FS returns the file system context.
func (c *SysContext) FS() *sys.FSContext {
func (c *Context) FS() *FSContext {
return c.fs
}
// RandSource is a source of random bytes and defaults to crypto/rand.Reader.
// see wazero.ModuleConfig WithRandSource
func (c *SysContext) RandSource() io.Reader {
func (c *Context) RandSource() io.Reader {
return c.randSource
}
@@ -92,24 +123,35 @@ func (eofReader) Read([]byte) (int, error) {
return 0, io.EOF
}
// DefaultSysContext returns SysContext with no values set.
// DefaultContext returns Context with no values set.
//
// Note: This isn't a constant because SysContext.openedFiles is currently mutable even when empty.
// Note: This isn't a constant because Context.openedFiles is currently mutable even when empty.
// TODO: Make it an error to open or close files when no FS was assigned.
func DefaultSysContext() *SysContext {
if sysCtx, err := NewSysContext(0, nil, nil, nil, nil, nil, nil, nil); err != nil {
panic(fmt.Errorf("BUG: DefaultSysContext should never error: %w", err))
func DefaultContext() *Context {
if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil); err != nil {
panic(fmt.Errorf("BUG: DefaultContext should never error: %w", err))
} else {
return sysCtx
}
}
var _ = DefaultSysContext() // Force panic on bug.
var _ = DefaultContext() // Force panic on bug.
var wt sys.Walltime = platform.FakeWalltime
var nt sys.Nanotime = platform.FakeNanotime
// NewSysContext is a factory function which helps avoid needing to know defaults or exporting all fields.
// NewContext is a factory function which helps avoid needing to know defaults or exporting all fields.
// Note: max is exposed for testing. max is only used for env/args validation.
func NewSysContext(max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randSource io.Reader, openedFiles map[uint32]*sys.FileEntry) (sysCtx *SysContext, err error) {
sysCtx = &SysContext{args: args, environ: environ}
func NewContext(
max uint32,
args, environ []string,
stdin io.Reader,
stdout, stderr io.Writer,
randSource io.Reader,
walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
openedFiles map[uint32]*FileEntry,
) (sysCtx *Context, err error) {
sysCtx = &Context{args: args, environ: environ}
if sysCtx.argsSize, err = nullTerminatedByteCount(max, args); err != nil {
return nil, fmt.Errorf("args invalid: %w", err)
@@ -143,11 +185,38 @@ func NewSysContext(max uint32, args, environ []string, stdin io.Reader, stdout,
sysCtx.randSource = randSource
}
sysCtx.fs = sys.NewFSContext(openedFiles)
if walltime != nil {
if clockResolutionInvalid(walltimeResolution) {
return nil, fmt.Errorf("invalid Walltime resolution: %d", walltimeResolution)
}
sysCtx.walltime = walltime
sysCtx.walltimeResolution = walltimeResolution
} else {
sysCtx.walltime = &wt
sysCtx.walltimeResolution = sys.ClockResolution(time.Microsecond.Nanoseconds())
}
if nanotime != nil {
if clockResolutionInvalid(nanotimeResolution) {
return nil, fmt.Errorf("invalid Nanotime resolution: %d", nanotimeResolution)
}
sysCtx.nanotime = nanotime
sysCtx.nanotimeResolution = nanotimeResolution
} else {
sysCtx.nanotime = &nt
sysCtx.nanotimeResolution = sys.ClockResolution(time.Nanosecond)
}
sysCtx.fs = NewFSContext(openedFiles)
return
}
// clockResolutionInvalid returns true if the value stored isn't reasonable.
func clockResolutionInvalid(resolution sys.ClockResolution) bool {
return resolution < 1 || resolution > sys.ClockResolution(time.Hour.Nanoseconds())
}
// nullTerminatedByteCount ensures the count or Nul-terminated length of the elements doesn't exceed max, and that no
// element includes the nul character.
func nullTerminatedByteCount(max uint32, elements []string) (uint32, error) {

289
internal/sys/sys_test.go Normal file
View File

@@ -0,0 +1,289 @@
package sys
import (
"bytes"
"crypto/rand"
"io"
"testing"
"time"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/sys"
)
func TestDefaultSysContext(t *testing.T) {
sysCtx, err := NewContext(
0, // max
nil, // args
nil, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
)
require.NoError(t, err)
require.Nil(t, sysCtx.Args())
require.Zero(t, sysCtx.ArgsSize())
require.Nil(t, sysCtx.Environ())
require.Zero(t, sysCtx.EnvironSize())
require.Equal(t, eofReader{}, sysCtx.Stdin())
require.Equal(t, io.Discard, sysCtx.Stdout())
require.Equal(t, io.Discard, sysCtx.Stderr())
require.Equal(t, &wt, sysCtx.walltime) // To compare functions, we can only compare pointers.
require.Equal(t, sys.ClockResolution(1_000), sysCtx.WalltimeResolution())
require.Equal(t, &nt, sysCtx.nanotime) // To compare functions, we can only compare pointers.
require.Equal(t, sys.ClockResolution(1), sysCtx.NanotimeResolution())
require.Equal(t, rand.Reader, sysCtx.RandSource())
require.Equal(t, NewFSContext(map[uint32]*FileEntry{}), sysCtx.FS())
}
func TestNewContext_Args(t *testing.T) {
tests := []struct {
name string
args []string
maxSize uint32
expectedSize uint32
expectedErr string
}{
{
name: "ok",
maxSize: 10,
args: []string{"a", "bc"},
expectedSize: 5,
},
{
name: "exceeds max count",
maxSize: 1,
args: []string{"a", "bc"},
expectedErr: "args invalid: exceeds maximum count",
},
{
name: "exceeds max size",
maxSize: 4,
args: []string{"a", "bc"},
expectedErr: "args invalid: exceeds maximum size",
},
{
name: "null character",
maxSize: 10,
args: []string{"a", string([]byte{'b', 0})},
expectedErr: "args invalid: contains NUL character",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := NewContext(
tc.maxSize, // max
tc.args,
nil, // environ
bytes.NewReader(make([]byte, 0)), // stdin
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
)
if tc.expectedErr == "" {
require.Nil(t, err)
require.Equal(t, tc.args, sysCtx.Args())
require.Equal(t, tc.expectedSize, sysCtx.ArgsSize())
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
}
func TestNewContext_Environ(t *testing.T) {
tests := []struct {
name string
environ []string
maxSize uint32
expectedSize uint32
expectedErr string
}{
{
name: "ok",
maxSize: 10,
environ: []string{"a=b", "c=de"},
expectedSize: 9,
},
{
name: "exceeds max count",
maxSize: 1,
environ: []string{"a=b", "c=de"},
expectedErr: "environ invalid: exceeds maximum count",
},
{
name: "exceeds max size",
maxSize: 4,
environ: []string{"a=b", "c=de"},
expectedErr: "environ invalid: exceeds maximum size",
},
{
name: "null character",
maxSize: 10,
environ: []string{"a=b", string(append([]byte("c=d"), 0))},
expectedErr: "environ invalid: contains NUL character",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := NewContext(
tc.maxSize, // max
nil, // args
tc.environ,
bytes.NewReader(make([]byte, 0)), // stdin
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
)
if tc.expectedErr == "" {
require.Nil(t, err)
require.Equal(t, tc.environ, sysCtx.Environ())
require.Equal(t, tc.expectedSize, sysCtx.EnvironSize())
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
}
func TestNewContext_Walltime(t *testing.T) {
tests := []struct {
name string
time *sys.Walltime
resolution sys.ClockResolution
expectedErr string
}{
{
name: "ok",
time: &wt,
resolution: 3,
},
{
name: "invalid resolution",
time: &wt,
resolution: 0,
expectedErr: "invalid Walltime resolution: 0",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := NewContext(
0, // max
nil, // args
nil,
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
tc.time, tc.resolution, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // openedFiles
)
if tc.expectedErr == "" {
require.Nil(t, err)
require.Equal(t, tc.time, sysCtx.walltime)
require.Equal(t, tc.resolution, sysCtx.WalltimeResolution())
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
}
func TestNewContext_Nanotime(t *testing.T) {
tests := []struct {
name string
time *sys.Nanotime
resolution sys.ClockResolution
expectedErr string
}{
{
name: "ok",
time: &nt,
resolution: 3,
},
{
name: "invalid resolution",
time: &nt,
resolution: 0,
expectedErr: "invalid Nanotime resolution: 0",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := NewContext(
0, // max
nil, // args
nil,
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // nanotime, nanotimeResolution
tc.time, tc.resolution, // nanotime, nanotimeResolution
nil, // openedFiles
)
if tc.expectedErr == "" {
require.Nil(t, err)
require.Equal(t, tc.time, sysCtx.nanotime)
require.Equal(t, tc.resolution, sysCtx.NanotimeResolution())
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
}
func Test_clockResolutionInvalid(t *testing.T) {
tests := []struct {
name string
resolution sys.ClockResolution
expected bool
}{
{
name: "ok",
resolution: 1,
},
{
name: "zero",
resolution: 0,
expected: true,
},
{
name: "too big",
resolution: sys.ClockResolution(time.Hour.Nanoseconds() * 2),
expected: true,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.expected, clockResolutionInvalid(tc.resolution))
})
}
}

View File

@@ -1,6 +0,0 @@
package sys
// TimeNowUnixNanoKey is a context.Context Value key. Its associated value should be a func() uint64.
//
// See https://github.com/tetratelabs/wazero/issues/491
type TimeNowUnixNanoKey struct{}

View File

@@ -6,13 +6,14 @@ import (
"sync/atomic"
"github.com/tetratelabs/wazero/api"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/sys"
)
// compile time check to ensure CallContext implements api.Module
var _ api.Module = &CallContext{}
func NewCallContext(ns *Namespace, instance *ModuleInstance, Sys *SysContext) *CallContext {
func NewCallContext(ns *Namespace, instance *ModuleInstance, Sys *internalsys.Context) *CallContext {
zero := uint64(0)
return &CallContext{memory: instance.Memory, module: instance, ns: ns, Sys: Sys, closed: &zero}
}
@@ -34,9 +35,16 @@ type CallContext struct {
memory api.Memory
ns *Namespace
// Note: This is a part of CallContext so that scope is correct and Close is coherent.
// Sys is exposed only for WASI
Sys *SysContext
// Sys is exposed for use in special imports such as WASI, assemblyscript
// and wasm_exec.
//
// Notes
//
// * This is a part of CallContext so that scope and Close is coherent.
// * This is not exposed outside this repository (as a host function
// parameter) because we haven't thought through capabilities based
// security implications.
Sys *internalsys.Context
// closed is the pointer used both to guard moduleEngine.CloseWithExitCode and to store the exit code.
//

View File

@@ -142,15 +142,15 @@ func TestCallContext_Close(t *testing.T) {
})
}
t.Run("calls SysContext.Close()", func(t *testing.T) {
sysCtx := DefaultSysContext()
t.Run("calls Context.Close()", func(t *testing.T) {
sysCtx := sys.DefaultContext()
sysCtx.FS().OpenFile(&sys.FileEntry{Path: "."})
m, err := s.Instantiate(context.Background(), ns, &Module{}, t.Name(), sysCtx, nil)
require.NoError(t, err)
// We use side effects to determine if Close in fact called SysContext.Close (without repeating sys_test.go).
// One side effect of SysContext.Close is that it clears the openedFiles map. Verify our base case.
// We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go).
// One side effect of Context.Close is that it clears the openedFiles map. Verify our base case.
fsCtx := sysCtx.FS()
_, ok := fsCtx.OpenedFile(3)
require.True(t, ok, "sysCtx.openedFiles was empty")
@@ -168,7 +168,7 @@ func TestCallContext_Close(t *testing.T) {
t.Run("error closing", func(t *testing.T) {
// Right now, the only way to err closing the sys context is if a File.Close erred.
sysCtx := DefaultSysContext()
sysCtx := sys.DefaultContext()
sysCtx.FS().OpenFile(&sys.FileEntry{Path: ".", File: &testFile{errors.New("error closing")}})
m, err := s.Instantiate(context.Background(), ns, &Module{}, t.Name(), sysCtx, nil)

View File

@@ -162,7 +162,7 @@ func TestNamespace_CloseWithExitCode(t *testing.T) {
}
t.Run("error closing", func(t *testing.T) {
sysCtx := DefaultSysContext()
sysCtx := sys.DefaultContext()
sysCtx.FS().OpenFile(&sys.FileEntry{Path: ".", File: &testFile{errors.New("error closing")}})
ns, m1, m2 := newTestNamespace()

View File

@@ -13,6 +13,7 @@ import (
experimentalapi "github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/ieee754"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/sys"
)
type (
@@ -344,7 +345,7 @@ func (s *Store) Instantiate(
ns *Namespace,
module *Module,
name string,
sys *SysContext,
sys *sys.Context,
functionListenerFactory experimentalapi.FunctionListenerFactory,
) (*CallContext, error) {
if ctx == nil {
@@ -385,7 +386,7 @@ func (s *Store) instantiate(
ns *Namespace,
module *Module,
name string,
sys *SysContext,
sys *sys.Context,
functionListenerFactory experimentalapi.FunctionListenerFactory,
modules map[string]*ModuleInstance,
) (*CallContext, error) {

View File

@@ -10,6 +10,7 @@ import (
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/testing/hammer"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/u64"
@@ -99,8 +100,8 @@ func TestStore_Instantiate(t *testing.T) {
)
require.NoError(t, err)
sys := DefaultSysContext()
mod, err := s.Instantiate(testCtx, ns, m, "", sys, nil)
sysCtx := sys.DefaultContext()
mod, err := s.Instantiate(testCtx, ns, m, "", sysCtx, nil)
require.NoError(t, err)
defer mod.Close(testCtx)
@@ -108,7 +109,7 @@ func TestStore_Instantiate(t *testing.T) {
require.Equal(t, ns.modules[""], mod.module)
require.Equal(t, ns.modules[""].Memory, mod.memory)
require.Equal(t, ns, mod.ns)
require.Equal(t, sys, mod.Sys)
require.Equal(t, sysCtx, mod.Sys)
})
}
@@ -210,7 +211,7 @@ func TestStore_hammer(t *testing.T) {
N = 100
}
hammer.NewHammer(t, P, N).Run(func(name string) {
mod, instantiateErr := s.Instantiate(testCtx, ns, importingModule, name, DefaultSysContext(), nil)
mod, instantiateErr := s.Instantiate(testCtx, ns, importingModule, name, sys.DefaultContext(), nil)
require.NoError(t, instantiateErr)
require.NoError(t, mod.Close(testCtx))
}, nil)

View File

@@ -1,150 +0,0 @@
package wasm
import (
"bytes"
"io"
"testing"
"github.com/tetratelabs/wazero/internal/testing/require"
)
func TestDefaultSysContext(t *testing.T) {
sys, err := NewSysContext(
0, // max
nil, // args
nil, // environ
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
nil, // openedFiles
)
require.NoError(t, err)
require.Nil(t, sys.Args())
require.Zero(t, sys.ArgsSize())
require.Nil(t, sys.Environ())
require.Zero(t, sys.EnvironSize())
require.Equal(t, eofReader{}, sys.Stdin())
require.Equal(t, io.Discard, sys.Stdout())
require.Equal(t, io.Discard, sys.Stderr())
require.Equal(t, sys, DefaultSysContext())
}
func TestNewSysContext_Args(t *testing.T) {
tests := []struct {
name string
args []string
maxSize uint32
expectedSize uint32
expectedErr string
}{
{
name: "ok",
maxSize: 10,
args: []string{"a", "bc"},
expectedSize: 5,
},
{
name: "exceeds max count",
maxSize: 1,
args: []string{"a", "bc"},
expectedErr: "args invalid: exceeds maximum count",
},
{
name: "exceeds max size",
maxSize: 4,
args: []string{"a", "bc"},
expectedErr: "args invalid: exceeds maximum size",
},
{
name: "null character",
maxSize: 10,
args: []string{"a", string([]byte{'b', 0})},
expectedErr: "args invalid: contains NUL character",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sys, err := NewSysContext(
tc.maxSize, // max
tc.args,
nil, // environ
bytes.NewReader(make([]byte, 0)), // stdin
nil, // stdout
nil, // stderr
nil, // randSource
nil, // openedFiles
)
if tc.expectedErr == "" {
require.Nil(t, err)
require.Equal(t, tc.args, sys.Args())
require.Equal(t, tc.expectedSize, sys.ArgsSize())
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
}
func TestNewSysContext_Environ(t *testing.T) {
tests := []struct {
name string
environ []string
maxSize uint32
expectedSize uint32
expectedErr string
}{
{
name: "ok",
maxSize: 10,
environ: []string{"a=b", "c=de"},
expectedSize: 9,
},
{
name: "exceeds max count",
maxSize: 1,
environ: []string{"a=b", "c=de"},
expectedErr: "environ invalid: exceeds maximum count",
},
{
name: "exceeds max size",
maxSize: 4,
environ: []string{"a=b", "c=de"},
expectedErr: "environ invalid: exceeds maximum size",
},
{
name: "null character",
maxSize: 10,
environ: []string{"a=b", string(append([]byte("c=d"), 0))},
expectedErr: "environ invalid: contains NUL character",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sys, err := NewSysContext(
tc.maxSize, // max
nil, // args
tc.environ,
bytes.NewReader(make([]byte, 0)), // stdin
nil, // stdout
nil, // stderr
nil, // randSource
nil, // openedFiles
)
if tc.expectedErr == "" {
require.Nil(t, err)
require.Equal(t, tc.environ, sys.Environ())
require.Equal(t, tc.expectedSize, sys.EnvironSize())
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/tetratelabs/wazero/api"
experimentalapi "github.com/tetratelabs/wazero/experimental"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/sys"
)
@@ -76,7 +77,7 @@ func (ns *namespace) InstantiateModule(
panic(fmt.Errorf("unsupported wazero.ModuleConfig implementation: %#v", mConfig))
}
var sysCtx *wasm.SysContext
var sysCtx *internalsys.Context
if sysCtx, err = config.toSysContext(); err != nil {
return
}

21
sys/clock.go Normal file
View File

@@ -0,0 +1,21 @@
package sys
import "context"
// ClockResolution is a positive granularity of clock precision in
// nanoseconds. For example, if the resolution is 1us, this returns 1000.
//
// Note: Some implementations return arbitrary resolution because there's
// no perfect alternative. For example, according to the source in time.go,
// windows monotonic resolution can be 15ms. See /RATIONALE.md.
type ClockResolution uint32
// Walltime returns the current time in epoch seconds with a nanosecond fraction.
type Walltime func(context.Context) (sec int64, nsec int32)
// Nanotime returns nanoseconds since an arbitrary start point, used to measure
// elapsed time. This is sometimes referred to as a tick or monotonic time.
//
// Note: There are no constraints on the value return except that it
// increments. For example, -1 is a valid if the next value is >= 0.
type Nanotime func(context.Context) int64

View File

@@ -559,8 +559,8 @@ func wasiFunctions() map[string]interface{} {
// 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, m api.Module, argv, argvBuf uint32) Errno {
sys := sysCtx(m)
return writeOffsetsAndNullTerminatedValues(ctx, m.Memory(), sys.Args(), argv, argvBuf)
sysCtx := getSysCtx(m)
return writeOffsetsAndNullTerminatedValues(ctx, m.Memory(), sysCtx.Args(), argv, argvBuf)
}
// ArgsSizesGet is the WASI function named functionArgsSizesGet that reads command-line argument data (WithArgs)
@@ -590,13 +590,13 @@ func (a *wasi) ArgsGet(ctx context.Context, m api.Module, argv, argvBuf uint32)
// 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, m api.Module, resultArgc, resultArgvBufSize uint32) Errno {
sys := sysCtx(m)
sysCtx := getSysCtx(m)
mem := m.Memory()
if !mem.WriteUint32Le(ctx, resultArgc, uint32(len(sys.Args()))) {
if !mem.WriteUint32Le(ctx, resultArgc, uint32(len(sysCtx.Args()))) {
return ErrnoFault
}
if !mem.WriteUint32Le(ctx, resultArgvBufSize, sys.ArgsSize()) {
if !mem.WriteUint32Le(ctx, resultArgvBufSize, sysCtx.ArgsSize()) {
return ErrnoFault
}
return ErrnoSuccess
@@ -629,8 +629,8 @@ func (a *wasi) ArgsSizesGet(ctx context.Context, m api.Module, resultArgc, resul
// 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, m api.Module, environ uint32, environBuf uint32) Errno {
sys := sysCtx(m)
return writeOffsetsAndNullTerminatedValues(ctx, m.Memory(), sys.Environ(), environ, environBuf)
env := getSysCtx(m).Environ()
return writeOffsetsAndNullTerminatedValues(ctx, m.Memory(), env, environ, environBuf)
}
// EnvironSizesGet is the WASI function named functionEnvironSizesGet that reads environment variable
@@ -661,13 +661,13 @@ func (a *wasi) EnvironGet(ctx context.Context, m api.Module, environ uint32, env
// 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, m api.Module, resultEnvironc uint32, resultEnvironBufSize uint32) Errno {
sys := sysCtx(m)
sysCtx := getSysCtx(m)
mem := m.Memory()
if !mem.WriteUint32Le(ctx, resultEnvironc, uint32(len(sys.Environ()))) {
if !mem.WriteUint32Le(ctx, resultEnvironc, uint32(len(sysCtx.Environ()))) {
return ErrnoFault
}
if !mem.WriteUint32Le(ctx, resultEnvironBufSize, sys.EnvironSize()) {
if !mem.WriteUint32Le(ctx, resultEnvironBufSize, sysCtx.EnvironSize()) {
return ErrnoFault
}
@@ -693,22 +693,19 @@ func (a *wasi) EnvironSizesGet(ctx context.Context, m api.Module, resultEnvironc
// 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, m api.Module, id uint32, resultResolution uint32) Errno {
// We choose arbitrary resolutions here because there's no perfect alternative. For example, according to the
// source in time.go, windows monotonic resolution can be 15ms. This chooses arbitrarily 1us for wall time and
// 1ns for monotonic. See RATIONALE.md for more context.
sysCtx := getSysCtx(m)
var resolution uint64 // ns
switch id {
case clockIDRealtime:
resolution = 1000 // 1us
resolution = uint64(sysCtx.WalltimeResolution())
case clockIDMonotonic:
resolution = 1 // 1ns
resolution = uint64(sysCtx.NanotimeResolution())
default:
// 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 ErrnoNosys
}
// fixed for GrainLang per #271 and Swift per https://github.com/tetratelabs/wazero/issues/526#issuecomment-1134034760
if !m.Memory().WriteUint64Le(ctx, resultResolution, resolution) {
return ErrnoFault
}
@@ -737,21 +734,15 @@ func (a *wasi) ClockResGet(ctx context.Context, m api.Module, id uint32, resultR
// See https://linux.die.net/man/3/clock_gettime
func (a *wasi) ClockTimeGet(ctx context.Context, m api.Module, id uint32, precision uint64, resultTimestamp uint32) Errno {
// TODO: precision is currently ignored.
sysCtx := getSysCtx(m)
var val uint64
switch id {
case clockIDRealtime:
clock := timeNowUnixNano
// Override Context when it is passed via context
if clockVal := ctx.Value(sys.TimeNowUnixNanoKey{}); clockVal != nil {
clockCtx, ok := clockVal.(func() uint64)
if !ok {
panic(fmt.Errorf("unsupported clock key: %v", clockVal))
}
clock = clockCtx
}
val = clock()
sec, nsec := sysCtx.Walltime(ctx)
val = (uint64(sec) * uint64(time.Second.Nanoseconds())) + uint64(nsec)
case clockIDMonotonic:
val = uint64(time.Since(monotonicClockBase))
val = uint64(sysCtx.Nanotime(ctx))
default:
// 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.
@@ -1011,13 +1002,13 @@ func (a *wasi) FdPwrite(ctx context.Context, m api.Module, fd, iovs, iovsCount u
// 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, m api.Module, fd, iovs, iovsCount, resultSize uint32) Errno {
sys, fsc := sysFSCtx(ctx, m)
sysCtx, fsCtx := sysFSCtx(ctx, m)
var reader io.Reader
if fd == fdStdin {
reader = sys.Stdin()
} else if f, ok := fsc.OpenedFile(fd); !ok || f.File == nil {
reader = sysCtx.Stdin()
} else if f, ok := fsCtx.OpenedFile(fd); !ok || f.File == nil {
return ErrnoBadf
} else {
reader = f.File
@@ -1180,18 +1171,18 @@ func (a *wasi) FdTell(ctx context.Context, m api.Module, fd, resultOffset uint32
// 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, m api.Module, fd, iovs, iovsCount, resultSize uint32) Errno {
sys, fsc := sysFSCtx(ctx, m)
sysCtx, fsCtx := sysFSCtx(ctx, m)
var writer io.Writer
switch fd {
case fdStdout:
writer = sys.Stdout()
writer = sysCtx.Stdout()
case fdStderr:
writer = sys.Stderr()
writer = sysCtx.Stderr()
default:
// Check to see if the file descriptor is available
if f, ok := fsc.OpenedFile(fd); !ok || f.File == nil {
if f, ok := fsCtx.OpenedFile(fd); !ok || f.File == nil {
return ErrnoBadf
// fs.FS doesn't declare io.Writer, but implementations such as os.File implement it.
} else if writer, ok = f.File.(io.Writer); !ok {
@@ -1393,8 +1384,8 @@ func (a *wasi) SchedYield(m api.Module) Errno {
// 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, m api.Module, buf uint32, bufLen uint32) (errno Errno) {
randomBytes := make([]byte, bufLen)
sys := sysCtx(m)
n, err := sys.RandSource().Read(randomBytes)
sysCtx := getSysCtx(m)
n, err := sysCtx.RandSource().Read(randomBytes)
if n != int(bufLen) || err != nil {
// TODO: handle different errors that syscal to entropy source can return
return ErrnoIo
@@ -1434,14 +1425,7 @@ const (
clockIDMonotonic = 1
)
// monotonicClockBase uses time.Now to ensure a monotonic clock reading on all platforms via time.Since.
var monotonicClockBase = time.Now()
func timeNowUnixNano() uint64 {
return uint64(time.Now().UnixNano())
}
func sysCtx(m api.Module) *wasm.SysContext {
func getSysCtx(m api.Module) *sys.Context {
if internal, ok := m.(*wasm.CallContext); !ok {
panic(fmt.Errorf("unsupported wasm.Module implementation: %v", m))
} else {
@@ -1449,7 +1433,7 @@ func sysCtx(m api.Module) *wasm.SysContext {
}
}
func sysFSCtx(ctx context.Context, m api.Module) (*wasm.SysContext, *sys.FSContext) {
func sysFSCtx(ctx context.Context, m api.Module) (*sys.Context, *sys.FSContext) {
if internal, ok := m.(*wasm.CallContext); !ok {
panic(fmt.Errorf("unsupported wasm.Module implementation: %v", m))
} else {

View File

@@ -3,6 +3,7 @@ package wasi_snapshot_preview1
import (
"testing"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -57,7 +58,7 @@ func Benchmark_EnvironGet(b *testing.B) {
})
}
func newModule(buf []byte, sys *wasm.SysContext) *wasm.CallContext {
func newModule(buf []byte, sys *sys.Context) *wasm.CallContext {
return wasm.NewCallContext(nil, &wasm.ModuleInstance{
Memory: &wasm.MemoryInstance{Min: 1, Buffer: buf},
}, sys)

View File

@@ -15,11 +15,11 @@ import (
"testing"
"testing/fstest"
"testing/iotest"
"time"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/platform"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
@@ -27,13 +27,14 @@ import (
"github.com/tetratelabs/wazero/sys"
)
const (
epochNanos = uint64(1640995200000000000) // midnight UTC 2022-01-01
seed = int64(42) // fixed seed value
)
const seed = int64(42) // fixed seed value
// testCtx ensures the fake clock is used for WASI functions.
var testCtx = experimental.WithTimeNowUnixNano(context.Background(), func() uint64 { return epochNanos })
var deterministicRandomSource = func() io.Reader {
return rand.New(rand.NewSource(seed))
}
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
var a = &wasi{}
@@ -600,18 +601,9 @@ func TestSnapshotPreview1_ClockTimeGet_Monotonic(t *testing.T) {
errno := tc.invocation()
require.Zero(t, errno, ErrnoName(errno))
start, ok := mod.Memory().ReadUint64Le(testCtx, resultTimestamp)
tick, ok := mod.Memory().ReadUint64Le(testCtx, resultTimestamp)
require.True(t, ok)
time.Sleep(1 * time.Millisecond)
errno = tc.invocation()
require.Zero(t, errno, ErrnoName(errno))
end, ok := mod.Memory().ReadUint64Le(testCtx, resultTimestamp)
require.True(t, ok)
// Time is monotonic
require.True(t, end > start)
require.Equal(t, uint64(platform.FakeEpochNanos), tick)
})
}
}
@@ -2105,11 +2097,7 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) {
offset := uint32(1) // offset,
t.Run("wasi.RandomGet", func(t *testing.T) {
source := rand.New(rand.NewSource(seed))
sysCtx, err := wasm.NewSysContext(math.MaxUint32, nil, nil, new(bytes.Buffer), nil, nil, source, nil)
require.NoError(t, err)
mod, _ := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, sysCtx)
mod, _ := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, nil)
defer mod.Close(testCtx)
maskMemory(t, testCtx, mod, len(expectedMemory))
@@ -2124,11 +2112,7 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) {
})
t.Run(functionRandomGet, func(t *testing.T) {
source := rand.New(rand.NewSource(seed))
sysCtx, err := wasm.NewSysContext(math.MaxUint32, nil, nil, new(bytes.Buffer), nil, nil, source, nil)
require.NoError(t, err)
mod, fn := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, sysCtx)
mod, fn := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, nil)
defer mod.Close(testCtx)
maskMemory(t, testCtx, mod, len(expectedMemory))
@@ -2198,17 +2182,24 @@ func TestSnapshotPreview1_RandomGet_SourceError(t *testing.T) {
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
var errCtx = experimental.WithTimeNowUnixNano(context.Background(), func() uint64 {
panic(errors.New("TimeNowUnixNano error"))
})
sysCtx, err := wasm.NewSysContext(math.MaxUint32, nil, nil, new(bytes.Buffer), nil, nil, tc.randSource, nil)
sysCtx, err := internalsys.NewContext(
math.MaxUint32,
nil,
nil,
new(bytes.Buffer),
nil,
nil,
tc.randSource,
nil, 0,
nil, 0,
nil,
)
require.NoError(t, err)
mod, _ := instantiateModule(errCtx, t, functionRandomGet, importRandomGet, sysCtx)
defer mod.Close(errCtx)
mod, _ := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, sysCtx)
defer mod.Close(testCtx)
errno := a.RandomGet(errCtx, mod, uint32(1), uint32(5)) // arbitrary offset and length
errno := a.RandomGet(testCtx, mod, uint32(1), uint32(5)) // arbitrary offset and length
require.Equal(t, ErrnoIo, errno, ErrnoName(errno))
})
}
@@ -2277,7 +2268,7 @@ 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 *wasm.SysContext) (api.Module, api.Function) {
func instantiateModule(ctx context.Context, t *testing.T, wasiFunction, wasiImport string, sysCtx *internalsys.Context) (api.Module, api.Function) {
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
_, err := Instantiate(testCtx, r)
@@ -2288,28 +2279,41 @@ func instantiateModule(ctx context.Context, t *testing.T, wasifunction, wasiimpo
(memory 1 1) ;; just an arbitrary size big enough for tests
(export "memory" (memory 0))
(export "%[1]s" (func $wasi.%[1]s))
)`, wasifunction, wasiimport))
)`, wasiFunction, wasiImport))
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()))
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)
fn := mod.ExportedFunction(wasiFunction)
require.NotNil(t, fn)
return mod, fn
}
func newSysContext(args, environ []string, openedFiles map[uint32]*internalsys.FileEntry) (sysCtx *wasm.SysContext, err error) {
return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, nil, openedFiles)
func newSysContext(args, environ []string, openedFiles map[uint32]*internalsys.FileEntry) (sysCtx *internalsys.Context, err error) {
return internalsys.NewContext(
math.MaxUint32,
args,
environ,
new(bytes.Buffer),
nil,
nil,
deterministicRandomSource(),
nil, 0,
nil, 0,
openedFiles,
)
}
func createFile(t *testing.T, pathName string, data []byte) (fs.File, fs.FS) {