Repository: tetratelabs/wazero
Pull request #368 (2022-03-14 08:10:14)

Adds stricter enforcement of Table element validation.
This enforces all ElementSegment rules prior to creating a new module
engine. This allows flexibility and less errors inside that function.

This also uses interface as table element to pass tests with -race

codefromthecrypt: @mathetake I picked in your https://github.com/tetratelabs/wazero/pull/353 as I couldn't find an easier way besides a polymorphic type to hold the tables at the moment. The problems with refactor it are about sharing and using the right table context in a function call. Right now, it seems like punting to interface is the best way forward.

Before I work more on tests, can you give a quick look that the approach is sound?
mathetake: looks fine to me
codefromthecrypt: ok next steps are basically backfilling tests of which there are many!
mathetake: https://github.com/tetratelabs/wazero/runs/5536658163?check_suite_focus=true

oh something weird  happens.. maybe double free of compiledFunctions.. 
codefromthecrypt: ok rebased... now will go back and backfill the unit tests.

Patch:
diff --git a/internal/leb128/leb128.go b/internal/leb128/leb128.go
index b9a033d3..2fcdfad4 100644
--- a/internal/leb128/leb128.go
+++ b/internal/leb128/leb128.go
@@ -32,7 +32,14 @@ var encodeCache = [0x80][]byte{
 // EncodeUint32 encodes the value into a buffer in LEB128 format
 //
 // See https://en.wikipedia.org/wiki/LEB128#Encode_unsigned_integer
-func EncodeUint32(value uint32) (buf []byte) {
+func EncodeUint32(value uint32) []byte {
+	return EncodeUint64(uint64(value))
+}
+
+// EncodeUint64 encodes the value into a buffer in LEB128 format
+//
+// See https://en.wikipedia.org/wiki/LEB128#Encode_unsigned_integer
+func EncodeUint64(value uint64) (buf []byte) {
 	if value < 0x80 {
 		return encodeCache[value]
 	}
diff --git a/internal/wasm/engine.go b/internal/wasm/engine.go
index eb352dec..2ea012cc 100644
--- a/internal/wasm/engine.go
+++ b/internal/wasm/engine.go
@@ -10,10 +10,12 @@ type Engine interface {
 	// * name is the name the module was instantiated with used for error handling.
 	// * importedFunctions: functions this module imports, already compiled in this engine.
 	// * moduleFunctions: functions declared in this module that must be compiled.
+	// * table: a possibly shared table used by this module. When nil tableInit will be nil.
+	// * tableInit: a mapping of TableInstance.Table index to the function index it should point to.
 	//
 	// Note: Input parameters must be pre-validated with internalwasm.Module Validate, to ensure no fields are invalid
 	// due to reasons such as out-of-bounds.
-	NewModuleEngine(name string, importedFunctions, moduleFunctions []*FunctionInstance) (ModuleEngine, error)
+	NewModuleEngine(name string, importedFunctions, moduleFunctions []*FunctionInstance, table *TableInstance, tableInit map[Index]Index) (ModuleEngine, error)
 }
 
 // ModuleEngine implements function calls for a given module.
@@ -22,12 +24,6 @@ type ModuleEngine interface {
 	io.Closer
 	// ^^ io.Closer not due to I/O, but to allow future static analysis to catch leaks (unclosed Closers).
 
-	// FunctionAddress returns the absolute address of the compiled function for index.
-	// The returned address is stored and used as a TableInstance.Table element.
-	//
-	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-funcaddr
-	FunctionAddress(index Index) uintptr
-
 	// Call invokes a function instance f with given parameters.
 	// Returns the results from the function.
 	// The ctx's context.Context will be the outer-most ancestor of the argument to wasm.Function.
diff --git a/internal/wasm/interpreter/interpreter.go b/internal/wasm/interpreter/interpreter.go
index d378e51d..2e7aec3e 100644
--- a/internal/wasm/interpreter/interpreter.go
+++ b/internal/wasm/interpreter/interpreter.go
@@ -9,7 +9,6 @@ import (
 	"runtime/debug"
 	"strings"
 	"sync"
-	"unsafe"
 
 	"github.com/tetratelabs/wazero/internal/moremath"
 	wasm "github.com/tetratelabs/wazero/internal/wasm"
@@ -161,7 +160,7 @@ func (me *moduleEngine) Close() (err error) {
 }
 
 // NewModuleEngine implements internalwasm.Engine NewModuleEngine
-func (e *engine) NewModuleEngine(name string, importedFunctions, moduleFunctions []*wasm.FunctionInstance) (wasm.ModuleEngine, error) {
+func (e *engine) NewModuleEngine(name string, importedFunctions, moduleFunctions []*wasm.FunctionInstance, table *wasm.TableInstance, tableInit map[wasm.Index]wasm.Index) (wasm.ModuleEngine, error) {
 	me := &moduleEngine{
 		name:                   name,
 		compiledFunctions:      make([]*compiledFunction, 0, len(importedFunctions)+len(moduleFunctions)),
@@ -202,6 +201,10 @@ func (e *engine) NewModuleEngine(name string, importedFunctions, moduleFunctions
 		// the future importing module can refer the function instance.
 		e.addCompiledFunction(f, compiled)
 	}
+
+	for elemIdx, funcidx := range tableInit { // Initialize any elements with compiled functions
+		table.Table[elemIdx] = me.compiledFunctions[funcidx]
+	}
 	return me, nil
 }
 
@@ -488,11 +491,6 @@ func (e *engine) lowerIROps(f *wasm.FunctionInstance,
 	return ret, nil
 }
 
-// FunctionAddress implements internalwasm.ModuleEngine FunctionAddress.
-func (me *moduleEngine) FunctionAddress(index wasm.Index) uintptr {
-	return uintptr(unsafe.Pointer(me.compiledFunctions[index]))
-}
-
 // Call implements internalwasm.ModuleEngine Call.
 func (me *moduleEngine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
 	compiled := me.compiledFunctions[f.Index]
@@ -665,8 +663,8 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *compiledFunctio
 				if offset >= uint64(len(table.Table)) {
 					panic(wasm.ErrRuntimeInvalidTableAccess)
 				}
-				targetCompiledFunction := (*compiledFunction)(unsafe.Pointer(table.Table[offset]))
-				if targetCompiledFunction == nil {
+				targetCompiledFunction, ok := table.Table[offset].(*compiledFunction)
+				if !ok {
 					panic(wasm.ErrRuntimeInvalidTableAccess)
 				} else if uint64(targetCompiledFunction.funcInstance.TypeID) != op.us[1] {
 					panic(wasm.ErrRuntimeIndirectCallTypeMismatch)
diff --git a/internal/wasm/jit/engine.go b/internal/wasm/jit/engine.go
index 771d7478..231cf70f 100644
--- a/internal/wasm/jit/engine.go
+++ b/internal/wasm/jit/engine.go
@@ -332,7 +332,7 @@ func (c *callFrame) String() string {
 }
 
 // NewModuleEngine implements internalwasm.Engine NewModuleEngine
-func (e *engine) NewModuleEngine(name string, importedFunctions, moduleFunctions []*wasm.FunctionInstance) (wasm.ModuleEngine, error) {
+func (e *engine) NewModuleEngine(name string, importedFunctions, moduleFunctions []*wasm.FunctionInstance, table *wasm.TableInstance, tableInit map[wasm.Index]wasm.Index) (wasm.ModuleEngine, error) {
 	imported := len(importedFunctions)
 	me := &moduleEngine{
 		name:                   name,
@@ -368,14 +368,12 @@ func (e *engine) NewModuleEngine(name string, importedFunctions, moduleFunctions
 		e.addCompiledFunction(f, compiled)
 	}
 
+	for elemIdx, funcidx := range tableInit { // Initialize any elements with compiled functions
+		table.Table[elemIdx] = me.compiledFunctions[funcidx]
+	}
 	return me, nil
 }
 
-// FunctionAddress implements internalwasm.ModuleEngine FunctionAddress
-func (me *moduleEngine) FunctionAddress(index wasm.Index) uintptr {
-	return uintptr(unsafe.Pointer(me.compiledFunctions[index]))
-}
-
 // Close implements io.Closer
 func (me *moduleEngine) Close() (err error) {
 	me.doClose(runtime.SetFinalizer) // extracted for testability
diff --git a/internal/wasm/jit/jit_amd64.go b/internal/wasm/jit/jit_amd64.go
index c3e4ed1f..94dd8514 100644
--- a/internal/wasm/jit/jit_amd64.go
+++ b/internal/wasm/jit/jit_amd64.go
@@ -1040,16 +1040,16 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 
 	// Next we check if the target's type matches the operation's one.
 	// In order to get the type instance's address, we have to multiply the offset
-	// by 8 as the offset is the "length" of table in Go's "[]uintptr",
-	// and size of uintptr equals 64 bit.
-	c.compileConstToRegisterInstruction(x86.ASHLQ, 3, offset.register)
+	// by 16 as the offset is the "length" of table in Go's "[]interface{}",
+	// and size of interface{} equals 16 bytes == (2^4).
+	c.compileConstToRegisterInstruction(x86.ASHLQ, 4, offset.register)
 
 	// Adds the address of wasm.Table[0] stored as callEngine.tableElement0Address to the offset.
 	c.compileMemoryToRegisterInstruction(x86.AADDQ,
 		reservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset, offset.register)
 
-	// "offset = *offset (== table[offset] == *compiledFunction type)"
-	c.compileMemoryToRegisterInstruction(x86.AMOVQ, offset.register, 0, offset.register)
+	// "offset = (*offset) + interfaceDataOffset (== table[offset] + interfaceDataOffset == *compiledFunction type)"
+	c.compileMemoryToRegisterInstruction(x86.AMOVQ, offset.register, interfaceDataOffset, offset.register)
 
 	// At this point offset.register holds the address of *compiledFunction (as uintptr) at wasm.Table[offset].
 	//
@@ -1080,7 +1080,7 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 	c.compileExitFromNativeCode(jitCallStatusCodeTypeMismatchOnIndirectCall)
 
 	c.addSetJmpOrigins(jumpIfTypeMatch)
-	if err := c.compileCallFunctionImpl(0, offset.register, targetFunctionType); err != nil {
+	if err = c.compileCallFunctionImpl(0, offset.register, targetFunctionType); err != nil {
 		return nil
 	}
 
@@ -3528,7 +3528,7 @@ func (c *amd64Compiler) compileConstF32(o *wazeroir.OperationConstF32) error {
 		return err
 	}
 
-	c.compileConstToRegisterInstruction(x86.AMOVL, int64(uint64(math.Float32bits(o.Value))), tmpReg)
+	c.compileConstToRegisterInstruction(x86.AMOVL, int64(math.Float32bits(o.Value)), tmpReg)
 	c.compileRegisterToRegister(x86.AMOVL, tmpReg, reg)
 	return nil
 }
@@ -3784,7 +3784,7 @@ func (c *amd64Compiler) compileCallFunctionImpl(index wasm.Index, compiledFuncti
 	}
 
 	// 2) Set callEngine.valueStackContext.stackBasePointer for the next function.
-	if offset := (int64(c.locationStack.sp) - int64(len(functype.Params))); offset > 0 {
+	if offset := int64(c.locationStack.sp) - int64(len(functype.Params)); offset > 0 {
 		// At this point, tmpRegister holds the old stack base pointer. We could get the new frame's
 		// stack base pointer by "old stack base pointer + old stack pointer - # of function params"
 		// See the comments in callEngine.pushCallFrame which does exactly the same calculation in Go.
diff --git a/internal/wasm/jit/jit_arm64.go b/internal/wasm/jit/jit_arm64.go
index f0309845..ff08ec45 100644
--- a/internal/wasm/jit/jit_arm64.go
+++ b/internal/wasm/jit/jit_arm64.go
@@ -1351,17 +1351,17 @@ func (c *arm64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 		reservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset,
 		tmp,
 	)
-	// "offset = tmp + (offset << 3) (== &table[offset])"
-	// Here we left shifting by 3 in order to get the offset in bytes,
-	// and the table element type is uintptr which is 8 bytes.
+	// "offset = tmp + (offset << 4) (== &table[offset])"
+	// Here we left shifting by 4 in order to get the offset in bytes,
+	// and the table element type is interface which is 16 bytes (two pointers).
 	c.compileAddInstructionWithLeftShiftedRegister(
-		offset.register, 3,
+		offset.register, 4,
 		tmp,
 		offset.register,
 	)
 
-	// "offset = *offset (== table[offset] == *compiledFunction type)"
-	c.compileMemoryToRegisterInstruction(arm64.AMOVD, offset.register, 0, offset.register)
+	// "offset = (*offset) + interfaceDataOffset (== table[offset] + interfaceDataOffset == *compiledFunction type)"
+	c.compileMemoryToRegisterInstruction(arm64.AMOVD, offset.register, interfaceDataOffset, offset.register)
 
 	// Check if the value of table[offset] equals zero, meaning that the target element is uninitialized.
 	c.compileTwoRegistersToNoneInstruction(arm64.ACMP, zeroRegister, offset.register)
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index 5cdda977..f937797f 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -153,6 +153,13 @@ type Module struct {
 	// 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
 	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
+	validatedElementSegments []*validatedElementSegment
 }
 
 // The wazero specific limitation described at RATIONALE.md.
@@ -207,15 +214,15 @@ func (m *Module) Validate(enabledFeatures Features) error {
 		return err
 	}
 
-	if err = m.validateGlobals(globals, MaximumGlobals); err != nil {
+	if err = m.validateImports(enabledFeatures); err != nil {
 		return err
 	}
 
-	if err = m.validateMemory(memory, globals); err != nil {
+	if err = m.validateGlobals(globals, MaximumGlobals); err != nil {
 		return err
 	}
 
-	if err = m.validateTable(table, globals); err != nil {
+	if err = m.validateMemory(memory, globals); err != nil {
 		return err
 	}
 
@@ -229,7 +236,7 @@ func (m *Module) Validate(enabledFeatures Features) error {
 		}
 	}
 
-	if err = m.validateImports(enabledFeatures); err != nil {
+	if _, err = m.validateTable(); err != nil {
 		return err
 	}
 
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index 6e44c323..abede19c 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -153,8 +153,8 @@ const (
 
 // addSections adds section elements to the ModuleInstance
 func (m *ModuleInstance) addSections(module *Module, importedFunctions, functions []*FunctionInstance,
-	importedGlobals, globals []*GlobalInstance, importedTable, table *TableInstance,
-	memory, importedMemory *MemoryInstance, typeInstances []*TypeInstance) {
+	importedGlobals, globals []*GlobalInstance, table *TableInstance, memory, importedMemory *MemoryInstance,
+	typeInstances []*TypeInstance) {
 
 	m.Types = typeInstances
 
@@ -169,11 +169,7 @@ func (m *ModuleInstance) addSections(module *Module, importedFunctions, function
 	m.Globals = append(m.Globals, importedGlobals...)
 	m.Globals = append(m.Globals, globals...)
 
-	if importedTable != nil {
-		m.Table = importedTable
-	} else {
-		m.Table = table
-	}
+	m.Table = table
 
 	if importedMemory != nil {
 		m.Memory = importedMemory
@@ -280,7 +276,12 @@ func (s *Store) Instantiate(ctx context.Context, module *Module, name string) (*
 		return nil, err
 	}
 
-	globals, table, memory := module.buildGlobals(importedGlobals), module.buildTable(), module.buildMemory()
+	table, tableInit, err := module.buildTable(importedTable, importedGlobals)
+	if err != nil {
+		s.deleteModule(name)
+		return nil, err
+	}
+	globals, memory := module.buildGlobals(importedGlobals), module.buildMemory()
 
 	// If there are no module-defined functions, assume this is a host module.
 	var functions []*FunctionInstance
@@ -295,28 +296,20 @@ func (s *Store) Instantiate(ctx context.Context, module *Module, name string) (*
 
 	// Now we have all instances from imports and local ones, so ready to create a new ModuleInstance.
 	m := &ModuleInstance{Name: name}
-	m.addSections(module, importedFunctions, functions, importedGlobals,
-		globals, importedTable, table, importedMemory, memory, types)
+	m.addSections(module, importedFunctions, functions, importedGlobals, globals, table, importedMemory, memory, types)
 
-	// Plus we are ready to compile functions.
-	m.Engine, err = s.engine.NewModuleEngine(name, importedFunctions, functions)
-	if err != nil {
-		s.deleteModule(name)
-		return nil, fmt.Errorf("compilation failed: %w", err)
-	}
-
-	if err = m.validateElements(module.ElementSection); err != nil {
+	if err = m.validateData(module.DataSection); err != nil {
 		s.deleteModule(name)
 		return nil, err
 	}
 
-	if err = m.validateData(module.DataSection); err != nil {
-		s.deleteModule(name)
-		return nil, err
+	// Plus we are ready to compile functions.
+	m.Engine, err = s.engine.NewModuleEngine(name, importedFunctions, functions, table, tableInit)
+	if err != nil {
+		return nil, fmt.Errorf("compilation failed: %w", err)
 	}
 
-	// Now all the validation passes, we are safe to mutate memory/table instances (possibly imported ones).
-	m.applyElements(module.ElementSection)
+	// Now all the validation passes, we are safe to mutate memory instances (possibly imported ones).
 	m.applyData(module.DataSection)
 
 	// Build the default context for calls to this module.
diff --git a/internal/wasm/table.go b/internal/wasm/table.go
index 6b104a2d..85170e97 100644
--- a/internal/wasm/table.go
+++ b/internal/wasm/table.go
@@ -1,6 +1,12 @@
 package internalwasm
 
-import "fmt"
+import (
+	"bytes"
+	"fmt"
+	"math"
+
+	"github.com/tetratelabs/wazero/internal/leb128"
+)
 
 // Table describes the limits of (function) elements in a table.
 type Table = limitsType
@@ -18,68 +24,197 @@ type ElementSegment struct {
 	Init []Index
 }
 
-// TableInstance represents a table instance in a store.
+// TableInstance represents a table of (ElemTypeFuncref) elements in a module.
+//
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-instances%E2%91%A0
 type TableInstance struct {
-	// Table holds the table elements managed by this table instance.
-	Table []uintptr
+	// Table holds the Engine-specific compiled functions mapped by element index.
+	Table []interface{}
 
-	// Min is the minimum (function) elements in this table.
+	// Min is the minimum (function) elements in this table and cannot grow to accommodate ElementSegment.
 	Min uint32
 
 	// Max if present is the maximum (function) elements in this table, or nil if unbounded.
 	Max *uint32
 }
 
-func (m *Module) buildTable() *TableInstance {
-	table := m.TableSection
-	if table != nil {
-		return &TableInstance{
-			Table: make([]uintptr, table.Min),
-			Min:   table.Min,
-			Max:   table.Max,
+// validatedElementSegment is like ElementSegment except the inputs are expanded and validated based on defining module.
+//
+// Note: The global imported at globalIdx may have an offset value that is out-of-bounds for the corresponding table.
+type validatedElementSegment struct {
+	// opcode is OpcodeGlobalGet or OpcodeI32Const
+	opcode Opcode
+
+	// arg is the only argument to opcode, which when applied results in the offset to add to init indices.
+	//  * OpcodeGlobalGet: position in the global index namespace of an imported Global ValueTypeI32 holding the offset.
+	//  * OpcodeI32Const: a constant ValueTypeI32 offset.
+	arg uint32
+
+	// init are a range of table elements whose values are positions in the function index namespace. This range
+	// replaces any values in TableInstance.Table at an offset arg which is a constant if opcode == OpcodeI32Const or
+	// derived from a globalIdx if opcode == OpcodeGlobalGet
+	init []Index
+}
+
+// validateTable ensures any ElementSegment is valid. This caches results via Module.validatedElementSegments.
+// Note: limitsType are validated by decoders, so not re-validated here.
+func (m *Module) validateTable() ([]*validatedElementSegment, error) {
+	if m.validatedElementSegments != nil {
+		return m.validatedElementSegments, nil
+	}
+
+	t := m.TableSection
+	imported := false
+	for _, im := range m.ImportSection {
+		if im.Type == ExternTypeTable {
+			t = im.DescTable
+			imported = true
+			break
 		}
 	}
-	return nil
-}
 
-func (m *Module) validateTable(table *Table, globals []*GlobalType) error {
-	for _, elem := range m.ElementSection {
-		if table == nil {
-			return fmt.Errorf("table index out of range")
+	elementCount := m.SectionElementCount(SectionIDElement)
+	if elementCount > 0 && t == nil {
+		return nil, fmt.Errorf("%s was defined, but not table", SectionIDName(SectionIDElement))
+	}
+
+	ret := make([]*validatedElementSegment, 0, elementCount)
+
+	// Create bounds checks as these can err prior to instantiation
+	funcCount := m.importCount(ExternTypeFunc) + m.SectionElementCount(SectionIDFunction)
+
+	// Now, we have to figure out which table elements can be resolved before instantiation and also fail early if there
+	// are any imported globals that are known to be invalid by their declarations.
+	for i, elem := range m.ElementSection {
+		idx := Index(i)
+
+		initCount := uint32(len(elem.Init))
+
+		// Any offset applied is to the element, not the function index: validate here if the funcidx is sound.
+		for ei, funcIdx := range elem.Init {
+			if funcIdx >= funcCount {
+				return nil, fmt.Errorf("%s[%d].init[%d] funcidx %d out of range", SectionIDName(SectionIDElement), idx, ei, funcIdx)
+			}
 		}
-		err := validateConstExpression(globals, elem.OffsetExpr, ValueTypeI32)
-		if err != nil {
-			return fmt.Errorf("invalid const expression for element: %w", err)
+
+		// global.get needs to be discovered during initialization
+		oc := elem.OffsetExpr.Opcode
+		if oc == OpcodeGlobalGet {
+			globalIdx, _, err := leb128.DecodeUint32(bytes.NewReader(elem.OffsetExpr.Data))
+			if err != nil {
+				return nil, fmt.Errorf("%s[%d] couldn't read global.get parameter: %w", SectionIDName(SectionIDElement), idx, err)
+			} else if err = m.verifyImportGlobalI32(SectionIDElement, idx, globalIdx); err != nil {
+				return nil, err
+			}
+
+			if initCount == 0 {
+				continue // Per https://github.com/WebAssembly/spec/issues/1427 init can be no-op, but validate anyway!
+			}
+
+			ret = append(ret, &validatedElementSegment{oc, globalIdx, elem.Init})
+		} else if oc == OpcodeI32Const {
+			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)
+			}
+			offset := Index(o)
+
+			// Per https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L117 we must pass if imported
+			// table has set its min=0. Per https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L142, we
+			// have to do fail if module-defined min=0.
+			if !imported {
+				if err = checkSegmentBounds(t.Min, uint64(initCount)+uint64(offset), idx); err != nil {
+					return nil, err
+				}
+			}
+
+			if initCount == 0 {
+				continue // Per https://github.com/WebAssembly/spec/issues/1427 init can be no-op, but validate anyway!
+			}
+
+			ret = append(ret, &validatedElementSegment{oc, offset, elem.Init})
+		} else {
+			return nil, fmt.Errorf("%s[%d] has an invalid const expression: %s", SectionIDName(SectionIDElement), idx, InstructionName(oc))
 		}
 	}
-	return nil
+
+	m.validatedElementSegments = ret
+	return ret, nil
 }
 
-func (m *ModuleInstance) validateElements(elements []*ElementSegment) (err error) {
-	for _, elem := range elements {
-		offset := int(executeConstExpression(m.Globals, elem.OffsetExpr).(int32))
-		ceil := offset + len(elem.Init)
+// buildTable returns a TableInstance if the module defines or imports a table.
+//  * importedTable: returned as `table` unmodified, if non-nil.
+//  * importedGlobals: include all instantiated, imported globals.
+//
+// If the result `init` is non-nil, it is the `tableInit` parameter of Engine.NewModuleEngine.
+//
+// Note: An error is only possible when an ElementSegment.OffsetExpr is out of range of the TableInstance.Min.
+func (m *Module) buildTable(importedTable *TableInstance, importedGlobals []*GlobalInstance) (table *TableInstance, init map[Index]Index, err error) {
+	// The module defining the table is the one that sets its Min/Max etc.
+	if m.TableSection != nil {
+		t := m.TableSection
+		table = &TableInstance{Table: make([]interface{}, t.Min), Min: t.Min, Max: t.Max}
+	} else {
+		table = importedTable
+	}
+	if table == nil {
+		return // no table
+	}
 
-		if offset < 0 || ceil > len(m.Table.Table) {
-			return fmt.Errorf("out of bounds table access")
+	elementSegments := m.validatedElementSegments
+	if len(elementSegments) == 0 {
+		return
+	}
+
+	init = make(map[Index]Index, table.Min)
+	for elemI, elem := range elementSegments {
+		var offset uint32
+		if elem.opcode == OpcodeGlobalGet {
+			global := importedGlobals[elem.arg]
+			offset = uint32(global.Val)
+		} else {
+			offset = elem.arg // constant
 		}
-		for _, elm := range elem.Init {
-			if elm >= uint32(len(m.Functions)) {
-				return fmt.Errorf("unknown function specified by element")
-			}
+
+		// Check to see if we are out-of-bounds
+		initCount := uint64(len(elem.init))
+		if err = checkSegmentBounds(table.Min, uint64(offset)+initCount, Index(elemI)); err != nil {
+			return
+		}
+
+		for i, funcidx := range elem.init {
+			init[offset+uint32(i)] = funcidx
 		}
 	}
 	return
 }
 
-func (m *ModuleInstance) applyElements(elements []*ElementSegment) {
-	for _, elem := range elements {
-		offset := int(executeConstExpression(m.Globals, elem.OffsetExpr).(int32))
-		table := m.Table.Table
-		for i, elm := range elem.Init {
-			pos := i + offset
-			table[pos] = m.Engine.FunctionAddress(elm)
+// checkSegmentBounds fails if the capacity needed for an ElementSegment.Init is larger than limitsType.Min
+//
+// WebAssembly 1.0 (20191205) doesn't forbid growing to accommodate element segments, and spectests are inconsistent.
+// For example, the spectests enforce elements within Table limitsType.Min, but ignore Import.DescTable min. What this
+// means is we have to delay offset checks on imported tables until we link to them.
+// Ex. https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L117 wants pass on min=0 for import
+// Ex. https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L142 wants fail on min=0 module-defined
+func checkSegmentBounds(min uint32, requireMin uint64, idx Index) error { // uint64 in case offset was set to -1
+	if requireMin > uint64(min) {
+		return fmt.Errorf("%s[%d].init exceeds min table size", SectionIDName(SectionIDElement), idx)
+	}
+	return nil
+}
+
+func (m *Module) verifyImportGlobalI32(sectionID SectionID, sectionIdx Index, idx uint32) error {
+	ig := uint32(math.MaxUint32) // +1 == 0
+	for i, im := range m.ImportSection {
+		if im.Type == ExternTypeGlobal {
+			ig++
+			if ig == idx {
+				if im.DescGlobal.ValType != ValueTypeI32 {
+					return fmt.Errorf("%s[%d] (global.get %d): import[%d].global.ValType != i32", SectionIDName(sectionID), sectionIdx, idx, i)
+				}
+				return nil
+			}
 		}
 	}
+	return fmt.Errorf("%s[%d] (global.get %d): out of range of imported globals", SectionIDName(sectionID), sectionIdx, idx)
 }
diff --git a/wasm.go b/wasm.go
index f24d4d5d..4fce90db 100644
--- a/wasm.go
+++ b/wasm.go
@@ -66,7 +66,7 @@ type Runtime interface {
 	//
 	// While a Module is pre-validated, there are a few situations which can cause an error:
 	//  * The Module name is already in use.
-	//  * The Module has a table element initializer that uses an imported global offset that puts it out of range.
+	//  * The Module has a table element initializer that resolves to an index outside the Table minimum size.
 	//  * The Module has a start function, and it failed to execute.
 	//
 	// Note: The last value of RuntimeConfig.WithContext is used for any start function.


Test patch:
diff --git a/internal/leb128/leb128_test.go b/internal/leb128/leb128_test.go
index a85c76a2..a23d7d92 100644
--- a/internal/leb128/leb128_test.go
+++ b/internal/leb128/leb128_test.go
@@ -3,6 +3,7 @@ package leb128
 import (
 	"bytes"
 	"fmt"
+	"math"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -20,12 +21,30 @@ func TestEncodeUint32(t *testing.T) {
 		{input: 16256, expected: []byte{0x80, 0x7f}},
 		{input: 624485, expected: []byte{0xe5, 0x8e, 0x26}},
 		{input: 165675008, expected: []byte{0x80, 0x80, 0x80, 0x4f}},
-		{input: 0xffffffff, expected: []byte{0xff, 0xff, 0xff, 0xff, 0xf}},
+		{input: uint32(math.MaxUint32), expected: []byte{0xff, 0xff, 0xff, 0xff, 0xf}},
 	} {
 		require.Equal(t, c.expected, EncodeUint32(c.input))
 	}
 }
 
+func TestEncodeUint64(t *testing.T) {
+	for _, c := range []struct {
+		input    uint64
+		expected []byte
+	}{
+		{input: 0, expected: []byte{0x00}},
+		{input: 1, expected: []byte{0x01}},
+		{input: 4, expected: []byte{0x04}},
+		{input: 16256, expected: []byte{0x80, 0x7f}},
+		{input: 624485, expected: []byte{0xe5, 0x8e, 0x26}},
+		{input: 165675008, expected: []byte{0x80, 0x80, 0x80, 0x4f}},
+		{input: math.MaxUint32, expected: []byte{0xff, 0xff, 0xff, 0xff, 0xf}},
+		{input: math.MaxUint64, expected: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1}},
+	} {
+		require.Equal(t, c.expected, EncodeUint64(c.input))
+	}
+}
+
 func TestDecodeUint32(t *testing.T) {
 	for _, c := range []struct {
 		bytes  []byte
diff --git a/internal/wasm/interpreter/interpreter_test.go b/internal/wasm/interpreter/interpreter_test.go
index 47ed177b..0870d68f 100644
--- a/internal/wasm/interpreter/interpreter_test.go
+++ b/internal/wasm/interpreter/interpreter_test.go
@@ -51,13 +51,118 @@ func TestEngine_NewModuleEngine(t *testing.T) {
 	e := NewEngine()
 
 	t.Run("sets module name", func(t *testing.T) {
-		me, err := e.NewModuleEngine(t.Name(), nil, nil)
+		me, err := e.NewModuleEngine(t.Name(), nil, nil, nil, nil)
 		require.NoError(t, err)
 		defer me.Close()
 		require.Equal(t, t.Name(), me.(*moduleEngine).name)
 	})
 }
 
+func TestEngine_NewModuleEngine_InitTable(t *testing.T) {
+	e := NewEngine()
+
+	t.Run("no table elements", func(t *testing.T) {
+		table := &wasm.TableInstance{Min: 2, Table: make([]interface{}, 2)}
+		var importedFunctions []*wasm.FunctionInstance
+		var moduleFunctions []*wasm.FunctionInstance
+		var tableInit map[wasm.Index]wasm.Index
+
+		// Instantiate the module, which has nothing but an empty table.
+		me, err := e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions, table, tableInit)
+		require.NoError(t, err)
+
+		// Since there are no elements to initialize, we expect the table to be nil.
+		require.Equal(t, table.Table, make([]interface{}, 2))
+
+		// Clean up.
+		require.NoError(t, me.Close())
+	})
+
+	t.Run("module-defined function", func(t *testing.T) {
+		table := &wasm.TableInstance{Min: 2, Table: make([]interface{}, 2)}
+		var importedFunctions []*wasm.FunctionInstance
+		moduleFunctions := []*wasm.FunctionInstance{
+			{Name: "1", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "2", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "3", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "4", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+		}
+		tableInit := map[wasm.Index]wasm.Index{0: 2}
+
+		// Instantiate the module whose table points to its own functions.
+		me, err := e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions, table, tableInit)
+		require.NoError(t, err)
+
+		// The functions mapped to the table are defined in the same moduleEngine
+		require.Equal(t, table.Table, []interface{}{me.(*moduleEngine).compiledFunctions[2], nil})
+
+		// Clean up.
+		require.NoError(t, me.Close())
+	})
+
+	t.Run("imported function", func(t *testing.T) {
+		table := &wasm.TableInstance{Min: 2, Table: make([]interface{}, 2)}
+		importedFunctions := []*wasm.FunctionInstance{
+			{Name: "1", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "2", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "3", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "4", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+		}
+		var moduleFunctions []*wasm.FunctionInstance
+		tableInit := map[wasm.Index]wasm.Index{0: 2}
+
+		// Imported functions are compiled before the importing module is instantiated.
+		imported, err := e.NewModuleEngine(t.Name(), nil, importedFunctions, nil, nil)
+		require.NoError(t, err)
+
+		// Instantiate the importing module, which is whose table is initialized.
+		importing, err := e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions, table, tableInit)
+		require.NoError(t, err)
+
+		// A moduleEngine's compiled function slice includes its imports, so the offsets is absolute.
+		require.Equal(t, table.Table, []interface{}{importing.(*moduleEngine).compiledFunctions[2], nil})
+
+		// Clean up.
+		require.NoError(t, importing.Close())
+		require.NoError(t, imported.Close())
+	})
+
+	t.Run("mixed functions", func(t *testing.T) {
+		table := &wasm.TableInstance{Min: 2, Table: make([]interface{}, 2)}
+		importedFunctions := []*wasm.FunctionInstance{
+			{Name: "1", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "2", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "3", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "4", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+		}
+		moduleFunctions := []*wasm.FunctionInstance{
+			{Name: "1", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "2", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "3", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "4", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+		}
+		tableInit := map[wasm.Index]wasm.Index{0: 0, 1: 4}
+
+		// Imported functions are compiled before the importing module is instantiated.
+		imported, err := e.NewModuleEngine(t.Name(), nil, importedFunctions, nil, nil)
+		require.NoError(t, err)
+
+		// Instantiate the importing module, which is whose table is initialized.
+		importing, err := e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions, table, tableInit)
+		require.NoError(t, err)
+
+		// A moduleEngine's compiled function slice includes its imports, so the offsets are absolute.
+		require.Equal(t, table.Table, []interface{}{
+			importing.(*moduleEngine).compiledFunctions[0],
+			importing.(*moduleEngine).compiledFunctions[4],
+		})
+
+		// Clean up.
+		require.NoError(t, importing.Close())
+		require.NoError(t, imported.Close())
+	})
+}
+
 func TestEngine_Call(t *testing.T) {
 	i64 := wasm.ValueTypeI64
 	m := &wasm.Module{
@@ -113,7 +218,7 @@ func TestEngine_Call_HostFn(t *testing.T) {
 		Module: module,
 	}
 
-	me, err := e.NewModuleEngine(t.Name(), nil, []*wasm.FunctionInstance{f})
+	me, err := e.NewModuleEngine(t.Name(), nil, []*wasm.FunctionInstance{f}, nil, nil)
 	require.NoError(t, err)
 
 	t.Run("defaults to module memory when call stack empty", func(t *testing.T) {
@@ -253,6 +358,8 @@ func TestEngineCompile_Errors(t *testing.T) {
 		_, err := e.NewModuleEngine(t.Name(),
 			[]*wasm.FunctionInstance{{Module: &wasm.ModuleInstance{Name: "uncompiled"}, Name: "fn"}},
 			nil, // moduleFunctions
+			nil, // table
+			nil, // tableInit
 		)
 		require.EqualError(t, err, "import[0] func[uncompiled.fn]: uncompiled")
 	})
@@ -268,7 +375,7 @@ func TestEngineCompile_Errors(t *testing.T) {
 		}
 
 		// initialize the module-engine containing imported functions
-		_, err := e.NewModuleEngine(t.Name(), nil, importedFunctions)
+		_, err := e.NewModuleEngine(t.Name(), nil, importedFunctions, nil, nil)
 		require.NoError(t, err)
 
 		require.Len(t, e.compiledFunctions, len(importedFunctions))
@@ -281,7 +388,7 @@ func TestEngineCompile_Errors(t *testing.T) {
 			}, Module: &wasm.ModuleInstance{}},
 		}
 
-		_, err = e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions)
+		_, err = e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions, nil, nil)
 		require.EqualError(t, err, "function[2/2] failed to lower to wazeroir: handling instruction: apply stack failed for call: reading immediates: EOF")
 
 		// On the compilation failure, all the compiled functions including succeeded ones must be released.
@@ -321,12 +428,12 @@ func TestClose(t *testing.T) {
 			e := NewEngine().(*engine)
 			if len(tc.importedFunctions) > 0 {
 				// initialize the module-engine containing imported functions
-				me, err := e.NewModuleEngine(t.Name(), nil, tc.importedFunctions)
+				me, err := e.NewModuleEngine(t.Name(), nil, tc.importedFunctions, nil, nil)
 				require.NoError(t, err)
 				require.Len(t, me.(*moduleEngine).compiledFunctions, len(tc.importedFunctions))
 			}
 
-			me, err := e.NewModuleEngine(t.Name(), tc.importedFunctions, tc.moduleFunctions)
+			me, err := e.NewModuleEngine(t.Name(), tc.importedFunctions, tc.moduleFunctions, nil, nil)
 			require.NoError(t, err)
 			require.Len(t, me.(*moduleEngine).compiledFunctions, len(tc.importedFunctions)+len(tc.moduleFunctions))
 
diff --git a/internal/wasm/jit/engine_test.go b/internal/wasm/jit/engine_test.go
index fcd1b1c7..5069b83d 100644
--- a/internal/wasm/jit/engine_test.go
+++ b/internal/wasm/jit/engine_test.go
@@ -106,7 +106,7 @@ func TestEngine_NewModuleEngine(t *testing.T) {
 	e := NewEngine()
 
 	t.Run("sets module name", func(t *testing.T) {
-		me, err := e.NewModuleEngine(t.Name(), nil, nil)
+		me, err := e.NewModuleEngine(t.Name(), nil, nil, nil, nil)
 		require.NoError(t, err)
 		defer me.Close()
 		require.Equal(t, t.Name(), me.(*moduleEngine).name)
@@ -149,6 +149,111 @@ func TestEngine_Call(t *testing.T) {
 	})
 }
 
+func TestEngine_NewModuleEngine_InitTable(t *testing.T) {
+	e := NewEngine()
+
+	t.Run("no table elements", func(t *testing.T) {
+		table := &wasm.TableInstance{Min: 2, Table: make([]interface{}, 2)}
+		var importedFunctions []*wasm.FunctionInstance
+		var moduleFunctions []*wasm.FunctionInstance
+		var tableInit map[wasm.Index]wasm.Index
+
+		// Instantiate the module, which has nothing but an empty table.
+		me, err := e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions, table, tableInit)
+		require.NoError(t, err)
+
+		// Since there are no elements to initialize, we expect the table to be nil.
+		require.Equal(t, table.Table, make([]interface{}, 2))
+
+		// Clean up.
+		require.NoError(t, me.Close())
+	})
+
+	t.Run("module-defined function", func(t *testing.T) {
+		table := &wasm.TableInstance{Min: 2, Table: make([]interface{}, 2)}
+		var importedFunctions []*wasm.FunctionInstance
+		moduleFunctions := []*wasm.FunctionInstance{
+			{Name: "1", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "2", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "3", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "4", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+		}
+		tableInit := map[wasm.Index]wasm.Index{0: 2}
+
+		// Instantiate the module whose table points to its own functions.
+		me, err := e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions, table, tableInit)
+		require.NoError(t, err)
+
+		// The functions mapped to the table are defined in the same moduleEngine
+		require.Equal(t, table.Table, []interface{}{me.(*moduleEngine).compiledFunctions[2], nil})
+
+		// Clean up.
+		require.NoError(t, me.Close())
+	})
+
+	t.Run("imported function", func(t *testing.T) {
+		table := &wasm.TableInstance{Min: 2, Table: make([]interface{}, 2)}
+		importedFunctions := []*wasm.FunctionInstance{
+			{Name: "1", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "2", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "3", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "4", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+		}
+		var moduleFunctions []*wasm.FunctionInstance
+		tableInit := map[wasm.Index]wasm.Index{0: 2}
+
+		// Imported functions are compiled before the importing module is instantiated.
+		imported, err := e.NewModuleEngine(t.Name(), nil, importedFunctions, nil, nil)
+		require.NoError(t, err)
+
+		// Instantiate the importing module, which is whose table is initialized.
+		importing, err := e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions, table, tableInit)
+		require.NoError(t, err)
+
+		// A moduleEngine's compiled function slice includes its imports, so the offsets is absolute.
+		require.Equal(t, table.Table, []interface{}{importing.(*moduleEngine).compiledFunctions[2], nil})
+
+		// Clean up.
+		require.NoError(t, importing.Close())
+		require.NoError(t, imported.Close())
+	})
+
+	t.Run("mixed functions", func(t *testing.T) {
+		table := &wasm.TableInstance{Min: 2, Table: make([]interface{}, 2)}
+		importedFunctions := []*wasm.FunctionInstance{
+			{Name: "1", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "2", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "3", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "4", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+		}
+		moduleFunctions := []*wasm.FunctionInstance{
+			{Name: "1", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "2", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "3", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+			{Name: "4", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
+		}
+		tableInit := map[wasm.Index]wasm.Index{0: 0, 1: 4}
+
+		// Imported functions are compiled before the importing module is instantiated.
+		imported, err := e.NewModuleEngine(t.Name(), nil, importedFunctions, nil, nil)
+		require.NoError(t, err)
+
+		// Instantiate the importing module, which is whose table is initialized.
+		importing, err := e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions, table, tableInit)
+		require.NoError(t, err)
+
+		// A moduleEngine's compiled function slice includes its imports, so the offsets are absolute.
+		require.Equal(t, table.Table, []interface{}{
+			importing.(*moduleEngine).compiledFunctions[0],
+			importing.(*moduleEngine).compiledFunctions[4],
+		})
+
+		// Clean up.
+		require.NoError(t, importing.Close())
+		require.NoError(t, imported.Close())
+	})
+}
+
 func TestEngine_Call_HostFn(t *testing.T) {
 	requireSupportedOSArch(t)
 
@@ -172,7 +277,7 @@ func TestEngine_Call_HostFn(t *testing.T) {
 		Module: module,
 	}
 
-	modEngine, err := e.NewModuleEngine(t.Name(), nil, []*wasm.FunctionInstance{f})
+	modEngine, err := e.NewModuleEngine(t.Name(), nil, []*wasm.FunctionInstance{f}, nil, nil)
 	require.NoError(t, err)
 
 	t.Run("defaults to module memory when call stack empty", func(t *testing.T) {
@@ -207,6 +312,8 @@ func TestEngineCompile_Errors(t *testing.T) {
 			t.Name(),
 			[]*wasm.FunctionInstance{{Module: &wasm.ModuleInstance{Name: "uncompiled"}, Name: "fn"}},
 			nil, // moduleFunctions
+			nil, // table
+			nil, // tableInit
 		)
 		require.EqualError(t, err, "import[0] func[uncompiled.fn]: uncompiled")
 	})
@@ -220,7 +327,7 @@ func TestEngineCompile_Errors(t *testing.T) {
 			{Name: "3", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
 			{Name: "4", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
 		}
-		_, err := e.NewModuleEngine(t.Name(), nil, importedFunctions)
+		_, err := e.NewModuleEngine(t.Name(), nil, importedFunctions, nil, nil)
 		require.NoError(t, err)
 
 		require.Len(t, e.compiledFunctions, len(importedFunctions))
@@ -233,7 +340,7 @@ func TestEngineCompile_Errors(t *testing.T) {
 			}, Module: &wasm.ModuleInstance{}},
 		}
 
-		_, err = e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions)
+		_, err = e.NewModuleEngine(t.Name(), importedFunctions, moduleFunctions, nil, nil)
 		require.EqualError(t, err, "function[2/2] failed to lower to wazeroir: handling instruction: apply stack failed for call: reading immediates: EOF")
 
 		// On the compilation failure, all the compiled functions including succeeded ones must be released.
@@ -310,13 +417,26 @@ func TestModuleEngine_Close_Concurrent(t *testing.T) {
 			e := newEngine()
 			var importedModuleEngine *moduleEngine
 			if len(tc.importedFunctions) > 0 {
-				modEngine, err := e.NewModuleEngine(fmt.Sprintf("%s - imported functions", t.Name()), nil, tc.importedFunctions)
+				// Instantiate the imported module
+				modEngine, err := e.NewModuleEngine(
+					fmt.Sprintf("%s - imported functions", t.Name()),
+					nil, // moduleFunctions
+					tc.importedFunctions,
+					nil, // table
+					nil, // tableInit
+				)
 				require.NoError(t, err)
 				importedModuleEngine = modEngine.(*moduleEngine)
 				require.Len(t, importedModuleEngine.compiledFunctions, len(tc.importedFunctions))
 			}
 
-			modEngine, err := e.NewModuleEngine(fmt.Sprintf("%s - module-defined functions", t.Name()), tc.importedFunctions, tc.moduleFunctions)
+			modEngine, err := e.NewModuleEngine(
+				fmt.Sprintf("%s - module-defined functions", t.Name()),
+				tc.importedFunctions,
+				tc.moduleFunctions,
+				nil, // table
+				nil, // tableInit
+			)
 			require.NoError(t, err)
 			require.Len(t, modEngine.(*moduleEngine).compiledFunctions, len(tc.importedFunctions)+len(tc.moduleFunctions))
 
diff --git a/internal/wasm/jit/jit_controlflow_test.go b/internal/wasm/jit/jit_controlflow_test.go
index 079672c4..fa5f3c64 100644
--- a/internal/wasm/jit/jit_controlflow_test.go
+++ b/internal/wasm/jit/jit_controlflow_test.go
@@ -528,7 +528,7 @@ func TestCompiler_compileBr(t *testing.T) {
 func TestCompiler_compileCallIndirect(t *testing.T) {
 	t.Run("out of bounds", func(t *testing.T) {
 		env := newJITEnvironment()
-		env.setTable(make([]uintptr, 10))
+		env.setTable(make([]interface{}, 10))
 		compiler := env.requireNewCompiler(t, nil)
 		err := compiler.compilePreamble()
 		require.NoError(t, err)
@@ -567,9 +567,9 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 		env.module().Types = []*wasm.TypeInstance{{Type: &wasm.FunctionType{}}}
 
 		// and the typeID doesn't match the table[targetOffset]'s type ID.
-		table := make([]uintptr, 10)
+		table := make([]interface{}, 10)
 		env.setTable(table)
-		table[0] = 0
+		table[0] = nil
 
 		// Place the offset value.
 		err = compiler.compileConstI32(targetOffset)
@@ -600,11 +600,11 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 		env.module().Types = []*wasm.TypeInstance{{Type: &wasm.FunctionType{}, TypeID: 1000}}
 		// Ensure that the module instance has the type information for targetOperation.TypeIndex,
 		// and the typeID doesn't match the table[targetOffset]'s type ID.
-		table := make([]uintptr, 10)
+		table := make([]interface{}, 10)
 		env.setTable(table)
 
 		cf := &compiledFunction{source: &wasm.FunctionInstance{TypeID: 50}}
-		table[0] = uintptr(unsafe.Pointer(cf))
+		table[0] = cf
 
 		// Place the offset value.
 		err = compiler.compileConstI32(targetOffset)
@@ -635,7 +635,7 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 				targetTypeID := wasm.FunctionTypeID(10) // Arbitrary number is fine for testing.
 				operation := &wazeroir.OperationCallIndirect{TypeIndex: 0}
 
-				table := make([]uintptr, 10)
+				table := make([]interface{}, 10)
 				env := newJITEnvironment()
 				env.setTable(table)
 
@@ -674,7 +674,7 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 							},
 						}
 						me.compiledFunctions = append(me.compiledFunctions, cf)
-						table[i] = uintptr(unsafe.Pointer(cf))
+						table[i] = cf
 					})
 				}
 
diff --git a/internal/wasm/jit/jit_initialization_test.go b/internal/wasm/jit/jit_initialization_test.go
index 3a8ae015..9a2b5e58 100644
--- a/internal/wasm/jit/jit_initialization_test.go
+++ b/internal/wasm/jit/jit_initialization_test.go
@@ -22,21 +22,21 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) {
 			moduleInstance: &wasm.ModuleInstance{
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
 				Memory:  &wasm.MemoryInstance{Buffer: make([]byte, 10)},
-				Table:   &wasm.TableInstance{Table: make([]uintptr, 20)},
+				Table:   &wasm.TableInstance{Table: make([]interface{}, 20)},
 			},
 		},
 		{
 			name: "globals nil",
 			moduleInstance: &wasm.ModuleInstance{
 				Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)},
-				Table:  &wasm.TableInstance{Table: make([]uintptr, 20)},
+				Table:  &wasm.TableInstance{Table: make([]interface{}, 20)},
 			},
 		},
 		{
 			name: "memory nil",
 			moduleInstance: &wasm.ModuleInstance{
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
-				Table:   &wasm.TableInstance{Table: make([]uintptr, 20)},
+				Table:   &wasm.TableInstance{Table: make([]interface{}, 20)},
 			},
 		},
 		{
@@ -49,7 +49,7 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) {
 		{
 			name: "table empty",
 			moduleInstance: &wasm.ModuleInstance{
-				Table: &wasm.TableInstance{Table: make([]uintptr, 0)},
+				Table: &wasm.TableInstance{Table: make([]interface{}, 0)},
 			},
 		},
 		{
diff --git a/internal/wasm/jit/jit_test.go b/internal/wasm/jit/jit_test.go
index d4e9fc29..71d8b992 100644
--- a/internal/wasm/jit/jit_test.go
+++ b/internal/wasm/jit/jit_test.go
@@ -86,7 +86,7 @@ func (j *jitEnv) getGlobal(index uint32) uint64 {
 	return j.moduleInstance.Globals[index].Val
 }
 
-func (j *jitEnv) setTable(table []uintptr) {
+func (j *jitEnv) setTable(table []interface{}) {
 	j.moduleInstance.Table = &wasm.TableInstance{Table: table}
 }
 
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index d1776e91..47ab9a8c 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -419,18 +419,13 @@ func newStore() *Store {
 }
 
 // NewModuleEngine implements Engine.NewModuleEngine
-func (e *mockEngine) NewModuleEngine(_ string, _, _ []*FunctionInstance) (ModuleEngine, error) {
+func (e *mockEngine) NewModuleEngine(_ string, _, _ []*FunctionInstance, _ *TableInstance, _ map[Index]Index) (ModuleEngine, error) {
 	if e.shouldCompileFail {
 		return nil, fmt.Errorf("some compilation error")
 	}
 	return &mockModuleEngine{callFailIndex: e.callFailIndex}, nil
 }
 
-// FunctionAddress implements ModuleEngine.FunctionAddress
-func (e *mockModuleEngine) FunctionAddress(index Index) uintptr {
-	return uintptr(index)
-}
-
 // Call implements ModuleEngine.Call
 func (e *mockModuleEngine) Call(ctx *ModuleContext, f *FunctionInstance, _ ...uint64) (results []uint64, err error) {
 	if e.callFailIndex >= 0 && f.Index == Index(e.callFailIndex) {
diff --git a/internal/wasm/table_test.go b/internal/wasm/table_test.go
index aae5b0a0..01e9430c 100644
--- a/internal/wasm/table_test.go
+++ b/internal/wasm/table_test.go
@@ -1,9 +1,12 @@
 package internalwasm
 
 import (
+	"math"
 	"testing"
 
 	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/wazero/internal/leb128"
 )
 
 func TestStore_resolveImports_table(t *testing.T) {
@@ -45,100 +48,847 @@ func TestStore_resolveImports_table(t *testing.T) {
 	})
 }
 
-func TestModule_buildTableInstance(t *testing.T) {
-	m := Module{TableSection: &Table{Min: 1}}
-	table := m.buildTable()
-	require.Equal(t, uint32(1), table.Min)
-}
+var codeEnd = &Code{Body: []byte{OpcodeEnd}}
 
 func TestModule_validateTable(t *testing.T) {
-	t.Run("invalid const expr", func(t *testing.T) {
-		m := Module{ElementSection: []*ElementSegment{{
-			OffsetExpr: &ConstantExpression{
-				Opcode: OpcodeUnreachable, // Invalid!
-			},
-		}}}
-		err := m.validateTable(&Table{}, nil)
-		require.Error(t, err)
-		require.EqualError(t, err, "invalid const expression for element: invalid opcode for const expression: 0x0")
-	})
-	t.Run("ok", func(t *testing.T) {
-		m := Module{ElementSection: []*ElementSegment{{
-			OffsetExpr: &ConstantExpression{
-				Opcode: OpcodeI32Const,
-				Data:   []byte{0x1},
+	three := uint32(3)
+	tests := []struct {
+		name     string
+		input    *Module
+		expected []*validatedElementSegment
+	}{
+		{
+			name:     "empty",
+			input:    &Module{},
+			expected: []*validatedElementSegment{},
+		},
+		{
+			name:     "min zero",
+			input:    &Module{TableSection: &Table{}},
+			expected: []*validatedElementSegment{},
+		},
+		{
+			name:     "min/max",
+			input:    &Module{TableSection: &Table{1, &three}},
+			expected: []*validatedElementSegment{},
+		},
+		{ // See: https://github.com/WebAssembly/spec/issues/1427
+			name: "constant derived element offset=0 and no index",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{Min: 1},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}},
+				},
 			},
-		}}}
-		err := m.validateTable(&Table{}, nil)
-		require.NoError(t, err)
-	})
-}
+			expected: []*validatedElementSegment{},
+		},
+		{
+			name: "constant derived element offset=0 and one index",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{Min: 1},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+			},
+			expected: []*validatedElementSegment{
+				{opcode: OpcodeI32Const, arg: 0, init: []uint32{0}},
+			},
+		},
+		{
+			name: "constant derived element offset - ignores min on imported table",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				ImportSection:   []*Import{{Type: ExternTypeTable, DescTable: &Table{}}},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+			},
+			expected: []*validatedElementSegment{
+				{opcode: OpcodeI32Const, arg: 0, init: []uint32{0}},
+			},
+		},
+		{
+			name: "constant derived element offset=0 and one index - imported table",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				ImportSection:   []*Import{{Type: ExternTypeTable, DescTable: &Table{Min: 1}}},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+			},
+			expected: []*validatedElementSegment{
+				{opcode: OpcodeI32Const, arg: 0, init: []uint32{0}},
+			},
+		},
+		{
+			name: "constant derived element offset and two indices",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{Min: 3},
+				FunctionSection: []Index{0, 0, 0, 0},
+				CodeSection:     []*Code{codeEnd, codeEnd, codeEnd, codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}},
+						Init:       []Index{0, 2},
+					},
+				},
+			},
+			expected: []*validatedElementSegment{
+				{opcode: OpcodeI32Const, arg: 1, init: []uint32{0, 2}},
+			},
+		},
+		{ // See: https://github.com/WebAssembly/spec/issues/1427
+			name: "imported global derived element offset and no index",
+			input: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{Min: 1},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}},
+				},
+			},
+			expected: []*validatedElementSegment{},
+		},
+		{
+			name: "imported global derived element offset and one index",
+			input: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{Min: 1},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+			},
+			expected: []*validatedElementSegment{
+				{opcode: OpcodeGlobalGet, arg: 0, init: []uint32{0}},
+			},
+		},
+		{
+			name: "imported global derived element offset and one index - imported table",
+			input: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeTable, DescTable: &Table{Min: 1}},
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+			},
+			expected: []*validatedElementSegment{
+				{opcode: OpcodeGlobalGet, arg: 0, init: []uint32{0}},
+			},
+		},
+		{
+			name: "imported global derived element offset - ignores min on imported table",
+			input: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeTable, DescTable: &Table{}},
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+			},
+			expected: []*validatedElementSegment{
+				{opcode: OpcodeGlobalGet, arg: 0, init: []uint32{0}},
+			},
+		},
+		{
+			name: "imported global derived element offset - two indices",
+			input: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI64}},
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{Min: 3},
+				FunctionSection: []Index{0, 0, 0, 0},
+				CodeSection:     []*Code{codeEnd, codeEnd, codeEnd, codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x1}},
+						Init:       []Index{0, 2},
+					},
+				},
+			},
+			expected: []*validatedElementSegment{
+				{opcode: OpcodeGlobalGet, arg: 1, init: []uint32{0, 2}},
+			},
+		},
+		{
+			name: "mixed elementSegments - const before imported global",
+			input: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI64}},
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{Min: 3},
+				FunctionSection: []Index{0, 0, 0, 0},
+				CodeSection:     []*Code{codeEnd, codeEnd, codeEnd, codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}},
+						Init:       []Index{0, 2},
+					},
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x1}},
+						Init:       []Index{1, 2},
+					},
+				},
+			},
+			expected: []*validatedElementSegment{
+				{opcode: OpcodeI32Const, arg: 1, init: []uint32{0, 2}},
+				{opcode: OpcodeGlobalGet, arg: 1, init: []uint32{1, 2}},
+			},
+		},
+	}
 
-func TestModuleInstance_applyElements(t *testing.T) {
-	functionCounts := uint32(0xa)
-	m := &ModuleInstance{
-		Table:     &TableInstance{Table: make([]uintptr, 10)},
-		Functions: make([]*FunctionInstance, 10),
-		Engine:    &mockModuleEngine{},
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			vt, err := tc.input.validateTable()
+			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.validatedElementSegments)
+			vt2, err := tc.input.validateTable()
+			require.NoError(t, err)
+			require.Equal(t, vt, vt2)
+		})
 	}
-	targetIndex, targetOffset := uint32(1), byte(0)
-	targetIndex2, targetOffset2 := functionCounts-1, byte(0x8)
-	m.Functions[targetIndex] = &FunctionInstance{Type: &FunctionType{}, Index: Index(targetIndex)}
-	m.Functions[targetIndex2] = &FunctionInstance{Type: &FunctionType{}, Index: Index(targetIndex2)}
-	m.applyElements([]*ElementSegment{
-		{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{targetOffset}}, Init: []uint32{targetIndex}},
-		{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{targetOffset2}}, Init: []uint32{targetIndex2}},
-	})
-	require.Equal(t, uintptr(targetIndex), m.Table.Table[targetOffset])
-	require.Equal(t, uintptr(targetIndex2), m.Table.Table[targetOffset2])
 }
 
-func TestModuleInstance_validateElements(t *testing.T) {
-	functionCounts := uint32(0xa)
-	m := &ModuleInstance{
-		Table:     &TableInstance{Table: make([]uintptr, 10)},
-		Functions: make([]*FunctionInstance, 10),
+func TestModule_validateTable_Errors(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       *Module
+		expectedErr string
+	}{
+		{
+			name: "constant derived element offset - decode error",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{
+						Opcode: OpcodeI32Const,
+						Data:   leb128.EncodeUint64(math.MaxUint64),
+					}, Init: []Index{0}},
+				},
+			},
+			expectedErr: "element[0] couldn't read i32.const parameter: overflows a 32-bit integer",
+		},
+		{
+			name: "constant derived element offset - wrong ValType",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeI64Const, Data: []byte{0x0}}, Init: []Index{0}},
+				},
+			},
+			expectedErr: "element[0] has an invalid const expression: i64.const",
+		},
+		{
+			name: "constant derived element offset - missing table",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, Init: []Index{0}},
+				},
+			},
+			expectedErr: "element was defined, but not table",
+		},
+		{
+			name: "constant derived element offset exceeds table min",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{Min: 1},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x2}}, Init: []Index{0}},
+				},
+			},
+			expectedErr: "element[0].init exceeds min table size",
+		},
+		{
+			name: "constant derived element offset puts init beyond table min",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{Min: 2},
+				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}},
+				},
+			},
+			expectedErr: "element[1].init exceeds min table size",
+		},
+		{ // See: https://github.com/WebAssembly/spec/issues/1427
+			name: "constant derived element offset beyond table min - no init elements",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{Min: 1},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x2}}},
+				},
+			},
+			expectedErr: "element[0].init exceeds min table size",
+		},
+		{
+			name: "constant derived element offset - funcidx out of range",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{Min: 1},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}}, Init: []Index{0, 1}},
+				},
+			},
+			expectedErr: "element[0].init[1] funcidx 1 out of range",
+		},
+		{
+			name: "imported global derived element offset - missing table",
+			input: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []Index{0}},
+				},
+			},
+			expectedErr: "element was defined, but not table",
+		},
+		{
+			name: "imported global derived element offset - funcidx out of range",
+			input: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{Min: 1},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []Index{0, 1}},
+				},
+			},
+			expectedErr: "element[0].init[1] funcidx 1 out of range",
+		},
+		{
+			name: "imported global derived element offset - wrong ValType",
+			input: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI64}},
+				},
+				TableSection:    &Table{},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []Index{0}},
+				},
+			},
+			expectedErr: "element[0] (global.get 0): import[0].global.ValType != i32",
+		},
+		{
+			name: "imported global derived element offset - decode error",
+			input: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{
+						Opcode: OpcodeGlobalGet,
+						Data:   leb128.EncodeUint64(math.MaxUint64),
+					}, Init: []Index{0}},
+				},
+			},
+			expectedErr: "element[0] couldn't read global.get parameter: overflows a 32-bit integer",
+		},
+		{
+			name: "imported global derived element offset - no imports",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{},
+				FunctionSection: []Index{0},
+				GlobalSection:   []*Global{{Type: &GlobalType{ValType: ValueTypeI32}}}, // ignored as not imported
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []Index{0}},
+				},
+			},
+			expectedErr: "element[0] (global.get 0): out of range of imported globals",
+		},
+		{
+			name: "imported global derived element offset - no imports are globals",
+			input: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeFunc, DescFunc: 0},
+				},
+				TableSection:    &Table{},
+				FunctionSection: []Index{0},
+				GlobalSection:   []*Global{{Type: &GlobalType{ValType: ValueTypeI32}}}, // ignored as not imported
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []Index{0}},
+				},
+			},
+			expectedErr: "element[0] (global.get 0): out of range of imported globals",
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := tc.input.validateTable()
+			require.EqualError(t, err, tc.expectedErr)
+		})
 	}
