Repository: tetratelabs/wazero
Pull request #2311 (2024-09-06 02:25:18)

Fix descriptor.Table buffer growth calc
Fixes InsertAt size calculation to avoid growing slices more than needed. On insert the mask should calculate the index in the slice not the number of elements in the table. Avoids a factor of 8 increase in table size. Small optimisation to use the capcity of the slice to reduce the average length whilst keeping allocations down. Clarified the grow method grows the table by n * 64 items.

emcfarlane: @mathetake added a testcase which shows the error and panic on main on edgecases. Renamed the file but its causing a large diff then intended, let me know if you;d like a diff format,
mathetake: sorry what was the intent for renaming the file and adding another test? I would prefer make it in the existing _test.go
emcfarlane: Its a `package descriptor_test` as it imports `sys` to avoid a cycle. Moved it to `_sys_test.go` to make it more clear not sure if theres a pattern I can follow?

mathetake: hah i see 
mathetake: ok i will defer to @achille-roussel for the rest. Thanks for the work here!

Patch:
diff --git a/experimental/memory.go b/experimental/memory.go
index e379bf05..26540648 100644
--- a/experimental/memory.go
+++ b/experimental/memory.go
@@ -35,6 +35,8 @@ type LinearMemory interface {
 	// Notes:
 	//   - To back a shared memory, Reallocate can't change the address of the
 	//     backing []byte (only its length/capacity may change).
+	//   - Reallocate may return nil if fails to grow the LinearMemory. This
+	//     condition may or may not be handled gracefully by the Wasm module.
 	Reallocate(size uint64) []byte
 	// Free the backing memory buffer.
 	Free()
diff --git a/internal/descriptor/table.go b/internal/descriptor/table.go
index 03761e6e..a20e1910 100644
--- a/internal/descriptor/table.go
+++ b/internal/descriptor/table.go
@@ -1,6 +1,9 @@
 package descriptor
 
-import "math/bits"
+import (
+	"math/bits"
+	"slices"
+)
 
 // Table is a data structure mapping 32 bit descriptor to items.
 //
@@ -37,23 +40,13 @@ func (t *Table[Key, Item]) Len() (n int) {
 	return n
 }
 
-// grow ensures that t has enough room for n items, potentially reallocating the
-// internal buffers if their capacity was too small to hold this many items.
+// grow grows the table by n * 64 items.
 func (t *Table[Key, Item]) grow(n int) {
-	// Round up to a multiple of 64 since this is the smallest increment due to
-	// using 64 bits masks.
-	n = (n*64 + 63) / 64
+	total := len(t.masks) + n
+	t.masks = slices.Grow(t.masks, n)[:total]
 
-	if n > len(t.masks) {
-		masks := make([]uint64, n)
-		copy(masks, t.masks)
-
-		items := make([]Item, n*64)
-		copy(items, t.items)
-
-		t.masks = masks
-		t.items = items
-	}
+	total = len(t.items) + n*64
+	t.items = slices.Grow(t.items, n*64)[:total]
 }
 
 // Insert inserts the given item to the table, returning the key that it is
@@ -78,13 +71,9 @@ insert:
 		}
 	}
 
+	// No free slot found, grow the table and retry.
 	offset = len(t.masks)
-	n := 2 * len(t.masks)
-	if n == 0 {
-		n = 1
-	}
-
-	t.grow(n)
+	t.grow(1)
 	goto insert
 }
 
