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: Implement random_get function #241

Merged
merged 12 commits into from
Feb 15, 2022
24 changes: 24 additions & 0 deletions wasi/testdata/random.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
;; This is a wat file to just export clock WASI API to the host environment for testing the APIs.
r8d8 marked this conversation as resolved.
Show resolved Hide resolved
;; This is currently separated as a wat file and pre-compiled because our text parser doesn't
;; implement 'memory' yet. After it supports 'memory', we can remove this file and embed this
;; wat file in the Go test code.
;;
;; Note: Although this is a raw wat file which should be moved under /tests/wasi in principle,
;; this file is put here for now, because this is a temporary file until the parser supports
;; the enough syntax, and this file will be embedded in unit test codes after that.
(module
(import "wasi_snapshot_preview1" "random_get"
(func $wasi.random_get (param $buf i32) (param $buf_len i32) (result (;errno;) i32)))
(memory 1) ;; just an arbitrary size big enough for tests
(export "memory" (memory 0))
;; Define wrapper functions instead of just exporting the imported WASI APIS for now
;; because wazero's interpreter has a bug that it crashes when an imported-and-exported host function
;; is called from the host environment, which will be fixed soon.
;; After it's fixed, these wrapper functions are no longer necessary.
(func $random_get (param i32 i32) (result i32)
local.get 0
local.get 1
call $wasi.random_get
)
(export "random_get" (func $random_get))
)
55 changes: 52 additions & 3 deletions wasi/wasi.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package wasi

