diff --git a/RATIONALE.md b/RATIONALE.md index 5e1aea21..5bdc48bf 100644 --- a/RATIONALE.md +++ b/RATIONALE.md @@ -186,9 +186,26 @@ See https://github.com/bytecodealliance/wasmtime/blob/2ca01ae9478f199337cf743a6a Their semantics match when `pathLen` == the length of `path`, so in practice this difference won't matter match. +## Signed encoding of integer global constant initializers +wazero treats integer global constant initializers signed as their interpretation is not known at declaration time. For +example, there is no signed integer [value type](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#value-types%E2%91%A0). + +To get at the problem, let's use an example. +``` +(global (export "start_epoch") i64 (i64.const 1620216263544)) +``` + +In both signed and unsigned LEB128 encoding, this value is the same bit pattern. The problem is that some numbers are +not. For example, 16256 is `807f` encoded as unsigned, but `80ff00` encoded as signed. + +While the specification mentions uninterpreted integers are in abstract [unsigned values](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#integers%E2%91%A0), +the binary encoding is clear that they are encoded [signed](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#integers%E2%91%A4). + +For consistency, we go with signed encoding in the special case of global constant initializers. + ## Implementation limitations -WebAssembly 1.0 (20191205) specification allows runtimes to [limit certain aspects of Wasm module or execution](https://www.w3.org/TR/2019/REC-2019/REC-wasm-core-1-20191205/#a2-implementation-limitations). +WebAssembly 1.0 (20191205) specification allows runtimes to [limit certain aspects of Wasm module or execution](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#a2-implementation-limitations). wazero limitations are imposed pragmatically and described below. diff --git a/builder.go b/builder.go index 2f856447..4bbbbd3d 100644 --- a/builder.go +++ b/builder.go @@ -107,7 +107,7 @@ type ModuleBuilder interface { // // Note: If a global is already exported with the same name, this overwrites it. // Note: The maximum value of v is math.MaxInt32 to match constraints of initialization in binary format. - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#value-types%E2%91%A2 + // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#value-types%E2%91%A0 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-globaltype ExportGlobalI32(name string, v int32) ModuleBuilder @@ -119,7 +119,7 @@ type ModuleBuilder interface { // // Note: If a global is already exported with the same name, this overwrites it. // Note: The maximum value of v is math.MaxInt64 to match constraints of initialization in binary format. - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#value-types%E2%91%A2 + // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#value-types%E2%91%A0 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-globaltype ExportGlobalI64(name string, v int64) ModuleBuilder @@ -202,8 +202,8 @@ func (b *moduleBuilder) ExportMemoryWithMax(name string, minPages, maxPages uint func (b *moduleBuilder) ExportGlobalI32(name string, v int32) ModuleBuilder { b.nameToGlobal[name] = &wasm.Global{ Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32}, - // Signed per https://www.w3.org/TR/wasm-core-1/#value-types%E2%91%A2 - Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeUint32(uint32(v))}, + // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md + Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(v)}, } return b } @@ -212,8 +212,8 @@ func (b *moduleBuilder) ExportGlobalI32(name string, v int32) ModuleBuilder { func (b *moduleBuilder) ExportGlobalI64(name string, v int64) ModuleBuilder { b.nameToGlobal[name] = &wasm.Global{ Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64}, - // Signed per https://www.w3.org/TR/wasm-core-1/#value-types%E2%91%A2 - Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeUint64(uint64(v))}, + // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md + Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(v)}, } return b } diff --git a/builder_test.go b/builder_test.go index 62be33aa..63c2c5a2 100644 --- a/builder_test.go +++ b/builder_test.go @@ -211,7 +211,7 @@ func TestNewModuleBuilder_Build(t *testing.T) { GlobalSection: []*wasm.Global{ { Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32}, - Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeUint32(1024)}, + Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(1024)}, }, }, ExportSection: map[string]*wasm.Export{ @@ -262,7 +262,7 @@ func TestNewModuleBuilder_Build(t *testing.T) { GlobalSection: []*wasm.Global{ { Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64}, - Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeUint64(math.MaxInt64)}, + Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(math.MaxInt64)}, }, }, ExportSection: map[string]*wasm.Export{ diff --git a/config.go b/config.go index 9d747bf1..7cb1c4b6 100644 --- a/config.go +++ b/config.go @@ -81,7 +81,7 @@ func (c *RuntimeConfig) WithContext(ctx context.Context) *RuntimeConfig { // * Zero is a valid value and results in a crash if any module uses memory. // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem -// See https://www.w3.org/TR/wasm-core-1/#memory-types%E2%91%A0 +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-types%E2%91%A0 func (c *RuntimeConfig) WithMemoryMaxPages(memoryMaxPages uint32) *RuntimeConfig { ret := c.clone() ret.memoryMaxPages = memoryMaxPages diff --git a/internal/ieee754/ieee754.go b/internal/ieee754/ieee754.go index 246cbcda..5968a6a1 100644 --- a/internal/ieee754/ieee754.go +++ b/internal/ieee754/ieee754.go @@ -7,7 +7,7 @@ import ( ) // DecodeFloat32 decodes a float32 in IEEE 754 binary representation. -// See https://www.w3.org/TR/wasm-core-1/#floating-point%E2%91%A2 +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#floating-point%E2%91%A2 func DecodeFloat32(r io.Reader) (float32, error) { buf := make([]byte, 4) _, err := io.ReadFull(r, buf) @@ -19,7 +19,7 @@ func DecodeFloat32(r io.Reader) (float32, error) { } // DecodeFloat64 decodes a float64 in IEEE 754 binary representation. -// See https://www.w3.org/TR/wasm-core-1/#floating-point%E2%91%A2 +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#floating-point%E2%91%A2 func DecodeFloat64(r io.Reader) (float64, error) { buf := make([]byte, 8) _, err := io.ReadFull(r, buf) diff --git a/internal/leb128/leb128.go b/internal/leb128/leb128.go index 2fcdfad4..85c539c7 100644 --- a/internal/leb128/leb128.go +++ b/internal/leb128/leb128.go @@ -29,6 +29,40 @@ var encodeCache = [0x80][]byte{ {0x70}, {0x71}, {0x72}, {0x73}, {0x74}, {0x75}, {0x76}, {0x77}, {0x78}, {0x79}, {0x7a}, {0x7b}, {0x7c}, {0x7d}, {0x7e}, {0x7f}, } +// EncodeInt32 encodes the signed value into a buffer in LEB128 format +// +// See https://en.wikipedia.org/wiki/LEB128#Encode_signed_integer +func EncodeInt32(value int32) []byte { + return EncodeInt64(int64(value)) +} + +// EncodeInt64 encodes the signed value into a buffer in LEB128 format +// +// See https://en.wikipedia.org/wiki/LEB128#Encode_signed_integer +func EncodeInt64(value int64) (buf []byte) { + for { + // Take 7 remaining low-order bits from the value into b. + b := uint8(value & 0x7f) + // Extract the sign bit. + s := uint8(value & 0x40) + value >>= 7 + + // The encoding unsigned numbers is simpler as it only needs to check if the value is non-zero to tell if there + // are more bits to encode. Signed is a little more complicated as you have to double-check the sign bit. + // If either case, set the high-order bit to tell the reader there are more bytes in this int. + if (value != -1 || s == 0) && (value != 0 || s != 0) { + b |= 0x80 + } + + // Append b into the buffer + buf = append(buf, b) + if b&0x80 == 0 { + break + } + } + return buf +} + // EncodeUint32 encodes the value into a buffer in LEB128 format // // See https://en.wikipedia.org/wiki/LEB128#Encode_unsigned_integer @@ -51,7 +85,7 @@ func EncodeUint64(value uint64) (buf []byte) { value = value >> 7 // If there are remaining bits, the value won't be zero: Set the high- - // order bit to tell the reader there are more bytes in this uint32. + // order bit to tell the reader there are more bytes in this uint. if value != 0 { b |= 0x80 } diff --git a/internal/leb128/leb128_test.go b/internal/leb128/leb128_test.go index 93e3f68e..147cd1d7 100644 --- a/internal/leb128/leb128_test.go +++ b/internal/leb128/leb128_test.go @@ -10,6 +10,58 @@ import ( "github.com/stretchr/testify/require" ) +func TestEncode_DecodeInt32(t *testing.T) { + for _, c := range []struct { + input int32 + expected []byte + }{ + {input: -165675008, expected: []byte{0x80, 0x80, 0x80, 0xb1, 0x7f}}, + {input: -624485, expected: []byte{0x9b, 0xf1, 0x59}}, + {input: -16256, expected: []byte{0x80, 0x81, 0x7f}}, + {input: -4, expected: []byte{0x7c}}, + {input: -1, expected: []byte{0x7f}}, + {input: 0, expected: []byte{0x00}}, + {input: 1, expected: []byte{0x01}}, + {input: 4, expected: []byte{0x04}}, + {input: 16256, expected: []byte{0x80, 0xff, 0x0}}, + {input: 624485, expected: []byte{0xe5, 0x8e, 0x26}}, + {input: 165675008, expected: []byte{0x80, 0x80, 0x80, 0xcf, 0x0}}, + {input: int32(math.MaxInt32), expected: []byte{0xff, 0xff, 0xff, 0xff, 0x7}}, + } { + require.Equal(t, c.expected, EncodeInt32(c.input)) + decoded, _, err := DecodeInt32(bytes.NewReader(c.expected)) + require.NoError(t, err) + require.Equal(t, c.input, decoded) + } +} + +func TestEncode_DecodeInt64(t *testing.T) { + for _, c := range []struct { + input int64 + expected []byte + }{ + {input: -math.MaxInt32, expected: []byte{0x81, 0x80, 0x80, 0x80, 0x78}}, + {input: -165675008, expected: []byte{0x80, 0x80, 0x80, 0xb1, 0x7f}}, + {input: -624485, expected: []byte{0x9b, 0xf1, 0x59}}, + {input: -16256, expected: []byte{0x80, 0x81, 0x7f}}, + {input: -4, expected: []byte{0x7c}}, + {input: -1, expected: []byte{0x7f}}, + {input: 0, expected: []byte{0x00}}, + {input: 1, expected: []byte{0x01}}, + {input: 4, expected: []byte{0x04}}, + {input: 16256, expected: []byte{0x80, 0xff, 0x0}}, + {input: 624485, expected: []byte{0xe5, 0x8e, 0x26}}, + {input: 165675008, expected: []byte{0x80, 0x80, 0x80, 0xcf, 0x0}}, + {input: math.MaxInt32, expected: []byte{0xff, 0xff, 0xff, 0xff, 0x7}}, + {input: math.MaxInt64, expected: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0}}, + } { + require.Equal(t, c.expected, EncodeInt64(c.input)) + decoded, _, err := DecodeInt64(bytes.NewReader(c.expected)) + require.NoError(t, err) + require.Equal(t, c.input, decoded) + } +} + func TestEncodeUint32(t *testing.T) { for _, c := range []struct { input uint32 diff --git a/internal/wasm/binary/const_expr.go b/internal/wasm/binary/const_expr.go index c9f9337d..dcde3006 100644 --- a/internal/wasm/binary/const_expr.go +++ b/internal/wasm/binary/const_expr.go @@ -21,8 +21,10 @@ func decodeConstantExpression(r *bytes.Reader) (*wasm.ConstantExpression, error) opcode := b switch opcode { case wasm.OpcodeI32Const: + // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md _, _, err = leb128.DecodeInt32(r) case wasm.OpcodeI64Const: + // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md _, _, err = leb128.DecodeInt64(r) case wasm.OpcodeF32Const: _, err = ieee754.DecodeFloat32(r) diff --git a/internal/wasm/binary/encoder_test.go b/internal/wasm/binary/encoder_test.go index a3d6d0cb..d08320ad 100644 --- a/internal/wasm/binary/encoder_test.go +++ b/internal/wasm/binary/encoder_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/tetratelabs/wazero/internal/leb128" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -180,7 +181,7 @@ func TestModule_Encode(t *testing.T) { GlobalSection: []*wasm.Global{ { Type: &wasm.GlobalType{ValType: i32, Mutable: true}, - Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: []byte{0}}, + Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(0)}, }, }, ExportSection: map[string]*wasm.Export{ diff --git a/internal/wasm/binary/global_test.go b/internal/wasm/binary/global_test.go index 5e1fe98a..83e1e185 100644 --- a/internal/wasm/binary/global_test.go +++ b/internal/wasm/binary/global_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/tetratelabs/wazero/internal/leb128" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -18,7 +19,7 @@ func TestEncodeGlobal(t *testing.T) { name: "const", input: &wasm.Global{ Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32}, - Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: []byte{1}}, + Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(1)}, }, expected: []byte{ wasm.ValueTypeI32, 0x00, // 0 == const @@ -29,7 +30,7 @@ func TestEncodeGlobal(t *testing.T) { name: "var", input: &wasm.Global{ Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true}, - Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: []byte{1}}, + Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(1)}, }, expected: []byte{ wasm.ValueTypeI32, 0x01, // 1 == var diff --git a/internal/wasm/counts_test.go b/internal/wasm/counts_test.go index c920fa64..d9e98c40 100644 --- a/internal/wasm/counts_test.go +++ b/internal/wasm/counts_test.go @@ -203,7 +203,7 @@ func TestModule_ImportGlobalCount(t *testing.T) { func TestModule_SectionElementCount(t *testing.T) { i32, f32 := ValueTypeI32, ValueTypeF32 zero := uint32(0) - empty := &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x00}} + empty := &ConstantExpression{Opcode: OpcodeI32Const, Data: const0} fn := reflect.ValueOf(func(api.Module) {}) tests := []struct { diff --git a/internal/wasm/global_test.go b/internal/wasm/global_test.go index 9aea570c..9cd2e1e2 100644 --- a/internal/wasm/global_test.go +++ b/internal/wasm/global_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/leb128" "github.com/tetratelabs/wazero/internal/u64" ) @@ -156,7 +157,7 @@ func TestPublicModule_Global(t *testing.T) { GlobalSection: []*Global{ { Type: &GlobalType{ValType: ValueTypeI32}, - Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{1}}, + Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, }, }, }, @@ -167,7 +168,7 @@ func TestPublicModule_Global(t *testing.T) { GlobalSection: []*Global{ { Type: &GlobalType{ValType: ValueTypeI32}, - Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{1}}, + Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, }, }, ExportSection: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}}, @@ -180,7 +181,7 @@ func TestPublicModule_Global(t *testing.T) { GlobalSection: []*Global{ { Type: &GlobalType{ValType: ValueTypeI64}, - Init: &ConstantExpression{Opcode: OpcodeI64Const, Data: []byte{1}}, + Init: &ConstantExpression{Opcode: OpcodeI64Const, Data: leb128.EncodeInt64(1)}, }, }, ExportSection: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}}, @@ -223,7 +224,7 @@ func TestPublicModule_Global(t *testing.T) { GlobalSection: []*Global{ { Type: &GlobalType{ValType: ValueTypeI32, Mutable: true}, - Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{1}}, + Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(1)}, }, }, ExportSection: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}}, @@ -238,7 +239,7 @@ func TestPublicModule_Global(t *testing.T) { GlobalSection: []*Global{ { Type: &GlobalType{ValType: ValueTypeI64, Mutable: true}, - Init: &ConstantExpression{Opcode: OpcodeI64Const, Data: []byte{1}}, + Init: &ConstantExpression{Opcode: OpcodeI64Const, Data: leb128.EncodeInt64(1)}, }, }, ExportSection: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}}, diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go index e64bd5e6..69284ae0 100644 --- a/internal/wasm/host_test.go +++ b/internal/wasm/host_test.go @@ -93,22 +93,22 @@ func TestNewHostModule(t *testing.T) { nameToGlobal: map[string]*Global{ "g2": { Type: &GlobalType{ValType: i32}, - Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(2)}, + Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)}, }, "g1": { Type: &GlobalType{ValType: i32}, - Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(1)}, + Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, }, }, expected: &Module{ GlobalSection: []*Global{ { Type: &GlobalType{ValType: i32}, - Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(1)}, + Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, }, { Type: &GlobalType{ValType: i32}, - Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(2)}, + Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)}, }, }, ExportSection: map[string]*Export{ @@ -129,7 +129,7 @@ func TestNewHostModule(t *testing.T) { nameToGlobal: map[string]*Global{ "g": { Type: &GlobalType{ValType: i32}, - Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(1)}, + Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, }, }, expected: &Module{ @@ -141,7 +141,7 @@ func TestNewHostModule(t *testing.T) { GlobalSection: []*Global{ { Type: &GlobalType{ValType: i32}, - Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(1)}, + Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, }, }, MemorySection: &Memory{Min: 1, Max: 1}, diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go index 27e077ae..eaecfd3d 100644 --- a/internal/wasm/memory.go +++ b/internal/wasm/memory.go @@ -169,7 +169,7 @@ func (m *MemoryInstance) PageSize() (result uint32) { // PagesToUnitOfBytes converts the pages to a human-readable form similar to what's specified. Ex. 1 -> "64Ki" // -// See https://www.w3.org/TR/wasm-core-1/#memory-instances%E2%91%A0 +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0 func PagesToUnitOfBytes(pages uint32) string { k := pages * 64 if k < 1024 { diff --git a/internal/wasm/module.go b/internal/wasm/module.go index ed08007a..08335230 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -152,14 +152,14 @@ type Module struct { // When present, the CodeSection must be nil. // // Note: This section currently has no serialization format, so is not encodable. - // See https://www.w3.org/TR/wasm-core-1/#host-functions%E2%91%A2 + // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#host-functions%E2%91%A2 HostFunctionSection []*reflect.Value // elementSegments are built on Validate when SectionIDElement is non-empty and all inputs are valid. // // Note: elementSegments retain Module.ElementSection order. Since an ElementSegment can overlap with another, order // preservation ensures a consistent initialization result. - // See https://www.w3.org/TR/wasm-core-1/#table-instances%E2%91%A0 + // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-instances%E2%91%A0 validatedElementSegments []*validatedElementSegment } @@ -393,12 +393,14 @@ func validateConstExpression(globals []*GlobalType, expr *ConstantExpression, ex r := bytes.NewReader(expr.Data) switch expr.Opcode { case OpcodeI32Const: + // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md _, _, err = leb128.DecodeInt32(r) if err != nil { return fmt.Errorf("read i32: %w", err) } actualType = ValueTypeI32 case OpcodeI64Const: + // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md _, _, err = leb128.DecodeInt64(r) if err != nil { return fmt.Errorf("read i64: %w", err) diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index e4ffd13e..71c50953 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -368,7 +368,7 @@ func TestModule_validateGlobals(t *testing.T) { m := Module{GlobalSection: []*Global{ { Type: &GlobalType{ValType: ValueTypeI32}, - Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0}}, + Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, }, }} err := m.validateGlobals(nil, 9) @@ -500,7 +500,7 @@ func TestModule_validateMemory(t *testing.T) { Init: []byte{0x1}, OffsetExpression: &ConstantExpression{ Opcode: OpcodeI32Const, - Data: []byte{0x1}, + Data: leb128.EncodeInt32(1), }, }}} err := m.validateMemory(&Memory{}, nil) @@ -670,7 +670,7 @@ func TestModule_buildGlobalInstances(t *testing.T) { { Type: &GlobalType{Mutable: false, ValType: ValueTypeI32}, Init: &ConstantExpression{Opcode: OpcodeI32Const, - Data: leb128.EncodeUint32(math.MaxInt32)}, + Data: leb128.EncodeInt32(math.MaxInt32)}, }, }} diff --git a/internal/wasm/store.go b/internal/wasm/store.go index 23f0b274..6a8b3cd8 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -474,8 +474,10 @@ func executeConstExpression(globals []*GlobalInstance, expr *ConstantExpression) r := bytes.NewReader(expr.Data) switch expr.Opcode { case OpcodeI32Const: + // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md v, _, _ = leb128.DecodeInt32(r) case OpcodeI64Const: + // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md v, _, _ = leb128.DecodeInt64(r) case OpcodeF32Const: v, _ = ieee754.DecodeFloat32(r) diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index 5b8b8e37..60eb11de 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/leb128" "github.com/tetratelabs/wazero/internal/testing/hammer" "github.com/tetratelabs/wazero/internal/u64" ) @@ -148,7 +149,7 @@ func TestStore_CloseModule(t *testing.T) { TypeSection: []*FunctionType{{}}, ImportSection: []*Import{{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}}, MemorySection: &Memory{Min: 1}, - GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}}}, + GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}}, TableSection: &Table{Min: 10}, }, importingModuleName, nil) require.NoError(t, err) @@ -192,7 +193,7 @@ func TestStore_hammer(t *testing.T) { FunctionSection: []uint32{0}, CodeSection: []*Code{{Body: []byte{OpcodeEnd}}}, MemorySection: &Memory{Min: 1}, - GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}}}, + GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}}, TableSection: &Table{Min: 10}, ImportSection: []*Import{ {Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}, @@ -680,21 +681,21 @@ func TestModuleInstance_validateData(t *testing.T) { { name: "ok", data: []*DataSegment{ - {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}, Init: []byte{0}}, - {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x2}}, Init: []byte{0}}, + {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []byte{0}}, + {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)}, Init: []byte{0}}, }, }, { name: "out of bounds - single one byte", data: []*DataSegment{ - {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x5}}, Init: []byte{0}}, + {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(5)}, Init: []byte{0}}, }, expErr: true, }, { name: "out of bounds - multi bytes", data: []*DataSegment{ - {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x3}}, Init: []byte{0, 1, 2}}, + {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(3)}, Init: []byte{0, 1, 2}}, }, expErr: true, }, @@ -714,8 +715,8 @@ func TestModuleInstance_validateData(t *testing.T) { func TestModuleInstance_applyData(t *testing.T) { m := &ModuleInstance{Memory: &MemoryInstance{Buffer: make([]byte, 10)}} m.applyData([]*DataSegment{ - {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, Init: []byte{0xa, 0xf}}, - {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x8}}, Init: []byte{0x1, 0x5}}, + {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []byte{0xa, 0xf}}, + {OffsetExpression: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(8)}, Init: []byte{0x1, 0x5}}, }) require.Equal(t, []byte{0xa, 0xf, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5}, m.Memory.Buffer) } diff --git a/internal/wasm/table.go b/internal/wasm/table.go index 34cd0bd8..7de65b79 100644 --- a/internal/wasm/table.go +++ b/internal/wasm/table.go @@ -113,7 +113,8 @@ func (m *Module) validateTable() ([]*validatedElementSegment, error) { ret = append(ret, &validatedElementSegment{oc, globalIdx, elem.Init}) } else if oc == OpcodeI32Const { - o, _, err := leb128.DecodeUint32(bytes.NewReader(elem.OffsetExpr.Data)) + // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md + o, _, err := leb128.DecodeInt32(bytes.NewReader(elem.OffsetExpr.Data)) if err != nil { return nil, fmt.Errorf("%s[%d] couldn't read i32.const parameter: %w", SectionIDName(SectionIDElement), idx, err) } diff --git a/internal/wasm/table_test.go b/internal/wasm/table_test.go index b250bf2f..3c3077f1 100644 --- a/internal/wasm/table_test.go +++ b/internal/wasm/table_test.go @@ -80,7 +80,7 @@ func TestModule_validateTable(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}}, }, }, expected: []*validatedElementSegment{}, @@ -94,7 +94,7 @@ func TestModule_validateTable(t *testing.T) { CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ { - OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, + OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []Index{0}, }, }, @@ -112,7 +112,7 @@ func TestModule_validateTable(t *testing.T) { CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ { - OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, + OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []Index{0}, }, }, @@ -130,7 +130,7 @@ func TestModule_validateTable(t *testing.T) { CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ { - OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, + OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []Index{0}, }, }, @@ -148,7 +148,7 @@ func TestModule_validateTable(t *testing.T) { CodeSection: []*Code{codeEnd, codeEnd, codeEnd, codeEnd}, ElementSection: []*ElementSegment{ { - OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}, + OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []Index{0, 2}, }, }, @@ -271,7 +271,7 @@ func TestModule_validateTable(t *testing.T) { CodeSection: []*Code{codeEnd, codeEnd, codeEnd, codeEnd}, ElementSection: []*ElementSegment{ { - OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}, + OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []Index{0, 2}, }, { @@ -334,7 +334,7 @@ func TestModule_validateTable_Errors(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI64Const, Data: []byte{0x0}}, Init: []Index{0}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI64Const, Data: const0}, Init: []Index{0}}, }, }, expectedErr: "element[0] has an invalid const expression: i64.const", @@ -346,7 +346,7 @@ func TestModule_validateTable_Errors(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, Init: []Index{0}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []Index{0}}, }, }, expectedErr: "element was defined, but not table", @@ -359,7 +359,7 @@ func TestModule_validateTable_Errors(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x2}}, Init: []Index{0}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)}, Init: []Index{0}}, }, }, expectedErr: "element[0].init exceeds min table size", @@ -372,8 +372,8 @@ func TestModule_validateTable_Errors(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}, Init: []Index{0}}, - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}, Init: []Index{0, 0}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []Index{0}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []Index{0, 0}}, }, }, expectedErr: "element[1].init exceeds min table size", @@ -386,7 +386,7 @@ func TestModule_validateTable_Errors(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x2}}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)}}, }, }, expectedErr: "element[0].init exceeds min table size", @@ -399,7 +399,7 @@ func TestModule_validateTable_Errors(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}, Init: []Index{0, 1}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []Index{0, 1}}, }, }, expectedErr: "element[0].init[1] funcidx 1 out of range", @@ -513,6 +513,9 @@ func TestModule_validateTable_Errors(t *testing.T) { } } +var const0 = leb128.EncodeInt32(0) +var const1 = leb128.EncodeInt32(1) + func TestModule_buildTable(t *testing.T) { three := uint32(3) tests := []struct { @@ -553,7 +556,7 @@ func TestModule_buildTable(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}}, }, validatedElementSegments: []*validatedElementSegment{}, }, @@ -568,7 +571,7 @@ func TestModule_buildTable(t *testing.T) { CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ { - OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, + OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []Index{0}, }, }, @@ -588,7 +591,7 @@ func TestModule_buildTable(t *testing.T) { CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ { - OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, + OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []Index{0}, }, }, @@ -606,7 +609,7 @@ func TestModule_buildTable(t *testing.T) { CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ { - OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, + OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []Index{0}, }, }, @@ -624,7 +627,7 @@ func TestModule_buildTable(t *testing.T) { CodeSection: []*Code{codeEnd, codeEnd, codeEnd, codeEnd}, ElementSection: []*ElementSegment{ { - OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}, + OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []Index{0, 2}, }, }, @@ -766,7 +769,7 @@ func TestModule_buildTable(t *testing.T) { CodeSection: []*Code{codeEnd, codeEnd, codeEnd, codeEnd}, ElementSection: []*ElementSegment{ { - OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}, + OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []Index{0, 2}, }, { @@ -822,7 +825,7 @@ func TestModule_buildTable_Errors(t *testing.T) { CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ { - OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, + OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []Index{0}, }, }, diff --git a/tests/spectest/spec_test.go b/tests/spectest/spec_test.go index 03aef6fa..5698d8ff 100644 --- a/tests/spectest/spec_test.go +++ b/tests/spectest/spec_test.go @@ -245,7 +245,7 @@ func addSpectestModule(t *testing.T, store *wasm.Store) { // (global (export "global_i32") i32 (i32.const 666)) mod.GlobalSection = append(mod.GlobalSection, &wasm.Global{ Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32}, - Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeUint32(666)}, + Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(666)}, }) mod.ExportSection["global_i32"] = &wasm.Export{Name: "global_i32", Index: 0, Type: wasm.ExternTypeGlobal} diff --git a/wasm_test.go b/wasm_test.go index a1a7aba6..d69287e0 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/leb128" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm/binary" ) @@ -164,6 +165,8 @@ func TestModule_Memory(t *testing.T) { // TestModule_Global only covers a couple cases to avoid duplication of internal/wasm/global_test.go func TestModule_Global(t *testing.T) { + globalVal := int64(100) // intentionally a value that differs in signed vs unsigned encoding + tests := []struct { name string module *wasm.Module // module as wat doesn't yet support globals @@ -180,7 +183,7 @@ func TestModule_Global(t *testing.T) { GlobalSection: []*wasm.Global{ { Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64, Mutable: true}, - Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: []byte{1}}, + Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(globalVal)}, }, }, }, @@ -188,7 +191,7 @@ func TestModule_Global(t *testing.T) { { name: "global exported", builder: func(r Runtime) ModuleBuilder { - return r.NewModuleBuilder(t.Name()).ExportGlobalI64("global", 1) + return r.NewModuleBuilder(t.Name()).ExportGlobalI64("global", globalVal) }, expected: true, }, @@ -198,7 +201,7 @@ func TestModule_Global(t *testing.T) { GlobalSection: []*wasm.Global{ { Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64, Mutable: true}, - Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: []byte{1}}, + Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(globalVal)}, }, }, ExportSection: map[string]*wasm.Export{ @@ -232,7 +235,7 @@ func TestModule_Global(t *testing.T) { require.Nil(t, global) return } - require.Equal(t, uint64(1), global.Get()) + require.Equal(t, uint64(globalVal), global.Get()) mutable, ok := global.(api.MutableGlobal) require.Equal(t, tc.expectedMutable, ok)