Files
wazero/internal/modgen/modgen_test.go
Crypt Keeper 45ff2fe12f Propagates context to all api interface methods that aren't constant (#502)
This prepares for exposing operations like Memory.Grow while keeping the
ability to trace what did that, by adding a `context.Context` initial
parameter. This adds this to all API methods that mutate or return
mutated data.

Before, we made a change to trace functions and general lifecycle
commands, but we missed this part. Ex. We track functions, but can't
track what closed the module, changed memory or a mutable constant.
Changing to do this now is not only more consistent, but helps us
optimize at least the interpreter to help users identify otherwise
opaque code that can cause harm. This is critical before we add more
functions that can cause harm, such as Memory.Grow.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2022-04-25 08:13:18 +08:00

731 lines
20 KiB
Go

package modgen
import (
"context"
"encoding/hex"
"math/rand"
"strconv"
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wasm/binary"
)
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
const (
i32 = wasm.ValueTypeI32
i64 = wasm.ValueTypeI64
f32 = wasm.ValueTypeF32
f64 = wasm.ValueTypeF64
)
// TestModGen is like a end-to-end test and verifies that our module generator only generates valid compilable modules.
func TestModGen(t *testing.T) {
tested := map[string]struct{}{}
rand := rand.New(rand.NewSource(0)) // use deterministic seed source for easy debugging.
runtime := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig().WithFinishedFeatures())
for _, size := range []int{1, 2, 5, 10, 50, 100} {
for i := 0; i < 100; i++ {
seed := make([]byte, size)
_, err := rand.Read(seed)
require.NoError(t, err)
encoded := hex.EncodeToString(seed)
if _, ok := tested[encoded]; ok {
continue
}
t.Run(encoded, func(t *testing.T) {
numTypes, numFunctions, numImports, numExports, numGlobals, numElements, numData :=
rand.Intn(size), rand.Intn(size), rand.Intn(size), rand.Intn(size), rand.Intn(size), rand.Intn(size), rand.Intn(size)
needStartSection := rand.Intn(2) == 0
// Generate a random WebAssembly module.
m := Gen(seed, wasm.FeatureMultiValue,
uint32(numTypes), uint32(numFunctions), uint32(numImports), uint32(numExports), uint32(numGlobals), uint32(numElements), uint32(numData), needStartSection)
// Encode the generated module (*wasm.Module) as binary.
bin := binary.EncodeModule(m)
// Pass the generated binary into our compilers.
code, err := runtime.CompileModule(testCtx, bin)
require.NoError(t, err)
err = code.Close(testCtx)
require.NoError(t, err)
})
}
}
}
// testRand implements randm for testing.
type testRand struct {
ints []int
intPos int
bufs [][]byte
bufPos int
}
var _ random = &testRand{}
func (tr *testRand) Intn(n int) int {
ret := tr.ints[tr.intPos] % n
tr.intPos = (tr.intPos + 1) % len(tr.ints)
return ret
}
func (tr *testRand) Read(p []byte) (n int, err error) {
buf := tr.bufs[tr.bufPos]
copy(p, buf)
tr.bufPos = (tr.bufPos + 1) % len(tr.bufs)
return
}
func newGenerator(size int, ints []int, bufs [][]byte) *generator {
return &generator{size: size,
rands: []random{&testRand{ints: ints, bufs: bufs}},
m: &wasm.Module{},
}
}
func TestGenerator_newFunctionType(t *testing.T) {
for _, tc := range []struct {
params, results int
exp *wasm.FunctionType
}{
{params: 1, results: 1, exp: &wasm.FunctionType{
Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i64}}},
{params: 1, results: 2, exp: &wasm.FunctionType{
Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i64, f32}}},
{params: 2, results: 2, exp: &wasm.FunctionType{
Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}}},
{params: 0, results: 2, exp: &wasm.FunctionType{
Results: []wasm.ValueType{i32, i64}}},
{params: 2, results: 0, exp: &wasm.FunctionType{
Params: []wasm.ValueType{i32, i64}}},
{params: 6, results: 6, exp: &wasm.FunctionType{
Params: []wasm.ValueType{i32, i64, f32, f64, i32, i32},
Results: []wasm.ValueType{i64, f32, f64, i32, i32, i64},
},
},
} {
t.Run(tc.exp.String(), func(t *testing.T) {
g := newGenerator(0, []int{0, 1, 2, 3, 4}, nil)
actual := g.newFunctionType(tc.params, tc.results)
require.Equal(t, tc.exp.String(), actual.String())
})
}
}
func TestGenerator_newValueType(t *testing.T) {
g := newGenerator(0, []int{0, 1, 2, 3, 4, 5}, nil)
require.Equal(t,
[]wasm.ValueType{i32, i64, f32, f64, i32},
[]wasm.ValueType{g.newValueType(), g.newValueType(), g.newValueType(), g.newValueType(), g.newValueType()},
)
}
func TestGenerator_typeSection(t *testing.T) {
g := newGenerator(100, []int{
// Params = 1, Reults = 1, i32, i32
1, 1, 0, 0,
// Params = 2, Results = 2, i32f32, i64f64
2, 2, 0, 2, 1, 3,
// Params = 0, Results = 4, f64f32i64i32
0, 4, 3, 2, 1, 0,
}, nil)
g.enabledFeature = wasm.FeatureMultiValue
g.numTypes = 3
g.genTypeSection()
expected := []*wasm.FunctionType{
{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}},
{Params: []wasm.ValueType{i32, f32}, Results: []wasm.ValueType{i64, f64}},
{Params: []wasm.ValueType{}, Results: []wasm.ValueType{f64, f32, i64, i32}},
}
require.Equal(t, len(expected), len(g.m.TypeSection))
for i := range expected {
require.Equal(t, expected[i].String(), g.m.TypeSection[i].String())
}
}
func TestGenerator_importSection(t *testing.T) {
five := uint32(5)
for i, tc := range []struct {
ints []int
numImports uint32
types []*wasm.FunctionType
exp []*wasm.Import
}{
{
types: []*wasm.FunctionType{{}},
numImports: 2,
ints: []int{
// function type with type of index = 0 x 2
0, 0,
0, 0,
},
exp: []*wasm.Import{
{Type: wasm.ExternTypeFunc, DescFunc: 0, Module: "module-0", Name: "0"},
{Type: wasm.ExternTypeFunc, DescFunc: 0, Module: "module-1", Name: "1"},
},
},
{
types: []*wasm.FunctionType{{}, {}},
numImports: 2,
ints: []int{
// function type with type of index = 1
0, 1,
// function type with type of index = 0
0, 0,
},
exp: []*wasm.Import{
{Type: wasm.ExternTypeFunc, DescFunc: 1, Module: "module-0", Name: "0"},
{Type: wasm.ExternTypeFunc, DescFunc: 0, Module: "module-1", Name: "1"},
},
},
{
types: []*wasm.FunctionType{{}, {}},
numImports: 2,
ints: []int{
// function type with type of index = 1
0, 1,
// global type with type f32, and mutable
1, 2, 0,
},
exp: []*wasm.Import{
{Type: wasm.ExternTypeFunc, DescFunc: 1, Module: "module-0", Name: "0"},
{Type: wasm.ExternTypeGlobal, DescGlobal: &wasm.GlobalType{
Mutable: true, ValType: f32,
}, Module: "module-1", Name: "1"},
},
},
{
types: []*wasm.FunctionType{{}, {}},
numImports: 2,
ints: []int{
// function type with type of index = 1
0, 1,
// global type with type f64, and immutable
1, 3, 1,
},
exp: []*wasm.Import{
{Type: wasm.ExternTypeFunc, DescFunc: 1, Module: "module-0", Name: "0"},
{Type: wasm.ExternTypeGlobal, DescGlobal: &wasm.GlobalType{
Mutable: false, ValType: f64,
}, Module: "module-1", Name: "1"},
},
},
{
numImports: 2,
ints: []int{
// global type with type i32, and mutable
0 /* func types are not given so 0 should be global */, 0, 0,
// global type with type f64, and immutable
1, 3, 1,
},
exp: []*wasm.Import{
{Type: wasm.ExternTypeGlobal, DescGlobal: &wasm.GlobalType{
Mutable: true, ValType: i32,
}, Module: "module-0", Name: "0"},
{Type: wasm.ExternTypeGlobal, DescGlobal: &wasm.GlobalType{
Mutable: false, ValType: f64,
}, Module: "module-1", Name: "1"},
},
},
{
types: []*wasm.FunctionType{{}, {}},
numImports: 3,
ints: []int{
// Memory import with min=1,max=3,
2, 1, 2, /* max is rand + min = 2 + 1 = 3 */
// Table import with min=1,max=5
2, 1, 4, /* max is rand + min = 4 + 1 = 5 */
// function type with type of index = 1
2, 1,
},
exp: []*wasm.Import{
{Type: wasm.ExternTypeMemory, DescMem: &wasm.Memory{
Min: 1, Max: 3, IsMaxEncoded: true,
}, Module: "module-0", Name: "0"},
{Type: wasm.ExternTypeTable, DescTable: &wasm.Table{
Min: 1, Max: &five,
}, Module: "module-1", Name: "1"},
{Type: wasm.ExternTypeFunc, DescFunc: 1, Module: "module-2", Name: "2"},
},
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
g := newGenerator(100, tc.ints, nil)
g.numImports = uint32(tc.numImports)
g.m.TypeSection = tc.types
g.genImportSection()
require.Equal(t, len(tc.exp), len(g.m.ImportSection))
for i := range tc.exp {
require.Equal(t, tc.exp[i], g.m.ImportSection[i])
}
})
}
}
func TestGenerator_functionSection(t *testing.T) {
t.Run("with types", func(t *testing.T) {
types := make([]*wasm.FunctionType, 10)
g := newGenerator(100, []int{
100001, 105, 102, 100000003, 104, 6, 9, 8, 7, 10000,
}, nil)
g.numFunctions = 10
g.m.TypeSection = types
g.genFunctionSection()
require.Equal(t, []uint32{1, 5, 2, 3, 4, 6, 9, 8, 7, 0}, g.m.FunctionSection)
})
t.Run("without types", func(t *testing.T) {
g := newGenerator(100, []int{10}, nil)
g.genFunctionSection()
require.Equal(t, []uint32(nil), g.m.FunctionSection)
})
}
func TestGenerator_tableSection(t *testing.T) {
t.Run("with table import", func(t *testing.T) {
g := newGenerator(100, []int{10}, nil)
g.m.ImportSection = append(g.m.ImportSection, &wasm.Import{Type: wasm.ExternTypeTable})
g.genTableSection()
require.Nil(t, g.m.TableSection)
})
t.Run("without table import", func(t *testing.T) {
g := newGenerator(100, []int{1, 100}, nil)
g.genTableSection()
expMax := uint32(101)
require.Equal(t, &wasm.Table{Min: 1, Max: &expMax}, g.m.TableSection)
})
}
func TestGenerator_memorySection(t *testing.T) {
t.Run("with memory import", func(t *testing.T) {
g := newGenerator(100, []int{10}, nil)
g.m.ImportSection = append(g.m.ImportSection, &wasm.Import{Type: wasm.ExternTypeMemory})
g.genMemorySection()
require.Nil(t, g.m.MemorySection)
})
t.Run("without memory import", func(t *testing.T) {
g := newGenerator(100, []int{1, 100}, nil)
g.genMemorySection()
require.Equal(t, &wasm.Memory{Min: 1, Max: 101, IsMaxEncoded: true}, g.m.MemorySection)
})
}
func TestGenerator_newConstExpr(t *testing.T) {
for i, tc := range []struct {
ints []int
bufs [][]byte
imports []*wasm.Import
exp *wasm.ConstantExpression
expType wasm.ValueType
}{
{
ints: []int{
0, // i32.const
100, // 100 literal
1, // non-negative
},
exp: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(100)},
expType: i32,
},
{
ints: []int{
0, // i2.const
100, // 100 literal
0, // negative
},
exp: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(-100)},
expType: i32,
},
{
ints: []int{
1, // i64.const
100, // 100 literal
1, // non-negative
},
exp: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(100)},
expType: i64,
},
{
ints: []int{
1, // i64.const
100, // 100 literal
0, // negative
},
exp: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(-100)},
expType: i64,
},
{
ints: []int{2}, // f32.const
bufs: [][]byte{{1, 2, 3, 4}},
exp: &wasm.ConstantExpression{Opcode: wasm.OpcodeF32Const, Data: []byte{1, 2, 3, 4}},
expType: f32,
},
{
ints: []int{3}, // f64.const
bufs: [][]byte{{1, 2, 3, 4, 5, 6, 7, 8}},
exp: &wasm.ConstantExpression{Opcode: wasm.OpcodeF64Const, Data: []byte{1, 2, 3, 4, 5, 6, 7, 8}},
expType: f64,
},
{
ints: []int{
4, // globa.get
1, // 2nd global.
},
imports: []*wasm.Import{
{},
{Type: wasm.ExternTypeGlobal, DescGlobal: &wasm.GlobalType{ValType: i32}},
{},
{Type: wasm.ExternTypeGlobal, DescGlobal: &wasm.GlobalType{ValType: f32}},
},
exp: &wasm.ConstantExpression{Opcode: wasm.OpcodeGlobalGet, Data: []byte{1}},
expType: f32,
},
{
ints: []int{
4, // globa.get
0, // 1st global.
},
imports: []*wasm.Import{
{},
{Type: wasm.ExternTypeGlobal, DescGlobal: &wasm.GlobalType{ValType: i32}},
{},
{Type: wasm.ExternTypeGlobal, DescGlobal: &wasm.GlobalType{ValType: f32}},
},
exp: &wasm.ConstantExpression{Opcode: wasm.OpcodeGlobalGet, Data: []byte{0}},
expType: i32,
},
{
ints: []int{
4, // there's no imports, so this should result in i32.const.
100, // 100 literal
1, // non-negative
},
exp: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(100)},
expType: i32,
},
} {
tc := tc
t.Run(strconv.Itoa(i), func(t *testing.T) {
g := newGenerator(100, tc.ints, tc.bufs)
g.m.ImportSection = tc.imports
actualExpr, actualType := g.newConstExpr()
require.Equal(t, tc.expType, actualType)
require.Equal(t, tc.exp, actualExpr)
})
}
}
func TestGenerator_globalSection(t *testing.T) {
g := newGenerator(100,
[]int{
// i32.const 123, mutable
0 /* i32.const */, 123, 1 /* non negative */, 0, /* mutable */
3 /* f64.const */, 1, /* immutable */
4 /* global.get */, 0 /* global index */, 1, /* immutable */
},
[][]byte{{1, 2, 3, 4, 5, 6, 7, 8}},
)
g.m.ImportSection = []*wasm.Import{{}, {},
{Type: wasm.ExternTypeGlobal, DescGlobal: &wasm.GlobalType{ValType: f32}},
}
g.numGlobals = 3
g.genGlobalSection()
expected := []*wasm.Global{
{
Type: &wasm.GlobalType{ValType: i32, Mutable: true},
Init: &wasm.ConstantExpression{
Opcode: wasm.OpcodeI32Const,
Data: leb128.EncodeInt32(123),
},
},
{
Type: &wasm.GlobalType{ValType: f64, Mutable: false},
Init: &wasm.ConstantExpression{
Opcode: wasm.OpcodeF64Const,
Data: []byte{1, 2, 3, 4, 5, 6, 7, 8},
},
},
{
Type: &wasm.GlobalType{ValType: f32, Mutable: false},
Init: &wasm.ConstantExpression{
Opcode: wasm.OpcodeGlobalGet,
Data: []byte{0},
},
},
}
actual := g.m.GlobalSection
require.Equal(t, len(expected), len(actual))
for i := range actual {
require.Equal(t, expected[i], actual[i])
}
}
func TestGenerator_exportSection(t *testing.T) {
m := &wasm.Module{
FunctionSection: make([]uint32, 10),
GlobalSection: []*wasm.Global{
{Type: &wasm.GlobalType{}},
{Type: &wasm.GlobalType{}},
{Type: &wasm.GlobalType{}},
{Type: &wasm.GlobalType{}},
{Type: &wasm.GlobalType{}},
},
TableSection: &wasm.Table{},
MemorySection: &wasm.Memory{},
}
g := newGenerator(100, []int{
2, // funcs[2]
13, // globals[3]
15, // table
16, // memory
7, // funcs[7]
}, nil)
g.m = m
g.numExports = 5
g.genExportSection()
expected := []*wasm.Export{
{Type: wasm.ExternTypeFunc, Index: 2, Name: "0"},
{Type: wasm.ExternTypeGlobal, Index: 3, Name: "1"},
{Type: wasm.ExternTypeTable, Index: 0, Name: "2"},
{Type: wasm.ExternTypeMemory, Index: 0, Name: "3"},
{Type: wasm.ExternTypeFunc, Index: 7, Name: "4"},
}
actual := g.m.ExportSection
require.Equal(t, len(expected), len(actual))
for i := range actual {
require.Equal(t, expected[i], actual[i])
}
}
func TestGenerator_startSection(t *testing.T) {
t.Run("with cand", func(t *testing.T) {
m := &wasm.Module{
ImportSection: []*wasm.Import{
{Type: wasm.ExternTypeFunc, DescFunc: 0},
{Type: wasm.ExternTypeFunc, DescFunc: 1}, // candidate
{Type: wasm.ExternTypeFunc, DescFunc: 2},
{Type: wasm.ExternTypeFunc, DescFunc: 1}, // candidate
},
FunctionSection: []uint32{
0, 0, 1 /* candidate */, 0,
},
TypeSection: []*wasm.FunctionType{
{Params: []wasm.ValueType{i32}},
{},
{Params: []wasm.ValueType{f32}},
{Params: []wasm.ValueType{f64}},
},
}
takePtr := func(v uint32) *uint32 { return &v }
for i, tc := range []struct {
ints []int
exp *wasm.Index
}{
{ints: []int{0}, exp: takePtr(1)},
{ints: []int{1}, exp: takePtr(3)},
{ints: []int{2}, exp: takePtr(6)},
{ints: []int{3}, exp: takePtr(1)},
} {
tc := tc
t.Run(strconv.Itoa(i), func(t *testing.T) {
g := newGenerator(100, tc.ints, nil)
g.needStartSection = true
g.m = m
g.genStartSection()
require.Equal(t, tc.exp, g.m.StartSection)
})
}
})
t.Run("no cand", func(t *testing.T) {
m := &wasm.Module{
ImportSection: []*wasm.Import{
{Type: wasm.ExternTypeFunc, DescFunc: 0},
{Type: wasm.ExternTypeFunc, DescFunc: 1},
{Type: wasm.ExternTypeFunc, DescFunc: 2},
{Type: wasm.ExternTypeFunc, DescFunc: 1},
},
FunctionSection: []uint32{
0, 1, 2, 0, 1, 2,
},
TypeSection: []*wasm.FunctionType{
{Params: []wasm.ValueType{i32}},
{Params: []wasm.ValueType{f32}},
{Params: []wasm.ValueType{f64}},
},
}
g := newGenerator(100, []int{0}, nil)
g.m = m
g.genStartSection()
require.Nil(t, g.m.StartSection)
})
}
func TestGenerator_elementSection(t *testing.T) {
t.Run("without table", func(t *testing.T) {
g := newGenerator(100, nil, nil)
g.genElementSection()
require.Nil(t, g.m.ElementSection)
})
t.Run("without function", func(t *testing.T) {
g := newGenerator(100, nil, nil)
g.m.TableSection = &wasm.Table{}
g.genElementSection()
require.Nil(t, g.m.ElementSection)
})
t.Run("ok", func(t *testing.T) {
for i, tc := range []struct {
ints []int
numElements uint32
exps []*wasm.ElementSegment
}{
{
numElements: 1,
ints: []int{
2 /* number of inedxes */, 0, 50, 98, /* offset */
},
exps: []*wasm.ElementSegment{
{
OffsetExpr: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(98)},
Init: []wasm.Index{0, 50},
},
},
},
{
numElements: 3,
ints: []int{
2 /* number of inedxes */, 25, 75, 0, /* offset */
1 /* number of inedxes */, 3, 99, /* offset */
10 /* number of inedxes */, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 /* offset */, 90,
},
exps: []*wasm.ElementSegment{
{
OffsetExpr: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(0)},
Init: []wasm.Index{25, 75},
},
{
OffsetExpr: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(99)},
Init: []wasm.Index{3},
},
{
OffsetExpr: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(90)},
Init: []wasm.Index{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
},
},
},
} {
tc := tc
t.Run(strconv.Itoa(i), func(t *testing.T) {
g := newGenerator(100, tc.ints, nil)
g.m = &wasm.Module{
TableSection: &wasm.Table{Min: 100},
FunctionSection: make([]uint32, 100),
}
g.numElements = tc.numElements
g.genElementSection()
actual := g.m.ElementSection
require.Equal(t, len(tc.exps), len(actual))
for i := range actual {
require.Equal(t, tc.exps[i], actual[i])
}
})
}
})
}
func TestGenerator_dataSection(t *testing.T) {
t.Run("without memory", func(t *testing.T) {
g := newGenerator(100, nil, nil)
g.genDataSection()
require.Nil(t, g.m.DataSection)
})
t.Run("min=0", func(t *testing.T) {
g := newGenerator(100, nil, nil)
g.m.MemorySection = &wasm.Memory{Min: 0}
g.genDataSection()
require.Nil(t, g.m.DataSection)
})
t.Run("ok", func(t *testing.T) {
for i, tc := range []struct {
ints []int
bufs [][]byte
numData uint32
exps []*wasm.DataSegment
}{
{
numData: 1,
ints: []int{
5, // offset
2, // size of inits
},
bufs: [][]byte{{0x1, 0x2}},
exps: []*wasm.DataSegment{
{
OffsetExpression: &wasm.ConstantExpression{
Opcode: wasm.OpcodeI32Const,
Data: leb128.EncodeUint32(5),
},
Init: []byte{0x1, 0x2},
},
},
},
{
numData: 1,
ints: []int{
int(wasm.MemoryMaxPages) - 1, // offset
1, // size of inits
},
bufs: [][]byte{{0x1}},
exps: []*wasm.DataSegment{
{
OffsetExpression: &wasm.ConstantExpression{
Opcode: wasm.OpcodeI32Const,
Data: leb128.EncodeUint32(uint32(wasm.MemoryMaxPages) - 1),
},
Init: []byte{0x1},
},
},
},
} {
tc := tc
t.Run(strconv.Itoa(i), func(t *testing.T) {
g := newGenerator(100, tc.ints, tc.bufs)
g.m.MemorySection = &wasm.Memory{Min: 1}
g.numData = tc.numData
g.genDataSection()
actual := g.m.DataSection
require.Equal(t, len(tc.exps), len(actual))
for i := range actual {
require.Equal(t, tc.exps[i], actual[i])
}
})
}
})
}