Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 10 commits into from
Jun 4, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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/clock vs/wasmedge vs/wasmer vs/wasmtime ; do \
cd ./internal/integration_test/$$d ; \
go test -bench=. . -tags='wasmedge' $(ensureCompilerFastest) ; \
cd - ;\
Expand Down Expand Up @@ -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:
Expand Down
51 changes: 50 additions & 1 deletion RATIONALE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
96 changes: 84 additions & 12 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -442,6 +445,31 @@ 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.
//
// 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.
//
// 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
Expand All @@ -466,19 +494,22 @@ type ModuleConfig interface {
}

type moduleConfig struct {
name string
startFunctions []string
stdin io.Reader
stdout io.Writer
stderr io.Writer
randSource io.Reader
args []string
name string
startFunctions []string
stdin io.Reader
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.
Expand All @@ -487,7 +518,7 @@ func NewModuleConfig() ModuleConfig {
startFunctions: []string{"_start"},
environKeys: map[string]int{},

fs: sys.NewFSConfig(),
fs: internalsys.NewFSConfig(),
}
}

Expand Down Expand Up @@ -553,6 +584,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
Expand Down Expand Up @@ -591,5 +652,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 wasm.NewSysContext(
math.MaxUint32,
c.args,
environ,
c.stdin,
c.stdout,
c.stderr,
c.randSource,
c.walltimeTime, c.walltimeResolution,
c.nanotimeTime, c.nanotimeResolution,
preopens,
)
}
Loading