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 #237

Closed
wants to merge 15 commits into from
Closed
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
7 changes: 7 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
70 changes: 70 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Contributing

We welcome contributions from the community. Please read the following guidelines carefully to maximize the chances of your PR being merged.

## Coding Style

- To ensure your change passes format checks, use run `make check`. To format your files, you can run `make format`.
- We follow standard Go table-driven tests and use the [`testify/require`](https://github.com/stretchr/testify#require-package) library to assert correctness. To verify all tests pass, you can run `make test`.

## DCO

We require DCO signoff line in every commit to this repo.

The sign-off is a simple line at the end of the explanation for the
patch, which certifies that you wrote it or otherwise have the right to
pass it on as an open-source patch. The rules are pretty simple: if you
can certify the below (from
[developercertificate.org](https://developercertificate.org/)):

```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```

then you just add a line to every git commit message:

Signed-off-by: Joe Smith <joe@gmail.com>

using your real name (sorry, no pseudonyms or anonymous contributions.)

You can add the sign off when creating the git commit via `git commit -s`.

## Code Reviews

* Indicate the priority of each comment, following this
[feedback ladder](https://www.netlify.com/blog/2020/03/05/feedback-ladders-how-we-encode-code-reviews-at-netlify/).
If none was indicated it will be treated as `[dust]`.
* A single approval is sufficient to merge, except when the change cuts
across several components; then it should be approved by at least one owner
of each component. If a reviewer asks for changes in a PR they should be
addressed before the PR is merged, even if another reviewer has already
approved the PR.
* During the review, address the comments and commit the changes _without_ squashing the commits.
This facilitates incremental reviews since the reviewer does not go through all the code again to
find out what has changed since the last review.
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,40 @@ portability features like cross compilation. Import wazero and extend your Go ap
language!

## Example

Here's an example of using wazero to invoke a Fibonacci function included in a Wasm binary.

While our [source for this](examples/testdata/fibonacci.go) is [TinyGo](https://tinygo.org/), it could have been written in
another language that targets Wasm, such as Rust.
While our [source for this](examples/testdata/fibonacci.go) is [TinyGo](https://tinygo.org/), it could have been written in another language that targets Wasm, such as AssemblyScript/C/C++/Rust/Zig.

```golang
package main

import (
"context"
"fmt"
"os"

"github.com/tetratelabs/wazero/wasi"
"github.com/tetratelabs/wazero/wasm"
"github.com/tetratelabs/wazero/wasm/binary"
"github.com/tetratelabs/wazero/wasm/interpreter"
)

func main() {
// Default context impl. by Go
ctx := context.Background()
// Read WebAssembly binary.
source, _ := os.ReadFile("fibonacci.wasm")
source, _ := os.ReadFile("examples/testdata/fibonacci.wasm")
// Decode the binary as WebAssembly module.
mod, _ := binary.DecodeModule(source)
// Initialize the execution environment called "store" with Interpreter-based engine.
store := wasm.NewStore(interpreter.NewEngine())
// To resolve WASI specific methods, such as `fd_write`
wasi.RegisterAPI(store)
// Instantiate the decoded module.
store.Instantiate(mod, "test")
// Execute the exported "fibonacci" function from the instantiated module.
ret, _, err := store.CallFunction("test", "fibonacci", 20)
ret, _, _ := store.CallFunction(ctx, "test", "fibonacci", 20)
// Give us the fibonacci number for 20, namely 6765!
fmt.Println(ret[0])
}
Expand Down Expand Up @@ -58,4 +75,4 @@ Currently any performance optimization hasn't been done to this runtime yet, and

However _theoretically speaking_, this project have the potential to compete with these state-of-the-art JIT-style runtimes. The rationale for that is it is well-know that [CGO is slow](https://github.com/golang/go/issues/19574). More specifically, if you make large amount of CGO calls which cross the boundary between Go and C (stack) space, then the usage of CGO could be a bottleneck.

Since we can do JIT compilation purely in Go, this runtime could be the fastest one for some use cases where we have to make large amount of CGO calls (e.g. Proxy-Wasm host environment, or request-based plugin systems).
Since we can do JIT compilation purely in Go, this runtime could be the fastest one for some use cases where we have to make large amount of CGO calls (e.g. Proxy-Wasm host environment, or request-based plugin systems).
11 changes: 7 additions & 4 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
This directory contains tests which use multiple packages. For example:

- `bench` contains benchmark tests.
- `codec` contains a test and benchmark on text and binary decoders.
- `engine` contains variety of e2e tests, mainly to ensure the consistency in the behavior between engines.
- `spectest` contains end-to-end tests with the [WebAssembly specification tests](https://github.com/WebAssembly/spec/tree/wg-1.0/test/core).
* `bench` contains benchmark tests.
* `codec` contains a test and benchmark on text and binary decoders.
* `engine` contains variety of e2e tests, mainly to ensure the consistency in the behavior between engines.
* `spectest` contains end-to-end tests with the [WebAssembly specification tests](https://github.com/WebAssembly/spec/tree/wg-1.0/test/core).

*Note*: this doesn't contain WASI tests, as there's not yet an [official testsuite](https://github.com/WebAssembly/WASI/issues/9).
Meanwhile, WASI functions are unit tested including via Text Format imports [here](../wasi/wasi_test.go)
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.
;; 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))
)
76 changes: 69 additions & 7 deletions wasi/wasi.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package wasi

import (
crand "crypto/rand"
"encoding/binary"
"errors"
"io"
"io/fs"
"math"
"math/rand"
"os"
"reflect"
"time"
Expand Down Expand Up @@ -143,7 +143,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
Copy link
Contributor

Choose a reason for hiding this comment

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

this drifted and also we are probably being too specific on the mechanics of random.

maybe For example, if bufLen = 5, we expect ctx.Memory.Buffer to contain 5 random bytes:

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
You mean not to use in comments concrete random bytes from specified seed?
And also not to check concrete random bytes form seed, cause crypto/rand non-deterministic and we can't seed it, right?
Just check in test that memory is filled

Copy link
Contributor

Choose a reason for hiding this comment

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

no I mean in this godoc we don't need to get into how secure random works. I still like that you are using tests that are resulting in a known value

Copy link
Contributor Author

@r8d8 r8d8 Feb 14, 2022

Choose a reason for hiding this comment

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

or we can seed math/rand from crypto/rand, like this

var seed int64
binary.Read(crypto_rand.Reader, binary.BigEndian, &seed)
math_rand.NewSource(seed)

and in tests still use simple seed and check concrete bytes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no I mean in this godoc we don't need to get into how secure random works. I still like that you are using tests that are resulting in a known value

got it, thanks for your feedback

// 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 +171,24 @@ const (
wasiSnapshotPreview1Name = "wasi_snapshot_preview1"
)

type RandomSource interface {
Read([]byte) (int, error)
Int31() (int32, error)
}

// Non-deterministic random source using crypto/rand
type CryptoRandomSource struct{}

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

func (c *CryptoRandomSource) Int31() (v int32, err error) {
err = binary.Read(crand.Reader, binary.BigEndian, &v)

return v, err
}

type api struct {
args *nullTerminatedStrings
stdin io.Reader
Expand All @@ -162,6 +197,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 +248,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 +381,7 @@ func newAPI(opts ...Option) *api {
timeNowUnixNano: func() uint64 {
return uint64(time.Now().UnixNano())
},
randSource: &CryptoRandomSource{},
}

// apply functional options
Expand All @@ -354,11 +391,16 @@ func newAPI(opts ...Option) *api {
return ret
}

func (a *api) randUnusedFD() uint32 {
fd := uint32(rand.Int31())
func (a *api) randUnusedFD() (uint32, error) {
v, err := a.randSource.Int31()
if err != nil {
return 0, err
}

fd := uint32(v)
for {
if _, ok := a.opened[fd]; !ok {
return fd
return fd, nil
}
fd = (fd + 1) % (1 << 31)
}
Expand Down Expand Up @@ -412,7 +454,10 @@ func (a *api) path_open(ctx *wasm.HostFunctionCallContext, fd, dirFlags, pathPtr
}
}

newFD := a.randUnusedFD()
newFD, err := a.randUnusedFD()
if err != nil {
return ErrnoInval
}

a.opened[newFD] = fileEntry{
file: f,
Expand Down Expand Up @@ -503,6 +548,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)
_, err := a.randSource.Read(random_bytes)
if err != nil {
return ErrnoInval
}

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

return ErrnoSuccess
}

func proc_exit(*wasm.HostFunctionCallContext, uint32) {
// TODO: implement
}
Expand Down
Loading