@@ -109,10 +98,10 @@ func (t *Table[Key, Item]) InsertAt(item Item, key Key) bool {
 	if key < 0 {
 		return false
 	}
-	if diff := int(key) - t.Len(); diff > 0 {
+	index := uint(key) / 64
+	if diff := int(index) - len(t.masks) + 1; diff > 0 {
 		t.grow(diff)
 	}
-	index := uint(key) / 64
 	shift := uint(key) % 64
 	t.masks[index] |= 1 << shift
 	t.items[key] = item
@@ -124,7 +113,8 @@ func (t *Table[Key, Item]) Delete(key Key) {
 	if key < 0 { // invalid key
 		return
 	}
-	if index, shift := key/64, key%64; int(index) < len(t.masks) {
+	if index := uint(key) / 64; int(index) < len(t.masks) {
+		shift := uint(key) % 64
 		mask := t.masks[index]
 		if (mask & (1 << shift)) != 0 {
 			var zero Item
diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go
index ee0b453c..5b5e6e9d 100644
--- a/internal/engine/interpreter/interpreter.go
+++ b/internal/engine/interpreter/interpreter.go
@@ -487,7 +487,7 @@ func (e *engine) setLabelAddress(op *uint64, label label, labelAddressResolution
 }
 
 // ResolveImportedFunction implements wasm.ModuleEngine.
-func (e *moduleEngine) ResolveImportedFunction(index, indexInImportedModule wasm.Index, importedModuleEngine wasm.ModuleEngine) {
+func (e *moduleEngine) ResolveImportedFunction(index, descFunc, indexInImportedModule wasm.Index, importedModuleEngine wasm.ModuleEngine) {
 	imported := importedModuleEngine.(*moduleEngine)
 	e.functions[index] = imported.functions[indexInImportedModule]
 }
diff --git a/internal/engine/wazevo/module_engine.go b/internal/engine/wazevo/module_engine.go
index efa1b9bb..8811feed 100644
--- a/internal/engine/wazevo/module_engine.go
+++ b/internal/engine/wazevo/module_engine.go
@@ -237,7 +237,7 @@ func (m *moduleEngine) putLocalMemory() {
 }
 
 // ResolveImportedFunction implements wasm.ModuleEngine.
-func (m *moduleEngine) ResolveImportedFunction(index, indexInImportedModule wasm.Index, importedModuleEngine wasm.ModuleEngine) {
+func (m *moduleEngine) ResolveImportedFunction(index, descFunc, indexInImportedModule wasm.Index, importedModuleEngine wasm.ModuleEngine) {
 	executableOffset, moduleCtxOffset, typeIDOffset := m.parent.offsets.ImportedFunctionOffset(index)
 	importedME := importedModuleEngine.(*moduleEngine)
 
@@ -245,12 +245,12 @@ func (m *moduleEngine) ResolveImportedFunction(index, indexInImportedModule wasm
 		indexInImportedModule -= wasm.Index(len(importedME.importedFunctions))
 	} else {
 		imported := &importedME.importedFunctions[indexInImportedModule]
-		m.ResolveImportedFunction(index, imported.indexInModule, imported.me)
+		m.ResolveImportedFunction(index, descFunc, imported.indexInModule, imported.me)
 		return // Recursively resolve the imported function.
 	}
 
 	offset := importedME.parent.functionOffsets[indexInImportedModule]
-	typeID := getTypeIDOf(indexInImportedModule, importedME.module)
+	typeID := m.module.TypeIDs[descFunc]
 	executable := &importedME.parent.executable[offset]
 	// Write functionInstance.
 	binary.LittleEndian.PutUint64(m.opaque[executableOffset:], uint64(uintptr(unsafe.Pointer(executable))))
@@ -261,28 +261,6 @@ func (m *moduleEngine) ResolveImportedFunction(index, indexInImportedModule wasm
 	m.importedFunctions[index] = importedFunction{me: importedME, indexInModule: indexInImportedModule}
 }
 
-func getTypeIDOf(funcIndex wasm.Index, m *wasm.ModuleInstance) wasm.FunctionTypeID {
-	source := m.Source
-
-	var typeIndex wasm.Index
-	if funcIndex >= source.ImportFunctionCount {
-		funcIndex -= source.ImportFunctionCount
-		typeIndex = source.FunctionSection[funcIndex]
-	} else {
-		var cnt wasm.Index
-		for i := range source.ImportSection {
-			if source.ImportSection[i].Type == wasm.ExternTypeFunc {
-				if cnt == funcIndex {
-					typeIndex = source.ImportSection[i].DescFunc
-					break
-				}
-				cnt++
-			}
-		}
-	}
-	return m.TypeIDs[typeIndex]
-}
-
 // ResolveImportedMemory implements wasm.ModuleEngine.
 func (m *moduleEngine) ResolveImportedMemory(importedModuleEngine wasm.ModuleEngine) {
 	importedME := importedModuleEngine.(*moduleEngine)
diff --git a/internal/sysfs/file.go b/internal/sysfs/file.go
index fdbf1fde..1b6d5f3d 100644
--- a/internal/sysfs/file.go
+++ b/internal/sysfs/file.go
@@ -269,7 +269,7 @@ func (f *fsFile) Readdir(n int) (dirents []experimentalsys.Dirent, errno experim
 
 	if f.reopenDir { // re-open the directory if needed.
 		f.reopenDir = false
-		if errno = adjustReaddirErr(f, f.closed, f.reopen()); errno != 0 {
+		if errno = adjustReaddirErr(f, f.closed, f.rewindDir()); errno != 0 {
 			return
 		}
 	}
@@ -418,19 +418,25 @@ func seek(s io.Seeker, offset int64, whence int) (int64, experimentalsys.Errno)
 	return newOffset, experimentalsys.UnwrapOSError(err)
 }
 
-// reopenFile allows re-opening a file for reasons such as applying flags or
-// directory iteration.
-type reopenFile func() experimentalsys.Errno
-
-// compile-time check to ensure fsFile.reopen implements reopenFile.
-var _ reopenFile = (*fsFile)(nil).reopen
-
-// reopen implements the same method as documented on reopenFile.
-func (f *fsFile) reopen() experimentalsys.Errno {
-	_ = f.close()
-	var err error
-	f.file, err = f.fs.Open(f.name)
-	return experimentalsys.UnwrapOSError(err)
+func (f *fsFile) rewindDir() experimentalsys.Errno {
+	// Reopen the directory to rewind it.
+	file, err := f.fs.Open(f.name)
+	if err != nil {
+		return experimentalsys.UnwrapOSError(err)
+	}
+	fi, err := file.Stat()
+	if err != nil {
+		return experimentalsys.UnwrapOSError(err)
+	}
+	// Can't check if it's still the same file,
+	// but is it still a directory, at least?
+	if !fi.IsDir() {
+		return experimentalsys.ENOTDIR
+	}
+	// Only update f on success.
+	_ = f.file.Close()
+	f.file = file
+	return 0
 }
 
 // readdirFile allows masking the `Readdir` function on os.File.
diff --git a/internal/sysfs/osfile.go b/internal/sysfs/osfile.go
index 490f0fa6..a9b01eb6 100644
--- a/internal/sysfs/osfile.go
+++ b/internal/sysfs/osfile.go
@@ -83,21 +83,12 @@ func (f *osFile) SetAppend(enable bool) (errno experimentalsys.Errno) {
 		f.flag &= ^experimentalsys.O_APPEND
 	}
 
-	// Clear any create or trunc flag, as we are re-opening, not re-creating.
-	f.flag &= ^(experimentalsys.O_CREAT | experimentalsys.O_TRUNC)
-
-	// appendMode (bool) cannot be changed later, so we have to re-open the
-	// file. https://github.com/golang/go/blob/go1.20/src/os/file_unix.go#L60
+	// appendMode cannot be changed later, so we have to re-open the file
+	// https://github.com/golang/go/blob/go1.23/src/os/file_unix.go#L60
 	return fileError(f, f.closed, f.reopen())
 }
 
-// compile-time check to ensure osFile.reopen implements reopenFile.
-var _ reopenFile = (*osFile)(nil).reopen
-
 func (f *osFile) reopen() (errno experimentalsys.Errno) {
-	// Clear any create flag, as we are re-opening, not re-creating.
-	f.flag &= ^experimentalsys.O_CREAT
-
 	var (
 		isDir  bool
 		offset int64
@@ -116,22 +107,47 @@ func (f *osFile) reopen() (errno experimentalsys.Errno) {
 		}
 	}
 
-	_ = f.close()
-	f.file, errno = OpenFile(f.path, f.flag, f.perm)
+	// Clear any create or trunc flag, as we are re-opening, not re-creating.
+	flag := f.flag &^ (experimentalsys.O_CREAT | experimentalsys.O_TRUNC)
+	file, errno := OpenFile(f.path, flag, f.perm)
+	if errno != 0 {
+		return errno
+	}
+	errno = f.checkSameFile(file)
 	if errno != 0 {
 		return errno
 	}
 
 	if !isDir {
-		_, err = f.file.Seek(offset, io.SeekStart)
+		_, err = file.Seek(offset, io.SeekStart)
 		if err != nil {
+			_ = file.Close()
 			return experimentalsys.UnwrapOSError(err)
 		}
 	}
 
+	// Only update f on success.
+	_ = f.file.Close()
+	f.file = file
+	f.fd = file.Fd()
 	return 0
 }
 
+func (f *osFile) checkSameFile(osf *os.File) experimentalsys.Errno {
+	fi1, err := f.file.Stat()
+	if err != nil {
+		return experimentalsys.UnwrapOSError(err)
+	}
+	fi2, err := osf.Stat()
+	if err != nil {
+		return experimentalsys.UnwrapOSError(err)
+	}
+	if os.SameFile(fi1, fi2) {
+		return 0
+	}
+	return experimentalsys.ENOENT
+}
+
 // IsNonblock implements the same method as documented on fsapi.File
 func (f *osFile) IsNonblock() bool {
 	return isNonblock(f)
diff --git a/internal/wasm/engine.go b/internal/wasm/engine.go
index 61a342ef..8c387e9e 100644
--- a/internal/wasm/engine.go
+++ b/internal/wasm/engine.go
@@ -44,9 +44,10 @@ type ModuleEngine interface {
 
 	// ResolveImportedFunction is used to add imported functions needed to make this ModuleEngine fully functional.
 	// 	- `index` is the function Index of this imported function.
+	// 	- `descFunc` is the type Index in Module.TypeSection of this imported function. It corresponds to Import.DescFunc.
 	// 	- `indexInImportedModule` is the function Index of the imported function in the imported module.
 	//	- `importedModuleEngine` is the ModuleEngine for the imported ModuleInstance.
-	ResolveImportedFunction(index, indexInImportedModule Index, importedModuleEngine ModuleEngine)
+	ResolveImportedFunction(index, descFunc, indexInImportedModule Index, importedModuleEngine ModuleEngine)
 
 	// ResolveImportedMemory is called when this module imports a memory from another module.
 	ResolveImportedMemory(importedModuleEngine ModuleEngine)
diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go
index 8e072fd1..9a1f0c6d 100644
--- a/internal/wasm/memory.go
+++ b/internal/wasm/memory.go
@@ -77,6 +77,7 @@ func NewMemoryInstance(memSec *Memory, allocator experimental.MemoryAllocator, m
 	if allocator != nil {
 		expBuffer = allocator.Allocate(capBytes, maxBytes)
 		buffer = expBuffer.Reallocate(minBytes)
+		_ = buffer[:minBytes] // Bounds check that the minimum was allocated.
 	} else if memSec.IsShared {
 		// Shared memory needs a fixed buffer, so allocate with the maximum size.
 		//
@@ -238,12 +239,15 @@ func (m *MemoryInstance) Grow(delta uint32) (result uint32, ok bool) {
 		return currentPages, true
 	}
 
-	// If exceeds the max of memory size, we push -1 according to the spec.
 	newPages := currentPages + delta
 	if newPages > m.Max || int32(delta) < 0 {
 		return 0, false
 	} else if m.expBuffer != nil {
 		buffer := m.expBuffer.Reallocate(MemoryPagesToBytesNum(newPages))
+		if buffer == nil {
+			// Allocator failed to grow.
+			return 0, false
+		}
 		if m.Shared {
 			if unsafe.SliceData(buffer) != unsafe.SliceData(m.Buffer) {
 				panic("shared memory cannot move, this is a bug in the memory allocator")
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index dda6e5b6..c7909c67 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -446,7 +446,7 @@ func (m *ModuleInstance) resolveImports(ctx context.Context, module *Module) (er
 					return
 				}
 
-				m.Engine.ResolveImportedFunction(i.IndexPerType, imported.Index, importedModule.Engine)
+				m.Engine.ResolveImportedFunction(i.IndexPerType, i.DescFunc, imported.Index, importedModule.Engine)
 			case ExternTypeTable:
 				expected := i.DescTable
 				importedTable := importedModule.Tables[imported.Index]
diff --git a/sys/stat.go b/sys/stat.go
index bb7b9e5d..32888554 100644
--- a/sys/stat.go
+++ b/sys/stat.go
@@ -29,9 +29,7 @@ type EpochNanos = int64
 // # Notes
 //
 //   - This is used for WebAssembly ABI emulating the POSIX `stat` system call.
-//     Fields included are required for WebAssembly ABI including wasip1
-//     (a.k.a. wasix) and wasi-filesystem (a.k.a. wasip2). See
-//     https://pubs.opengroup.org/onlinepubs/9699919799/functions/stat.html
+//     See https://pubs.opengroup.org/onlinepubs/9699919799/functions/stat.html
 //   - Fields here are required for WebAssembly ABI including wasip1
 //     (a.k.a. wasix) and wasi-filesystem (a.k.a. wasip2).
 //   - This isn't the same as syscall.Stat_t because wazero supports Windows,


Test patch:
diff --git a/internal/descriptor/export_test.go b/internal/descriptor/export_test.go
new file mode 100644
index 00000000..1a2d377d
--- /dev/null
+++ b/internal/descriptor/export_test.go
@@ -0,0 +1,11 @@
+package descriptor
+
+// Masks returns the masks of the table for testing purposes.
+func Masks[Key ~int32, Item any](t *Table[Key, Item]) []uint64 {
+	return t.masks
+}
+
+// Items returns the items of the table for testing purposes.
+func Items[Key ~int32, Item any](t *Table[Key, Item]) []Item {
+	return t.items
+}
diff --git a/internal/descriptor/table_test.go b/internal/descriptor/table_test.go
index 9713a27a..1b44c4e7 100644
--- a/internal/descriptor/table_test.go
+++ b/internal/descriptor/table_test.go
@@ -3,6 +3,7 @@ package descriptor_test
 import (
 	"testing"
 
+	"github.com/tetratelabs/wazero/internal/descriptor"
 	"github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
@@ -10,9 +11,8 @@ import (
 func TestFileTable(t *testing.T) {
 	table := new(sys.FileTable)
 
-	if n := table.Len(); n != 0 {
-		t.Errorf("new table is not empty: length=%d", n)
-	}
+	n := table.Len()
+	require.Equal(t, 0, n, "new table is not empty: length=%d", n)
 
 	// The id field is used as a sentinel value.
 	v0 := &sys.FileEntry{Name: "1"}
@@ -38,16 +38,12 @@ func TestFileTable(t *testing.T) {
 		{key: k1, val: v1},
 		{key: k2, val: v2},
 	} {
-		if v, ok := table.Lookup(lookup.key); !ok {
-			t.Errorf("value not found for key '%v'", lookup.key)
-		} else if v.Name != lookup.val.Name {
-			t.Errorf("wrong value returned for key '%v': want=%v got=%v", lookup.key, lookup.val.Name, v.Name)
-		}
+		v, ok := table.Lookup(lookup.key)
+		require.True(t, ok, "value not found for key '%v'", lookup.key)
+		require.Equal(t, lookup.val.Name, v.Name, "wrong value returned for key '%v'", lookup.key)
 	}
 
-	if n := table.Len(); n != 3 {
-		t.Errorf("wrong table length: want=3 got=%d", n)
-	}
+	require.Equal(t, 3, table.Len(), "wrong table length: want=3 got=%d", table.Len())
 
 	k0Found := false
 	k1Found := false
@@ -62,9 +58,7 @@ func TestFileTable(t *testing.T) {
 		case k2:
 			k2Found, want = true, v2
 		}
-		if v.Name != want.Name {
-			t.Errorf("wrong value found ranging over '%v': want=%v got=%v", k, want.Name, v.Name)
-		}
+		require.Equal(t, want.Name, v.Name, "wrong value found ranging over table")
 		return true
 	})
 
@@ -76,9 +70,7 @@ func TestFileTable(t *testing.T) {
 		{key: k1, ok: k1Found},
 		{key: k2, ok: k2Found},
 	} {
-		if !found.ok {
-			t.Errorf("key not found while ranging over table: %v", found.key)
-		}
+		require.True(t, found.ok, "key not found while ranging over table: %v", found.key)
 	}
 
 	for i, deletion := range []struct {
@@ -89,12 +81,10 @@ func TestFileTable(t *testing.T) {
 		{key: k2},
 	} {
 		table.Delete(deletion.key)
-		if _, ok := table.Lookup(deletion.key); ok {
-			t.Errorf("item found after deletion of '%v'", deletion.key)
-		}
-		if n, want := table.Len(), 3-(i+1); n != want {
-			t.Errorf("wrong table length after deletion: want=%d got=%d", want, n)
-		}
+		_, ok := table.Lookup(deletion.key)
+		require.False(t, ok, "item found after deletion of '%v'", deletion.key)
+		n, want := table.Len(), 3-(i+1)
+		require.Equal(t, want, n, "wrong table length after deletion: want=%d got=%d", want, n)
 	}
 }
 
@@ -134,3 +124,82 @@ func BenchmarkFileTableLookup(b *testing.B) {
 		b.Error("wrong file returned by lookup")
 	}
 }
+
+func Test_sizeOfTable(t *testing.T) {
+	tests := []struct {
+		name         string
+		operation    func(*descriptor.Table[int32, string])
+		expectedSize int
+	}{
+		{
+			name:         "empty table",
+			operation:    func(table *descriptor.Table[int32, string]) {},
+			expectedSize: 0,
+		},
+		{
+			name: "1 insert",
+			operation: func(table *descriptor.Table[int32, string]) {
+				table.Insert("a")
+			},
+			expectedSize: 1,
+		},
+		{
+			name: "32 inserts",
+			operation: func(table *descriptor.Table[int32, string]) {
+				for i := 0; i < 32; i++ {
+					table.Insert("a")
+				}
+			},
+			expectedSize: 1,
+		},
+		{
+			name: "257 inserts",
+			operation: func(table *descriptor.Table[int32, string]) {
+				for i := 0; i < 257; i++ {
+					table.Insert("a")
+				}
+			},
+			expectedSize: 5,
+		},
+		{
+			name: "1 insert at 63",
+			operation: func(table *descriptor.Table[int32, string]) {
+				table.InsertAt("a", 63)
+			},
+			expectedSize: 1,
+		},
+		{
+			name: "1 insert at 64",
+			operation: func(table *descriptor.Table[int32, string]) {
+				table.InsertAt("a", 64)
+			},
+			expectedSize: 2,
+		},
+		{
+			name: "1 insert at 257",
+			operation: func(table *descriptor.Table[int32, string]) {
+				table.InsertAt("a", 257)
+			},
+			expectedSize: 5,
+		},
+		{
+			name: "insert at until 320",
+			operation: func(table *descriptor.Table[int32, string]) {
+				for i := int32(0); i < 320; i++ {
+					table.InsertAt("a", i)
+				}
+			},
+			expectedSize: 5,
+		},
+	}
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			table := new(descriptor.Table[int32, string])
+			tc.operation(table)
+			require.Equal(t, tc.expectedSize, len(descriptor.Masks(table)))
+			require.Equal(t, tc.expectedSize*64, len(descriptor.Items(table)))
+		})
+	}
+}
diff --git a/internal/engine/wazevo/module_engine_test.go b/internal/engine/wazevo/module_engine_test.go
index 45234e19..7e6bbf33 100644
--- a/internal/engine/wazevo/module_engine_test.go
+++ b/internal/engine/wazevo/module_engine_test.go
@@ -152,28 +152,19 @@ func TestModuleEngine_setupOpaque(t *testing.T) {
 }
 
 func TestModuleEngine_ResolveImportedFunction(t *testing.T) {
-	const begin = 5000
-	m := &moduleEngine{
-		opaque:            make([]byte, 10000),
-		importedFunctions: make([]importedFunction, 4),
-		parent: &compiledModule{offsets: wazevoapi.ModuleContextOffsetData{
-			ImportedFunctionsBegin: begin,
-		}},
-	}
-
 	var op1, op2 byte = 0xaa, 0xbb
-	im1 := &moduleEngine{
+	importing := &moduleEngine{
 		opaquePtr: &op1,
 		parent: &compiledModule{
 			executables:     &executables{executable: make([]byte, 1000)},
 			functionOffsets: []int{1, 5, 10},
 		},
 		module: &wasm.ModuleInstance{
-			TypeIDs: []wasm.FunctionTypeID{0, 0, 0, 0, 111, 222, 333},
+			TypeIDs: []wasm.FunctionTypeID{0, 0, 0, 888, 111, 222, 333},
 			Source:  &wasm.Module{FunctionSection: []wasm.Index{4, 5, 6}},
 		},
 	}
-	im2 := &moduleEngine{
+	imported := &moduleEngine{
 		opaquePtr: &op2,
 		parent: &compiledModule{
 			executables:     &executables{executable: make([]byte, 1000)},
@@ -185,10 +176,20 @@ func TestModuleEngine_ResolveImportedFunction(t *testing.T) {
 		},
 	}
 
-	m.ResolveImportedFunction(0, 0, im1)
-	m.ResolveImportedFunction(1, 0, im2)
-	m.ResolveImportedFunction(2, 2, im1)
-	m.ResolveImportedFunction(3, 1, im1)
+	const begin = 5000
+	m := &moduleEngine{
+		opaque:            make([]byte, 10000),
+		importedFunctions: make([]importedFunction, 4),
+		parent: &compiledModule{offsets: wazevoapi.ModuleContextOffsetData{
+			ImportedFunctionsBegin: begin,
+		}},
+		module: importing.module,
+	}
+
+	m.ResolveImportedFunction(0, 4, 0, importing)
+	m.ResolveImportedFunction(1, 3, 0, imported)
+	m.ResolveImportedFunction(2, 6, 2, importing)
+	m.ResolveImportedFunction(3, 5, 1, importing)
 
 	for i, tc := range []struct {
 		index      int
@@ -196,10 +197,10 @@ func TestModuleEngine_ResolveImportedFunction(t *testing.T) {
 		executable *byte
 		expTypeID  wasm.FunctionTypeID
 	}{
-		{index: 0, op: &op1, executable: &im1.parent.executable[1], expTypeID: 111},
-		{index: 1, op: &op2, executable: &im2.parent.executable[50], expTypeID: 999},
-		{index: 2, op: &op1, executable: &im1.parent.executable[10], expTypeID: 333},
-		{index: 3, op: &op1, executable: &im1.parent.executable[5], expTypeID: 222},
+		{index: 0, op: &op1, executable: &importing.parent.executable[1], expTypeID: 111},
+		{index: 1, op: &op2, executable: &imported.parent.executable[50], expTypeID: 888},
+		{index: 2, op: &op1, executable: &importing.parent.executable[10], expTypeID: 333},
+		{index: 3, op: &op1, executable: &importing.parent.executable[5], expTypeID: 222},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
 			buf := m.opaque[begin+wazevoapi.FunctionInstanceSize*tc.index:]
@@ -216,15 +217,6 @@ func TestModuleEngine_ResolveImportedFunction(t *testing.T) {
 }
 
 func TestModuleEngine_ResolveImportedFunction_recursive(t *testing.T) {
-	const begin = 5000
-	m := &moduleEngine{
-		opaque:            make([]byte, 10000),
-		importedFunctions: make([]importedFunction, 4),
-		parent: &compiledModule{offsets: wazevoapi.ModuleContextOffsetData{
-			ImportedFunctionsBegin: begin,
-		}},
-	}
-
 	var importingOp, importedOp byte = 0xaa, 0xbb
 	imported := &moduleEngine{
 		opaquePtr: &importedOp,
@@ -245,13 +237,23 @@ func TestModuleEngine_ResolveImportedFunction_recursive(t *testing.T) {
 		},
 		importedFunctions: []importedFunction{{me: imported, indexInModule: 0}},
 		module: &wasm.ModuleInstance{
-			TypeIDs: []wasm.FunctionTypeID{0, 222, 0},
+			TypeIDs: []wasm.FunctionTypeID{999, 222, 0},
 			Source:  &wasm.Module{FunctionSection: []wasm.Index{1}},
 		},
 	}
 
-	m.ResolveImportedFunction(0, 0, importing)
-	m.ResolveImportedFunction(1, 1, importing)
+	const begin = 5000
+	m := &moduleEngine{
+		opaque:            make([]byte, 10000),
+		importedFunctions: make([]importedFunction, 4),
+		parent: &compiledModule{offsets: wazevoapi.ModuleContextOffsetData{
+			ImportedFunctionsBegin: begin,
+		}},
+		module: importing.module,
+	}
+
+	m.ResolveImportedFunction(0, 0, 0, importing)
+	m.ResolveImportedFunction(1, 1, 1, importing)
 
 	for i, tc := range []struct {
 		index      int
@@ -259,7 +261,7 @@ func TestModuleEngine_ResolveImportedFunction_recursive(t *testing.T) {
 		executable *byte
 		expTypeID  wasm.FunctionTypeID
 	}{
-		{index: 0, op: &importedOp, executable: &imported.parent.executable[10], expTypeID: 111},
+		{index: 0, op: &importedOp, executable: &imported.parent.executable[10], expTypeID: 999},
 		{index: 1, op: &importingOp, executable: &importing.parent.executable[500], expTypeID: 222},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
@@ -312,26 +314,6 @@ func Test_functionInstance_offsets(t *testing.T) {
 	require.Equal(t, typeID, ptr+16)
 }
 
-func Test_getTypeIDOf(t *testing.T) {
-	m := &wasm.ModuleInstance{
-		TypeIDs: []wasm.FunctionTypeID{111, 222, 333, 444},
-		Source: &wasm.Module{
-			ImportFunctionCount: 1,
-			ImportSection: []wasm.Import{
-				{Type: wasm.ExternTypeMemory},
-				{Type: wasm.ExternTypeTable},
-				{Type: wasm.ExternTypeFunc, DescFunc: 3},
-			},
-			FunctionSection: []wasm.Index{2, 1, 0},
-		},
-	}
-
-	require.Equal(t, wasm.FunctionTypeID(444), getTypeIDOf(0, m))
-	require.Equal(t, wasm.FunctionTypeID(333), getTypeIDOf(1, m))
-	require.Equal(t, wasm.FunctionTypeID(222), getTypeIDOf(2, m))
-	require.Equal(t, wasm.FunctionTypeID(111), getTypeIDOf(3, m))
-}
-
 func Test_newAlignedOpaque(t *testing.T) {
 	for i := 0; i < 100; i++ {
 		s := 16 * (i + 10)
diff --git a/internal/sysfs/file_test.go b/internal/sysfs/file_test.go
index 425cf342..9bcd1a22 100644
--- a/internal/sysfs/file_test.go
+++ b/internal/sysfs/file_test.go
@@ -116,6 +116,50 @@ func TestWriteFdNonblock(t *testing.T) {
 	t.Fatal("writeFd should return EAGAIN at some point")
 }
 
+func TestFileReopenFileUpdatesFD(t *testing.T) {
+	tmpDir := t.TempDir()
+	path := path.Join(tmpDir, "file")
+
+	// Open the file twice. Closing the first file, frees its fd.
+	// Then reopening the second file will take over the first fd.
+	// If reopening the file doesn't update the fd, they won't match.
+	f0 := requireOpenFile(t, path, experimentalsys.O_RDWR|experimentalsys.O_CREAT, 0o600)
+	f1 := requireOpenFile(t, path, experimentalsys.O_RDWR, 0o600)
+	defer f1.Close()
+	f0.Close()
+
+	of, ok := f1.(*osFile)
+	require.True(t, ok)
+
+	require.EqualErrno(t, 0, of.reopen())
+	require.Equal(t, of.file.Fd(), of.fd)
+}
+
+func TestFileReopenFileChecksSameFile(t *testing.T) {
+	tmpDir := t.TempDir()
+	path := path.Join(tmpDir, "file")
+
+	f := requireOpenFile(t, path, experimentalsys.O_RDWR|experimentalsys.O_CREAT, 0o600)
+	defer f.Close()
+
+	require.NoError(t, os.Remove(path))
+
+	of, ok := f.(*osFile)
+	require.True(t, ok)
+
+	// Path does not exist anymore.
+	require.EqualErrno(t, experimentalsys.ENOENT, of.reopen())
+	require.Equal(t, of.file.Fd(), of.fd)
+
+	tmp, err := os.Create(path)
+	require.NoError(t, err)
+	defer tmp.Close()
+
+	// Path exists, but is not the same file.
+	require.EqualErrno(t, experimentalsys.ENOENT, of.reopen())
+	require.Equal(t, of.file.Fd(), of.fd)
+}
+
 func TestFileSetAppend(t *testing.T) {
 	tmpDir := t.TempDir()
 
@@ -124,6 +168,7 @@ func TestFileSetAppend(t *testing.T) {
 
 	// Open without APPEND.
 	f, errno := OpenOSFile(fPath, experimentalsys.O_RDWR, 0o600)
+	defer f.Close()
 	require.EqualErrno(t, 0, errno)
 	require.False(t, f.IsAppend())
 
diff --git a/internal/wasm/memory_test.go b/internal/wasm/memory_test.go
index d10c8276..16cb01b6 100644
--- a/internal/wasm/memory_test.go
+++ b/internal/wasm/memory_test.go
@@ -33,13 +33,15 @@ func TestMemoryBytesNumToPages(t *testing.T) {
 
 func TestMemoryInstance_Grow_Size(t *testing.T) {
 	tests := []struct {
-		name         string
-		capEqualsMax bool
-		expAllocator bool
+		name          string
+		capEqualsMax  bool
+		expAllocator  bool
+		failAllocator bool
 	}{
 		{name: ""},
 		{name: "capEqualsMax", capEqualsMax: true},
 		{name: "expAllocator", expAllocator: true},
+		{name: "failAllocator", failAllocator: true},
 	}
 
 	for _, tt := range tests {
@@ -58,6 +60,9 @@ func TestMemoryInstance_Grow_Size(t *testing.T) {
 			case tc.expAllocator:
 				expBuffer := sliceAllocator(0, maxBytes)
 				m = &MemoryInstance{Max: max, Buffer: expBuffer.Reallocate(0), expBuffer: expBuffer}
+			case tc.failAllocator:
+				expBuffer := sliceAllocator(0, maxBytes)
+				m = &MemoryInstance{Max: max * 2, Buffer: expBuffer.Reallocate(0), expBuffer: expBuffer}
 			}
 			m.ownerModuleEngine = me
 
@@ -1023,6 +1028,9 @@ type sliceBuffer struct {
 func (b *sliceBuffer) Free() {}
 
 func (b *sliceBuffer) Reallocate(size uint64) []byte {
+	if size > b.max {
+		return nil
+	}
 	if cap := uint64(cap(b.buf)); size > cap {
 		b.buf = append(b.buf[:cap], make([]byte, size-cap)...)
 	} else {
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index 08297591..a54ef182 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -341,7 +341,7 @@ func TestStore_Instantiate_Errors(t *testing.T) {
 				importedModuleName: {{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}},
 				"non-exist":        {{Name: "fn", DescFunc: 0}},
 			},
-		}, importingModuleName, nil, nil)
+		}, importingModuleName, nil, []FunctionTypeID{0})
 		require.EqualError(t, err, "module[non-exist] not instantiated")
 	})
 
@@ -485,7 +485,7 @@ func (e *mockModuleEngine) FunctionInstanceReference(i Index) Reference {
 }
 
 // ResolveImportedFunction implements the same method as documented on wasm.ModuleEngine.
-func (e *mockModuleEngine) ResolveImportedFunction(index, importedIndex Index, _ ModuleEngine) {
+func (e *mockModuleEngine) ResolveImportedFunction(index, _, importedIndex Index, _ ModuleEngine) {
 	e.resolveImportsCalled[index] = importedIndex
 }
 
@@ -742,7 +742,7 @@ func Test_resolveImports(t *testing.T) {
 				},
 			}
 
-			m := &ModuleInstance{Engine: &mockModuleEngine{resolveImportsCalled: map[Index]Index{}}, s: s, Source: module}
+			m := &ModuleInstance{Engine: &mockModuleEngine{resolveImportsCalled: map[Index]Index{}}, s: s, Source: module, TypeIDs: []FunctionTypeID{0, 1}}
 			err := m.resolveImports(context.Background(), module)
 			require.NoError(t, err)