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

wasi: Implements wasi_snapshot_preview1.poll_oneoff for relative clock events #629

Merged
merged 4 commits into from
Jun 17, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 27 additions & 3 deletions RATIONALE.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,23 +417,23 @@ is to satisfy WASI:

See https://github.com/WebAssembly/wasi-clocks

## Why default to fake time?
### 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 does fake time increase on reading?
### Why does fake time increase on reading?

Both the fake nanotime and walltime increase by 1ms on reading. Particularly in
the case of nanotime, this prevents spinning. For example, when Go compiles
`time.Sleep` using `GOOS=js GOARCH=wasm`, nanotime is used in a loop. If that
never increases, the gouroutine is mistaken for being busy. This would be worse
if a compiler implement sleep using nanotime, yet doesn't check for spinning!

## Why not `time.Clock`?
### 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
Expand Down Expand Up @@ -473,6 +473,30 @@ to a POSIX method.
Writing assembly would allow making syscalls without CGO, but comes with the cost that it will require implementations
across many combinations of OS and architecture.

## sys.Nanosleep

All major programming languages have a `sleep` mechanism to block for a
duration. Sleep is typically implemented by a WASI `poll_oneoff` relative clock
subscription.

For example, the below ends up calling `wasi_snapshot_preview1.poll_oneoff`:

```zig
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it

const std = @import("std");
pub fn main() !void {
std.time.sleep(std.time.ns_per_s * 5);
}
```

Besides Zig, this is also the case with TinyGo (`-target=wasi`) and Rust
(`--target wasm32-wasi`). This isn't the case with Go (`GOOS=js GOARCH=wasm`),
though. In the latter case, wasm loops on `sys.Nanotime`.

We decided to expose `sys.Nanosleep` to allow overriding the implementation
used in the common case, even if it isn't used by Go, because this gives an
easy and efficient closure over a common program function. We also documented
`sys.Nanotime` to warn users that some compilers don't optimize sleep.

## Signed encoding of integer global constant initializers
wazero treats integer global constant initializers signed as their interpretation is not known at declaration time. For
example, there is no signed integer [value type](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#value-types%E2%91%A0).
Expand Down
46 changes: 44 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,15 +487,43 @@ type ModuleConfig interface {
// 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.
// Notes:
// * This does not default to time.Since as that violates sandboxing.
// * Some compilers implement sleep by looping on sys.Nanotime (ex. Go).
// * If you set this, you should probably set WithNanosleep also.
// * 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

// WithNanosleep configures the how to pause the current goroutine for at
// least the configured nanoseconds. Defaults to return immediately.
//
// Ex. To override with your own sleep function:
// moduleConfig = moduleConfig.
// WithNanosleep(func(ctx context.Context, ns int64) {
// rel := unix.NsecToTimespec(ns)
// remain := unix.Timespec{}
// for { // loop until no more time remaining
// err := unix.ClockNanosleep(unix.CLOCK_MONOTONIC, 0, &rel, &remain)
// --snip--
//
// Notes:
// * This primarily supports `poll_oneoff` for relative clock events.
// * This does not default to time.Sleep as that violates sandboxing.
// * Some compilers implement sleep by looping on sys.Nanotime (ex. Go).
// * If you set this, you should probably set WithNanotime also.
// * Use WithSysNanosleep for a usable implementation.
WithNanosleep(sys.Nanosleep) ModuleConfig

// WithSysNanosleep uses time.Sleep for sys.Nanosleep.
//
// See WithNanosleep
WithSysNanosleep() 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 Down Expand Up @@ -530,6 +558,7 @@ type moduleConfig struct {
walltimeResolution sys.ClockResolution
nanotime *sys.Nanotime
nanotimeResolution sys.ClockResolution
nanosleep *sys.Nanosleep
args []string
// environ is pair-indexed to retain order similar to os.Environ.
environ []string
Expand Down Expand Up @@ -650,6 +679,18 @@ func (c *moduleConfig) WithSysNanotime() ModuleConfig {
return c.WithNanotime(platform.Nanotime, sys.ClockResolution(1))
}

// WithNanosleep implements ModuleConfig.WithNanosleep
func (c *moduleConfig) WithNanosleep(nanosleep sys.Nanosleep) ModuleConfig {
ret := *c // copy
ret.nanosleep = &nanosleep
return &ret
}

// WithSysNanosleep implements ModuleConfig.WithSysNanosleep
func (c *moduleConfig) WithSysNanosleep() ModuleConfig {
return c.WithNanosleep(platform.Nanosleep)
}

// WithRandSource implements ModuleConfig.WithRandSource
func (c *moduleConfig) WithRandSource(source io.Reader) ModuleConfig {
ret := c.clone()
Expand Down Expand Up @@ -698,6 +739,7 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
c.randSource,
c.walltime, c.walltimeResolution,
c.nanotime, c.nanotimeResolution,
c.nanosleep,
preopens,
)
}
36 changes: 28 additions & 8 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // openedFiles
),
},
Expand All @@ -341,7 +342,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution

nil, // nanosleep
nil, // openedFiles
),
},
Expand All @@ -358,7 +359,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution

nil, // nanosleep
nil, // openedFiles
),
},
Expand All @@ -375,7 +376,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution

nil, // nanosleep
nil, // openedFiles
),
},
Expand All @@ -392,7 +393,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution

nil, // nanosleep
nil, // openedFiles
),
},
Expand All @@ -409,6 +410,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // openedFiles
),
},
Expand All @@ -425,7 +427,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution

nil, // nanosleep
nil, // openedFiles
),
},
Expand All @@ -442,7 +444,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution

nil, // nanosleep
nil, // openedFiles
),
},
Expand All @@ -459,7 +461,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution

nil, // nanosleep
nil, // openedFiles
),
},
Expand All @@ -476,7 +478,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution

nil, // nanosleep
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS},
4: {Path: ".", FS: testFS},
Expand All @@ -496,6 +498,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS2},
4: {Path: ".", FS: testFS2},
Expand All @@ -515,6 +518,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: ".", FS: testFS},
},
Expand All @@ -533,6 +537,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS},
4: {Path: ".", FS: testFS2},
Expand All @@ -552,6 +557,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: ".", FS: testFS},
4: {Path: "/", FS: testFS2},
Expand Down Expand Up @@ -715,6 +721,18 @@ func TestModuleConfig_toSysContext_WithNanotime(t *testing.T) {
})
}

// TestModuleConfig_toSysContext_WithNanosleep has to test differently because
// we can't compare function pointers when functions are passed by value.
func TestModuleConfig_toSysContext_WithNanosleep(t *testing.T) {
sysCtx, err := NewModuleConfig().
WithNanosleep(func(ctx context.Context, ns int64) {
require.Equal(t, testCtx, ctx)
}).(*moduleConfig).toSysContext()
require.NoError(t, err)
// If below pass, the context was correct!
sysCtx.Nanosleep(testCtx, 2)
}

func TestModuleConfig_toSysContext_Errors(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -820,6 +838,7 @@ func requireSysContext(
randSource io.Reader,
walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
nanosleep *sys.Nanosleep,
openedFiles map[uint32]*internalsys.FileEntry,
) *internalsys.Context {
sysCtx, err := internalsys.NewContext(
Expand All @@ -832,6 +851,7 @@ func requireSysContext(
randSource,
walltime, walltimeResolution,
nanotime, nanotimeResolution,
nanosleep,
openedFiles,
)
require.NoError(t, err)
Expand Down
9 changes: 9 additions & 0 deletions internal/platform/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func NewFakeNanotime() *sys.Nanotime {
return &nt
}

// FakeNanosleep implements sys.Nanosleep by returning without sleeping.
func FakeNanosleep(context.Context, int64) {
}

// Walltime implements sys.Walltime with time.Now.
//
// Note: This is only notably less efficient than it could be is reading
Expand Down Expand Up @@ -66,3 +70,8 @@ func nanotimePortable() int64 {
func Nanotime(context.Context) int64 {
return nanotime()
}

// Nanosleep implements sys.Nanosleep with time.Sleep.
func Nanosleep(_ context.Context, ns int64) {
time.Sleep(time.Duration(ns))
}
10 changes: 10 additions & 0 deletions internal/platform/time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,13 @@ func Test_Nanotime(t *testing.T) {
})
}
}

func Test_Nanosleep(t *testing.T) {
ns := int64(50 * time.Millisecond)
start := Nanotime(context.Background())
Nanosleep(context.Background(), ns)

duration := Nanotime(context.Background()) - start
max := ns * 2 // max scheduling delay
require.True(t, duration > 0 && duration < max, "Nanosleep(%d) slept for %d", ns, duration)
}
26 changes: 24 additions & 2 deletions internal/sys/sys.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Context struct {
walltimeResolution sys.ClockResolution
nanotime *sys.Nanotime
nanotimeResolution sys.ClockResolution
nanosleep *sys.Nanosleep
randSource io.Reader

fs *FSContext
Expand Down Expand Up @@ -103,8 +104,21 @@ func (c *Context) NanotimeResolution() sys.ClockResolution {
return c.nanotimeResolution
}

// Nanosleep implements sys.Nanosleep.
func (c *Context) Nanosleep(ctx context.Context, ns int64) {
(*(c.nanosleep))(ctx, ns)
}

// FS returns the file system context.
func (c *Context) FS() *FSContext {
func (c *Context) FS(ctx context.Context) *FSContext {
// Override Context when it is passed via context
if fsValue := ctx.Value(FSKey{}); fsValue != nil {
fsCtx, ok := fsValue.(*FSContext)
if !ok {
panic(fmt.Errorf("unsupported fs key: %v", fsValue))
}
return fsCtx
}
return c.fs
}

Expand All @@ -128,14 +142,15 @@ func (eofReader) Read([]byte) (int, error) {
// 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 DefaultContext() *Context {
if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil); err != nil {
if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, nil); err != nil {
panic(fmt.Errorf("BUG: DefaultContext should never error: %w", err))
} else {
return sysCtx
}
}

var _ = DefaultContext() // Force panic on bug.
var ns sys.Nanosleep = platform.FakeNanosleep

// 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.
Expand All @@ -147,6 +162,7 @@ func NewContext(
randSource io.Reader,
walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
nanosleep *sys.Nanosleep,
openedFiles map[uint32]*FileEntry,
) (sysCtx *Context, err error) {
sysCtx = &Context{args: args, environ: environ}
Expand Down Expand Up @@ -205,6 +221,12 @@ func NewContext(
sysCtx.nanotimeResolution = sys.ClockResolution(time.Nanosecond)
}

if nanosleep != nil {
sysCtx.nanosleep = nanosleep
} else {
sysCtx.nanosleep = &ns
}

sysCtx.fs = NewFSContext(openedFiles)

return
Expand Down
Loading