import (
crand "crypto/rand"
"encoding/binary"
"errors"
"io"
Expand Down Expand Up @@ -143,7 +144,24 @@ type API interface {
// TODO: ProcExit
// TODO: ProcRaise
// TODO: SchedYield
// TODO: RandomGet

// RandomGet is a WASI function that write random data in buffer (rand.Read()).
//
// * buf - is a offset to write random values
// * bufLen - size of random data in bytes
//
// For example, if `HostFunctionCallContext.Randomizer` initialized
// with random seed `rand.NewSource(42)`, we expect `ctx.Memory.Buffer` to contain:
//
// bufLen (5)
// +--------------------------+
// | |
// []byte{?, 0x53, 0x8c, 0x7f, 0x96, 0xb1, ?}
// buf --^
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-bufLen-size---errno
RandomGet(ctx *wasm.HostFunctionCallContext, buf, bufLen uint32) Errno

// TODO: SockRecv
// TODO: SockSend
// TODO: SockShutdown
Expand All @@ -154,6 +172,17 @@ const (
wasiSnapshotPreview1Name = "wasi_snapshot_preview1"
)

type RandomSource interface {
r8d8 marked this conversation as resolved.
Show resolved Hide resolved
Read([]byte) (int, error)
}

// Non-deterministic random source using crypto/rand
type CryptoRandomSource struct{}
r8d8 marked this conversation as resolved.
Show resolved Hide resolved

func (c *CryptoRandomSource) Read(p []byte) (n int, err error) {
return crand.Read(p)
}

type api struct {
args *nullTerminatedStrings
stdin io.Reader
Expand All @@ -162,6 +191,7 @@ type api struct {
opened map[uint32]fileEntry
// timeNowUnixNano is mutable for testing
timeNowUnixNano func() uint64
randSource RandomSource
}

func (a *api) register(store *wasm.Store) (err error) {
Expand Down Expand Up @@ -212,7 +242,7 @@ func (a *api) register(store *wasm.Store) (err error) {
{FunctionProcExit, proc_exit},
// TODO: FunctionProcRaise
// TODO: FunctionSchedYield
// TODO: FunctionRandomGet
{FunctionRandomGet, a.RandomGet},
// TODO: FunctionSockRecv
// TODO: FunctionSockSend
// TODO: FunctionSockShutdown
Expand Down Expand Up @@ -345,6 +375,7 @@ func newAPI(opts ...Option) *api {
timeNowUnixNano: func() uint64 {
return uint64(time.Now().UnixNano())
},
randSource: &CryptoRandomSource{},
}

// apply functional options
Expand All @@ -355,7 +386,8 @@ func newAPI(opts ...Option) *api {
}

func (a *api) randUnusedFD() uint32 {
fd := uint32(rand.Int31())
r8d8 marked this conversation as resolved.
Show resolved Hide resolved
v := rand.Int31()
fd := uint32(v)
for {
if _, ok := a.opened[fd]; !ok {
return fd
Expand Down Expand Up @@ -503,6 +535,23 @@ func (a *api) fd_close(ctx *wasm.HostFunctionCallContext, fd uint32) (err Errno)
return ErrnoSuccess
}

// RandomGet implements API.RandomGet
func (a *api) RandomGet(ctx *wasm.HostFunctionCallContext, buf uint32, bufLen uint32) (errno Errno) {
if !ctx.Memory.ValidateAddrRange(buf, uint64(bufLen)) {
return ErrnoInval
}

random_bytes := make([]byte, bufLen)
r8d8 marked this conversation as resolved.
Show resolved Hide resolved
_, err := a.randSource.Read(random_bytes)
if err != nil {
return ErrnoInval
Copy link
Member

Choose a reason for hiding this comment

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

ErronoInval is for invalid arguments right? Is this the correct error for here?

Copy link
Member

@mathetake mathetake Feb 14, 2022

Choose a reason for hiding this comment

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

I would recommend take a look at spec and also WASI-libc and see how other impl(e.g. Wasm time) handle errors

Copy link
Contributor Author

@r8d8 r8d8 Feb 15, 2022

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

FYI for bad memory offsets, we discussed offline to use ErrnoFault. Main thing is to document what is used and be consistent even if no one else is :D

Later, people can argue validly with these choices and probably we need to raise an issue on the wasi spec as it is underspecified. Main thing now is to document what you do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@codefromthecrypt thanks for info.
So, for invalid buf, bufLen return error is ErrnoFault. Am I getting right?
And how to deal with errors from random source ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think ErrnoIO for the source is fine for now. we can change it easily later if we find a most standard code

Copy link
Contributor

Choose a reason for hiding this comment

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

I found the issue that has been punted quite a while so far WebAssembly/WASI#215

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Error codes stuff gets you confused even more in functions that a more compilated than random_get. Spec need to address it.

Copy link
Contributor

Choose a reason for hiding this comment

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

totally agree. appreciate if you can make your case on the spec issue because it was 2 years already so somehow they aren't prioritizing. maybe if more scream they will 🤷

}

copy(ctx.Memory.Buffer[buf:buf+bufLen], random_bytes)

return ErrnoSuccess
}

func proc_exit(*wasm.HostFunctionCallContext, uint32) {
// TODO: implement
}
Expand Down
84 changes: 83 additions & 1 deletion wasi/wasi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package wasi
import (
"context"
_ "embed"
mrand "math/rand"
r8d8 marked this conversation as resolved.
Show resolved Hide resolved
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -312,7 +313,88 @@ func TestAPI_ClockTimeGet_Errors(t *testing.T) {
// TODO: TestAPI_ProcExit TestAPI_ProcExit_Errors
// TODO: TestAPI_ProcRaise TestAPI_ProcRaise_Errors
// TODO: TestAPI_SchedYield TestAPI_SchedYield_Errors
// TODO: TestAPI_RandomGet TestAPI_RandomGet_Errors

// randomWat is a wasm module to call random_get.
//go:embed testdata/random.wat
var randomWat []byte

// Non-deterministic random rource using crypto/rand
type DummyRandomSource struct {
r8d8 marked this conversation as resolved.
Show resolved Hide resolved
rng *mrand.Rand
}

func (d *DummyRandomSource) Read(p []byte) (n int, err error) {
return d.rng.Read(p)
}

func NewDummyRandomSource(seed int64) RandomSource {
s := mrand.NewSource(seed)

return &DummyRandomSource{
rng: mrand.New(s),
}
}

func TestAPI_RandomGet(t *testing.T) {
store, wasiAPI := instantiateWasmStore(t, randomWat, "test")
maskLength := 7 // number of bytes to write '?' to tell what we've written
expectedMemory := []byte{
'?', // random bytes in `buf` is after this
0x53, 0x8c, 0x7f, 0x96, 0xb1, // random data from seed value of 42
'?', // stopped after encoding
} // tr

var bufLen = uint32(5) // arbitrary buffer size,
var buf = uint32(1) // offset,
var seed = int64(42) // and seed value

wasiAPI.(*api).randSource = NewDummyRandomSource(seed)

t.Run("API.RandomGet", func(t *testing.T) {
maskMemory(store, maskLength)
// provide a host context with a seed value for random generator
hContext := wasm.NewHostFunctionCallContext(context.Background(), store.Memories[0])

errno := wasiAPI.RandomGet(hContext, buf, bufLen)
require.Equal(t, ErrnoSuccess, errno)
require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
})
}

func TestAPI_RandomGet_Errors(t *testing.T) {
store, _ := instantiateWasmStore(t, randomWat, "test")

memorySize := uint32(len(store.Memories[0].Buffer))
validAddress := uint32(0) // arbitrary valid address as arguments to args_sizes_get. We chose 0 here.
tests := []struct {
name string
buf uint32
bufLen uint32
}{
{
name: "random buffer out-of-memory",
buf: memorySize,
bufLen: 1,
},

{
name: "random buffer size exceeds the maximum valid address by 1",
buf: validAddress,
bufLen: memorySize + 1,
},
}

for _, tt := range tests {
tc := tt

t.Run(tc.name, func(t *testing.T) {
ret, _, err := store.CallFunction(context.Background(), "test", FunctionRandomGet, uint64(tc.buf), uint64(tc.bufLen))
require.NoError(t, err)
require.Equal(t, uint64(ErrnoInval), ret[0]) // ret[0] is returned errno
})
}
r8d8 marked this conversation as resolved.
Show resolved Hide resolved
}

// TODO: TestAPI_SockRecv TestAPI_SockRecv_Errors
// TODO: TestAPI_SockSend TestAPI_SockSend_Errors
// TODO: TestAPI_SockShutdown TestAPI_SockShutdown_Errors
Expand Down