-	for _, tc := range []struct {
-		name     string
-		elements []*ElementSegment
-		expErr   bool
+}
+
+func TestModule_buildTable(t *testing.T) {
+	three := uint32(3)
+	tests := []struct {
+		name            string
+		module          *Module
+		importedTable   *TableInstance
+		importedGlobals []*GlobalInstance
+		expectedTable   *TableInstance
+		expectedInit    map[Index]Index
 	}{
 		{
-			name: "ok",
-			elements: []*ElementSegment{
-				{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, Init: []uint32{0, functionCounts - 1}},
+			name: "empty",
+			module: &Module{
+				validatedElementSegments: []*validatedElementSegment{},
+			},
+		},
+		{
+			name: "min zero",
+			module: &Module{
+				TableSection:             &Table{},
+				validatedElementSegments: []*validatedElementSegment{},
+			},
+			expectedTable: &TableInstance{Table: make([]interface{}, 0), Min: 0},
+		},
+		{
+			name: "min/max",
+			module: &Module{
+				TableSection:             &Table{1, &three},
+				validatedElementSegments: []*validatedElementSegment{},
+			},
+			expectedTable: &TableInstance{Table: make([]interface{}, 1), Min: 1, Max: &three},
+		},
+		{ // See: https://github.com/WebAssembly/spec/issues/1427
+			name: "constant derived element offset=0 and no index",
+			module: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{Min: 1},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}},
+				},
+				validatedElementSegments: []*validatedElementSegment{},
+			},
+			expectedTable: &TableInstance{Table: make([]interface{}, 1), Min: 1},
+		},
+		{
+			name: "constant derived element offset=0 and one index",
+			module: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{Min: 1},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeI32Const, arg: 0, init: []uint32{0}},
+				},
 			},
