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:
4
Makefile
4
Makefile
@@ -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:
|
||||
|
||||
51
RATIONALE.md
51
RATIONALE.md
@@ -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
|
||||
|
||||
@@ -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)
|
||||
...
|
||||
```
|
||||
|
||||
@@ -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
102
config.go
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
220
config_test.go
220
config_test.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if !platform.IsSupported() {
|
||||
if !platform.CompilerSupported() {
|
||||
os.Exit(0)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
9
internal/integration_test/vs/time/go.mod
Normal file
9
internal/integration_test/vs/time/go.mod
Normal 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 => ../../../..
|
||||
2
internal/integration_test/vs/time/go.sum
Normal file
2
internal/integration_test/vs/time/go.sum
Normal 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=
|
||||
58
internal/integration_test/vs/time/time_test.go
Normal file
58
internal/integration_test/vs/time/time_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
48
internal/platform/time.go
Normal 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()
|
||||
}
|
||||
11
internal/platform/time_cgo.go
Normal file
11
internal/platform/time_cgo.go
Normal 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
|
||||
7
internal/platform/time_notcgo.go
Normal file
7
internal/platform/time_notcgo.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !cgo
|
||||
|
||||
package platform
|
||||
|
||||
func nanotime() int64 {
|
||||
return nanotimePortable()
|
||||
}
|
||||
47
internal/platform/time_test.go
Normal file
47
internal/platform/time_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
289
internal/sys/sys_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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{}
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
21
sys/clock.go
Normal 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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user