Repository: tetratelabs/wazero
Pull request #1914 (2024-01-12 03:15:05)

threads: add decoding of shared memory


Patch:
diff --git a/internal/wasm/binary/limits.go b/internal/wasm/binary/limits.go
index 1ec8d7c3..ff2d73b5 100644
--- a/internal/wasm/binary/limits.go
+++ b/internal/wasm/binary/limits.go
@@ -10,7 +10,9 @@ import (
 // decodeLimitsType returns the `limitsType` (min, max) decoded with the WebAssembly 1.0 (20191205) Binary Format.
 //
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#limits%E2%91%A6
-func decodeLimitsType(r *bytes.Reader) (min uint32, max *uint32, err error) {
+//
+// Extended in threads proposal: https://webassembly.github.io/threads/core/binary/types.html#limits
+func decodeLimitsType(r *bytes.Reader) (min uint32, max *uint32, shared bool, err error) {
 	var flag byte
 	if flag, err = r.ReadByte(); err != nil {
 		err = fmt.Errorf("read leading byte: %v", err)
@@ -18,12 +20,12 @@ func decodeLimitsType(r *bytes.Reader) (min uint32, max *uint32, err error) {
 	}
 
 	switch flag {
-	case 0x00:
+	case 0x00, 0x02:
 		min, _, err = leb128.DecodeUint32(r)
 		if err != nil {
 			err = fmt.Errorf("read min of limit: %v", err)
 		}
-	case 0x01:
+	case 0x01, 0x03:
 		min, _, err = leb128.DecodeUint32(r)
 		if err != nil {
 			err = fmt.Errorf("read min of limit: %v", err)
@@ -36,7 +38,10 @@ func decodeLimitsType(r *bytes.Reader) (min uint32, max *uint32, err error) {
 			max = &m
 		}
 	default:
-		err = fmt.Errorf("%v for limits: %#x != 0x00 or 0x01", ErrInvalidByte, flag)
+		err = fmt.Errorf("%v for limits: %#x not in (0x00, 0x01, 0x02, 0x03)", ErrInvalidByte, flag)
 	}
+
+	shared = flag == 0x02 || flag == 0x03
+
 	return
 }
diff --git a/internal/wasm/binary/memory.go b/internal/wasm/binary/memory.go
index fd6b32a6..aca61994 100644
--- a/internal/wasm/binary/memory.go
+++ b/internal/wasm/binary/memory.go
@@ -2,6 +2,7 @@ package binary
 
 import (
 	"bytes"
+	"fmt"
 
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -14,13 +15,23 @@ func decodeMemory(
 	memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32),
 	memoryLimitPages uint32,
 ) (*wasm.Memory, error) {
-	min, maxP, err := decodeLimitsType(r)
+	min, maxP, shared, err := decodeLimitsType(r)
 	if err != nil {
 		return nil, err
 	}
 
+	if shared {
+		// TODO(anuraaga): Check threads feature is enabled
+
+		// This restriction may be lifted in the future.
+		// https://webassembly.github.io/threads/core/binary/types.html#memory-types
+		if maxP == nil {
+			return nil, fmt.Errorf("shared memory requires a maximum size to be specified")
+		}
+	}
+
 	min, capacity, max := memorySizer(min, maxP)
-	mem := &wasm.Memory{Min: min, Cap: capacity, Max: max, IsMaxEncoded: maxP != nil}
+	mem := &wasm.Memory{Min: min, Cap: capacity, Max: max, IsMaxEncoded: maxP != nil, IsShared: shared}
 
 	return mem, mem.Validate(memoryLimitPages)
 }
diff --git a/internal/wasm/binary/table.go b/internal/wasm/binary/table.go
index cb5aa75e..353ec756 100644
--- a/internal/wasm/binary/table.go
+++ b/internal/wasm/binary/table.go
@@ -23,7 +23,8 @@ func decodeTable(r *bytes.Reader, enabledFeatures api.CoreFeatures, ret *wasm.Ta
 		}
 	}
 
-	ret.Min, ret.Max, err = decodeLimitsType(r)
+	var shared bool
+	ret.Min, ret.Max, shared, err = decodeLimitsType(r)
 	if err != nil {
 		return fmt.Errorf("read limits: %v", err)
 	}
@@ -35,5 +36,8 @@ func decodeTable(r *bytes.Reader, enabledFeatures api.CoreFeatures, ret *wasm.Ta
 			return fmt.Errorf("table size minimum must not be greater than maximum")
 		}
 	}
+	if shared {
+		return fmt.Errorf("tables cannot be marked as shared")
+	}
 	return
 }
diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go
index bce037e4..38781449 100644
--- a/internal/wasm/memory.go
+++ b/internal/wasm/memory.go
@@ -36,6 +36,7 @@ type MemoryInstance struct {
 
 	Buffer        []byte
 	Min, Cap, Max uint32
+	Shared        bool
 	// definition is known at compile time.
 	definition api.MemoryDefinition
 }
@@ -49,6 +50,7 @@ func NewMemoryInstance(memSec *Memory) *MemoryInstance {
 		Min:    memSec.Min,
 		Cap:    memSec.Cap,
 		Max:    memSec.Max,
+		Shared: memSec.IsShared,
 	}
 }
 
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index a5aaf6ad..c019730e 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -766,6 +766,8 @@ type Memory struct {
 	Min, Cap, Max uint32
 	// IsMaxEncoded true if the Max is encoded in the original binary.
 	IsMaxEncoded bool
+	// IsShared true if the memory is shared for access from multiple agents.
+	IsShared bool
 }
 
 // Validate ensures values assigned to Min, Cap and Max are within valid thresholds.