+			expectedTable: &TableInstance{Table: make([]interface{}, 1), Min: 1},
+			expectedInit:  map[Index]Index{0: 0},
 		},
 		{
-			name: "ok on edge",
-			elements: []*ElementSegment{
-				{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x8}}, Init: []uint32{0, functionCounts - 1}},
+			name: "constant derived element offset - ignores min on imported table",
+			module: &Module{
+				TypeSection:     []*FunctionType{{}},
+				ImportSection:   []*Import{{Type: ExternTypeTable, DescTable: &Table{}}},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeI32Const, arg: 0, init: []uint32{0}},
+				},
 			},
 		},
 		{
-			name: "out of bounds",
-			elements: []*ElementSegment{
-				{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x9}}, Init: []uint32{0, functionCounts - 1}},
+			name: "constant derived element offset=0 and one index - imported table",
+			module: &Module{
+				TypeSection:     []*FunctionType{{}},
+				ImportSection:   []*Import{{Type: ExternTypeTable, DescTable: &Table{Min: 1}}},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeI32Const, arg: 0, init: []uint32{0}},
+				},
 			},
-			expErr: true,
 		},
 		{
-			name: "unknown function",
-			elements: []*ElementSegment{
-				{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}}, Init: []uint32{0, functionCounts}},
+			name: "constant derived element offset and two indices",
+			module: &Module{
+				TypeSection:     []*FunctionType{{}},
+				TableSection:    &Table{Min: 3},
+				FunctionSection: []Index{0, 0, 0, 0},
+				CodeSection:     []*Code{codeEnd, codeEnd, codeEnd, codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}},
+						Init:       []Index{0, 2},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeI32Const, arg: 1, init: []uint32{0, 2}},
+				},
 			},
