From ca36651a36f2e26e244e3d6b404f2357606afe50 Mon Sep 17 00:00:00 2001 From: Takeshi Yoneda Date: Mon, 25 Apr 2022 09:15:28 +0900 Subject: [PATCH] Bulk memory binary encoding and validation (#494) Signed-off-by: Takeshi Yoneda Co-authored-by: Adrian Cole --- internal/wasm/binary/decoder.go | 5 + internal/wasm/binary/decoder_test.go | 6 + internal/wasm/binary/section.go | 10 + internal/wasm/binary/section_test.go | 13 + internal/wasm/features.go | 33 ++- internal/wasm/features_test.go | 2 +- internal/wasm/func_validation.go | 93 +++++++ internal/wasm/func_validation_test.go | 375 ++++++++++++++++++++++++++ internal/wasm/instruction.go | 33 ++- internal/wasm/module.go | 26 ++ internal/wasm/module_test.go | 34 +++ 11 files changed, 619 insertions(+), 11 deletions(-) diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go index 6fc7f3ff7a..c1f0e03d8f 100644 --- a/internal/wasm/binary/decoder.go +++ b/internal/wasm/binary/decoder.go @@ -94,6 +94,11 @@ func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryMaxPages u m.CodeSection, err = decodeCodeSection(r) case wasm.SectionIDData: m.DataSection, err = decodeDataSection(r) + case wasm.SectionIDDataCount: + if err := enabledFeatures.Require(wasm.FeatureBulkMemoryOperations); err != nil { + return nil, fmt.Errorf("data count section not supported as %v", err) + } + m.DataCountSection, err = decodeDataCountSection(r) default: err = ErrInvalidSectionID } diff --git a/internal/wasm/binary/decoder_test.go b/internal/wasm/binary/decoder_test.go index ab450ca399..97e9246b4e 100644 --- a/internal/wasm/binary/decoder_test.go +++ b/internal/wasm/binary/decoder_test.go @@ -110,6 +110,12 @@ func TestDecodeModule(t *testing.T) { require.NoError(t, e) require.Equal(t, &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "simple"}}, m) }) + t.Run("data count section disabled", func(t *testing.T) { + input := append(append(Magic, version...), + wasm.SectionIDDataCount, 1, 0) + _, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryMaxPages) + require.EqualError(t, e, `data count section not supported as feature "bulk-memory-operations" is disabled`) + }) } func TestDecodeModule_Errors(t *testing.T) { diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go index 96f763a959..1bca9f8bdb 100644 --- a/internal/wasm/binary/section.go +++ b/internal/wasm/binary/section.go @@ -3,6 +3,7 @@ package binary import ( "bytes" "fmt" + "io" "github.com/tetratelabs/wazero/internal/leb128" "github.com/tetratelabs/wazero/internal/wasm" @@ -168,6 +169,15 @@ func decodeDataSection(r *bytes.Reader) ([]*wasm.DataSegment, error) { return result, nil } +func decodeDataCountSection(r *bytes.Reader) (count *uint32, err error) { + v, _, err := leb128.DecodeUint32(r) + if err != nil && err != io.EOF { + // data count is optional, so EOF is fine. + return nil, err + } + return &v, nil +} + // encodeSection encodes the sectionID, the size of its contents in bytes, followed by the contents. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#sections%E2%91%A0 func encodeSection(sectionID wasm.SectionID, contents []byte) []byte { diff --git a/internal/wasm/binary/section_test.go b/internal/wasm/binary/section_test.go index 0bafef43f5..522c93656a 100644 --- a/internal/wasm/binary/section_test.go +++ b/internal/wasm/binary/section_test.go @@ -204,3 +204,16 @@ func TestEncodeFunctionSection(t *testing.T) { func TestEncodeStartSection(t *testing.T) { require.Equal(t, []byte{wasm.SectionIDStart, 0x01, 0x05}, encodeStartSection(5)) } + +func TestDecodeDataCountSection(t *testing.T) { + t.Run("ok", func(t *testing.T) { + v, err := decodeDataCountSection(bytes.NewReader([]byte{0x1})) + require.NoError(t, err) + require.Equal(t, uint32(1), *v) + }) + t.Run("eof", func(t *testing.T) { + // EOF is fine as the datacount is optional. + _, err := decodeDataCountSection(bytes.NewReader([]byte{})) + require.NoError(t, err) + }) +} diff --git a/internal/wasm/features.go b/internal/wasm/features.go index afa49c4f70..54e07f1069 100644 --- a/internal/wasm/features.go +++ b/internal/wasm/features.go @@ -47,17 +47,32 @@ const ( // FeatureNonTrappingFloatToIntConversion decides if parsing should succeed on the following instructions: // - // * [OpcodeMiscPrefix, OpcodeMiscI32TruncSatF32S] - // * [OpcodeMiscPrefix, OpcodeMiscI32TruncSatF32U] - // * [OpcodeMiscPrefix, OpcodeMiscI64TruncSatF32S] - // * [OpcodeMiscPrefix, OpcodeMiscI64TruncSatF32U] - // * [OpcodeMiscPrefix, OpcodeMiscI32TruncSatF64S] - // * [OpcodeMiscPrefix, OpcodeMiscI32TruncSatF64U] - // * [OpcodeMiscPrefix, OpcodeMiscI64TruncSatF64S] - // * [OpcodeMiscPrefix, OpcodeMiscI64TruncSatF64U] + // * [ OpcodeMiscPrefix, OpcodeMiscI32TruncSatF32S] + // * [ OpcodeMiscPrefix, OpcodeMiscI32TruncSatF32U] + // * [ OpcodeMiscPrefix, OpcodeMiscI64TruncSatF32S] + // * [ OpcodeMiscPrefix, OpcodeMiscI64TruncSatF32U] + // * [ OpcodeMiscPrefix, OpcodeMiscI32TruncSatF64S] + // * [ OpcodeMiscPrefix, OpcodeMiscI32TruncSatF64U] + // * [ OpcodeMiscPrefix, OpcodeMiscI64TruncSatF64S] + // * [ OpcodeMiscPrefix, OpcodeMiscI64TruncSatF64U] // // See https://github.com/WebAssembly/spec/blob/main/proposals/nontrapping-float-to-int-conversion/Overview.md FeatureNonTrappingFloatToIntConversion + + // FeatureBulkMemoryOperations decides if parsing should succeed on the following instructions: + // + // * [ OpcodeMiscPrefix, OpcodeMiscMemoryInit] + // * [ OpcodeMiscPrefix, OpcodeMiscDataDrop] + // * [ OpcodeMiscPrefix, OpcodeMiscMemoryCopy] + // * [ OpcodeMiscPrefix, OpcodeMiscMemoryFill] + // * [ OpcodeMiscPrefix, OpcodeMiscTableInit] + // * [ OpcodeMiscPrefix, OpcodeMiscElemDrop] + // * [ OpcodeMiscPrefix, OpcodeMiscTableCopy] + // + // Also, if the parsing should succeed with the presence of SectionIDDataCount. + // + //See https://github.com/WebAssembly/spec/blob/main/proposals/bulk-memory-operations/Overview.md + FeatureBulkMemoryOperations ) // Set assigns the value for the given feature. @@ -111,6 +126,8 @@ func featureName(f Features) string { case FeatureNonTrappingFloatToIntConversion: // match https://github.com/WebAssembly/spec/blob/main/proposals/nontrapping-float-to-int-conversion/Overview.md return "nontrapping-float-to-int-conversion" + case FeatureBulkMemoryOperations: + return "bulk-memory-operations" } return "" } diff --git a/internal/wasm/features_test.go b/internal/wasm/features_test.go index 3690e5a8b5..06ed50506f 100644 --- a/internal/wasm/features_test.go +++ b/internal/wasm/features_test.go @@ -62,7 +62,7 @@ func TestFeatures_String(t *testing.T) { {name: "multi-value", feature: FeatureMultiValue, expected: "multi-value"}, {name: "features", feature: FeatureMutableGlobal | FeatureMultiValue, expected: "mutable-global|multi-value"}, {name: "undefined", feature: 1 << 63, expected: ""}, - {name: "all", feature: FeaturesFinished, expected: "mutable-global|sign-extension-ops|multi-value|nontrapping-float-to-int-conversion"}, + {name: "all", feature: FeaturesFinished, expected: "mutable-global|sign-extension-ops|multi-value|nontrapping-float-to-int-conversion|bulk-memory-operations"}, } for _, tt := range tests { diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go index 14fd28d81a..0cb8174fb0 100644 --- a/internal/wasm/func_validation.go +++ b/internal/wasm/func_validation.go @@ -752,6 +752,99 @@ func (m *Module) validateFunctionWithMaxStackValues( return fmt.Errorf("cannot pop the operand for %s: %v", miscInstructionNames[miscOpcode], err) } valueTypeStack.push(outType) + } else if miscOpcode >= OpcodeMiscMemoryInit && miscOpcode <= OpcodeMiscTableCopy { + if err := enabledFeatures.Require(FeatureBulkMemoryOperations); err != nil { + return fmt.Errorf("%s invalid as %v", miscInstructionNames[miscOpcode], err) + } + var params []ValueType + switch miscOpcode { + case OpcodeMiscMemoryInit, OpcodeMiscMemoryCopy, OpcodeMiscMemoryFill, OpcodeMiscDataDrop: + if memory == nil { + return fmt.Errorf("memory must exist for %s", MiscInstructionName(miscOpcode)) + } + if miscOpcode != OpcodeMiscDataDrop { + params = []ValueType{ValueTypeI32, ValueTypeI32, ValueTypeI32} + } + + if miscOpcode == OpcodeMiscMemoryInit || miscOpcode == OpcodeMiscDataDrop { + if m.DataCountSection == nil { + return fmt.Errorf("%s requires data count section", MiscInstructionName(miscOpcode)) + } + + // We need to read the index to the data section. + pc++ + index, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("failed to read data segment index for %s: %v", MiscInstructionName(miscOpcode), err) + } + if int(index) >= len(m.DataSection) { + return fmt.Errorf("index %d out of range of data section(len=%d)", index, len(m.DataSection)) + } + pc += num - 1 + } + + if miscOpcode != OpcodeMiscDataDrop { + pc++ + val, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("failed to read memory index for %s: %v", MiscInstructionName(miscOpcode), err) + } + if val != 0 || num != 1 { + return fmt.Errorf("%s reserved byte must be zero encoded with 1 byte", MiscInstructionName(miscOpcode)) + } + if miscOpcode == OpcodeMiscMemoryCopy { + pc++ + // memory.copy needs two memory index which are reserved as zero. + val, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("failed to read memory index for %s: %v", MiscInstructionName(miscOpcode), err) + } + if val != 0 || num != 1 { + return fmt.Errorf("%s reserved byte must be zero encoded with 1 byte", MiscInstructionName(miscOpcode)) + } + } + } + case OpcodeMiscTableInit, OpcodeMiscElemDrop, OpcodeMiscTableCopy: + if table == nil { + return fmt.Errorf("table must exist for %s", MiscInstructionName(miscOpcode)) + } + if miscOpcode != OpcodeMiscElemDrop { + params = []ValueType{ValueTypeI32, ValueTypeI32, ValueTypeI32} + } + if miscOpcode == OpcodeMiscTableInit || miscOpcode == OpcodeMiscElemDrop { + pc++ + index, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("failed to read element segment index for %s: %v", MiscInstructionName(miscOpcode), err) + } + if int(index) >= len(m.ElementSection) { + return fmt.Errorf("index %d out of range of element section(len=%d)", index, len(m.ElementSection)) + } + pc += num - 1 + } + + if miscOpcode != OpcodeMiscElemDrop { + pc++ + _, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("failed to read table index for %s: %v", MiscInstructionName(miscOpcode), err) + } + if miscOpcode == OpcodeMiscTableCopy { + pc += num + // table.copy needs two table index which are reserved as zero. + _, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("failed to read table index for %s: %v", MiscInstructionName(miscOpcode), err) + } + pc += num - 1 + } + } + } + for _, p := range params { + if err := valueTypeStack.popAndVerifyType(p); err != nil { + return fmt.Errorf("cannot pop the operand for %s: %v", miscInstructionNames[miscOpcode], err) + } + } } } else if op == OpcodeBlock { bt, num, err := DecodeBlockType(types, bytes.NewReader(body[pc+1:]), enabledFeatures) diff --git a/internal/wasm/func_validation_test.go b/internal/wasm/func_validation_test.go index 7e13fdfca4..407d194729 100644 --- a/internal/wasm/func_validation_test.go +++ b/internal/wasm/func_validation_test.go @@ -257,6 +257,381 @@ func TestModule_ValidateFunction_MultiValue(t *testing.T) { } } +func TestModule_ValidateFunction_BulkMemoryOperations(t *testing.T) { + t.Run("ok", func(t *testing.T) { + for _, op := range []OpcodeMisc{ + OpcodeMiscMemoryInit, OpcodeMiscDataDrop, OpcodeMiscMemoryCopy, + OpcodeMiscMemoryFill, OpcodeMiscTableInit, OpcodeMiscElemDrop, OpcodeMiscTableCopy, + } { + t.Run(MiscInstructionName(op), func(t *testing.T) { + var body []byte + if op != OpcodeMiscDataDrop && op != OpcodeMiscElemDrop { + body = append(body, OpcodeI32Const, 1, OpcodeI32Const, 2, OpcodeI32Const, 3) + } + + body = append(body, OpcodeMiscPrefix, op) + if op != OpcodeMiscDataDrop && op != OpcodeMiscMemoryFill && op != OpcodeMiscElemDrop { + body = append(body, 0, 0) + } else { + body = append(body, 0) + } + + body = append(body, OpcodeEnd) + + c := uint32(0) + m := &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: body}}, + DataSection: []*DataSegment{{}}, + ElementSection: []*ElementSegment{{}}, + DataCountSection: &c, + } + err := m.validateFunction(FeatureBulkMemoryOperations, 0, []Index{0}, nil, &Memory{}, &Table{}) + require.NoError(t, err) + }) + } + }) + t.Run("errors", func(t *testing.T) { + for _, tc := range []struct { + body []byte + dataSection []*DataSegment + elementSection []*ElementSegment + dataCountSectionNil bool + memory *Memory + table *Table + flag Features + expectedErr string + }{ + // memory.init + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryInit}, + flag: FeatureBulkMemoryOperations, + memory: nil, + expectedErr: "memory must exist for memory.init", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryInit}, + flag: Features20191205, + expectedErr: `memory.init invalid as feature "bulk-memory-operations" is disabled`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryInit}, + flag: FeatureBulkMemoryOperations, + dataCountSectionNil: true, + expectedErr: `memory must exist for memory.init`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryInit}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: "failed to read data segment index for memory.init: EOF", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryInit, 100 /* data section out of range */}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + dataSection: []*DataSegment{{}}, + expectedErr: "index 100 out of range of data section(len=1)", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryInit, 0}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + dataSection: []*DataSegment{{}}, + expectedErr: "failed to read memory index for memory.init: EOF", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryInit, 0, 1}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + dataSection: []*DataSegment{{}}, + expectedErr: "memory.init reserved byte must be zero encoded with 1 byte", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryInit, 0, 0}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + dataSection: []*DataSegment{{}}, + expectedErr: "cannot pop the operand for memory.init: i32 missing", + }, + { + body: []byte{OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscMemoryInit, 0, 0}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + dataSection: []*DataSegment{{}}, + expectedErr: "cannot pop the operand for memory.init: i32 missing", + }, + { + body: []byte{OpcodeI32Const, 0, OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscMemoryInit, 0, 0}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + dataSection: []*DataSegment{{}}, + expectedErr: "cannot pop the operand for memory.init: i32 missing", + }, + // data.drop + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscDataDrop}, + flag: FeatureBulkMemoryOperations, + memory: nil, + expectedErr: "memory must exist for data.drop", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscDataDrop}, + flag: Features20191205, + expectedErr: `data.drop invalid as feature "bulk-memory-operations" is disabled`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscDataDrop}, + dataCountSectionNil: true, + memory: &Memory{}, + flag: FeatureBulkMemoryOperations, + expectedErr: `data.drop requires data count section`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscDataDrop}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: "failed to read data segment index for data.drop: EOF", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscDataDrop, 100 /* data section out of range */}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + dataSection: []*DataSegment{{}}, + expectedErr: "index 100 out of range of data section(len=1)", + }, + // memory.copy + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryCopy}, + flag: FeatureBulkMemoryOperations, + memory: nil, + expectedErr: "memory must exist for memory.copy", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryCopy}, + flag: Features20191205, + expectedErr: `memory.copy invalid as feature "bulk-memory-operations" is disabled`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryCopy}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: `failed to read memory index for memory.copy: EOF`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryCopy, 0}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: "failed to read memory index for memory.copy: EOF", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryCopy, 0, 1}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: "memory.copy reserved byte must be zero encoded with 1 byte", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryCopy, 0, 0}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: "cannot pop the operand for memory.copy: i32 missing", + }, + { + body: []byte{OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscMemoryCopy, 0, 0}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: "cannot pop the operand for memory.copy: i32 missing", + }, + { + body: []byte{OpcodeI32Const, 0, OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscMemoryCopy, 0, 0}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: "cannot pop the operand for memory.copy: i32 missing", + }, + // memory.fill + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryFill}, + flag: FeatureBulkMemoryOperations, + memory: nil, + expectedErr: "memory must exist for memory.fill", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryFill}, + flag: Features20191205, + expectedErr: `memory.fill invalid as feature "bulk-memory-operations" is disabled`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryFill}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: `failed to read memory index for memory.fill: EOF`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryFill, 1}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: `memory.fill reserved byte must be zero encoded with 1 byte`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscMemoryFill, 0}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: "cannot pop the operand for memory.fill: i32 missing", + }, + { + body: []byte{OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscMemoryFill, 0}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: "cannot pop the operand for memory.fill: i32 missing", + }, + { + body: []byte{OpcodeI32Const, 0, OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscMemoryFill, 0}, + flag: FeatureBulkMemoryOperations, + memory: &Memory{}, + expectedErr: "cannot pop the operand for memory.fill: i32 missing", + }, + // table.init + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit}, + flag: FeatureBulkMemoryOperations, + expectedErr: "table must exist for table.init", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit}, + flag: Features20191205, + table: &Table{}, + expectedErr: `table.init invalid as feature "bulk-memory-operations" is disabled`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + expectedErr: "failed to read element segment index for table.init: EOF", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit, 100 /* data section out of range */}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + elementSection: []*ElementSegment{{}}, + expectedErr: "index 100 out of range of element section(len=1)", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit, 0}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + elementSection: []*ElementSegment{{}}, + expectedErr: "failed to read table index for table.init: EOF", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit, 0, 0}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + elementSection: []*ElementSegment{{}}, + expectedErr: "cannot pop the operand for table.init: i32 missing", + }, + { + body: []byte{OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscTableInit, 0, 0}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + elementSection: []*ElementSegment{{}}, + expectedErr: "cannot pop the operand for table.init: i32 missing", + }, + { + body: []byte{OpcodeI32Const, 0, OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscTableInit, 0, 0}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + elementSection: []*ElementSegment{{}}, + expectedErr: "cannot pop the operand for table.init: i32 missing", + }, + // elem.drop + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscElemDrop}, + flag: FeatureBulkMemoryOperations, + expectedErr: "table must exist for elem.drop", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscElemDrop}, + flag: Features20191205, + table: &Table{}, + expectedErr: `elem.drop invalid as feature "bulk-memory-operations" is disabled`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscElemDrop}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + expectedErr: "failed to read element segment index for elem.drop: EOF", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscElemDrop, 100 /* element section out of range */}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + elementSection: []*ElementSegment{{}}, + expectedErr: "index 100 out of range of element section(len=1)", + }, + // table.copy + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy}, + flag: FeatureBulkMemoryOperations, + memory: nil, + expectedErr: "table must exist for table.copy", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy}, + flag: Features20191205, + table: &Table{}, + expectedErr: `table.copy invalid as feature "bulk-memory-operations" is disabled`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + expectedErr: `failed to read table index for table.copy: EOF`, + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy, 0}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + expectedErr: "failed to read table index for table.copy: EOF", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy, 0, 0}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + expectedErr: "cannot pop the operand for table.copy: i32 missing", + }, + { + body: []byte{OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscTableCopy, 0, 0}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + expectedErr: "cannot pop the operand for table.copy: i32 missing", + }, + { + body: []byte{OpcodeI32Const, 0, OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscTableCopy, 0, 0}, + flag: FeatureBulkMemoryOperations, + table: &Table{}, + expectedErr: "cannot pop the operand for table.copy: i32 missing", + }, + } { + t.Run(tc.expectedErr, func(t *testing.T) { + m := &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: tc.body}}, + ElementSection: tc.elementSection, + DataSection: tc.dataSection, + } + if !tc.dataCountSectionNil { + c := uint32(0) + m.DataCountSection = &c + } + err := m.validateFunction(tc.flag, 0, []Index{0}, nil, tc.memory, tc.table) + require.EqualError(t, err, tc.expectedErr) + }) + } + }) +} + var ( f32, f64, i32, i64 = ValueTypeF32, ValueTypeF64, ValueTypeI32, ValueTypeI64 f32i32_v = &FunctionType{Params: []ValueType{f32, i32}} diff --git a/internal/wasm/instruction.go b/internal/wasm/instruction.go index 221446b242..b5c08964f9 100644 --- a/internal/wasm/instruction.go +++ b/internal/wasm/instruction.go @@ -252,8 +252,8 @@ const ( OpcodeI64Extend32S Opcode = 0xc4 // OpcodeMiscPrefix is the prefix of various multi-byte opcodes. - // Introduced in nontrapping float to int conversion proposal. - // https://github.com/WebAssembly/spec/blob/ce4b6c4d47eb06098cc7ab2e81f24748da822f20/proposals/nontrapping-float-to-int-conversion/Overview.md + // Introduced in FeatureNonTrappingFloatToIntConversion, but used in other + // features, such as FeatureBulkMemoryOperations. OpcodeMiscPrefix Opcode = 0xfc ) @@ -273,6 +273,17 @@ const ( OpcodeMiscI64TruncSatF32U OpcodeMisc = 0x05 OpcodeMiscI64TruncSatF64S OpcodeMisc = 0x06 OpcodeMiscI64TruncSatF64U OpcodeMisc = 0x07 + + // Below are toggled with FeatureBulkMemoryOperations. + // https://github.com/WebAssembly/spec/blob/main/proposals/bulk-memory-operations/Overview.md + + OpcodeMiscMemoryInit OpcodeMisc = 0x08 + OpcodeMiscDataDrop OpcodeMisc = 0x09 + OpcodeMiscMemoryCopy OpcodeMisc = 0x0a + OpcodeMiscMemoryFill OpcodeMisc = 0x0b + OpcodeMiscTableInit OpcodeMisc = 0x0c + OpcodeMiscElemDrop OpcodeMisc = 0x0d + OpcodeMiscTableCopy OpcodeMisc = 0x0e ) const ( @@ -640,6 +651,8 @@ var instructionNames = [256]string{ OpcodeI64Extend8S: OpcodeI64Extend8SName, OpcodeI64Extend16S: OpcodeI64Extend16SName, OpcodeI64Extend32S: OpcodeI64Extend32SName, + + OpcodeMiscPrefix: OpcodeMiscPrefixName, } // InstructionName returns the instruction corresponding to this binary Opcode. @@ -657,6 +670,14 @@ const ( OpcodeI64TruncSatF32UName = "i64.trunc_sat_f32_u" OpcodeI64TruncSatF64SName = "i64.trunc_sat_f64_s" OpcodeI64TruncSatF64UName = "i64.trunc_sat_f64_u" + + OpcodeMemoryInitName = "memory.init" + OpcodeDataDropName = "data.drop" + OpcodeMemoryCopyName = "memory.copy" + OpcodeMemoryFillName = "memory.fill" + OpcodeTableInitName = "table.init" + OpcodeElemDropName = "elem.drop" + OpcodeTableCopyName = "table.copy" ) var miscInstructionNames = [256]string{ @@ -668,6 +689,14 @@ var miscInstructionNames = [256]string{ OpcodeMiscI64TruncSatF32U: OpcodeI64TruncSatF32UName, OpcodeMiscI64TruncSatF64S: OpcodeI64TruncSatF64SName, OpcodeMiscI64TruncSatF64U: OpcodeI64TruncSatF64UName, + + OpcodeMiscMemoryInit: OpcodeMemoryInitName, + OpcodeMiscDataDrop: OpcodeDataDropName, + OpcodeMiscMemoryCopy: OpcodeMemoryCopyName, + OpcodeMiscMemoryFill: OpcodeMemoryFillName, + OpcodeMiscTableInit: OpcodeTableInitName, + OpcodeMiscElemDrop: OpcodeElemDropName, + OpcodeMiscTableCopy: OpcodeTableCopyName, } // MiscInstructionName returns the instruction corresponding to this miscellaneous Opcode. diff --git a/internal/wasm/module.go b/internal/wasm/module.go index b9cba9e60b..02586c4248 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -163,6 +163,13 @@ type Module struct { // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-instances%E2%91%A0 validatedElementSegments []*validatedElementSegment + // DataCountSection is the optional section and holds the number of data segments in the data section. + // + // Note: This may exist in WebAssembly 2.0 or WebAssembly 1.0 with FeatureBulkMemoryOperations. + // See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#data-count-section + // See https://github.com/WebAssembly/spec/blob/main/proposals/bulk-memory-operations/Overview.md + DataCountSection *uint32 + // ID is the sha256 value of the source code (text/binary) and is used for caching. ID ModuleID } @@ -253,6 +260,9 @@ func (m *Module) Validate(enabledFeatures Features) error { return err } + if err := m.validateDataCountSection(); err != nil { + return err + } return nil } @@ -442,6 +452,14 @@ func validateConstExpression(globals []*GlobalType, expr *ConstantExpression, ex return nil } +func (m *Module) validateDataCountSection() (err error) { + if m.DataCountSection != nil && int(*m.DataCountSection) != len(m.DataSection) { + err = fmt.Errorf("data count section (%d) doesn't match the length of data section (%d)", + *m.DataCountSection, len(m.DataSection)) + } + return +} + func (m *Module) buildGlobals(importedGlobals []*GlobalInstance) (globals []*GlobalInstance) { for _, gs := range m.GlobalSection { var gv uint64 @@ -763,6 +781,12 @@ const ( SectionIDElement SectionIDCode SectionIDData + + // SectionIDDataCount may exist in WebAssembly 2.0 or WebAssembly 1.0 with FeatureBulkMemoryOperations enabled. + // + // See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#data-count-section + // See https://github.com/WebAssembly/spec/blob/main/proposals/bulk-memory-operations/Overview.md + SectionIDDataCount ) // SectionIDHostFunction is a pseudo-section ID for host functions. @@ -800,6 +824,8 @@ func SectionIDName(sectionID SectionID) string { return "data" case SectionIDHostFunction: return "host_function" + case SectionIDDataCount: + return "data_count" } return "unknown" } diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index 0bd3e33c28..12bca65004 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -736,3 +736,37 @@ func TestModule_buildMemoryInstance(t *testing.T) { require.Equal(t, max, mem.Max) }) } + +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) + } + }) +}