mirror of
https://github.com/wazero/wazero
synced 2026-06-21 14:12:37 +00:00
bfb20e0ba7
Add experimental support to the Exception Handling spec. Exception handling adds structured error propagation to WebAssembly through `tags` (typed exception signatures), `throw/throw_ref` (raising exceptions), and `try_table` (catching exceptions with typed `catch` clauses). A new value type, `exnref`, represents a reference to a caught exception. Excluding tests and spec suites, the entire feature amounts to a little less than 2000 lines of code. Feature flag: `experimental.CoreFeaturesExceptionHandling` ## What's the use for this? In the spirit of always having a real use case for the specs we implement, I did check this implementation against the one large Wasm C++ codebase I am aware about, i.e. https://github.com/klippa-app/go-pdfium, and I verified that the entire suite, compiled with `emcc+wasm-opt` does work; incidentally, much of the existing support code, based on Emscripten's longjmp/setjmp, can be dropped (thx @jerbob92 for the help!) Broadly speaking, I don't know how much EH-enabled Wasm code you will find in the wild. For instance, code compiled using Clang's C++ backend does not really throw new-style exceptions, unless you compile WASI-SDK with some flags, or you build with `emcc` and translate old-style exceptions using `wasm-opt` (see exceptions_test.go) In short, this spec should be really seen as a step towards supporting the GC spec. The design decisions (esp. regarding the test suite) follow from this reasoning. ## Spec Suite `wast2json` **does not** support the EH suite, but `wasm-tools` does, albeit with a tiny quirk in the way negative numbers are rendered. In this PR _I am not_ moving the other suites to `wasm-tools` though. The spec test suite passes entirely, except for two tests that are intentionally skipped. These tests assume **a very small subset** of typed references is implemented; specifically, the distinction between nullable and non-nullable function references (`(ref null $t)` vs `(ref $t)`). I have decided to just flatten all non-nullable refs to nullable refs at decode time, so we cannot detect the type mismatch the spec expects in these cases. Passing the **full suite is actually straightforward** because it only requires introducing a non-nullable variant for each ref type: strictly speaking the 2 failing tests are only for related nullable vs non-nullable **func refs** -- so I can special-case these and/or add non-nullable types for each known value; however, we will **need** to rethink the representation of Values for the GC proposal anyway (because this will introduce custom type indices, and these won't fit our currently byte-sized `ValueType` enum). The subtype-checking functions (`isRefSubtypeOf`, `isStrictRefSubtypeOf`) are kept as abstraction points but currently they are just equalities. Nevertheless, as proven by the CPP example and https://github.com/klippa-app/go-pdfium this PR provides a starting point that is already useful (even if it is for limited cases). ## Interpreter Exception handling in the interpreter: **Within a frame**, `try_table` blocks are compiled into a static exception table with PC ranges. When `throw` or `throw_ref` executes, `searchExceptionTable` scans backwards (inner handlers first) for a catch clause whose PC range covers the current instruction. If a match is found, `applyExceptionHandler` adjusts the operand stack depth and jumps to the catch target PC directly. **Across frames**, if no handler is found in the current frame, `panic(&thrownException{...})` propagates up the Go call stack. Each cross-function call goes through `callWithUnwind()`, which uses `defer/recover` to intercept the panic. Its `canRestore` method unwinds `ce.frames` to the caller's depth and searches that frame's exception table. If a handler is found, `doRestore` applies it and `callWithUnwind` returns `true` (frame unwound -- caller refreshes locals and continues). If not, the panic re-propagates to the next outer `callWithUnwind`. Note on the implementation: this mechanism is unified with the existing `snapshot/restore` API through the `restorable` interface: both `*thrownException` and `*snapshot` implement `canRestore`/`doRestore`, so `callWithUnwind` handles both with a single defer/recover path. The short-circuit check (`len(exceptionTable) == 0 && no snapshotter`) skips the defer/recover overhead entirely for frames that don't need it. ## Compiler The compiler could not reuse the snapshot/restore mechanism directly, but it follows a similar pattern. One cool thing is that exception handling could be implemented entirely as an SSA-level lowering with new exit codes; there is no backend-specific code. Throwing uses a two-phase ABI: 1. `ExitCodeThrowAlloc`: exits to the Go runtime to allocate an `Exception` struct on the heap and writes a pointer to its params array into the execution context. Then we return control to compiled code, which stores the tag parameters directly into the struct. 2. `ExitCodeThrow`: exits to the Go runtime again to search for a matching handler, restore the stack checkpoint, and branch to the catch target. The idea is that we NEED to allocate the parameter slice dynamically, so we delegate to the Go runtime. Incidentally, this also allows us to unify `throw` and `throwRef`; in the latter case, we skip the `alloc` phase, and we exit straight to `ExitCodeThrow`. ### Trampolines The compiler uses four trampolines to return to host to manage exception state: | Trampoline | Exit Code | Purpose | |---|---|---| | **ThrowAlloc** | `ExitCodeThrowAlloc` | Allocates an `Exception` struct on the Go heap, writes `&exn.Params[0]` into `execCtx` so compiled code can store tag parameters directly. | | **Throw** | `ExitCodeThrow` | Searches `tryHandlers` for a matching catch clause, restores the stack checkpoint on match, sets `caughtExceptionClauseIdx` for the compiled dispatch. Used for both `throw` (after ThrowAlloc) and `throw_ref` (directly). | | **TryTableEnter** | `ExitCodeTryTableEnter` | Clones the current stack as a checkpoint, pushes a `tryHandler` with the catch clause table and saved module instance. Sets `caughtExceptionClauseIdx = -1` (no exception). | | **TryTableLeave** | `ExitCodeTryTableLeave` | Pops the most recent `tryHandler`. Emitted at `try_table` block ends, and before any branch or return that exits a `try_table` scope. | ### Lowering As mentioned earlier, this entirely resolved at the SSA-level. In particular: - **Entry**: when a `try_table` is encountered, the compiler emits a call to the TryTableEnter trampoline. Control returns to compiled code, which reads `caughtExceptionClauseIdx` from the execution context — if it's -1 (no exception), execution continues into the `try_table` body. - **Normal exit**: when execution reaches the `End` opcode of the `try_table` block, a TryTableLeave trampoline call is emitted. However, every TryTableEnter must have a matching TryTableLeave on every control flow path that doesn't throw; so we also "TryTableLeaves" also for the following: - **Early exit (branch)**: a `br`, `br_if`, or `br_table` can jump to a label outside the `try_table`, skipping its `End`. `emitTryTableLeaves(depth)` walks the control frame stack and emits one TryTableLeave for each `try_table` frame the branch crosses. For `br_if`, this happens in a trampoline basic block that only executes on the taken path. - **Early exit (return)**: `return`, `return_call`, and `return_call_indirect` call `emitTryTableLeaves` with the full control stack depth, popping all active handlers before leaving the frame. - **Throw**: on `throw`/`throw_ref`, compiled code exits to the dispatch loop via ExitCodeThrow. `doHandleException` searches `tryHandlers` innermost-to-outermost. On match, it restores the cloned stack checkpoint, sets `caughtExceptionClauseIdx`, and re-enters compiled code at the handler's return address. The compiled dispatch branches to the matching catch clause's handler block. ### Caveats - As described above, we are skipping 2 spec tests (will be addressed when we implement typed references) - Because of that, I am _not_ enabling by default the fuzzer: I would have to special-case for typed references and/or TODOs I have left behind; I did run the fuzzer with some special-casing while debugging the issue with pdfium (#2488) and did not find issues (the fuzzer did go frequently OOM though); in fact, the pdfium issue was actually unrelated (!) --------- Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
1134 lines
35 KiB
Go
1134 lines
35 KiB
Go
package wasm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/tetratelabs/wazero/api"
|
|
"github.com/tetratelabs/wazero/experimental"
|
|
"github.com/tetratelabs/wazero/internal/leb128"
|
|
"github.com/tetratelabs/wazero/internal/testing/require"
|
|
"github.com/tetratelabs/wazero/internal/u32"
|
|
"github.com/tetratelabs/wazero/internal/u64"
|
|
)
|
|
|
|
func TestFunctionType_String(t *testing.T) {
|
|
tests := []struct {
|
|
functype *FunctionType
|
|
exp string
|
|
}{
|
|
{functype: &FunctionType{}, exp: "v_v"},
|
|
{functype: &FunctionType{Params: []ValueType{ValueTypeI32}}, exp: "i32_v"},
|
|
{functype: &FunctionType{Params: []ValueType{ValueTypeI32, ValueTypeF64}}, exp: "i32f64_v"},
|
|
{functype: &FunctionType{Params: []ValueType{ValueTypeF32, ValueTypeI32, ValueTypeF64}}, exp: "f32i32f64_v"},
|
|
{functype: &FunctionType{Results: []ValueType{ValueTypeI64}}, exp: "v_i64"},
|
|
{functype: &FunctionType{Results: []ValueType{ValueTypeI64, ValueTypeF32}}, exp: "v_i64f32"},
|
|
{functype: &FunctionType{Results: []ValueType{ValueTypeF32, ValueTypeI32, ValueTypeF64}}, exp: "v_f32i32f64"},
|
|
{functype: &FunctionType{Params: []ValueType{ValueTypeI32}, Results: []ValueType{ValueTypeI64}}, exp: "i32_i64"},
|
|
{functype: &FunctionType{Params: []ValueType{ValueTypeI64, ValueTypeF32}, Results: []ValueType{ValueTypeI64, ValueTypeF32}}, exp: "i64f32_i64f32"},
|
|
{functype: &FunctionType{Params: []ValueType{ValueTypeI64, ValueTypeF32, ValueTypeF64}, Results: []ValueType{ValueTypeF32, ValueTypeI32, ValueTypeF64}}, exp: "i64f32f64_f32i32f64"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.functype.String(), func(t *testing.T) {
|
|
require.Equal(t, tc.exp, tc.functype.String())
|
|
require.Equal(t, tc.exp, tc.functype.key())
|
|
require.Equal(t, tc.exp, tc.functype.string)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsReferenceValueType(t *testing.T) {
|
|
refTypes := []ValueType{ValueTypeFuncref, ValueTypeExternref, ValueTypeExnref}
|
|
for _, vt := range refTypes {
|
|
require.True(t, isReferenceValueType(vt), "expected %#x to be a reference type", vt)
|
|
}
|
|
nonRefTypes := []ValueType{ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64, ValueTypeV128}
|
|
for _, vt := range nonRefTypes {
|
|
require.False(t, isReferenceValueType(vt), "expected %#x to not be a reference type", vt)
|
|
}
|
|
}
|
|
|
|
func TestSectionIDName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input SectionID
|
|
expected string
|
|
}{
|
|
{"custom", SectionIDCustom, "custom"},
|
|
{"type", SectionIDType, "type"},
|
|
{"import", SectionIDImport, "import"},
|
|
{"function", SectionIDFunction, "function"},
|
|
{"table", SectionIDTable, "table"},
|
|
{"memory", SectionIDMemory, "memory"},
|
|
{"global", SectionIDGlobal, "global"},
|
|
{"export", SectionIDExport, "export"},
|
|
{"start", SectionIDStart, "start"},
|
|
{"element", SectionIDElement, "element"},
|
|
{"code", SectionIDCode, "code"},
|
|
{"data", SectionIDData, "data"},
|
|
{"unknown", 100, "unknown"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
require.Equal(t, tc.expected, SectionIDName(tc.input))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMemory_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mem *Memory
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "ok",
|
|
mem: &Memory{Min: 2, Cap: 2, Max: 2},
|
|
},
|
|
{
|
|
name: "cap < min",
|
|
mem: &Memory{Min: 2, Cap: 1, Max: 2},
|
|
expectedErr: "capacity 1 pages (64 Ki) less than minimum 2 pages (128 Ki)",
|
|
},
|
|
{
|
|
name: "cap > maxLimit",
|
|
mem: &Memory{Min: 2, Cap: math.MaxUint32, Max: 2},
|
|
expectedErr: "capacity 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
|
|
},
|
|
{
|
|
name: "max < min",
|
|
mem: &Memory{Min: 2, Cap: 2, Max: 0, IsMaxEncoded: true},
|
|
expectedErr: "min 2 pages (128 Ki) > max 0 pages (0 Ki)",
|
|
},
|
|
{
|
|
name: "min > limit",
|
|
mem: &Memory{Min: math.MaxUint32},
|
|
expectedErr: "min 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
|
|
},
|
|
{
|
|
name: "max > limit",
|
|
mem: &Memory{Max: math.MaxUint32, IsMaxEncoded: true},
|
|
expectedErr: "max 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.mem.Validate(MemoryLimitPages)
|
|
if tc.expectedErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.EqualError(t, err, tc.expectedErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestModule_allDeclarations(t *testing.T) {
|
|
tests := []struct {
|
|
module *Module
|
|
expectedFunctions []Index
|
|
expectedGlobals []GlobalType
|
|
expectedMemory *Memory
|
|
expectedTables []Table
|
|
expectedTags []Index
|
|
}{
|
|
// Functions.
|
|
{
|
|
module: &Module{
|
|
ImportSection: []Import{{Type: ExternTypeFunc, DescFunc: 10000}},
|
|
FunctionSection: []Index{10, 20, 30},
|
|
},
|
|
expectedFunctions: []Index{10000, 10, 20, 30},
|
|
},
|
|
{
|
|
module: &Module{
|
|
FunctionSection: []Index{10, 20, 30},
|
|
},
|
|
expectedFunctions: []Index{10, 20, 30},
|
|
},
|
|
{
|
|
module: &Module{
|
|
ImportSection: []Import{{Type: ExternTypeFunc, DescFunc: 10000}},
|
|
},
|
|
expectedFunctions: []Index{10000},
|
|
},
|
|
// Globals.
|
|
{
|
|
module: &Module{
|
|
ImportSection: []Import{{Type: ExternTypeGlobal, DescGlobal: GlobalType{Mutable: false}}},
|
|
GlobalSection: []Global{{Type: GlobalType{Mutable: true}}},
|
|
},
|
|
expectedGlobals: []GlobalType{{Mutable: false}, {Mutable: true}},
|
|
},
|
|
{
|
|
module: &Module{
|
|
GlobalSection: []Global{{Type: GlobalType{Mutable: true}}},
|
|
},
|
|
expectedGlobals: []GlobalType{{Mutable: true}},
|
|
},
|
|
{
|
|
module: &Module{
|
|
ImportSection: []Import{{Type: ExternTypeGlobal, DescGlobal: GlobalType{Mutable: false}}},
|
|
},
|
|
expectedGlobals: []GlobalType{{Mutable: false}},
|
|
},
|
|
// Memories.
|
|
{
|
|
module: &Module{
|
|
ImportSection: []Import{{Type: ExternTypeMemory, DescMem: &Memory{Min: 1, Max: 10}}},
|
|
},
|
|
expectedMemory: &Memory{Min: 1, Max: 10},
|
|
},
|
|
{
|
|
module: &Module{
|
|
MemorySection: &Memory{Min: 100},
|
|
},
|
|
expectedMemory: &Memory{Min: 100},
|
|
},
|
|
// Tables.
|
|
{
|
|
module: &Module{
|
|
ImportSection: []Import{{Type: ExternTypeTable, DescTable: Table{Min: 1}}},
|
|
},
|
|
expectedTables: []Table{{Min: 1}},
|
|
},
|
|
{
|
|
module: &Module{
|
|
TableSection: []Table{{Min: 10}},
|
|
},
|
|
expectedTables: []Table{{Min: 10}},
|
|
},
|
|
// Tags.
|
|
{
|
|
module: &Module{
|
|
ImportSection: []Import{{Type: ExternTypeTag, DescTag: 5}},
|
|
},
|
|
expectedTags: []Index{5},
|
|
},
|
|
{
|
|
module: &Module{
|
|
TagSection: []Tag{{Type: 3}},
|
|
},
|
|
expectedTags: []Index{3},
|
|
},
|
|
{
|
|
module: &Module{
|
|
ImportSection: []Import{{Type: ExternTypeTag, DescTag: 5}},
|
|
TagSection: []Tag{{Type: 3}},
|
|
},
|
|
expectedTags: []Index{5, 3},
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
tc := tt
|
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
|
functions, globals, memory, tables, tags, err := tc.module.AllDeclarations()
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expectedFunctions, functions)
|
|
require.Equal(t, tc.expectedGlobals, globals)
|
|
require.Equal(t, tc.expectedTables, tables)
|
|
require.Equal(t, tc.expectedMemory, memory)
|
|
require.Equal(t, tc.expectedTags, tags)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateConstExpression(t *testing.T) {
|
|
t.Run("invalid opcode", func(t *testing.T) {
|
|
expr := NewConstantExpressionFromOpcode(OpcodeNop, nil)
|
|
err := validateConstExpression(nil, 0, &expr, valueTypeUnknown)
|
|
require.Error(t, err)
|
|
})
|
|
for _, vt := range []ValueType{ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64} {
|
|
t.Run(ValueTypeName(vt), func(t *testing.T) {
|
|
t.Run("valid", func(t *testing.T) {
|
|
var expr ConstantExpression
|
|
switch vt {
|
|
case ValueTypeI32:
|
|
expr = NewConstantExpressionFromI32(1)
|
|
case ValueTypeI64:
|
|
expr = NewConstantExpressionFromI64(2)
|
|
case ValueTypeF32:
|
|
expr = NewConstantExpressionFromOpcode(OpcodeF32Const, u32.LeBytes(uint32(api.EncodeF32(math.MaxFloat32))))
|
|
case ValueTypeF64:
|
|
expr = NewConstantExpressionFromOpcode(OpcodeF64Const, u64.LeBytes(api.EncodeF64(math.MaxFloat64)))
|
|
}
|
|
|
|
err := validateConstExpression(nil, 0, &expr, vt)
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("invalid", func(t *testing.T) {
|
|
// Empty data must be failure.
|
|
var expr ConstantExpression
|
|
switch vt {
|
|
case ValueTypeI32:
|
|
expr = NewConstantExpressionFromOpcode(OpcodeI32Const, nil)
|
|
case ValueTypeI64:
|
|
expr = NewConstantExpressionFromOpcode(OpcodeI64Const, nil)
|
|
case ValueTypeF32:
|
|
expr = NewConstantExpressionFromOpcode(OpcodeF32Const, nil)
|
|
case ValueTypeF64:
|
|
expr = NewConstantExpressionFromOpcode(OpcodeF64Const, nil)
|
|
}
|
|
err := validateConstExpression(nil, 0, &expr, vt)
|
|
require.Error(t, err)
|
|
})
|
|
})
|
|
}
|
|
t.Run("ref types", func(t *testing.T) {
|
|
t.Run("ref.func", func(t *testing.T) {
|
|
expr := NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{5})
|
|
err := validateConstExpression(nil, 10, &expr, ValueTypeFuncref)
|
|
require.NoError(t, err)
|
|
err = validateConstExpression(nil, 2, &expr, ValueTypeFuncref)
|
|
require.EqualError(t, err, "ref.func index out of range [5] with length 1")
|
|
})
|
|
t.Run("ref.null", func(t *testing.T) {
|
|
expr := NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeFuncref})
|
|
err := validateConstExpression(nil, 0,
|
|
&expr,
|
|
ValueTypeFuncref)
|
|
require.NoError(t, err)
|
|
expr = NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeExternref})
|
|
err = validateConstExpression(nil, 0,
|
|
&expr,
|
|
ValueTypeExternref)
|
|
require.NoError(t, err)
|
|
expr = NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{0xff})
|
|
err = validateConstExpression(nil, 0,
|
|
&expr,
|
|
ValueTypeExternref)
|
|
require.EqualError(t, err, "invalid type for ref.null: 0xff")
|
|
})
|
|
})
|
|
t.Run("global expr", func(t *testing.T) {
|
|
t.Run("failed to read global index", func(t *testing.T) {
|
|
// Empty data for global index is invalid.
|
|
expr := NewConstantExpressionFromOpcode(OpcodeGlobalGet, make([]byte, 0))
|
|
err := validateConstExpression(nil, 0, &expr, valueTypeUnknown)
|
|
require.Error(t, err)
|
|
})
|
|
t.Run("global index out of range", func(t *testing.T) {
|
|
// Data holds the index in leb128 and this time the value exceeds len(globals) (=0).
|
|
expr := NewConstantExpressionFromOpcode(OpcodeGlobalGet, []byte{1})
|
|
var globals []GlobalType
|
|
err := validateConstExpression(globals, 0, &expr, valueTypeUnknown)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("type mismatch", func(t *testing.T) {
|
|
for _, vt := range []ValueType{
|
|
ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64,
|
|
} {
|
|
t.Run(ValueTypeName(vt), func(t *testing.T) {
|
|
// The index specified in Data equals zero.
|
|
expr := NewConstantExpressionFromOpcode(OpcodeGlobalGet, []byte{0})
|
|
globals := []GlobalType{{ValType: valueTypeUnknown}}
|
|
|
|
err := validateConstExpression(globals, 0, &expr, vt)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
})
|
|
t.Run("ok", func(t *testing.T) {
|
|
for _, vt := range []ValueType{
|
|
ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64,
|
|
} {
|
|
t.Run(ValueTypeName(vt), func(t *testing.T) {
|
|
// The index specified in Data equals zero.
|
|
expr := NewConstantExpressionFromOpcode(OpcodeGlobalGet, []byte{0})
|
|
globals := []GlobalType{{ValType: vt}}
|
|
|
|
err := validateConstExpression(globals, 0, &expr, vt)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestModule_Validate_Errors(t *testing.T) {
|
|
zero := Index(0)
|
|
tests := []struct {
|
|
name string
|
|
input *Module
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "StartSection points to an invalid func",
|
|
input: &Module{
|
|
TypeSection: nil,
|
|
FunctionSection: []uint32{0},
|
|
CodeSection: []Code{{Body: []byte{OpcodeEnd}}},
|
|
StartSection: &zero,
|
|
},
|
|
expectedErr: "invalid start function: func[0] has an invalid type",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.input.Validate(api.CoreFeaturesV1)
|
|
require.EqualError(t, err, tc.expectedErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestModule_validateStartSection(t *testing.T) {
|
|
t.Run("no start section", func(t *testing.T) {
|
|
m := Module{}
|
|
err := m.validateStartSection()
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("invalid type", func(t *testing.T) {
|
|
for _, ft := range []FunctionType{
|
|
{Params: []ValueType{ValueTypeI32}},
|
|
{Results: []ValueType{ValueTypeI32}},
|
|
{Params: []ValueType{ValueTypeI32}, Results: []ValueType{ValueTypeI32}},
|
|
} {
|
|
t.Run(ft.String(), func(t *testing.T) {
|
|
index := uint32(0)
|
|
m := Module{StartSection: &index, FunctionSection: []uint32{0}, TypeSection: []FunctionType{ft}}
|
|
err := m.validateStartSection()
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
})
|
|
t.Run("imported valid func", func(t *testing.T) {
|
|
index := Index(1)
|
|
m := Module{
|
|
StartSection: &index,
|
|
TypeSection: []FunctionType{{}, {Results: []ValueType{ValueTypeI32}}},
|
|
ImportFunctionCount: 2,
|
|
ImportSection: []Import{
|
|
{Type: ExternTypeFunc, DescFunc: 1},
|
|
// import with index 1 is global but this should be skipped when searching imported functions.
|
|
{Type: ExternTypeGlobal},
|
|
{Type: ExternTypeFunc, DescFunc: 0}, // This one must be selected.
|
|
},
|
|
}
|
|
err := m.validateStartSection()
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestModule_validateGlobals(t *testing.T) {
|
|
t.Run("too many globals", func(t *testing.T) {
|
|
m := Module{}
|
|
err := m.validateGlobals(make([]GlobalType, 10), 0, 9)
|
|
require.Error(t, err)
|
|
require.EqualError(t, err, "too many globals in a module")
|
|
})
|
|
t.Run("global index out of range", func(t *testing.T) {
|
|
m := Module{GlobalSection: []Global{
|
|
{
|
|
Type: GlobalType{ValType: ValueTypeI32},
|
|
// Trying to reference globals[1] which is not imported.
|
|
Init: NewConstantExpressionFromOpcode(OpcodeGlobalGet, []byte{1}),
|
|
},
|
|
}}
|
|
err := m.validateGlobals(nil, 0, 9)
|
|
require.Error(t, err)
|
|
require.EqualError(t, err, "global index out of range")
|
|
})
|
|
t.Run("invalid const expression", func(t *testing.T) {
|
|
m := Module{GlobalSection: []Global{
|
|
{
|
|
Type: GlobalType{ValType: valueTypeUnknown},
|
|
Init: NewConstantExpressionFromOpcode(OpcodeUnreachable, nil),
|
|
},
|
|
}}
|
|
err := m.validateGlobals(nil, 0, 9)
|
|
require.Error(t, err)
|
|
require.EqualError(t, err, "invalid opcode for const expression: 0x0")
|
|
})
|
|
t.Run("ok", func(t *testing.T) {
|
|
m := Module{GlobalSection: []Global{
|
|
{
|
|
Type: GlobalType{ValType: ValueTypeI32},
|
|
Init: NewConstantExpressionFromI32(0),
|
|
},
|
|
}}
|
|
err := m.validateGlobals(nil, 0, 9)
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("ok with imported global", func(t *testing.T) {
|
|
m := Module{
|
|
ImportGlobalCount: 1,
|
|
GlobalSection: []Global{
|
|
{
|
|
Type: GlobalType{ValType: ValueTypeI32},
|
|
// Trying to reference globals[1] which is imported.
|
|
Init: NewConstantExpressionFromOpcode(OpcodeGlobalGet, []byte{0}),
|
|
},
|
|
},
|
|
ImportSection: []Import{{Type: ExternTypeGlobal}},
|
|
}
|
|
globalDeclarations := []GlobalType{
|
|
{ValType: ValueTypeI32}, // Imported one.
|
|
{}, // the local one trying to validate.
|
|
}
|
|
err := m.validateGlobals(globalDeclarations, 0, 9)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestModule_validateFunctions(t *testing.T) {
|
|
t.Run("ok", func(t *testing.T) {
|
|
m := Module{
|
|
TypeSection: []FunctionType{v_v},
|
|
FunctionSection: []uint32{0},
|
|
CodeSection: []Code{{Body: []byte{OpcodeI32Const, 0, OpcodeDrop, OpcodeEnd}}},
|
|
}
|
|
err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex)
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("too many functions", func(t *testing.T) {
|
|
m := Module{}
|
|
err := m.validateFunctions(api.CoreFeaturesV1, []uint32{1, 2, 3, 4}, nil, nil, nil, nil, 3)
|
|
require.Error(t, err)
|
|
require.EqualError(t, err, "too many functions (4) in a module")
|
|
})
|
|
t.Run("function, but no code", func(t *testing.T) {
|
|
m := Module{
|
|
TypeSection: []FunctionType{v_v},
|
|
FunctionSection: []Index{0},
|
|
CodeSection: nil,
|
|
}
|
|
err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex)
|
|
require.Error(t, err)
|
|
require.EqualError(t, err, "code count (0) != function count (1)")
|
|
})
|
|
t.Run("function out of range of code", func(t *testing.T) {
|
|
m := Module{
|
|
TypeSection: []FunctionType{v_v},
|
|
FunctionSection: []Index{1},
|
|
CodeSection: []Code{{Body: []byte{OpcodeEnd}}},
|
|
}
|
|
err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex)
|
|
require.Error(t, err)
|
|
require.EqualError(t, err, "invalid function[0]: type section index 1 out of range")
|
|
})
|
|
t.Run("invalid", func(t *testing.T) {
|
|
m := Module{
|
|
TypeSection: []FunctionType{v_v},
|
|
FunctionSection: []Index{0},
|
|
CodeSection: []Code{{Body: []byte{OpcodeF32Abs}}},
|
|
}
|
|
err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "invalid function[0]: cannot pop the 1st f32 operand")
|
|
})
|
|
t.Run("in- exported", func(t *testing.T) {
|
|
m := Module{
|
|
TypeSection: []FunctionType{v_v},
|
|
FunctionSection: []Index{0},
|
|
CodeSection: []Code{{Body: []byte{OpcodeF32Abs}}},
|
|
ExportSection: []Export{{Name: "f1", Type: ExternTypeFunc, Index: 0}},
|
|
}
|
|
err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), `invalid function[0] export["f1"]: cannot pop the 1st f32`)
|
|
})
|
|
t.Run("in- exported after import", func(t *testing.T) {
|
|
m := Module{
|
|
ImportFunctionCount: 1,
|
|
TypeSection: []FunctionType{v_v},
|
|
ImportSection: []Import{{Type: ExternTypeFunc}},
|
|
FunctionSection: []Index{0},
|
|
CodeSection: []Code{{Body: []byte{OpcodeF32Abs}}},
|
|
ExportSection: []Export{{Name: "f1", Type: ExternTypeFunc, Index: 1}},
|
|
}
|
|
err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), `invalid function[0] export["f1"]: cannot pop the 1st f32`)
|
|
})
|
|
t.Run("in- exported twice", func(t *testing.T) {
|
|
m := Module{
|
|
TypeSection: []FunctionType{v_v},
|
|
FunctionSection: []Index{0},
|
|
CodeSection: []Code{{Body: []byte{OpcodeF32Abs}}},
|
|
ExportSection: []Export{
|
|
{Name: "f1", Type: ExternTypeFunc, Index: 0},
|
|
{Name: "f2", Type: ExternTypeFunc, Index: 0},
|
|
},
|
|
}
|
|
err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), `invalid function[0] export["f1","f2"]: cannot pop the 1st f32`)
|
|
})
|
|
}
|
|
|
|
func TestModule_validateMemory(t *testing.T) {
|
|
t.Run("active data segment exits but memory not declared", func(t *testing.T) {
|
|
m := Module{DataSection: []DataSegment{{OffsetExpression: ConstantExpression{}}}}
|
|
err := m.validateMemory(nil, nil, api.CoreFeaturesV1)
|
|
require.Error(t, err)
|
|
require.Contains(t, "unknown memory", err.Error())
|
|
})
|
|
t.Run("invalid const expr", func(t *testing.T) {
|
|
m := Module{DataSection: []DataSegment{{
|
|
OffsetExpression: NewConstantExpressionFromOpcode(OpcodeUnreachable, nil),
|
|
}}}
|
|
err := m.validateMemory(&Memory{}, nil, api.CoreFeaturesV1)
|
|
require.EqualError(t, err, "calculate offset: invalid opcode for const expression: 0x0")
|
|
})
|
|
t.Run("ok", func(t *testing.T) {
|
|
m := Module{DataSection: []DataSegment{{
|
|
Init: []byte{0x1},
|
|
OffsetExpression: NewConstantExpressionFromI32(1),
|
|
}}}
|
|
err := m.validateMemory(&Memory{}, nil, api.CoreFeaturesV1)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestModule_validateImports(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
enabledFeatures api.CoreFeatures
|
|
i *Import
|
|
expectedErr string
|
|
}{
|
|
{name: "empty import section"},
|
|
{
|
|
name: "reject empty named module",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
i: &Import{Module: "", Name: "n", Type: ExternTypeFunc, DescFunc: 0},
|
|
expectedErr: "import[0] has an empty module name",
|
|
},
|
|
{
|
|
name: "func",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
i: &Import{Module: "m", Name: "n", Type: ExternTypeFunc, DescFunc: 0},
|
|
},
|
|
{
|
|
name: "func type index out of range ",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
i: &Import{Module: "m", Name: "n", Type: ExternTypeFunc, DescFunc: 100},
|
|
expectedErr: "invalid import[\"m\".\"n\"] function: type index out of range",
|
|
},
|
|
{
|
|
name: "global var disabled",
|
|
enabledFeatures: api.CoreFeaturesV1.SetEnabled(api.CoreFeatureMutableGlobal, false),
|
|
i: &Import{
|
|
Module: "m",
|
|
Name: "n",
|
|
Type: ExternTypeGlobal,
|
|
DescGlobal: GlobalType{ValType: ValueTypeI32, Mutable: true},
|
|
},
|
|
expectedErr: `invalid import["m"."n"] global: feature "mutable-global" is disabled`,
|
|
},
|
|
{
|
|
name: "table",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
i: &Import{
|
|
Module: "m",
|
|
Name: "n",
|
|
Type: ExternTypeTable,
|
|
DescTable: Table{Min: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "memory",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
i: &Import{
|
|
Module: "m",
|
|
Name: "n",
|
|
Type: ExternTypeMemory,
|
|
DescMem: &Memory{Min: 1},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
m := Module{TypeSection: []FunctionType{{}}}
|
|
if tc.i != nil {
|
|
m.ImportSection = []Import{*tc.i}
|
|
}
|
|
err := m.validateImports(tc.enabledFeatures)
|
|
if tc.expectedErr != "" {
|
|
require.EqualError(t, err, tc.expectedErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestModule_validateExports(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
enabledFeatures api.CoreFeatures
|
|
exportSection []Export
|
|
functions []Index
|
|
globals []GlobalType
|
|
memory *Memory
|
|
tables []Table
|
|
expectedErr string
|
|
}{
|
|
{name: "empty export section", exportSection: []Export{}},
|
|
{
|
|
name: "func",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
exportSection: []Export{{Type: ExternTypeFunc, Index: 0}},
|
|
functions: []Index{100 /* arbitrary type id*/},
|
|
},
|
|
{
|
|
name: "func out of range",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
exportSection: []Export{{Type: ExternTypeFunc, Index: 1, Name: "e"}},
|
|
functions: []Index{100 /* arbitrary type id*/},
|
|
expectedErr: `unknown function for export["e"]`,
|
|
},
|
|
{
|
|
name: "global const",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
exportSection: []Export{{Type: ExternTypeGlobal, Index: 0}},
|
|
globals: []GlobalType{{ValType: ValueTypeI32}},
|
|
},
|
|
{
|
|
name: "global var",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
exportSection: []Export{{Type: ExternTypeGlobal, Index: 0}},
|
|
globals: []GlobalType{{ValType: ValueTypeI32, Mutable: true}},
|
|
},
|
|
{
|
|
name: "global var disabled",
|
|
enabledFeatures: api.CoreFeaturesV1.SetEnabled(api.CoreFeatureMutableGlobal, false),
|
|
exportSection: []Export{{Type: ExternTypeGlobal, Index: 0, Name: "e"}},
|
|
globals: []GlobalType{{ValType: ValueTypeI32, Mutable: true}},
|
|
expectedErr: `invalid export["e"] global[0]: feature "mutable-global" is disabled`,
|
|
},
|
|
{
|
|
name: "global out of range",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
exportSection: []Export{{Type: ExternTypeGlobal, Index: 1, Name: "e"}},
|
|
globals: []GlobalType{{}},
|
|
expectedErr: `unknown global for export["e"]`,
|
|
},
|
|
{
|
|
name: "table",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
exportSection: []Export{{Type: ExternTypeTable, Index: 0}},
|
|
tables: []Table{{}},
|
|
},
|
|
{
|
|
name: "multiple tables",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
exportSection: []Export{{Type: ExternTypeTable, Index: 0}, {Type: ExternTypeTable, Index: 1}, {Type: ExternTypeTable, Index: 2}},
|
|
tables: []Table{{}, {}, {}},
|
|
},
|
|
{
|
|
name: "table out of range",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
exportSection: []Export{{Type: ExternTypeTable, Index: 1, Name: "e"}},
|
|
tables: []Table{},
|
|
expectedErr: `table for export["e"] out of range`,
|
|
},
|
|
{
|
|
name: "memory",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
exportSection: []Export{{Type: ExternTypeMemory, Index: 0}},
|
|
memory: &Memory{},
|
|
},
|
|
{
|
|
name: "memory out of range",
|
|
enabledFeatures: api.CoreFeaturesV1,
|
|
exportSection: []Export{{Type: ExternTypeMemory, Index: 0, Name: "e"}},
|
|
tables: []Table{},
|
|
expectedErr: `memory for export["e"] out of range`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
m := Module{ExportSection: tc.exportSection}
|
|
err := m.validateExports(tc.enabledFeatures, tc.functions, tc.globals, tc.memory, tc.tables, nil)
|
|
if tc.expectedErr != "" {
|
|
require.EqualError(t, err, tc.expectedErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestModule_buildGlobals(t *testing.T) {
|
|
const localFuncRefInstructionIndex = uint32(0xffff)
|
|
|
|
minusOne := int32(-1)
|
|
m := &Module{
|
|
ImportGlobalCount: 2,
|
|
GlobalSection: []Global{
|
|
{
|
|
Type: GlobalType{Mutable: true, ValType: ValueTypeF64},
|
|
Init: NewConstantExpressionFromOpcode(OpcodeF64Const, u64.LeBytes(api.EncodeF64(math.MaxFloat64))),
|
|
},
|
|
{
|
|
Type: GlobalType{Mutable: false, ValType: ValueTypeI32},
|
|
Init: NewConstantExpressionFromI32(math.MaxInt32),
|
|
},
|
|
{
|
|
Type: GlobalType{Mutable: false, ValType: ValueTypeI32},
|
|
Init: NewConstantExpressionFromI32(minusOne),
|
|
},
|
|
{
|
|
Type: GlobalType{Mutable: false, ValType: ValueTypeV128},
|
|
Init: NewConstantExpressionFromOpcode(OpcodeVecV128Const, []byte{
|
|
1, 0, 0, 0, 0, 0, 0, 0,
|
|
2, 0, 0, 0, 0, 0, 0, 0,
|
|
}),
|
|
},
|
|
{
|
|
Type: GlobalType{Mutable: false, ValType: ValueTypeExternref},
|
|
Init: NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeExternref}),
|
|
},
|
|
{
|
|
Type: GlobalType{Mutable: false, ValType: ValueTypeFuncref},
|
|
Init: NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeFuncref}),
|
|
},
|
|
{
|
|
Type: GlobalType{Mutable: false, ValType: ValueTypeFuncref},
|
|
Init: NewConstantExpressionFromOpcode(OpcodeRefFunc, leb128.EncodeUint32(localFuncRefInstructionIndex)),
|
|
},
|
|
{
|
|
Type: GlobalType{Mutable: false, ValType: ValueTypeExternref},
|
|
Init: NewConstantExpressionFromOpcode(OpcodeGlobalGet, []byte{0}),
|
|
},
|
|
{
|
|
Type: GlobalType{Mutable: false, ValType: ValueTypeFuncref},
|
|
Init: NewConstantExpressionFromOpcode(OpcodeGlobalGet, []byte{1}),
|
|
},
|
|
},
|
|
}
|
|
|
|
imported := []*GlobalInstance{
|
|
{Type: GlobalType{ValType: ValueTypeExternref}, Val: 0x54321},
|
|
{Type: GlobalType{ValType: ValueTypeFuncref}, Val: 0x12345},
|
|
}
|
|
|
|
mi := &ModuleInstance{
|
|
Globals: make([]*GlobalInstance, m.ImportGlobalCount+uint32(len(m.GlobalSection))),
|
|
Engine: &mockModuleEngine{},
|
|
}
|
|
|
|
mi.Globals[0], mi.Globals[1] = imported[0], imported[1]
|
|
|
|
mi.buildGlobals(m, func(funcIndex Index) Reference {
|
|
require.Equal(t, localFuncRefInstructionIndex, funcIndex)
|
|
return 0x99999
|
|
})
|
|
expectedGlobals := []*GlobalInstance{
|
|
imported[0], imported[1],
|
|
{Type: GlobalType{ValType: ValueTypeF64, Mutable: true}, Val: api.EncodeF64(math.MaxFloat64)},
|
|
{Type: GlobalType{ValType: ValueTypeI32, Mutable: false}, Val: uint64(int32(math.MaxInt32))},
|
|
// Higher bits are must be zeroed for i32 globals, not signed-extended. See #656.
|
|
{Type: GlobalType{ValType: ValueTypeI32, Mutable: false}, Val: uint64(uint32(minusOne))},
|
|
{Type: GlobalType{ValType: ValueTypeV128, Mutable: false}, Val: 0x1, ValHi: 0x2},
|
|
{Type: GlobalType{ValType: ValueTypeExternref, Mutable: false}, Val: 0},
|
|
{Type: GlobalType{ValType: ValueTypeFuncref, Mutable: false}, Val: 0},
|
|
{Type: GlobalType{ValType: ValueTypeFuncref, Mutable: false}, Val: 0x99999},
|
|
{Type: GlobalType{ValType: ValueTypeExternref, Mutable: false}, Val: 0x54321},
|
|
{Type: GlobalType{ValType: ValueTypeFuncref, Mutable: false}, Val: 0x12345},
|
|
}
|
|
require.Equal(t, expectedGlobals, mi.Globals)
|
|
}
|
|
|
|
func TestModule_buildMemoryInstance(t *testing.T) {
|
|
t.Run("nil", func(t *testing.T) {
|
|
m := ModuleInstance{}
|
|
m.buildMemory(&Module{}, nil)
|
|
require.Nil(t, m.MemoryInstance)
|
|
})
|
|
t.Run("non-nil", func(t *testing.T) {
|
|
min := uint32(1)
|
|
max := uint32(10)
|
|
mDef := MemoryDefinition{moduleName: "foo"}
|
|
m := ModuleInstance{}
|
|
m.buildMemory(&Module{
|
|
MemorySection: &Memory{Min: min, Cap: min, Max: max},
|
|
MemoryDefinitionSection: []MemoryDefinition{mDef},
|
|
}, nil)
|
|
mem := m.MemoryInstance
|
|
require.Equal(t, min, mem.Min)
|
|
require.Equal(t, max, mem.Max)
|
|
require.Equal(t, &mDef, mem.definition)
|
|
})
|
|
}
|
|
|
|
func TestModule_validateDataCountSection(t *testing.T) {
|
|
t.Run("ok", func(t *testing.T) {
|
|
for _, m := range []*Module{
|
|
{
|
|
DataSection: []DataSegment{},
|
|
DataCountSection: nil,
|
|
},
|
|
{
|
|
DataSection: []DataSegment{{}, {}},
|
|
DataCountSection: nil,
|
|
},
|
|
} {
|
|
err := m.validateDataCountSection()
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
t.Run("error", func(t *testing.T) {
|
|
count := uint32(1)
|
|
for _, m := range []*Module{
|
|
{
|
|
DataSection: []DataSegment{},
|
|
DataCountSection: &count,
|
|
},
|
|
{
|
|
DataSection: []DataSegment{{}, {}},
|
|
DataCountSection: &count,
|
|
},
|
|
} {
|
|
err := m.validateDataCountSection()
|
|
require.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestModule_declaredFunctionIndexes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mod *Module
|
|
exp map[Index]struct{}
|
|
expErr string
|
|
}{
|
|
{
|
|
name: "empty",
|
|
mod: &Module{},
|
|
exp: map[uint32]struct{}{},
|
|
},
|
|
{
|
|
name: "global",
|
|
mod: &Module{
|
|
ExportSection: []Export{
|
|
{Index: 10, Type: ExternTypeFunc},
|
|
{Index: 1000, Type: ExternTypeGlobal},
|
|
},
|
|
},
|
|
exp: map[uint32]struct{}{10: {}},
|
|
},
|
|
{
|
|
name: "export",
|
|
mod: &Module{
|
|
ExportSection: []Export{
|
|
{Index: 1000, Type: ExternTypeGlobal},
|
|
{Index: 10, Type: ExternTypeFunc},
|
|
},
|
|
},
|
|
exp: map[uint32]struct{}{10: {}},
|
|
},
|
|
{
|
|
name: "element",
|
|
mod: &Module{
|
|
ElementSection: []ElementSegment{
|
|
{
|
|
Mode: ElementModeActive,
|
|
Init: []ConstantExpression{
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{0}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeExternref}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{5}),
|
|
},
|
|
},
|
|
{
|
|
Mode: ElementModeDeclarative,
|
|
Init: []ConstantExpression{
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{1}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeExternref}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{5}),
|
|
},
|
|
},
|
|
{
|
|
Mode: ElementModePassive,
|
|
Init: []ConstantExpression{
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{5}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{2}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeExternref}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeExternref}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
exp: map[uint32]struct{}{0: {}, 1: {}, 2: {}, 5: {}},
|
|
},
|
|
{
|
|
name: "all",
|
|
mod: &Module{
|
|
ExportSection: []Export{
|
|
{Index: 10, Type: ExternTypeGlobal},
|
|
{Index: 1000, Type: ExternTypeFunc},
|
|
},
|
|
GlobalSection: []Global{
|
|
{
|
|
Init: NewConstantExpressionFromI32(-1),
|
|
},
|
|
{
|
|
Init: NewConstantExpressionFromOpcode(OpcodeRefFunc, leb128.EncodeInt32(123)),
|
|
},
|
|
},
|
|
ElementSection: []ElementSegment{
|
|
{
|
|
Mode: ElementModeActive,
|
|
Init: []ConstantExpression{
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{0}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeExternref}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{5}),
|
|
},
|
|
},
|
|
{
|
|
Mode: ElementModeDeclarative,
|
|
Init: []ConstantExpression{
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{1}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeExternref}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{5}),
|
|
},
|
|
},
|
|
{
|
|
Mode: ElementModePassive,
|
|
Init: []ConstantExpression{
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{5}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefFunc, []byte{2}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeExternref}),
|
|
NewConstantExpressionFromOpcode(OpcodeRefNull, []byte{ValueTypeExternref}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
exp: map[uint32]struct{}{0: {}, 1: {}, 2: {}, 5: {}, 123: {}, 1000: {}},
|
|
},
|
|
{
|
|
mod: &Module{
|
|
GlobalSection: []Global{
|
|
{
|
|
Init: NewConstantExpressionFromOpcode(OpcodeRefFunc, nil),
|
|
},
|
|
},
|
|
},
|
|
name: "invalid global",
|
|
expErr: `global[0] failed to initialize: unexpected EOF`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
actual, err := tc.mod.declaredFunctionIndexes(api.CoreFeaturesV2 | experimental.CoreFeaturesExtendedConst)
|
|
if tc.expErr != "" {
|
|
require.EqualError(t, err, tc.expErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.exp, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestModule_AssignModuleID(t *testing.T) {
|
|
getID := func(bin []byte, lsns []experimental.FunctionListener, withEnsureTermination bool) ModuleID {
|
|
m := Module{}
|
|
m.AssignModuleID(bin, lsns, withEnsureTermination)
|
|
return m.ID
|
|
}
|
|
|
|
ml := &mockListener{}
|
|
|
|
// Ensures that different args always produce the different IDs.
|
|
exists := map[ModuleID]struct{}{}
|
|
for i, tc := range []struct {
|
|
bin []byte
|
|
withEnsureTermination bool
|
|
listeners []experimental.FunctionListener
|
|
}{
|
|
{bin: []byte{1, 2, 3}, withEnsureTermination: false},
|
|
{bin: []byte{1, 2, 3}, withEnsureTermination: true},
|
|
{
|
|
bin: []byte{1, 2, 3},
|
|
listeners: []experimental.FunctionListener{ml},
|
|
withEnsureTermination: false,
|
|
},
|
|
{
|
|
bin: []byte{1, 2, 3},
|
|
listeners: []experimental.FunctionListener{ml},
|
|
withEnsureTermination: true,
|
|
},
|
|
{
|
|
bin: []byte{1, 2, 3},
|
|
listeners: []experimental.FunctionListener{nil, ml},
|
|
withEnsureTermination: true,
|
|
},
|
|
{
|
|
bin: []byte{1, 2, 3},
|
|
listeners: []experimental.FunctionListener{ml, ml},
|
|
withEnsureTermination: true,
|
|
},
|
|
{bin: []byte{1, 2, 3, 4}, withEnsureTermination: false},
|
|
{bin: []byte{1, 2, 3, 4}, withEnsureTermination: true},
|
|
{
|
|
bin: []byte{1, 2, 3, 4},
|
|
listeners: []experimental.FunctionListener{ml},
|
|
withEnsureTermination: false,
|
|
},
|
|
{
|
|
bin: []byte{1, 2, 3, 4},
|
|
listeners: []experimental.FunctionListener{ml},
|
|
withEnsureTermination: true,
|
|
},
|
|
{
|
|
bin: []byte{1, 2, 3, 4},
|
|
listeners: []experimental.FunctionListener{nil},
|
|
withEnsureTermination: true,
|
|
},
|
|
{
|
|
bin: []byte{1, 2, 3, 4},
|
|
listeners: []experimental.FunctionListener{nil, ml},
|
|
withEnsureTermination: true,
|
|
},
|
|
{
|
|
bin: []byte{1, 2, 3, 4},
|
|
listeners: []experimental.FunctionListener{ml, ml},
|
|
withEnsureTermination: true,
|
|
},
|
|
{
|
|
bin: []byte{1, 2, 3, 4},
|
|
listeners: []experimental.FunctionListener{ml, ml},
|
|
withEnsureTermination: false,
|
|
},
|
|
} {
|
|
id := getID(tc.bin, tc.listeners, tc.withEnsureTermination)
|
|
_, exist := exists[id]
|
|
require.False(t, exist, i)
|
|
exists[id] = struct{}{}
|
|
}
|
|
}
|
|
|
|
type mockListener struct{}
|
|
|
|
func (m mockListener) Before(context.Context, api.Module, api.FunctionDefinition, []uint64, experimental.StackIterator) {
|
|
}
|
|
|
|
func (m mockListener) After(context.Context, api.Module, api.FunctionDefinition, []uint64) {}
|
|
|
|
func (m mockListener) Abort(context.Context, api.Module, api.FunctionDefinition, error) {}
|