-			expErr: true,
+			expectedTable: &TableInstance{Table: make([]interface{}, 3), Min: 3},
+			expectedInit:  map[Index]Index{1: 0, 2: 2},
 		},
-	} {
-		tc := tc
+		{ // See: https://github.com/WebAssembly/spec/issues/1427
+			name: "imported global derived element offset and no index",
+			module: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{Min: 1},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}},
+				},
+				validatedElementSegments: []*validatedElementSegment{},
+			},
+			importedGlobals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}, Val: 1}},
+			expectedTable:   &TableInstance{Table: make([]interface{}, 1), Min: 1},
+		},
+		{
+			name: "imported global derived element offset and one index",
+			module: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{Min: 2},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeGlobalGet, arg: 0, init: []uint32{0}},
+				},
+			},
+			importedGlobals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}, Val: 1}},
+			expectedTable:   &TableInstance{Table: make([]interface{}, 2), Min: 2},
+			expectedInit:    map[Index]Index{1: 0},
+		},
+		{
+			name: "imported global derived element offset and one index - imported table",
+			module: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeTable, DescTable: &Table{Min: 1}},
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeGlobalGet, arg: 0, init: []uint32{0}},
+				},
+			},
+			importedGlobals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}, Val: 1}},
+			importedTable:   &TableInstance{Table: make([]interface{}, 2), Min: 2},
+			expectedInit:    map[Index]Index{1: 0},
+		},
+		{
+			name: "imported global derived element offset - ignores min on imported table",
+			module: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeTable, DescTable: &Table{}},
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeGlobalGet, arg: 0, init: []uint32{0}},
+				},
+			},
+			importedGlobals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}, Val: 1}},
+			importedTable:   &TableInstance{Table: make([]interface{}, 2), Min: 2},
+			expectedInit:    map[Index]Index{1: 0},
+		},
+		{
+			name: "imported global derived element offset - two indices",
+			module: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI64}},
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{Min: 3},
+				FunctionSection: []Index{0, 0, 0, 0},
+				CodeSection:     []*Code{codeEnd, codeEnd, codeEnd, codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x1}},
+						Init:       []Index{0, 2},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeGlobalGet, arg: 1, init: []uint32{0, 2}},
+				},
+			},
+			importedGlobals: []*GlobalInstance{
+				{Type: &GlobalType{ValType: ValueTypeI64}, Val: 3},
+				{Type: &GlobalType{ValType: ValueTypeI32}, Val: 1},
+			},
+			expectedTable: &TableInstance{Table: make([]interface{}, 3), Min: 3},
+			expectedInit:  map[Index]Index{1: 0, 2: 2},
+		},
+		{
+			name: "mixed elementSegments - const before imported global",
+			module: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI64}},
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{Min: 3},
+				FunctionSection: []Index{0, 0, 0, 0},
+				CodeSection:     []*Code{codeEnd, codeEnd, codeEnd, codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x1}},
+						Init:       []Index{0, 2},
+					},
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x1}},
+						Init:       []Index{1, 2},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeI32Const, arg: 1, init: []uint32{0, 2}},
+					{opcode: OpcodeGlobalGet, arg: 1, init: []uint32{1, 2}},
+				},
+			},
+			importedGlobals: []*GlobalInstance{
+				{Type: &GlobalType{ValType: ValueTypeI64}, Val: 3},
+				{Type: &GlobalType{ValType: ValueTypeI32}, Val: 1},
+			},
+			expectedTable: &TableInstance{Table: make([]interface{}, 3), Min: 3},
+			expectedInit:  map[Index]Index{1: 1, 2: 2},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
 		t.Run(tc.name, func(t *testing.T) {
-			err := m.validateElements(tc.elements)
-			if tc.expErr {
-				require.Error(t, err)
+			table, init, err := tc.module.buildTable(tc.importedTable, tc.importedGlobals)
+			require.NoError(t, err)
+			if tc.importedTable != nil { // buildTable shouldn't touch the imported one
+				require.Same(t, tc.importedTable, table)
 			} else {
-				require.NoError(t, err)
+				require.Equal(t, tc.expectedTable, table)
 			}
+			require.Equal(t, tc.expectedInit, init)
+		})
+	}
+}
+
+// TestModule_buildTable_Errors covers the only late error conditions possible.
+func TestModule_buildTable_Errors(t *testing.T) {
+	tests := []struct {
+		name            string
+		module          *Module
+		importedTable   *TableInstance
+		importedGlobals []*GlobalInstance
+		expectedErr     string
+	}{
+		{
+			name: "constant derived element offset exceeds table min - imported table",
+			module: &Module{
+				TypeSection:     []*FunctionType{{}},
+				ImportSection:   []*Import{{Type: ExternTypeTable, DescTable: &Table{}}},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeI32Const, arg: 2, init: []uint32{0}},
+				},
+			},
+			importedTable: &TableInstance{Table: make([]interface{}, 2), Min: 2},
+			expectedErr:   "element[0].init exceeds min table size",
+		},
+		{
+			name: "imported global derived element offset exceeds table min",
+			module: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{Min: 2},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeGlobalGet, arg: 0, init: []uint32{0}},
+				},
+			},
+			importedGlobals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}, Val: 2}},
+			expectedErr:     "element[0].init exceeds min table size",
+		},
+		{
+			name: "imported global derived element offset exceeds table min imported table",
+			module: &Module{
+				TypeSection: []*FunctionType{{}},
+				ImportSection: []*Import{
+					{Type: ExternTypeTable, DescTable: &Table{}},
+					{Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}},
+				},
+				TableSection:    &Table{Min: 2},
+				FunctionSection: []Index{0},
+				CodeSection:     []*Code{codeEnd},
+				ElementSection: []*ElementSegment{
+					{
+						OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}},
+						Init:       []Index{0},
+					},
+				},
+				validatedElementSegments: []*validatedElementSegment{
+					{opcode: OpcodeGlobalGet, arg: 0, init: []uint32{0}},
+				},
+			},
+			importedTable:   &TableInstance{Table: make([]interface{}, 2), Min: 2},
+			importedGlobals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}, Val: 2}},
+			expectedErr:     "element[0].init exceeds min table size",
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			_, _, err := tc.module.buildTable(tc.importedTable, tc.importedGlobals)
+			require.EqualError(t, err, tc.expectedErr)
 		})
 	}
 }
diff --git a/tests/spectest/spec_test.go b/tests/spectest/spec_test.go
index 1715d873..88719872 100644
--- a/tests/spectest/spec_test.go
+++ b/tests/spectest/spec_test.go
@@ -322,6 +322,7 @@ func runTest(t *testing.T, newEngine func() wasm.Engine) {
 
 						mod, err := binary.DecodeModule(buf, wasm.Features20191205)
 						require.NoError(t, err, msg)
+						require.NoError(t, mod.Validate(wasm.Features20191205))
 
 						moduleName := c.Name
 						if moduleName == "" { // When "(module ...) directive doesn't have name.