diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..4810a013 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/spectestcase"] + path = tests/spectestcase + url = https://github.com/ontio/testsuite diff --git a/.travis.yml b/.travis.yml index c18430e9..d219a6c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,7 @@ script: - GOARCH=386 go install -v $TAGS ./... - GOARCH=amd64 go install -v $TAGS ./... - go run ./ci/run-tests.go $COVERAGE + - cd tests && bash ./run_testcase.sh after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/exec/call.go b/exec/call.go index 0250364e..6fb3b14e 100644 --- a/exec/call.go +++ b/exec/call.go @@ -4,7 +4,11 @@ package exec -import "errors" +import ( + "errors" + + "github.com/go-interpreter/wagon/wasm" +) var ( // ErrSignatureMismatch is the error value used while trapping the VM when @@ -31,7 +35,11 @@ func (vm *VM) callIndirect() { if int(tableIndex) >= len(vm.module.TableIndexSpace[0]) { panic(ErrUndefinedElementIndex) } - elemIndex := vm.module.TableIndexSpace[0][tableIndex] + tableEntry := vm.module.TableIndexSpace[0][tableIndex] + if !tableEntry.Initialized { + panic(wasm.UninitializedTableEntryError(tableIndex)) + } + elemIndex := tableEntry.Index fnActual := vm.module.FunctionIndexSpace[elemIndex] if len(fnExpect.ParamTypes) != len(fnActual.Sig.ParamTypes) { diff --git a/exec/memory.go b/exec/memory.go index d703d310..75972ffd 100644 --- a/exec/memory.go +++ b/exec/memory.go @@ -19,9 +19,9 @@ func (vm *VM) fetchBaseAddr() int { // inBounds returns true when the next vm.fetchBaseAddr() + offset // indices are in bounds accesses to the linear memory. -func (vm *VM) inBounds(offset int) bool { - addr := endianess.Uint32(vm.ctx.code[vm.ctx.pc:]) + uint32(vm.ctx.stack[len(vm.ctx.stack)-1]) - return int(addr)+offset < len(vm.memory) +func (vm *VM) inBounds(offset uint32) bool { + addr := uint64(endianess.Uint32(vm.ctx.code[vm.ctx.pc:])) + uint64(uint32(vm.ctx.stack[len(vm.ctx.stack)-1])) + return addr+uint64(offset) < uint64(len(vm.memory)) } // curMem returns a slice to the memory segment pointed to by @@ -208,7 +208,16 @@ func (vm *VM) currentMemory() { func (vm *VM) growMemory() { _ = vm.fetchInt8() // reserved (https://github.com/WebAssembly/design/blob/27ac254c854994103c24834a994be16f74f54186/BinaryEncoding.md#memory-related-operators-described-here) curLen := len(vm.memory) / wasmPageSize - n := vm.popInt32() + n := vm.popUint32() + + maxMem := vm.module.Memory.Entries[0].Limits.Maximum + newPage := uint64(n + uint32(len(vm.memory)/wasmPageSize)) + + if newPage > 1<<16 || newPage > uint64(maxMem) { + vm.pushInt32(-1) + return + } + vm.memory = append(vm.memory, make([]byte, n*wasmPageSize)...) vm.pushInt32(int32(curLen)) } diff --git a/exec/num.go b/exec/num.go index 4274dd59..f7ecbcc2 100644 --- a/exec/num.go +++ b/exec/num.go @@ -5,6 +5,7 @@ package exec import ( + "errors" "math" "math/bits" ) @@ -34,6 +35,9 @@ func (vm *VM) i32Mul() { func (vm *VM) i32DivS() { v2 := vm.popInt32() v1 := vm.popInt32() + if v1 == math.MinInt32 && v2 == -1 { + panic(errors.New("integer overflow")) + } vm.pushInt32(v1 / v2) } @@ -76,19 +80,19 @@ func (vm *VM) i32Xor() { func (vm *VM) i32Shl() { v2 := vm.popUint32() v1 := vm.popUint32() - vm.pushUint32(v1 << v2) + vm.pushUint32(v1 << (v2 % 32)) } func (vm *VM) i32ShrU() { v2 := vm.popUint32() v1 := vm.popUint32() - vm.pushUint32(v1 >> v2) + vm.pushUint32(v1 >> (v2 % 32)) } func (vm *VM) i32ShrS() { v2 := vm.popUint32() v1 := vm.popInt32() - vm.pushInt32(v1 >> v2) + vm.pushInt32(v1 >> (v2 % 32)) } func (vm *VM) i32Rotl() { @@ -194,6 +198,9 @@ func (vm *VM) i64Mul() { func (vm *VM) i64DivS() { v2 := vm.popInt64() v1 := vm.popInt64() + if v1 == math.MinInt64 && v2 == -1 { + panic(errors.New("integer overflow")) + } vm.pushInt64(v1 / v2) } @@ -230,19 +237,19 @@ func (vm *VM) i64Xor() { func (vm *VM) i64Shl() { v2 := vm.popUint64() v1 := vm.popUint64() - vm.pushUint64(v1 << v2) + vm.pushUint64(v1 << (v2 % 64)) } func (vm *VM) i64ShrS() { v2 := vm.popUint64() v1 := vm.popInt64() - vm.pushInt64(v1 >> v2) + vm.pushInt64(v1 >> (v2 % 64)) } func (vm *VM) i64ShrU() { v2 := vm.popUint64() v1 := vm.popUint64() - vm.pushUint64(v1 >> v2) + vm.pushUint64(v1 >> (v2 % 64)) } func (vm *VM) i64Rotl() { diff --git a/exec/vm.go b/exec/vm.go index ecaaac09..408c99c4 100644 --- a/exec/vm.go +++ b/exec/vm.go @@ -207,6 +207,26 @@ func (vm *VM) Memory() []byte { return vm.memory } +// GetExportEntry returns ExportEntry of this VM's Wasm module. +func (vm *VM) GetExportEntry(name string) (wasm.ExportEntry, bool) { + entry, ok := vm.module.Export.Entries[name] + return entry, ok +} + +// GetGlobal returns the global value represented as uint64 defined in this VM's Wasm module. +func (vm *VM) GetGlobal(name string) (uint64, bool) { + entry, ok := vm.GetExportEntry(name) + if !ok { + return 0, false + } + index := entry.Index + if int64(index) >= int64(len(vm.globals)) { + return 0, false + } + + return vm.globals[index], true +} + func (vm *VM) pushBool(v bool) { if v { vm.pushUint64(1) diff --git a/go.mod b/go.mod index 749a7da2..831cde05 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,6 @@ require ( github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e // indirect ) + +// users in some countries cannot access golang.org. +replace golang.org/x/sys => github.com/golang/sys v0.0.0-20190306220234-b354f8bf4d9e diff --git a/go.sum b/go.sum index 8f45e7c4..1860f0ef 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/golang/sys v0.0.0-20190306220234-b354f8bf4d9e h1:Yv9Nf3qPhnbsludlsbTa1z6lOpYbJxHKoh5lXyqfP3I= +github.com/golang/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc h1:RTUQlKzoZZVG3umWNzOYeFecQLIh+dbxXvJp1zPQJTI= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= -golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e h1:UndnRDGP/JcdZX1LBubo1fJ3Jt6GnKREteLJvysiiPE= -golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/tests/run_testcase.sh b/tests/run_testcase.sh new file mode 100644 index 00000000..dca376db --- /dev/null +++ b/tests/run_testcase.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -ex +rm -rf temp +mkdir temp +cd temp + +if ! which wast2json ; then + wget https://github.com/WebAssembly/wabt/releases/download/1.0.13/wabt-1.0.13-linux.tar.gz + tar -xzvf wabt-1.0.13-linux.tar.gz + WAST2JSON=wabt-1.0.13/wast2json +else + WAST2JSON=wast2json +fi + + +go build $TAGS -o spec_test ../spec_test_runner.go + +for file in ../spectestcase/*.wast ; do + ${WAST2JSON} ${file} +done + +for json in *.json ; do + ./spec_test ${json} +done diff --git a/tests/spec_test_runner.go b/tests/spec_test_runner.go new file mode 100644 index 00000000..91f20934 --- /dev/null +++ b/tests/spec_test_runner.go @@ -0,0 +1,196 @@ +// Copyright 2020 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 main + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "path" + "path/filepath" + + "github.com/go-interpreter/wagon/exec" + "github.com/go-interpreter/wagon/wasm" +) + +// this file is based on github.com/perlin-network/life/spec/test_runner/runner.go + +type Config struct { + SourceFilename string `json:"source_filename"` + Commands []Command `json:"commands"` +} + +type Command struct { + Type string `json:"type"` + Line int `json:"line"` + Filename string `json:"filename"` + Name string `json:"name"` + Action CmdAction `json:"action"` + Text string `json:"text"` + ModuleType string `json:"module_type"` + Expected []ValueInfo `json:"expected"` +} + +type CmdAction struct { + Type string `json:"type"` + Module string `json:"module"` + Field string `json:"field"` + Args []ValueInfo `json:"args"` + Expected []ValueInfo `json:"expected"` +} + +type ValueInfo struct { + Type string `json:"type"` + Value string `json:"value"` +} + +func LoadConfigFromFile(filename string) *Config { + raw, err := ioutil.ReadFile(filename) + if err != nil { + panic(err) + } + var cfg Config + err = json.Unmarshal(raw, &cfg) + if err != nil { + panic(err) + } + return &cfg +} + +func (c *Config) Run(cfgPath string) { + var vm *exec.VM + namedVMs := make(map[string]*exec.VM) + + dir, _ := filepath.Split(cfgPath) + + for _, cmd := range c.Commands { + switch cmd.Type { + case "module": + input, err := ioutil.ReadFile(path.Join(dir, cmd.Filename)) + if err != nil { + panic(err) + } + m, err := wasm.ReadModule(bytes.NewBuffer(input), nil) + if err != nil { + log.Fatalf("could not read module: %v", err) + } + + vm, err = exec.NewVM(m) + if err != nil { + panic(fmt.Errorf("l%d: %s, could not create VM: %v", cmd.Line, cfgPath, err)) + } + vm.RecoverPanic = true + if cmd.Name != "" { + namedVMs[cmd.Name] = vm + } + case "assert_return", "action": + localVM := vm + if cmd.Action.Module != "" { + if target, ok := namedVMs[cmd.Action.Module]; ok { + localVM = target + } else { + panic("named module not found") + } + } + if localVM == nil { + panic("module not found") + } + + switch cmd.Action.Type { + case "invoke": + entryID, ok := localVM.GetExportEntry(cmd.Action.Field) + if !ok { + panic("export not found (func)") + } + args := make([]uint64, 0) + for _, arg := range cmd.Action.Args { + var val uint64 + fmt.Sscanf(arg.Value, "%d", &val) + args = append(args, val) + } + ret, err := localVM.ExecCode(int64(entryID.Index), args...) + if err != nil { + panic(err) + } + if len(cmd.Expected) != 0 { + var _exp uint64 + fmt.Sscanf(cmd.Expected[0].Value, "%d", &_exp) + exp := int64(_exp) + var result int64 + if cmd.Expected[0].Type == "i32" || cmd.Expected[0].Type == "f32" { + result = int64(ret.(uint32)) + exp = int64(uint32(exp)) + } else { + result = int64(ret.(uint64)) + } + if result != exp { + panic(fmt.Errorf("l%d: %s, ret mismatch: got %d, expected %d", cmd.Line, cfgPath, result, exp)) + } + } + case "get": + val, ok := localVM.GetGlobal(cmd.Action.Field) + if !ok { + panic("export not found (global)") + } + var exp uint64 + fmt.Sscanf(cmd.Expected[0].Value, "%d", &exp) + if cmd.Expected[0].Type == "i32" || cmd.Expected[0].Type == "f32" { + val = uint64(uint32(val)) + exp = uint64(uint32(exp)) + } + if val != exp { + panic(fmt.Errorf("val mismatch: got %d, expected %d\n", val, exp)) + } + default: + panic(cmd.Action.Type) + } + case "assert_trap": + localVM := vm + if cmd.Action.Module != "" { + if target, ok := namedVMs[cmd.Action.Module]; ok { + localVM = target + } else { + panic("named module not found") + } + } + if localVM == nil { + panic("module not found") + } + switch cmd.Action.Type { + case "invoke": + entryID, ok := localVM.GetExportEntry(cmd.Action.Field) + if !ok { + panic("export not found (func)") + } + args := make([]uint64, 0) + for _, arg := range cmd.Action.Args { + var val uint64 + fmt.Sscanf(arg.Value, "%d", &val) + args = append(args, val) + } + _, err := localVM.ExecCode(int64(entryID.Index), args...) + if err == nil { + panic(fmt.Errorf("L%d: %s, expect a trap\n", cmd.Line, cfgPath)) + } + default: + panic(cmd.Action.Type) + } + + case "assert_malformed", "assert_invalid", "assert_exhaustion", "assert_unlinkable", + "assert_return_canonical_nan", "assert_return_arithmetic_nan": + fmt.Printf("skipping %s\n", cmd.Type) + default: + panic(cmd.Type) + } + fmt.Printf("PASS L%d: %s\n", cmd.Line, cfgPath) + } +} + +func main() { + cfg := LoadConfigFromFile(os.Args[1]) + cfg.Run(os.Args[1]) +} diff --git a/tests/spectestcase b/tests/spectestcase new file mode 160000 index 00000000..1438a002 --- /dev/null +++ b/tests/spectestcase @@ -0,0 +1 @@ +Subproject commit 1438a002e18629d0b219ff689d2e5de770377431 diff --git a/wasm/index.go b/wasm/index.go index 6b1ea8ee..63a1185c 100644 --- a/wasm/index.go +++ b/wasm/index.go @@ -11,12 +11,28 @@ import ( "reflect" ) +type OutsizeError struct { + ImmType string + Size uint64 + Max uint64 +} + +func (e OutsizeError) Error() string { + return fmt.Sprintf("validate: %s size overflow (%v), max (%v)", e.ImmType, e.Size, e.Max) +} + type InvalidTableIndexError uint32 func (e InvalidTableIndexError) Error() string { return fmt.Sprintf("wasm: Invalid table to table index space: %d", uint32(e)) } +type UninitializedTableEntryError uint32 + +func (e UninitializedTableEntryError) Error() string { + return fmt.Sprintf("wasm: Uninitialized table entry at index: %d", uint32(e)) +} + type InvalidValueTypeInitExprError struct { Wanted reflect.Kind Got reflect.Kind @@ -201,13 +217,22 @@ func (m *Module) populateTables() error { table := m.TableIndexSpace[elem.Index] //use uint64 to avoid overflow - if uint64(offset)+uint64(len(elem.Elems)) > uint64(len(table)) { - data := make([]uint32, uint64(offset)+uint64(len(elem.Elems))) - copy(data[offset:], elem.Elems) + totalSize := uint64(offset) + uint64(len(elem.Elems)) + if totalSize > uint64(len(table)) { + maxAllowSize := uint64(m.Table.Entries[elem.Index].Limits.Maximum) + if totalSize > maxAllowSize { + return OutsizeError{"Table", totalSize, maxAllowSize} + } + data := make([]TableEntry, totalSize) copy(data, table) + for i, index := range elem.Elems { + data[offset+uint32(i)] = TableEntry{Index: index, Initialized: true} + } m.TableIndexSpace[elem.Index] = data } else { - copy(table[offset:], elem.Elems) + for i, index := range elem.Elems { + table[offset+uint32(i)] = TableEntry{Index: index, Initialized: true} + } } } @@ -222,7 +247,12 @@ func (m *Module) GetTableElement(index int) (uint32, error) { return 0, InvalidTableIndexError(index) } - return m.TableIndexSpace[0][index], nil + entry := m.TableIndexSpace[0][index] + if !entry.Initialized { + return 0, UninitializedTableEntryError(index) + } + + return entry.Index, nil } func (m *Module) populateLinearMemory() error { diff --git a/wasm/module.go b/wasm/module.go index 9f7502be..4e820999 100644 --- a/wasm/module.go +++ b/wasm/module.go @@ -59,7 +59,7 @@ type Module struct { // function indices into the global function space // the limit of each table is its capacity (cap) - TableIndexSpace [][]uint32 + TableIndexSpace [][]TableEntry LinearMemoryIndexSpace [][]byte imports struct { @@ -70,6 +70,12 @@ type Module struct { } } +// TableEntry represents a table index and tracks its initialized state. +type TableEntry struct { + Index uint32 + Initialized bool +} + // Custom returns a custom section with a specific name, if it exists. func (m *Module) Custom(name string) *SectionCustom { for _, s := range m.Customs { @@ -139,7 +145,7 @@ func ReadModule(r io.Reader, resolvePath ResolveFunc) (*Module, error) { m.LinearMemoryIndexSpace = make([][]byte, 1) if m.Table != nil { - m.TableIndexSpace = make([][]uint32, int(len(m.Table.Entries))) + m.TableIndexSpace = make([][]TableEntry, int(len(m.Table.Entries))) } if m.Import != nil && resolvePath != nil { diff --git a/wasm/types.go b/wasm/types.go index b7a72dba..9a2f0176 100644 --- a/wasm/types.go +++ b/wasm/types.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "math" "github.com/go-interpreter/wagon/wasm/leb128" ) @@ -338,6 +339,7 @@ func (lim *ResizableLimits) UnmarshalWASM(r io.Reader) error { return err } + lim.Maximum = math.MaxUint32 if lim.Flags&0x1 != 0 { m, err := leb128.ReadVarUint32(r) if err != nil {