Test patch:
diff --git a/internal/testing/binaryencoding/import.go b/internal/testing/binaryencoding/import.go
index 0646a19c..8abffde2 100644
--- a/internal/testing/binaryencoding/import.go
+++ b/internal/testing/binaryencoding/import.go
@@ -19,13 +19,13 @@ func EncodeImport(i *wasm.Import) []byte {
 		data = append(data, leb128.EncodeUint32(i.DescFunc)...)
 	case wasm.ExternTypeTable:
 		data = append(data, wasm.RefTypeFuncref)
-		data = append(data, EncodeLimitsType(i.DescTable.Min, i.DescTable.Max)...)
+		data = append(data, EncodeLimitsType(i.DescTable.Min, i.DescTable.Max, false)...)
 	case wasm.ExternTypeMemory:
 		maxPtr := &i.DescMem.Max
 		if !i.DescMem.IsMaxEncoded {
 			maxPtr = nil
 		}
-		data = append(data, EncodeLimitsType(i.DescMem.Min, maxPtr)...)
+		data = append(data, EncodeLimitsType(i.DescMem.Min, maxPtr, i.DescMem.IsShared)...)
 	case wasm.ExternTypeGlobal:
 		g := i.DescGlobal
 		var mutable byte
diff --git a/internal/testing/binaryencoding/limits.go b/internal/testing/binaryencoding/limits.go
index 016e4dc7..ebc2421c 100644
--- a/internal/testing/binaryencoding/limits.go
+++ b/internal/testing/binaryencoding/limits.go
@@ -7,9 +7,19 @@ import (
 // EncodeLimitsType returns the `limitsType` (min, max) encoded in WebAssembly 1.0 (20191205) Binary Format.
 //
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#limits%E2%91%A6
-func EncodeLimitsType(min uint32, max *uint32) []byte {
-	if max == nil {
-		return append(leb128.EncodeUint32(0x00), leb128.EncodeUint32(min)...)
+//
+// Extended in threads proposal: https://webassembly.github.io/threads/core/binary/types.html#limits
+func EncodeLimitsType(min uint32, max *uint32, shared bool) []byte {
+	var flag uint32
+	if max != nil {
+		flag = 0x01
+	}
+	if shared {
+		flag |= 0x02
+	}
+	ret := append(leb128.EncodeUint32(flag), leb128.EncodeUint32(min)...)
+	if max != nil {
+		ret = append(ret, leb128.EncodeUint32(*max)...)
 	}
-	return append(leb128.EncodeUint32(0x01), append(leb128.EncodeUint32(min), leb128.EncodeUint32(*max)...)...)
+	return ret
 }
diff --git a/internal/testing/binaryencoding/memory.go b/internal/testing/binaryencoding/memory.go
index 938667a6..2c853330 100644
--- a/internal/testing/binaryencoding/memory.go
+++ b/internal/testing/binaryencoding/memory.go
@@ -12,5 +12,5 @@ func EncodeMemory(i *wasm.Memory) []byte {
 	if !i.IsMaxEncoded {
 		maxPtr = nil
 	}
-	return EncodeLimitsType(i.Min, maxPtr)
+	return EncodeLimitsType(i.Min, maxPtr, i.IsShared)
 }
diff --git a/internal/testing/binaryencoding/table.go b/internal/testing/binaryencoding/table.go
index 2985dbdb..57f83db7 100644
--- a/internal/testing/binaryencoding/table.go
+++ b/internal/testing/binaryencoding/table.go
@@ -8,5 +8,5 @@ import (
 //
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-table
 func EncodeTable(i *wasm.Table) []byte {
-	return append([]byte{i.Type}, EncodeLimitsType(i.Min, i.Max)...)
+	return append([]byte{i.Type}, EncodeLimitsType(i.Min, i.Max, false)...)
 }
diff --git a/internal/wasm/binary/limits_test.go b/internal/wasm/binary/limits_test.go
index e6f66393..7e65b4d6 100644
--- a/internal/wasm/binary/limits_test.go
+++ b/internal/wasm/binary/limits_test.go
@@ -18,6 +18,7 @@ func TestLimitsType(t *testing.T) {
 		name     string
 		min      uint32
 		max      *uint32
+		shared   bool
 		expected []byte
 	}{
 		{
@@ -45,21 +46,52 @@ func TestLimitsType(t *testing.T) {
 			max:      &largest,
 			expected: []byte{0x1, 0xff, 0xff, 0xff, 0xff, 0xf, 0xff, 0xff, 0xff, 0xff, 0xf},
 		},
+		{
+			name:     "min 0, shared",
+			shared:   true,
+			expected: []byte{0x2, 0},
+		},
+		{
+			name:     "min 0, max 0, shared",
+			max:      &zero,
+			shared:   true,
+			expected: []byte{0x3, 0, 0},
+		},
+		{
+			name:     "min largest, shared",
+			min:      largest,
+			shared:   true,
+			expected: []byte{0x2, 0xff, 0xff, 0xff, 0xff, 0xf},
+		},
+		{
+			name:     "min 0, max largest, shared",
+			max:      &largest,
+			shared:   true,
+			expected: []byte{0x3, 0, 0xff, 0xff, 0xff, 0xff, 0xf},
+		},
+		{
+			name:     "min largest max largest, shared",
+			min:      largest,
+			max:      &largest,
+			shared:   true,
+			expected: []byte{0x3, 0xff, 0xff, 0xff, 0xff, 0xf, 0xff, 0xff, 0xff, 0xff, 0xf},
+		},
 	}
 
 	for _, tt := range tests {
 		tc := tt
 
-		b := binaryencoding.EncodeLimitsType(tc.min, tc.max)
+		b := binaryencoding.EncodeLimitsType(tc.min, tc.max, tc.shared)
 		t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) {
 			require.Equal(t, tc.expected, b)
 		})
 
 		t.Run(fmt.Sprintf("decode - %s", tc.name), func(t *testing.T) {
-			min, max, err := decodeLimitsType(bytes.NewReader(b))
+			min, max, shared, err := decodeLimitsType(bytes.NewReader(b))
 			require.NoError(t, err)
 			require.Equal(t, min, tc.min)
 			require.Equal(t, max, tc.max)
+			require.Equal(t, shared, tc.shared)
 		})
 	}
 }
diff --git a/internal/wasm/binary/memory_test.go b/internal/wasm/binary/memory_test.go
index 32bfa188..8a861336 100644
--- a/internal/wasm/binary/memory_test.go
+++ b/internal/wasm/binary/memory_test.go
@@ -163,6 +163,11 @@ func TestMemoryType(t *testing.T) {
 			memoryLimitPages: 512,
 			expected:         []byte{0x1, 0, 0x80, 0x80, 0x4},
 		},
+		{
+			name:     "min 0, max 1, shared",
+			input:    &wasm.Memory{Max: 1, IsMaxEncoded: true, IsShared: true},
+			expected: []byte{0x3, 0, 1},
+		},
 	}
 
 	for _, tt := range tests {
@@ -212,6 +217,11 @@ func TestDecodeMemoryType_Errors(t *testing.T) {
 			input:       []byte{0x1, 0, 0xff, 0xff, 0xff, 0xff, 0xf},
 			expectedErr: "max 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
 		},
+		{
+			name:        "shared but no max",
+			input:       []byte{0x2, 0, 0x80, 0x80, 0x4},
+			expectedErr: "shared memory requires a maximum size to be specified",
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/internal/wasm/binary/table_test.go b/internal/wasm/binary/table_test.go
index 73ec6d4d..5f0bbaf6 100644
--- a/internal/wasm/binary/table_test.go
+++ b/internal/wasm/binary/table_test.go
@@ -93,6 +93,12 @@ func TestDecodeTableType_Errors(t *testing.T) {
 			expectedErr: "table min must be at most 134217728",
 			features:    api.CoreFeatureReferenceTypes,
 		},
+		{
+			name:        "shared",
+			input:       []byte{wasm.RefTypeFuncref, 0x2, 0},
+			expectedErr: "tables cannot be marked as shared",
+			// Shared tables are an error even if threads are enabled.
+		},
 	}
 
 	for _, tt := range tests {