Skip to content
This repository has been archived by the owner on May 11, 2020. It is now read-only.

Pass the VM pointer to host go functions #59

Closed
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
96 changes: 90 additions & 6 deletions exec/call_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import (
func TestHostCall(t *testing.T) {
const secretValue = 0xdeadbeef

var secretVariable int = 0
var secretVariable int

// a host function that can be called by WASM code.
testHostFunction := func() {
testHostFunction := func(proc *Process) {
secretVariable = secretValue
}

Expand Down Expand Up @@ -99,11 +99,11 @@ var moduleCallHost = []byte{
0x08, 0x01, 0x06, 0x00, 0x41, 0x00, 0x10, 0x00, 0x0B,
}

func add3(x int32) int32 {
func add3(proc *Process, x int32) int32 {
return x + 3
}

func importer(name string) (*wasm.Module, error) {
func importer(name string, f func(*Process, int32) int32) (*wasm.Module, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

we could modify importer to take the f func(*Process, int32) int32) argument, close around it and return the usual func(string) (*wasm.Module, error) importer function...

(just a suggestion. it might make things less obvious, though...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought of that, and I think this is not the right approach: I can only imagine one real-life use case when a host function needs not access the memory: reading integer value from a system-specific file (like /proc/$$/fd or /dev/urandom) and that is by no means the most general use case, so it feels like an unnecessary complexity to me.
Furthermore, the spec specifies that the store should be passed to host functions so it seems normal to do that in any case, just as the spec expects an interpreter to.

Copy link
Contributor

Choose a reason for hiding this comment

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

ok.

m := wasm.NewModule()
m.Types = &wasm.SectionTypes{
// List of all function types available in this module.
Expand All @@ -119,7 +119,44 @@ func importer(name string) (*wasm.Module, error) {
m.FunctionIndexSpace = []wasm.Function{
{
Sig: &m.Types.Entries[0],
Host: reflect.ValueOf(add3),
Host: reflect.ValueOf(f),
Body: &wasm.FunctionBody{},
},
}
m.Export = &wasm.SectionExports{
Entries: map[string]wasm.ExportEntry{
"_native": {
FieldStr: "_naive",
Kind: wasm.ExternalFunction,
Index: 0,
},
},
}

return m, nil
}

func invalidAdd3(x int32) int32 {
return x + 3
}

func invalidImporter(name string) (*wasm.Module, error) {
m := wasm.NewModule()
m.Types = &wasm.SectionTypes{
// List of all function types available in this module.
// There is only one: (func [int32] -> [int32])
Entries: []wasm.FunctionSig{
{
Form: 0,
ParamTypes: []wasm.ValueType{wasm.ValueTypeI32},
ReturnTypes: []wasm.ValueType{wasm.ValueTypeI32},
},
},
}
m.FunctionIndexSpace = []wasm.Function{
{
Sig: &m.Types.Entries[0],
Host: reflect.ValueOf(invalidAdd3),
Body: &wasm.FunctionBody{},
},
}
Expand All @@ -137,7 +174,7 @@ func importer(name string) (*wasm.Module, error) {
}

func TestHostSymbolCall(t *testing.T) {
m, err := wasm.ReadModule(bytes.NewReader(moduleCallHost), importer)
m, err := wasm.ReadModule(bytes.NewReader(moduleCallHost), func(n string) (*wasm.Module, error) { return importer(n, add3) })
if err != nil {
t.Fatalf("Could not read module: %v", err)
}
Expand All @@ -153,3 +190,50 @@ func TestHostSymbolCall(t *testing.T) {
t.Fatalf("Did not get the right value. Got %d, wanted %d", rtrns, 3)
}
}

func TestGoFunctionCallChecksForFirstArgument(t *testing.T) {
m, err := wasm.ReadModule(bytes.NewReader(moduleCallHost), invalidImporter)
if err != nil {
t.Fatalf("Could not read module: %v", err)
}
defer func() {
if r := recover(); r == nil {
t.Errorf("This code should have panicked.")
} else {
if r != "exec: the first argument of a host function was int32, expected ptr" {
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks brittle.
but I must admit we don't have a safer mechanism in place to handle this kind of thing...

t.Errorf("This should have panicked because of the wrong type being used as a first argument, and it panicked because of %v", r)
}
}
}()
vm, err := NewVM(m)
if err != nil {
t.Fatalf("Could not instantiate vm: %v", err)
}
_, err = vm.ExecCode(1)
if err != nil {
t.Fatalf("Error executing the default function: %v", err)
}
}

func terminate(proc *Process, x int32) int32 {
proc.Terminate()
return 3
}

func TestHostTerminate(t *testing.T) {
m, err := wasm.ReadModule(bytes.NewReader(moduleCallHost), func(n string) (*wasm.Module, error) { return importer(n, terminate) })
if err != nil {
t.Fatalf("Could not read module: %v", err)
}
vm, err := NewVM(m)
if err != nil {
t.Fatalf("Could not instantiate vm: %v", err)
}
_, err = vm.ExecCode(1)
if err != nil {
t.Fatalf("Error executing the default function: %v", err)
}
if vm.abort == false || vm.ctx.pc > 0xa {
t.Fatalf("Terminate did not abort execution: abort=%v, pc=%#x", vm.abort, vm.ctx.pc)
}
}
12 changes: 11 additions & 1 deletion exec/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,20 @@ type goFunction struct {
}

func (fn goFunction) call(vm *VM, index int64) {
// numIn = # of call inputs + vm, as the function expects
// an additional *VM argument
numIn := fn.typ.NumIn()
args := make([]reflect.Value, numIn)
proc := NewProcess(vm)

for i := numIn - 1; i >= 0; i-- {
// Pass proc as an argument. Check that the function indeed
// expects a *Process argument.
if reflect.ValueOf(proc).Kind() != fn.typ.In(0).Kind() {
panic(fmt.Sprintf("exec: the first argument of a host function was %s, expected %s", fn.typ.In(0).Kind(), reflect.ValueOf(vm).Kind()))
}
args[0] = reflect.ValueOf(proc)

for i := numIn - 1; i >= 1; i-- {
val := reflect.New(fn.typ.In(i)).Elem()
raw := vm.popUint64()
kind := fn.typ.In(i).Kind()
Expand Down
65 changes: 64 additions & 1 deletion exec/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"io"
"math"

"github.com/go-interpreter/wagon/disasm"
Expand Down Expand Up @@ -67,6 +68,8 @@ type VM struct {
// A panic can occur either when executing an invalid VM
// or encountering an invalid instruction, e.g. `unreachable`.
RecoverPanic bool

abort bool // Flag for host functions to terminate execution
}

// As per the WebAssembly spec: https://github.com/WebAssembly/design/blob/27ac254c854994103c24834a994be16f74f54186/Semantics.md#linear-memory
Expand Down Expand Up @@ -320,7 +323,7 @@ func (vm *VM) ExecCode(fnIndex int64, args ...uint64) (rtrn interface{}, err err

func (vm *VM) execCode(compiled compiledFunction) uint64 {
outer:
for int(vm.ctx.pc) < len(vm.ctx.code) {
for int(vm.ctx.pc) < len(vm.ctx.code) && !vm.abort {
op := vm.ctx.code[vm.ctx.pc]
vm.ctx.pc++
switch op {
Expand Down Expand Up @@ -397,3 +400,63 @@ outer:
}
return 0
}

// Process is a proxy passed to host functions in order to access
// things such as memory and control.
type Process struct {
vm *VM
}

// NewProcess creates a VM interface object for host functions
func NewProcess(vm *VM) *Process {
return &Process{vm: vm}
}

// ReadAt implements the ReaderAt interface: it copies into p
// the content of memory at offset off.
func (proc *Process) ReadAt(p []byte, off int64) (int, error) {
mem := proc.vm.Memory()

var length int
if len(mem) < len(p)+int(off) {
length = len(mem) - int(off)
} else {
length = len(p)
}

copy(p, mem[off:off+int64(length)])

var err error
if length < len(p) {
err = io.ErrShortBuffer
}

return length, err
}

// WriteAt implements the WriterAt interface: it writes the content of p
// into the VM memory at offset off.
func (proc *Process) WriteAt(p []byte, off int64) (int, error) {
mem := proc.vm.Memory()

var length int
if len(mem) < len(p)+int(off) {
length = len(mem) - int(off)
} else {
length = len(p)
}

copy(mem[off:], p[:length])

var err error
if length < len(p) {
err = io.ErrShortWrite
}

return length, err
}

// Terminate stops the execution of the current module.
func (proc *Process) Terminate() {
proc.vm.abort = true
}
112 changes: 112 additions & 0 deletions exec/vm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2018 The go-interpreter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package exec

import (
"testing"
)

var (
smallMemoryVM = &VM{memory: []byte{1, 2, 3}}
emptyMemoryVM = &VM{memory: []byte{}}
smallMemoryProcess = &Process{vm: smallMemoryVM}
emptyMemoryProcess = &Process{vm: emptyMemoryVM}
tooBigABuffer = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
)

func TestNormalWrite(t *testing.T) {
vm := &VM{memory: make([]byte, 300)}
proc := &Process{vm: vm}
n, err := proc.WriteAt(tooBigABuffer, 0)
if err != nil {
t.Fatalf("Found an error when writing: %v", err)
}
if n != len(tooBigABuffer) {
t.Fatalf("Number of written bytes was %d, should have been %d", n, len(tooBigABuffer))
}
}

func TestWriteBoundary(t *testing.T) {
n, err := smallMemoryProcess.WriteAt(tooBigABuffer, 0)
if err == nil {
t.Fatal("Should have reported an error and didn't")
}
if n != len(smallMemoryVM.memory) {
t.Fatalf("Number of written bytes was %d, should have been 0", n)
}
}

func TestReadBoundary(t *testing.T) {
buf := make([]byte, 300)
n, err := smallMemoryProcess.ReadAt(buf, 0)
if err == nil {
t.Fatal("Should have reported an error and didn't")
}
if n != len(smallMemoryVM.memory) {
t.Fatalf("Number of written bytes was %d, should have been 0", n)
}
}

func TestReadEmpty(t *testing.T) {
buf := make([]byte, 300)
n, err := emptyMemoryProcess.ReadAt(buf, 0)
if err == nil {
t.Fatal("Should have reported an error and didn't")
}
if n != 0 {
t.Fatalf("Number of written bytes was %d, should have been 0", n)
}
}

func TestReadOffset(t *testing.T) {
buf0 := make([]byte, 2)
n0, err := smallMemoryProcess.ReadAt(buf0, 0)
if err != nil {
t.Fatalf("Error reading 1-byte buffer: %v", err)
}
if n0 != 2 {
t.Fatalf("Read %d bytes, expected 2", n0)
}

buf1 := make([]byte, 1)
n1, err := smallMemoryProcess.ReadAt(buf1, 1)
if err != nil {
t.Fatalf("Error reading 1-byte buffer: %v", err)
}
if n1 != 1 {
t.Fatalf("Read %d bytes, expected 1.", n0)
}

if buf0[1] != buf1[0] {
t.Fatal("Read two different bytes from what should be the same location")
}
}

func TestWriteEmpty(t *testing.T) {
n, err := emptyMemoryProcess.WriteAt(tooBigABuffer, 0)
if err == nil {
t.Fatal("Should have reported an error and didn't")
}
if n != 0 {
t.Fatalf("Number of written bytes was %d, should have been 0", n)
}
}

func TestWriteOffset(t *testing.T) {
vm := &VM{memory: make([]byte, 300)}
proc := &Process{vm: vm}

n, err := proc.WriteAt(tooBigABuffer, 2)
if err != nil {
t.Fatalf("error writing to buffer: %v", err)
}
if n != len(tooBigABuffer) {
t.Fatalf("Number of written bytes was %d, should have been %d", n, len(tooBigABuffer))
}

if vm.memory[0] != 0 || vm.memory[1] != 0 || vm.memory[2] != tooBigABuffer[0] {
t.Fatal("Writing at offset didn't work")
}
}