Repository: tetratelabs/wazero
Pull request #987 (2022-12-31 03:16:05)

Removes internal dependency on fs.FS
As noted in slack, we are unlikley to long term use fs.FS internally. This ensures we attempt to cast to syscallfs.FS for all I/O by panicing on fs.Open.



Patch:
diff --git a/api/wasm.go b/api/wasm.go
index a45dcdbf..6be8d243 100644
--- a/api/wasm.go
+++ b/api/wasm.go
@@ -147,6 +147,10 @@ type Module interface {
 	// ExportedFunction returns a function exported from this module or nil if it wasn't.
 	ExportedFunction(name string) Function
 
+	// ExportedFunctionDefinitions returns all the exported function
+	// definitions in this module, keyed on export name.
+	ExportedFunctionDefinitions() map[string]FunctionDefinition
+
 	// TODO: Table
 
 	// ExportedMemory returns a memory exported from this module or nil if it wasn't.
@@ -157,6 +161,13 @@ type Module interface {
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi
 	ExportedMemory(name string) Memory
 
+	// ExportedMemoryDefinitions returns all the exported memory definitions
+	// in this module, keyed on export name.
+	//
+	// Note: As of WebAssembly Core Specification 2.0, there can be at most one
+	// memory.
+	ExportedMemoryDefinitions() map[string]MemoryDefinition
+
 	// ExportedGlobal a global exported from this module or nil if it wasn't.
 	ExportedGlobal(name string) Global
 
diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go
index 9d4aac73..c256ecab 100644
--- a/cmd/wazero/wazero.go
+++ b/cmd/wazero/wazero.go
@@ -169,7 +169,12 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
 		host := mount[0]
 		guest := mount[1]
 		if guest == "" { // guest is root
-			rootFS = writefs.DirFS(host)
+			var err error
+			rootFS, err = writefs.NewDirFS(host)
+			if err != nil {
+				fmt.Fprintf(stdErr, "invalid root mount %s: %v\n", host, err)
+				exit(1)
+			}
 		} else { // TODO: subfs
 			rootFS = &compositeFS{
 				paths: map[string]fs.FS{guest: os.DirFS(host)},
diff --git a/experimental/writefs/writefs.go b/experimental/writefs/writefs.go
index 4dc41893..6dbf34ca 100644
--- a/experimental/writefs/writefs.go
+++ b/experimental/writefs/writefs.go
@@ -13,17 +13,28 @@ import (
 	"github.com/tetratelabs/wazero/internal/syscallfs"
 )
 
-// DirFS creates a writeable filesystem at the given path on the host filesystem.
+// NewDirFS creates a writeable filesystem at the given path on the host
+// filesystem.
 //
 // This is like os.DirFS, but allows creation and deletion of files and
 // directories, as well as timestamp modifications. None of which are supported
 // in fs.FS.
 //
+// The following errors are expected:
+//   - syscall.EINVAL: `dir` is invalid.
+//   - syscall.ENOENT: `dir` doesn't exist.
+//   - syscall.ENOTDIR: `dir` exists, but is not a directory.
+//
 // # Isolation
 //
 // Symbolic links can escape the root path as files are opened via os.OpenFile
 // which cannot restrict following them.
-func DirFS(dir string) fs.FS {
-	// writefs.DirFS is intentionally internal as it is still evolving
-	return syscallfs.DirFS(dir)
+//
+// # This is wazero-only
+//
+// Do not attempt to use the result as a fs.FS, as it will panic. This is a
+// bridge to a future filesystem abstraction made for wazero.
+func NewDirFS(dir string) (fs.FS, error) {
+	// syscallfs.DirFS is intentionally internal as it is still evolving
+	return syscallfs.NewDirFS(dir)
 }
diff --git a/internal/engine/compiler/impl_amd64.go b/internal/engine/compiler/impl_amd64.go
index 60163b6d..c7b13152 100644
--- a/internal/engine/compiler/impl_amd64.go
+++ b/internal/engine/compiler/impl_amd64.go
@@ -186,6 +186,12 @@ func (c *amd64Compiler) compileGoDefinedHostFunction() error {
 
 	// Initializes the reserved stack base pointer which is used to retrieve the call frame stack.
 	c.compileReservedStackBasePointerInitialization()
+
+	// Go function can change the module state in arbitrary way, so we have to force
+	// the callEngine.moduleContext initialization on the function return. To do so,
+	// we zero-out callEngine.moduleInstanceAddress.
+	c.assembler.CompileConstToMemory(amd64.MOVQ,
+		0, amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset)
 	return c.compileReturnFunction()
 }
 
diff --git a/internal/engine/compiler/impl_arm64.go b/internal/engine/compiler/impl_arm64.go
index 98793c56..692fff04 100644
--- a/internal/engine/compiler/impl_arm64.go
+++ b/internal/engine/compiler/impl_arm64.go
@@ -398,6 +398,14 @@ func (c *arm64Compiler) compileGoDefinedHostFunction() error {
 
 	// Initializes the reserved stack base pointer which is used to retrieve the call frame stack.
 	c.compileReservedStackBasePointerRegisterInitialization()
+
+	// Go function can change the module state in arbitrary way, so we have to force
+	// the callEngine.moduleContext initialization on the function return. To do so,
+	// we zero-out callEngine.moduleInstanceAddress.
+	c.assembler.CompileRegisterToMemory(arm64.STRD,
+		arm64.RegRZR,
+		arm64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset)
+
 	return c.compileReturnFunction()
 }
 
diff --git a/internal/sys/fs.go b/internal/sys/fs.go
index a683478b..a9c03a3e 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -205,7 +205,12 @@ func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, root fs.FS) (fsc *F
 	// this is a real file or not. ex. `file.(*os.File)`.
 	//
 	// Note: We don't use fs.ReadDirFS as this isn't implemented by os.DirFS.
-	rootDir, err := root.Open(".")
+	var rootDir fs.File
+	if sfs, ok := root.(syscallfs.FS); ok {
+		rootDir, err = sfs.OpenFile(".", os.O_RDONLY, 0)
+	} else {
+		rootDir, err = root.Open(".")
+	}
 	if err != nil {
 		// This could fail because someone made a special-purpose file system,
 		// which only passes certain filenames and not ".".
@@ -309,7 +314,8 @@ func (c *FSContext) OpenFile(name string, flags int, perm fs.FileMode) (newFD ui
 		case flags&os.O_TRUNC != 0:
 			return 0, syscall.ENOSYS
 		default:
-			f, err = c.openFile(name)
+			// only time fs.FS is used
+			f, err = c.fs.Open(c.cleanPath(name))
 		}
 	}
 
@@ -332,12 +338,12 @@ func (c *FSContext) Rmdir(name string) (err error) {
 }
 
 func (c *FSContext) StatPath(name string) (fs.FileInfo, error) {
-	f, err := c.openFile(name)
+	fd, err := c.OpenFile(name, os.O_RDONLY, 0)
 	if err != nil {
 		return nil, err
 	}
-	defer f.Close()
-	return f.Stat()
+	defer c.CloseFile(fd)
+	return c.StatFile(fd)
 }
 
 // Unlink is like syscall.Unlink.
@@ -360,10 +366,6 @@ func (c *FSContext) Utimes(name string, atimeSec, atimeNsec, mtimeSec, mtimeNsec
 	return
 }
 
-func (c *FSContext) openFile(name string) (fs.File, error) {
-	return c.fs.Open(c.cleanPath(name))
-}
-
 func (c *FSContext) cleanPath(name string) string {
 	if len(name) == 0 {
 		return name
diff --git a/internal/syscallfs/dirfs.go b/internal/syscallfs/dirfs.go
new file mode 100644
index 00000000..77b1edd1
--- /dev/null
+++ b/internal/syscallfs/dirfs.go
@@ -0,0 +1,78 @@
+package syscallfs
+
+import (
+	"fmt"
+	"io/fs"
+	"os"
+	"path"
+	"syscall"
+)
+
+func NewDirFS(dir string) (FS, error) {
+	if stat, err := os.Stat(dir); err != nil {
+		return nil, syscall.ENOENT
+	} else if !stat.IsDir() {
+		return nil, syscall.ENOTDIR
+	}
+	return dirFS(dir), nil
+}
+
+type dirFS string
+
+// Open implements the same method as documented on fs.FS
+func (dir dirFS) Open(name string) (fs.File, error) {
+	panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
+}
+
+// OpenFile implements FS.OpenFile
+func (dir dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
+	if !fs.ValidPath(name) {
+		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
+	}
+	return os.OpenFile(path.Join(string(dir), name), flag, perm)
+}
+
+// Mkdir implements FS.Mkdir
+func (dir dirFS) Mkdir(name string, perm fs.FileMode) error {
+	if !fs.ValidPath(name) {
+		return &fs.PathError{Op: "mkdir", Path: name, Err: fs.ErrInvalid}
+	}
+
+	err := os.Mkdir(path.Join(string(dir), name), perm)
+
+	return adjustMkdirError(err)
+}
+
+// Rmdir implements FS.Rmdir
+func (dir dirFS) Rmdir(name string) error {
+	if !fs.ValidPath(name) {
+		return syscall.EINVAL
+	}
+
+	err := syscall.Rmdir(path.Join(string(dir), name))
+
+	return adjustRmdirError(err)
+}
+
+// Unlink implements FS.Unlink
+func (dir dirFS) Unlink(name string) error {
+	if !fs.ValidPath(name) {
+		return syscall.EINVAL
+	}
+
+	err := syscall.Unlink(path.Join(string(dir), name))
+
+	return adjustUnlinkError(err)
+}
+
+// Utimes implements FS.Utimes
+func (dir dirFS) Utimes(name string, atimeSec, atimeNsec, mtimeSec, mtimeNsec int64) error {
+	if !fs.ValidPath(name) {
+		return syscall.EINVAL
+	}
+
+	return syscall.UtimesNano(path.Join(string(dir), name), []syscall.Timespec{
+		{Sec: atimeSec, Nsec: atimeNsec},
+		{Sec: mtimeSec, Nsec: mtimeNsec},
+	})
+}
diff --git a/internal/syscallfs/syscallfs.go b/internal/syscallfs/syscallfs.go
index a3319186..0edacb84 100644
--- a/internal/syscallfs/syscallfs.go
+++ b/internal/syscallfs/syscallfs.go
@@ -2,9 +2,6 @@ package syscallfs
 
 import (
 	"io/fs"
-	"os"
-	"path"
-	"syscall"
 )
 
 // FS is a writeable fs.FS bridge backed by syscall functions needed for ABI
@@ -14,15 +11,22 @@ import (
 //
 // See https://github.com/golang/go/issues/45757
 type FS interface {
-	fs.FS
+	// Open is only defined to match the signature of fs.FS until we remove it.
+	// Once we are done bridging, we will remove this function. Meanwhile,
+	// using it will panic to ensure internal code doesn't depend on it.
+	Open(name string) (fs.File, error)
 
 	// OpenFile is similar to os.OpenFile, except the path is relative to this
 	// file system.
 	OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error)
+	// ^^ TODO: Switch to syscall.Open, though this implies defining and
+	// coercing flags and perms similar to what is done in os.OpenFile.
 
 	// Mkdir is similar to os.Mkdir, except the path is relative to this file
 	// system.
 	Mkdir(name string, perm fs.FileMode) error
+	// ^^ TODO: Switch to syscall.Mkdir, though this implies defining and
+	// coercing flags and perms similar to what is done in os.Mkdir.
 
 	// Utimes is similar to syscall.UtimesNano, except the path is relative to
 	// this file system.
@@ -65,67 +69,3 @@ type FS interface {
 	//   - syscall.EISDIR: `path` exists, but is a directory.
 	Unlink(path string) error
 }
-
-func DirFS(dir string) FS {
-	return dirFS(dir)
-}
-
-type dirFS string
-
-// Open implements the same method as documented on fs.FS
-func (dir dirFS) Open(name string) (fs.File, error) {
-	return dir.OpenFile(name, os.O_RDONLY, 0) // same as os.Open(string)
-}
-
-// OpenFile implements FS.OpenFile
-func (dir dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
-	if !fs.ValidPath(name) {
-		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
-	}
-	return os.OpenFile(path.Join(string(dir), name), flag, perm)
-}
-
-// Mkdir implements FS.Mkdir
-func (dir dirFS) Mkdir(name string, perm fs.FileMode) error {
-	if !fs.ValidPath(name) {
-		return &fs.PathError{Op: "mkdir", Path: name, Err: fs.ErrInvalid}
-	}
-
-	err := os.Mkdir(path.Join(string(dir), name), perm)
-
-	return adjustMkdirError(err)
-}
-
-// Rmdir implements FS.Rmdir
-func (dir dirFS) Rmdir(name string) error {
-	if !fs.ValidPath(name) {
-		return syscall.EINVAL
-	}
-
-	err := syscall.Rmdir(path.Join(string(dir), name))
-
-	return adjustRmdirError(err)
-}
-
-// Unlink implements FS.Unlink
-func (dir dirFS) Unlink(name string) error {
-	if !fs.ValidPath(name) {
-		return syscall.EINVAL
-	}
-
-	err := syscall.Unlink(path.Join(string(dir), name))
-
-	return adjustUnlinkError(err)
-}
-
-// Utimes implements FS.Utimes
-func (dir dirFS) Utimes(name string, atimeSec, atimeNsec, mtimeSec, mtimeNsec int64) error {
-	if !fs.ValidPath(name) {
-		return syscall.EINVAL
-	}
-
-	return syscall.UtimesNano(path.Join(string(dir), name), []syscall.Timespec{
-		{Sec: atimeSec, Nsec: atimeNsec},
-		{Sec: mtimeSec, Nsec: mtimeNsec},
-	})
-}
diff --git a/internal/wasm/call_context.go b/internal/wasm/call_context.go
index 7e70838e..bb8b2620 100644
--- a/internal/wasm/call_context.go
+++ b/internal/wasm/call_context.go
@@ -135,6 +135,21 @@ func (m *CallContext) ExportedMemory(name string) api.Memory {
 	return m.memory
 }
 
+// ExportedMemoryDefinitions implements the same method as documented on
+// api.Module.
+func (m *CallContext) ExportedMemoryDefinitions() map[string]api.MemoryDefinition {
+	// Special case as we currently only support one memory.
+	if mem := m.module.Memory; mem != nil {
+		// Now, find out if it is exported
+		for name, exp := range m.module.Exports {
+			if exp.Type == ExternTypeMemory {
+				return map[string]api.MemoryDefinition{name: mem.definition}
+			}
+		}
+	}
+	return map[string]api.MemoryDefinition{}
+}
+
 // ExportedFunction implements the same method as documented on api.Module.
 func (m *CallContext) ExportedFunction(name string) api.Function {
 	exp, err := m.module.getExport(name, ExternTypeFunc)
@@ -145,6 +160,18 @@ func (m *CallContext) ExportedFunction(name string) api.Function {
 	return m.function(&m.module.Functions[exp.Index])
 }
 
+// ExportedFunctionDefinitions implements the same method as documented on
+// api.Module.
+func (m *CallContext) ExportedFunctionDefinitions() map[string]api.FunctionDefinition {
+	result := map[string]api.FunctionDefinition{}
+	for name, exp := range m.module.Exports {
+		if exp.Type == ExternTypeFunc {
+			result[name] = m.module.Functions[exp.Index].Definition
+		}
+	}
+	return result
+}
+
 // Module is exposed for emscripten.
 func (m *CallContext) Module() *ModuleInstance {
 	return m.module
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index 6d20d2ff..8eb3011d 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -657,6 +657,7 @@ func (m *Module) buildMemory() (mem *MemoryInstance) {
 	memSec := m.MemorySection
 	if memSec != nil {
 		mem = NewMemoryInstance(memSec)
+		mem.definition = m.MemoryDefinitionSection[0]
 	}
 	return
 }


Test patch:
diff --git a/experimental/writefs/writefs_example_test.go b/experimental/writefs/writefs_example_test.go
index 79253a22..8652c49d 100644
--- a/experimental/writefs/writefs_example_test.go
+++ b/experimental/writefs/writefs_example_test.go
@@ -2,6 +2,7 @@ package writefs_test
 
 import (
 	_ "embed"
+	"log"
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/experimental/writefs"
@@ -12,5 +13,9 @@ var config wazero.ModuleConfig //nolint
 // This shows how to use writefs.DirFS to map paths relative to "/work/appA",
 // as "/". Unlike os.DirFS, these paths will be writable.
 func Example_dirFS() {
-	config = wazero.NewModuleConfig().WithFS(writefs.DirFS("/work/appA"))
+	fs, err := writefs.NewDirFS("/work/appA")
+	if err != nil {
+		log.Panicln(err)
+	}
+	config = wazero.NewModuleConfig().WithFS(fs)
 }
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index 0b8a4e60..8f36e965 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -1892,7 +1892,10 @@ func Test_fdWrite_Errors(t *testing.T) {
 
 func Test_pathCreateDirectory(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
+	fs, err := writefs.NewDirFS(tmpDir)
+	require.NoError(t, err)
+
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
 	defer r.Close(testCtx)
 
 	// set up the initial memory to include the path name starting at an offset.
@@ -1920,11 +1923,14 @@ func Test_pathCreateDirectory(t *testing.T) {
 
 func Test_pathCreateDirectory_Errors(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
+	fs, err := writefs.NewDirFS(tmpDir)
+	require.NoError(t, err)
+
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
 	defer r.Close(testCtx)
 
 	file := "file"
-	err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
+	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
 
 	dir := "dir"
@@ -2240,7 +2246,8 @@ func Test_pathOpen(t *testing.T) {
 	osDir := t.TempDir()      // open before loop to ensure no locking problems.
 	writefsDir := t.TempDir() // open before loop to ensure no locking problems.
 	os := os.DirFS(osDir)
-	writeFS := writefs.DirFS(writefsDir)
+	writeFS, err := writefs.NewDirFS(writefsDir)
+	require.NoError(t, err)
 
 	fileName := "file"
 	fileContents := []byte("012")
@@ -2633,7 +2640,10 @@ func Test_pathReadlink(t *testing.T) {
 
 func Test_pathRemoveDirectory(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
+	fs, err := writefs.NewDirFS(tmpDir)
+	require.NoError(t, err)
+
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
 	defer r.Close(testCtx)
 
 	// set up the initial memory to include the path name starting at an offset.
@@ -2643,7 +2653,7 @@ func Test_pathRemoveDirectory(t *testing.T) {
 	require.True(t, ok)
 
 	// create the directory
-	err := os.Mkdir(realPath, 0o700)
+	err = os.Mkdir(realPath, 0o700)
 	require.NoError(t, err)
 
 	dirFD := sys.FdRoot
@@ -2663,11 +2673,14 @@ func Test_pathRemoveDirectory(t *testing.T) {
 
 func Test_pathRemoveDirectory_Errors(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
+	fs, err := writefs.NewDirFS(tmpDir)
+	require.NoError(t, err)
+
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
 	defer r.Close(testCtx)
 
 	file := "file"
-	err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
+	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
 
 	dirNotEmpty := "notempty"
@@ -2805,7 +2818,10 @@ func Test_pathSymlink(t *testing.T) {
 
 func Test_pathUnlinkFile(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
+	fs, err := writefs.NewDirFS(tmpDir)
+	require.NoError(t, err)
+
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
 	defer r.Close(testCtx)
 
 	// set up the initial memory to include the path name starting at an offset.
@@ -2815,7 +2831,7 @@ func Test_pathUnlinkFile(t *testing.T) {
 	require.True(t, ok)
 
 	// create the file
-	err := os.WriteFile(realPath, []byte{}, 0o600)
+	err = os.WriteFile(realPath, []byte{}, 0o600)
 	require.NoError(t, err)
 
 	dirFD := sys.FdRoot
@@ -2835,11 +2851,14 @@ func Test_pathUnlinkFile(t *testing.T) {
 
 func Test_pathUnlinkFile_Errors(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
+	fs, err := writefs.NewDirFS(tmpDir)
+	require.NoError(t, err)
+
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
 	defer r.Close(testCtx)
 
 	file := "file"
-	err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
+	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
 
 	dir := "dir"
diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go
index 6001d794..d9499343 100644
--- a/internal/engine/compiler/engine_test.go
+++ b/internal/engine/compiler/engine_test.go
@@ -58,6 +58,11 @@ func TestCompiler_Engine_NewModuleEngine(t *testing.T) {
 	enginetest.RunTestEngine_NewModuleEngine(t, et)
 }
 
+func TestCompiler_MemoryGrowInRecursiveCall(t *testing.T) {
+	defer functionLog.Reset()
+	enginetest.RunTestEngine_MemoryGrowInRecursiveCall(t, et)
+}
+
 func TestCompiler_ModuleEngine_LookupFunction(t *testing.T) {
 	defer functionLog.Reset()
 	enginetest.RunTestModuleEngine_LookupFunction(t, et)
diff --git a/internal/engine/interpreter/interpreter_test.go b/internal/engine/interpreter/interpreter_test.go
index 55e95372..ff58ac08 100644
--- a/internal/engine/interpreter/interpreter_test.go
+++ b/internal/engine/interpreter/interpreter_test.go
@@ -95,6 +95,11 @@ func (e engineTester) CompiledFunctionPointerValue(me wasm.ModuleEngine, funcInd
 	return uint64(uintptr(unsafe.Pointer(internal.functions[funcIndex])))
 }
 
+func TestInterpreter_MemoryGrowInRecursiveCall(t *testing.T) {
+	defer functionLog.Reset()
+	enginetest.RunTestEngine_MemoryGrowInRecursiveCall(t, et)
+}
+
 func TestInterpreter_Engine_NewModuleEngine(t *testing.T) {
 	enginetest.RunTestEngine_NewModuleEngine(t, et)
 }
diff --git a/internal/gojs/fs_test.go b/internal/gojs/fs_test.go
index 9bdb75eb..b69fe382 100644
--- a/internal/gojs/fs_test.go
+++ b/internal/gojs/fs_test.go
@@ -33,8 +33,9 @@ empty:
 func Test_writefs(t *testing.T) {
 	t.Parallel()
 	tmpDir := t.TempDir()
+	fs, err := writefs.NewDirFS(tmpDir)
+	require.NoError(t, err)
 
-	fs := writefs.DirFS(tmpDir)
 	// test expects to write under /tmp
 	require.NoError(t, os.Mkdir(path.Join(tmpDir, "tmp"), 0o700))
 
diff --git a/internal/gojs/misc_test.go b/internal/gojs/misc_test.go
index 510567aa..b48c1113 100644
--- a/internal/gojs/misc_test.go
+++ b/internal/gojs/misc_test.go
@@ -45,7 +45,6 @@ func Test_stdio(t *testing.T) {
 }
 
 func Test_stdio_large(t *testing.T) {
-	t.Skip("TODO: #980 memory out of bounds when run with compiler")
 	t.Parallel()
 
 	size := 2 * 1024 * 1024 // 2MB
diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go
index d588b609..71aec8e8 100644
--- a/internal/integration_test/spectest/spectest.go
+++ b/internal/integration_test/spectest/spectest.go
@@ -362,6 +362,7 @@ func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *was
 	mod.ExportSection = append(mod.ExportSection, &wasm.Export{Name: "table", Index: 0, Type: wasm.ExternTypeTable})
 
 	maybeSetMemoryCap(mod)
+	mod.BuildMemoryDefinitions()
 	mod.BuildFunctionDefinitions()
 
 	err = mod.Validate(enabledFeatures)
@@ -432,6 +433,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(
 						}
 
 						maybeSetMemoryCap(mod)
+						mod.BuildMemoryDefinitions()
 						mod.BuildFunctionDefinitions()
 						err = s.Engine.CompileModule(ctx, mod, nil)
 						require.NoError(t, err, msg)
@@ -603,6 +605,7 @@ func requireInstantiationError(t *testing.T, ctx context.Context, s *wasm.Store,
 	mod.AssignModuleID(buf)
 
 	maybeSetMemoryCap(mod)
+	mod.BuildMemoryDefinitions()
 	mod.BuildFunctionDefinitions()
 	err = s.Engine.CompileModule(ctx, mod, nil)
 	if err != nil {
diff --git a/internal/syscallfs/syscallfs_test.go b/internal/syscallfs/dirfs_test.go
similarity index 83%
rename from internal/syscallfs/syscallfs_test.go
rename to internal/syscallfs/dirfs_test.go
index 3bb7d817..8082df83 100644
--- a/internal/syscallfs/syscallfs_test.go
+++ b/internal/syscallfs/dirfs_test.go
@@ -8,43 +8,15 @@ import (
 	"runtime"
 	"syscall"
 	"testing"
-	"testing/fstest"
 
 	"github.com/tetratelabs/wazero/internal/platform"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
-var testFiles = map[string]string{
-	"empty.txt":        "",
-	"test.txt":         "animals\n",
-	"sub/test.txt":     "greet sub dir\n",
-	"sub/sub/test.txt": "greet sub sub dir\n",
-}
-
-func TestDirFS_TestFS(t *testing.T) {
-	if runtime.GOOS == "windows" {
-		// This abstraction is a toe-hold, but we'll have to sort windows with
-		// our ideal filesystem tester.
-		t.Skip("TODO: windows")
-	}
-	dir := t.TempDir()
-	require.NoError(t, os.MkdirAll(path.Join(dir, "sub", "sub"), 0o700))
-
-	expected := make([]string, 0, len(testFiles))
-	for name, data := range testFiles {
-		expected = append(expected, name)
-		require.NoError(t, os.WriteFile(path.Join(dir, name), []byte(data), 0o600))
-	}
-
-	if err := fstest.TestFS(DirFS(dir), expected...); err != nil {
-		t.Fatal(err)
-	}
-}
-
 func TestDirFS_MkDir(t *testing.T) {
 	dir := t.TempDir()
 
-	testFS := DirFS(dir)
+	testFS := dirFS(dir)
 
 	name := "mkdir"
 	realPath := path.Join(dir, name)
@@ -74,7 +46,7 @@ func TestDirFS_MkDir(t *testing.T) {
 func TestDirFS_Rmdir(t *testing.T) {
 	dir := t.TempDir()
 
-	testFS := DirFS(dir)
+	testFS := dirFS(dir)
 
 	name := "rmdir"
 	realPath := path.Join(dir, name)
@@ -114,7 +86,7 @@ func TestDirFS_Rmdir(t *testing.T) {
 func TestDirFS_Unlink(t *testing.T) {
 	dir := t.TempDir()
 
-	testFS := DirFS(dir)
+	testFS := dirFS(dir)
 
 	name := "unlink"
 	realPath := path.Join(dir, name)
@@ -145,7 +117,7 @@ func TestDirFS_Unlink(t *testing.T) {
 func TestDirFS_Utimes(t *testing.T) {
 	tmpDir := t.TempDir()
 
-	testFS := DirFS(tmpDir)
+	testFS := dirFS(tmpDir)
 
 	file := "file"
 	err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go
index f735d256..c20b68b9 100644
--- a/internal/testing/enginetest/enginetest.go
+++ b/internal/testing/enginetest/enginetest.go
@@ -55,6 +55,65 @@ type EngineTester interface {
 	CompiledFunctionPointerValue(tme wasm.ModuleEngine, funcIndex wasm.Index) uint64
 }
 
+// RunTestEngine_MemoryGrowInRecursiveCall ensures that it's safe to grow memory in the recursive Wasm calls.
+func RunTestEngine_MemoryGrowInRecursiveCall(t *testing.T, et EngineTester) {
+	enabledFeatures := api.CoreFeaturesV1
+	e := et.NewEngine(enabledFeatures)
+	s, ns := wasm.NewStore(enabledFeatures, e)
+
+	const hostModuleName = "env"
+	const hostFnName = "grow_memory"
+	var growFn api.Function
+	hm, err := wasm.NewHostModule(hostModuleName, map[string]interface{}{hostFnName: func() {
+		// Does the recursive call into Wasm, which grows memory.
+		_, err := growFn.Call(context.Background())
+		require.NoError(t, err)
+	}}, map[string]*wasm.HostFuncNames{hostFnName: {}}, enabledFeatures)
+	require.NoError(t, err)
+
+	err = s.Engine.CompileModule(testCtx, hm, nil)
+	require.NoError(t, err)
+
+	_, err = s.Instantiate(testCtx, ns, hm, hostModuleName, nil)
+	require.NoError(t, err)
+
+	m := &wasm.Module{
+		TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{}, Results: []wasm.ValueType{}}},
+		FunctionSection: []wasm.Index{0, 0},
+		CodeSection: []*wasm.Code{
+			{
+				Body: []byte{
+					// Calls the imported host function, which in turn calls the next in-Wasm function recursively.
+					wasm.OpcodeCall, 0,
+					// Access the memory and this should succeed as we already had memory grown at this point.
+					wasm.OpcodeI32Const, 0,
+					wasm.OpcodeI32Load, 0x2, 0x0,
+					wasm.OpcodeDrop,
+					wasm.OpcodeEnd,
+				},
+			},
+			{
+				// Grows memory by 1 page.
+				Body: []byte{wasm.OpcodeI32Const, 1, wasm.OpcodeMemoryGrow, wasm.OpcodeDrop, wasm.OpcodeEnd},
+			},
+		},
+		MemorySection: &wasm.Memory{Max: 1000},
+		ImportSection: []*wasm.Import{{Module: hostModuleName, Name: hostFnName, DescFunc: 0}},
+	}
+	m.BuildFunctionDefinitions()
+	m.BuildMemoryDefinitions()
+
+	err = s.Engine.CompileModule(testCtx, m, nil)
+	require.NoError(t, err)
+
+	inst, err := s.Instantiate(testCtx, ns, m, t.Name(), nil)
+	require.NoError(t, err)
+
+	growFn = inst.Function(2)
+	_, err = inst.Function(1).Call(context.Background())
+	require.NoError(t, err)
+}
+
 func RunTestEngine_NewModuleEngine(t *testing.T, et EngineTester) {
 	e := et.NewEngine(api.CoreFeaturesV1)
 
diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go
index 9839ac9f..6bf582f4 100644
--- a/internal/wasm/module_test.go
+++ b/internal/wasm/module_test.go
@@ -842,10 +842,15 @@ func TestModule_buildMemoryInstance(t *testing.T) {
 	t.Run("non-nil", func(t *testing.T) {
 		min := uint32(1)
 		max := uint32(10)
-		m := Module{MemorySection: &Memory{Min: min, Cap: min, Max: max}}
+		mDef := &MemoryDefinition{moduleName: "foo"}
+		m := Module{
+			MemorySection:           &Memory{Min: min, Cap: min, Max: max},
+			MemoryDefinitionSection: []*MemoryDefinition{mDef},
+		}
 		mem := m.buildMemory()
 		require.Equal(t, min, mem.Min)
 		require.Equal(t, max, mem.Max)
+		require.Equal(t, mDef, mem.definition)
 	})
 }
 
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index e0c95d12..95017b5f 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -29,33 +29,35 @@ func TestModuleInstance_Memory(t *testing.T) {
 			input: &Module{},
 		},
 		{
-			name:  "memory not exported",
-			input: &Module{MemorySection: &Memory{Min: 1, Cap: 1}},
-		},
-		{
-			name:  "memory not exported, one page",
-			input: &Module{MemorySection: &Memory{Min: 1, Cap: 1}},
+			name: "memory not exported, one page",
+			input: &Module{
+				MemorySection:           &Memory{Min: 1, Cap: 1},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+			},
 		},
 		{
 			name: "memory exported, different name",
 			input: &Module{
-				MemorySection: &Memory{Min: 1, Cap: 1},
-				ExportSection: []*Export{{Type: ExternTypeMemory, Name: "momory", Index: 0}},
+				MemorySection:           &Memory{Min: 1, Cap: 1},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+				ExportSection:           []*Export{{Type: ExternTypeMemory, Name: "momory", Index: 0}},
 			},
 		},
 		{
 			name: "memory exported, but zero length",
 			input: &Module{
-				MemorySection: &Memory{},
-				ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
+				MemorySection:           &Memory{},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+				ExportSection:           []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
 			},
 			expected: true,
 		},
 		{
 			name: "memory exported, one page",
 			input: &Module{
-				MemorySection: &Memory{Min: 1, Cap: 1},
-				ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
+				MemorySection:           &Memory{Min: 1, Cap: 1},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+				ExportSection:           []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
 			},
 			expected:    true,
 			expectedLen: 65536,
@@ -63,8 +65,9 @@ func TestModuleInstance_Memory(t *testing.T) {
 		{
 			name: "memory exported, two pages",
 			input: &Module{
-				MemorySection: &Memory{Min: 2, Cap: 2},
-				ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
+				MemorySection:           &Memory{Min: 2, Cap: 2},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+				ExportSection:           []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
 			},
 			expected:    true,
 			expectedLen: 65536 * 2,
@@ -151,11 +154,12 @@ func TestStore_CloseWithExitCode(t *testing.T) {
 			require.NoError(t, err)
 
 			m2, err := s.Instantiate(testCtx, ns, &Module{
-				TypeSection:   []*FunctionType{v_v},
-				ImportSection: []*Import{{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}},
-				MemorySection: &Memory{Min: 1, Cap: 1},
-				GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}},
-				TableSection:  []*Table{{Min: 10}},
+				TypeSection:             []*FunctionType{v_v},
+				ImportSection:           []*Import{{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}},
+				MemorySection:           &Memory{Min: 1, Cap: 1},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+				GlobalSection:           []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}},
+				TableSection:            []*Table{{Min: 10}},
 			}, importingModuleName, nil)
 			require.NoError(t, err)
 
@@ -191,10 +195,11 @@ func TestStore_hammer(t *testing.T) {
 	require.True(t, ok)
 
 	importingModule := &Module{
-		TypeSection:     []*FunctionType{v_v},
-		FunctionSection: []uint32{0},
-		CodeSection:     []*Code{{Body: []byte{OpcodeEnd}}},
-		MemorySection:   &Memory{Min: 1, Cap: 1},
+		TypeSection:             []*FunctionType{v_v},
+		FunctionSection:         []uint32{0},
+		CodeSection:             []*Code{{Body: []byte{OpcodeEnd}}},
+		MemorySection:           &Memory{Min: 1, Cap: 1},
+		MemoryDefinitionSection: []*MemoryDefinition{{}},
 		GlobalSection: []*Global{{
 			Type: &GlobalType{ValType: ValueTypeI32},
 			Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(1)},
diff --git a/runtime_test.go b/runtime_test.go
index 310cfc1c..8c749bb4 100644
--- a/runtime_test.go
+++ b/runtime_test.go
@@ -225,8 +225,13 @@ func TestModule_Memory(t *testing.T) {
 			mem := module.ExportedMemory("memory")
 			if tc.expected {
 				require.Equal(t, tc.expectedLen, mem.Size())
+				defs := module.ExportedMemoryDefinitions()
+				require.Equal(t, 1, len(defs))
+				def := defs["memory"]
+				require.Equal(t, tc.expectedLen>>16, def.Min())
 			} else {
 				require.Nil(t, mem)
+				require.Zero(t, len(module.ExportedMemoryDefinitions()))
 			}
 		})
 	}
@@ -525,7 +530,11 @@ func TestRuntime_CloseWithExitCode(t *testing.T) {
 			require.NoError(t, err)
 
 			func1 := m1.ExportedFunction("func")
+			require.Equal(t, map[string]api.FunctionDefinition{"func": func1.Definition()},
+				m1.ExportedFunctionDefinitions())
 			func2 := m2.ExportedFunction("func")
+			require.Equal(t, map[string]api.FunctionDefinition{"func": func2.Definition()},
+				m2.ExportedFunctionDefinitions())
 
 			// Modules not closed so calls succeed