Repository: tetratelabs/wazero
Pull request #443 (2022-04-06 01:46:36)

Consistently uses LEB128 signed encoding for global constants
Global constants can be defined in wasm or in ModuleBuilder. In either
case, they end up being decoded and interpreted during instantiation.
This chooses signed encoding to avoid surprises. A more comprehensive
explanation was added to RATIONALE.md, but the motivation was a global
100 coming out negative.



Patch:
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/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/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/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/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/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)
 			}


Test patch:
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/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/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/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_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_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)