Repository: tetratelabs/wazero
Pull request #549 (2022-05-12 08:21:06)

Adds limit on the number of tables per module
Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>



Patch:
diff --git a/RATIONALE.md b/RATIONALE.md
index 36e1033c..36ad484f 100644
--- a/RATIONALE.md
+++ b/RATIONALE.md
@@ -403,6 +403,16 @@ Theoretically, a module can declare globals (including imports) up to 2^32 times
 That is because internally we store globals in a slice with pointer types (meaning 8 bytes on 64-bit platforms), and therefore 2^27 globals
 means that we have 1 GiB size of slice which seems large enough for most applications.
 
+### Number of tables in a module
+
+While the the spec says that a module can have up to 2^32 tables, wazero limits this to 2^27 = 134,217,728.
+One of the reasons is even that number would occupy 1GB in the pointers tables alone. Not only that, we access tables slice by
+table index by using 32-bit signed offset in the JIT implementation, which means that the table index of 2^27 can reach 2^27 * 8 (pointer size on 64-bit machines) = 2^30 offsets in bytes.
+
+We _believe_ that all use cases are fine with the limitation, but also note that we have no way to test wazero runtimes under these unusual circumstances.
+
+If a module reaches this limit, an error is returned at the compilation phase.
+
 ## JIT engine implementation
 
 See [wasm/jit/RATIONALE.md](internal/wasm/jit/RATIONALE.md).
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index 9f7f4254..1444f203 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -187,6 +187,7 @@ type ModuleID = [sha256.Size]byte
 const (
 	MaximumGlobals       = uint32(1 << 27)
 	MaximumFunctionIndex = uint32(1 << 27)
+	MaximumTableIndex    = uint32(1 << 27)
 )
 
 // AssignModuleID calculates a sha256 checksum on `source` and set Module.ID to the result.
@@ -261,7 +262,7 @@ func (m *Module) Validate(enabledFeatures Features) error {
 		}
 	} // No need to validate host functions as NewHostModule validates
 
-	if _, err = m.validateTable(enabledFeatures, tables); err != nil {
+	if _, err = m.validateTable(enabledFeatures, tables, MaximumTableIndex); err != nil {
 		return err
 	}
 
@@ -351,7 +352,7 @@ func (m *Module) validateFunctions(enabledFeatures Features, functions []Index,
 // To summarize, the function indexes OpcodeRefFunc can refer include:
 //  * existing in an element section regardless of its mode (active, passive, declarative).
 //  * defined as globals whose value type is ValueRefFunc.
-//  * used as an exported functions.
+//  * used as an exported function.
 //
 // See https://github.com/WebAssembly/reference-types/issues/31
 // See https://github.com/WebAssembly/reference-types/issues/76
diff --git a/internal/wasm/table.go b/internal/wasm/table.go
index ad87eca2..d6a8fcb5 100644
--- a/internal/wasm/table.go
+++ b/internal/wasm/table.go
@@ -144,7 +144,11 @@ type validatedActiveElementSegment struct {
 
 // validateTable ensures any ElementSegment is valid. This caches results via Module.validatedActiveElementSegments.
 // Note: limitsType are validated by decoders, so not re-validated here.
-func (m *Module) validateTable(enabledFeatures Features, tables []*Table) ([]*validatedActiveElementSegment, error) {
+func (m *Module) validateTable(enabledFeatures Features, tables []*Table, maximumTableIndex uint32) ([]*validatedActiveElementSegment, error) {
+	if len(tables) > int(maximumTableIndex) {
+		return nil, fmt.Errorf("too many tables in a module: %d given with limit %d", len(tables), maximumTableIndex)
+	}
+
 	if m.validatedActiveElementSegments != nil {
 		return m.validatedActiveElementSegments, nil
 	}


Test patch:
diff --git a/internal/wasm/table_test.go b/internal/wasm/table_test.go
index b7e32840..03103d28 100644
--- a/internal/wasm/table_test.go
+++ b/internal/wasm/table_test.go
@@ -55,6 +55,7 @@ func TestStore_resolveImports_table(t *testing.T) {
 var codeEnd = &Code{Body: []byte{OpcodeEnd}}
 
 func TestModule_validateTable(t *testing.T) {
+	const maxTableIndex = 5
 	three := uint32(3)
 	tests := []struct {
 		name     string
@@ -71,6 +72,11 @@ func TestModule_validateTable(t *testing.T) {
 			input:    &Module{TableSection: []*Table{{}}},
 			expected: []*validatedActiveElementSegment{},
 		},
+		{
+			name:     "maximum number of tables",
+			input:    &Module{TableSection: []*Table{{}, {}, {}, {}, {}}},
+			expected: []*validatedActiveElementSegment{},
+		},
 		{
 			name:     "min/max",
 			input:    &Module{TableSection: []*Table{{Min: 1, Max: &three}}},
@@ -312,13 +318,13 @@ func TestModule_validateTable(t *testing.T) {
 			_, _, _, tables, err := tc.input.AllDeclarations()
 			require.NoError(t, err)
 
-			vt, err := tc.input.validateTable(Features20191205, tables)
+			vt, err := tc.input.validateTable(Features20191205, tables, maxTableIndex)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, vt)
 
 			// Ensure it was cached. We have to use Equal not Same because this is a slice, not a pointer.
 			require.Equal(t, vt, tc.input.validatedActiveElementSegments)
-			vt2, err := tc.input.validateTable(Features20191205, tables)
+			vt2, err := tc.input.validateTable(Features20191205, tables, maxTableIndex)
 			require.NoError(t, err)
 			require.Equal(t, vt, vt2)
 		})
@@ -326,11 +332,19 @@ func TestModule_validateTable(t *testing.T) {
 }
 
 func TestModule_validateTable_Errors(t *testing.T) {
+	const maxTableIndex = 5
 	tests := []struct {
 		name        string
 		input       *Module
 		expectedErr string
 	}{
+		{
+			name: "too many tables",
+			input: &Module{
+				TableSection: []*Table{{}, {}, {}, {}, {}, {}},
+			},
+			expectedErr: "too many tables in a module: 6 given with limit 5",
+		},
 		{
 			name: "non funcref",
 			input: &Module{
@@ -578,7 +592,7 @@ func TestModule_validateTable_Errors(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			_, _, _, tables, err := tc.input.AllDeclarations()
 			require.NoError(t, err)
-			_, err = tc.input.validateTable(Features20191205, tables)
+			_, err = tc.input.validateTable(Features20191205, tables, maxTableIndex)
 			require.EqualError(t, err, tc.expectedErr)
 		})
 	}