Repository: tetratelabs/wazero
Pull request #1002 (2023-01-04 00:22:25)

Adds checks into the Makefile for 32-bit platforms
Developers don't commonly think about 32-bit platforms and in one instance we broke a release due to this (quickly fixed in pre.7). This ensures arm and 386, used by dapr and trivy respectively are tested. It also helps prove these are in use to avoid a later developer removing support by thinking they aren't.

Don't read into these results, as they are new releases. This is just an example that shows the output of the commands:
```bash
$ gh release view -R aquasecurity/trivy --json assets --jq 'first(.assets[] | select(.name| test("Linux-32bit.*tar.gz")) | {url, downloadCount})'
{"downloadCount":1,"url":"https://github.com/aquasecurity/trivy/releases/download/v0.36.0/trivy_0.36.0_Linux-32bit.tar.gz"
$ gh release view -R dapr/dapr --json assets --jq 'first(.assets[] | select(.name = "daprd_linux_arm.tar.gz") | {url, downloadCount})'
{"downloadCount":16,"url":"https://github.com/dapr/dapr/releases/download/v1.9.5/dapr-operator.yaml"}
```

apologies to @knqyf263 and @berndverst

codefromthecrypt: I think this check is valid for arm, but would love @berndverst to ack, just to make sure I didn't miss something. Since this runs on every PR, I'm hoping we can use it to trust contributors change wrt 32-bit compilation, without a lot of nervousness.
mathetake: why this passes without any change while still failing to build on arm/linux?
codefromthecrypt: long answer

pre.7 fixed platform code which didn't fall back to unsupported properly. The following is the fallback that's correct, basically the opposite of what compiler supports. so pre.7 already *should* work on arm, just this PR explicitly verifies it, at the cost of a longer `make check`

```go
//go:build !(amd64 || arm64) || !(darwin || linux || freebsd || windows)
```
codefromthecrypt: aside: we aren't currently publishing the `wazero` CLI on release, but doing so would also catch that.. at least building it would. I don't expect we want to release the CLI until the syntax stabilizes in the next month or two

(reminder 1.0 target slipped from end of feb to end of march, so that's when all api drift and cli drift should be done)

Patch:
diff --git a/Makefile b/Makefile
index db7b5359..7bf78841 100644
--- a/Makefile
+++ b/Makefile
@@ -210,10 +210,21 @@ format:
 	@go run $(gofumpt) -l -w .
 	@go run $(gosimports) -local github.com/tetratelabs/ -w $(shell find . -name '*.go' -type f)
 
-.PHONY: check
+.PHONY: check  # Pre-flight check for pull requests
 check:
-	@GOARCH=amd64 GOOS=dragonfly go build ./... # Check if the internal/platform can be built on compiler-unsupported platforms
-	@GOARCH=386 GOOS=linux go build ./... # Check if the internal/platform can be built on compiler-unsupported platforms
+# The following checks help ensure our platform-specific code used for system
+# calls safely falls back on a platform unsupported by the compiler engine.
+# This makes sure the intepreter can be used. Most often the package that can
+# drift here is "platform" or "syscallfs":
+#
+# Ensure we build on an arbitrary operating system
+	@GOARCH=amd64 GOOS=dragonfly go build ./...
+# Ensure we build on linux arm for Dapr:
+#	gh release view -R dapr/dapr --json assets --jq 'first(.assets[] | select(.name = "daprd_linux_arm.tar.gz") | {url, downloadCount})'
+	@GOARCH=arm GOOS=linux go build ./...
+# Ensure we build on linux 386 for Trivy:
+#	gh release view -R aquasecurity/trivy --json assets --jq 'first(.assets[] | select(.name| test("Linux-32bit.*tar.gz")) | {url, downloadCount})'
+	@GOARCH=386 GOOS=linux go build ./...
 	@$(MAKE) lint golangci_lint_goarch=arm64
 	@$(MAKE) lint golangci_lint_goarch=amd64
 	@$(MAKE) format
diff --git a/experimental/logging/log_listener.go b/experimental/logging/log_listener.go
index 304bfc8e..e8cf77ae 100644
--- a/experimental/logging/log_listener.go
+++ b/experimental/logging/log_listener.go
@@ -82,7 +82,7 @@ func (f *loggingListenerFactory) NewListener(fnd api.FunctionDefinition) experim
 		if f.fsOnly && !wasilogging.IsFilesystemFunction(fnd) {
 			return nil
 		}
-		pLoggers, rLoggers = wasilogging.Config(fnd)
+		pSampler, pLoggers, rLoggers = wasilogging.Config(fnd)
 	case "go":
 		// TODO: Now, gojs logging is filesystem only, but will need to be
 		// updated later.
@@ -94,7 +94,7 @@ func (f *loggingListenerFactory) NewListener(fnd api.FunctionDefinition) experim
 		if f.fsOnly {
 			return nil
 		}
-		pLoggers, rLoggers = logging.ValueLoggers(fnd)
+		pLoggers, rLoggers = logging.Config(fnd)
 	}
 
 	var before, after string
diff --git a/imports/assemblyscript/assemblyscript.go b/imports/assemblyscript/assemblyscript.go
index 554750b1..4e2833ec 100644
--- a/imports/assemblyscript/assemblyscript.go
+++ b/imports/assemblyscript/assemblyscript.go
@@ -165,8 +165,8 @@ func abortWithMessage(ctx context.Context, mod api.Module, stack []uint64) {
 	columnNumber := uint32(stack[3])
 
 	// Don't panic if there was a problem reading the message
-	stderr := fsc.FdWriter(internalsys.FdStderr)
-	if msg, msgOk := readAssemblyScriptString(mem, message); msgOk {
+	stderr := internalsys.WriterForFile(fsc, internalsys.FdStderr)
+	if msg, msgOk := readAssemblyScriptString(mem, message); msgOk && stderr != nil {
 		if fn, fnOk := readAssemblyScriptString(mem, fileName); fnOk {
 			_, _ = fmt.Fprintf(stderr, "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber)
 		}
@@ -200,8 +200,7 @@ var traceStdout = &wasm.HostFunc{
 		IsHostFunction: true,
 		GoFunc: api.GoModuleFunc(func(_ context.Context, mod api.Module, stack []uint64) {
 			fsc := mod.(*wasm.CallContext).Sys.FS()
-			stdout := fsc.FdWriter(internalsys.FdStdout)
-			traceTo(mod, stack, stdout)
+			traceTo(mod, stack, internalsys.WriterForFile(fsc, internalsys.FdStdout))
 		}),
 	},
 }
@@ -209,8 +208,7 @@ var traceStdout = &wasm.HostFunc{
 // traceStderr implements trace to the configured Stderr.
 var traceStderr = traceStdout.WithGoModuleFunc(func(_ context.Context, mod api.Module, stack []uint64) {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
-	stderr := fsc.FdWriter(internalsys.FdStderr)
-	traceTo(mod, stack, stderr)
+	traceTo(mod, stack, internalsys.WriterForFile(fsc, internalsys.FdStderr))
 })
 
 // traceTo implements the function "trace" in AssemblyScript. e.g.
@@ -224,6 +222,9 @@ var traceStderr = traceStdout.WithGoModuleFunc(func(_ context.Context, mod api.M
 //
 // See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L61
 func traceTo(mod api.Module, params []uint64, writer io.Writer) {
+	if writer == nil {
+		return // closed
+	}
 	message := uint32(params[0])
 	nArgs := uint32(params[1])
 	arg0 := api.DecodeF64(params[2])
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index 50fa306f..2bd28431 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -12,6 +12,7 @@ import (
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/platform"
 	"github.com/tetratelabs/wazero/internal/sys"
+	"github.com/tetratelabs/wazero/internal/syscallfs"
 	. "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -57,8 +58,8 @@ func fdCloseFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 	fd := uint32(params[0])
 
-	if ok := fsc.CloseFile(fd); !ok {
-		return ErrnoBadf
+	if err := fsc.CloseFile(fd); err != nil {
+		return ToErrno(err)
 	}
 	return ErrnoSuccess
 }
@@ -121,20 +122,19 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) Errno {
 		return ErrnoFault
 	}
 
-	stat, err := fsc.StatFile(fd)
-	if err != nil {
-		return ToErrno(err)
-	}
-
-	filetype := getWasiFiletype(stat.Mode())
 	var fdflags uint16
-
-	// Determine if it is writeable
-	if w := fsc.FdWriter(fd); w != nil {
+	var stat fs.FileInfo
+	var err error
+	if f, ok := fsc.LookupFile(fd); !ok {
+		return ErrnoBadf
+	} else if stat, err = f.File.Stat(); err != nil {
+		return ToErrno(err)
+	} else if _, ok := f.File.(io.Writer); ok {
 		// TODO: maybe cache flags to open instead
 		fdflags = FD_APPEND
 	}
 
+	filetype := getWasiFiletype(stat.Mode())
 	writeFdstat(buf, filetype, fdflags)
 
 	return ErrnoSuccess
@@ -233,7 +233,7 @@ func fdFilestatGetFunc(mod api.Module, fd, resultBuf uint32) Errno {
 		return ErrnoFault
 	}
 
-	stat, err := fsc.StatFile(fd)
+	stat, err := sys.StatFile(fsc, fd)
 	if err != nil {
 		return ToErrno(err)
 	}
@@ -358,14 +358,16 @@ func fdPrestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno {
 		return ErrnoBadf
 	}
 
-	entry, ok := fsc.OpenedFile(fd)
-	if !ok {
-		return ErrnoBadf
+	var name string
+	if f, ok := fsc.LookupFile(fd); !ok {
+		return ErrnoBadf // closed
+	} else {
+		name = f.Name
 	}
 
 	// Upper 32-bits are zero because...
 	// * Zero-value 8-bit tag, and 3-byte zero-value padding
-	prestat := uint64(len(entry.Name) << 32)
+	prestat := uint64(len(name) << 32)
 	if !mod.Memory().WriteUint64Le(resultPrestat, prestat) {
 		return ErrnoFault
 	}
@@ -417,17 +419,19 @@ func fdPrestatDirNameFn(_ context.Context, mod api.Module, params []uint64) Errn
 		return ErrnoBadf
 	}
 
-	f, ok := fsc.OpenedFile(fd)
-	if !ok {
-		return ErrnoBadf
+	var name string
+	if f, ok := fsc.LookupFile(fd); !ok {
+		return ErrnoBadf // closed
+	} else {
+		name = f.Name
 	}
 
 	// Some runtimes may have another semantics. See /RATIONALE.md
-	if uint32(len(f.Name)) < pathLen {
+	if uint32(len(name)) < pathLen {
 		return ErrnoNametoolong
 	}
 
-	if !mod.Memory().Write(path, []byte(f.Name)[:pathLen]) {
+	if !mod.Memory().Write(path, []byte(name)[:pathLen]) {
 		return ErrnoFault
 	}
 	return ErrnoSuccess
@@ -519,21 +523,21 @@ func fdReadOrPread(mod api.Module, params []uint64, isPread bool) Errno {
 		resultNread = uint32(params[3])
 	}
 
-	r := fsc.FdReader(fd)
-	if r == nil {
+	r, ok := fsc.LookupFile(fd)
+	if !ok {
 		return ErrnoBadf
 	}
 
-	read := r.Read
+	read := r.File.Read
 	if isPread {
-		if ra, ok := r.(io.ReaderAt); ok {
+		if ra, ok := r.File.(io.ReaderAt); ok {
 			// ReadAt is the Go equivalent to pread.
 			read = func(p []byte) (int, error) {
 				n, err := ra.ReadAt(p, offset)
 				offset += int64(n)
 				return n, err
 			}
-		} else if s, ok := r.(io.Seeker); ok {
+		} else if s, ok := r.File.(io.Seeker); ok {
 			// Unfortunately, it is often not supported.
 			// See /RATIONALE.md "fd_pread: io.Seeker fallback when io.ReaderAt is not supported"
 			initialOffset, err := s.Seek(0, io.SeekCurrent)
@@ -853,7 +857,7 @@ func writeDirent(buf []byte, dNext uint64, dNamlen uint32, dType bool) {
 
 // openedDir returns the directory and ErrnoSuccess if the fd points to a readable directory.
 func openedDir(fsc *sys.FSContext, fd uint32) (fs.ReadDirFile, *sys.ReadDir, Errno) {
-	if f, ok := fsc.OpenedFile(fd); !ok {
+	if f, ok := fsc.LookupFile(fd); !ok {
 		return nil, nil, ErrnoBadf
 	} else if d, ok := f.File.(fs.ReadDirFile); !ok {
 		// fd_readdir docs don't indicate whether to return ErrnoNotdir or
@@ -933,7 +937,7 @@ func fdSeekFn(_ context.Context, mod api.Module, params []uint64) Errno {
 
 	var seeker io.Seeker
 	// Check to see if the file descriptor is available
-	if f, ok := fsc.OpenedFile(fd); !ok {
+	if f, ok := fsc.LookupFile(fd); !ok {
 		return ErrnoBadf
 		// fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it.
 	} else if seeker, ok = f.File.(io.Seeker); !ok {
@@ -1040,7 +1044,7 @@ func fdWriteFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	iovsCount := uint32(params[2])
 	resultNwritten := uint32(params[3])
 
-	writer := fsc.FdWriter(fd)
+	writer := sys.WriterForFile(fsc, fd)
 	if writer == nil {
 		return ErrnoBadf
 	}
@@ -1118,10 +1122,8 @@ func pathCreateDirectoryFn(_ context.Context, mod api.Module, params []uint64) E
 		return errno
 	}
 
-	if fd, err := fsc.Mkdir(pathName, 0o700); err != nil {
+	if err := fsc.FS().Mkdir(pathName, 0o700); err != nil {
 		return ToErrno(err)
-	} else {
-		_ = fsc.CloseFile(fd)
 	}
 
 	return ErrnoSuccess
@@ -1183,20 +1185,23 @@ func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno
 	}
 	pathName := string(b)
 
-	// Prepend the path if necessary.
-	if dir, ok := fsc.OpenedFile(dirfd); !ok {
-		return ErrnoBadf
-	} else if _, ok := dir.File.(fs.ReadDirFile); !ok {
-		return ErrnoNotdir // TODO: cache filetype instead of poking.
-	} else {
+	// os.File implements ReadDirFile, so we have to check with stat.
+	stat, err := sys.StatFile(fsc, dirfd)
+	if err != nil {
+		return ToErrno(err)
+	}
+
+	if !stat.IsDir() {
+		return ErrnoNotdir
+	} else { // prepend the name
 		// TODO: consolidate "at" logic with path_open as same issues occur.
-		pathName = path.Join(dir.Name, pathName)
+		pathName = path.Join(stat.Name(), pathName)
 	}
 
 	// Stat the file without allocating a file descriptor
-	stat, errnoResult := statFile(fsc, pathName)
-	if errnoResult != ErrnoSuccess {
-		return errnoResult
+	stat, err = syscallfs.StatPath(fsc.FS(), pathName)
+	if err != nil {
+		return ToErrno(err)
 	}
 
 	// Write the stat result to memory
@@ -1315,22 +1320,20 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
 
 	fileOpenFlags, isDir := openFlags(oflags, fdflags)
 
-	var newFD uint32
-	var err error
 	if isDir && oflags&O_CREAT != 0 {
 		return ErrnoInval // use pathCreateDirectory!
-	} else {
-		newFD, err = fsc.OpenFile(pathName, fileOpenFlags, 0o600)
 	}
 
+	newFD, err := fsc.OpenFile(pathName, fileOpenFlags, 0o600)
 	if err != nil {
 		return ToErrno(err)
 	}
 
 	// Check any flags that require the file to evaluate.
 	if isDir {
-		if errno := failIfNotDirectory(fsc, newFD); errno != ErrnoSuccess {
-			return errno
+		if stat, err := sys.StatFile(fsc, newFD); err != nil || !stat.IsDir() {
+			_ = fsc.CloseFile(newFD)
+			return ErrnoNotdir
 		}
 	}
 
@@ -1358,8 +1361,8 @@ func atPath(fsc *sys.FSContext, mem api.Memory, dirFd, path, pathLen uint32) (st
 		// "/tmp/foo/bar" not "/bar".
 	}
 
-	if _, ok := fsc.OpenedFile(dirFd); !ok {
-		return "", ErrnoBadf
+	if _, ok := fsc.LookupFile(dirFd); !ok {
+		return "", ErrnoBadf // closed
 	}
 
 	b, ok := mem.Read(path, pathLen)
@@ -1392,17 +1395,6 @@ func openFlags(oflags, fdflags uint16) (openFlags int, isDir bool) {
 	return
 }
 
-func failIfNotDirectory(fsc *sys.FSContext, fd uint32) Errno {
-	// Lookup the previous file
-	if f, ok := fsc.OpenedFile(fd); !ok {
-		return ErrnoBadf
-	} else if _, ok := f.File.(fs.ReadDirFile); !ok {
-		_ = fsc.CloseFile(fd)
-		return ErrnoNotdir
-	}
-	return ErrnoSuccess
-}
-
 // pathReadlink is the WASI function named PathReadlinkName that reads the
 // contents of a symbolic link.
 //
@@ -1453,7 +1445,7 @@ func pathRemoveDirectoryFn(_ context.Context, mod api.Module, params []uint64) E
 		return errno
 	}
 
-	if err := fsc.Rmdir(pathName); err != nil {
+	if err := fsc.FS().Rmdir(pathName); err != nil {
 		return ToErrno(err)
 	}
 
@@ -1512,7 +1504,7 @@ func pathRenameFn(_ context.Context, mod api.Module, params []uint64) Errno {
 		return errno
 	}
 
-	if err := fsc.Rename(oldPathName, newPathName); err != nil {
+	if err := fsc.FS().Rename(oldPathName, newPathName); err != nil {
 		return ToErrno(err)
 	}
 
@@ -1568,20 +1560,9 @@ func pathUnlinkFileFn(_ context.Context, mod api.Module, params []uint64) Errno
 		return errno
 	}
 
-	if err := fsc.Unlink(pathName); err != nil {
+	if err := fsc.FS().Unlink(pathName); err != nil {
 		return ToErrno(err)
 	}
 
 	return ErrnoSuccess
 }
-
-// statFile attempts to stat the file at the given path. Errors coerce to WASI
-// Errno.
-func statFile(fsc *sys.FSContext, name string) (stat fs.FileInfo, errno Errno) {
-	var err error
-	stat, err = fsc.StatPath(name)
-	if err != nil {
-		errno = ToErrno(err)
-	}
-	return
-}
diff --git a/imports/wasi_snapshot_preview1/poll.go b/imports/wasi_snapshot_preview1/poll.go
index 367c517d..e3abb924 100644
--- a/imports/wasi_snapshot_preview1/poll.go
+++ b/imports/wasi_snapshot_preview1/poll.go
@@ -4,6 +4,7 @@ import (
 	"context"
 
 	"github.com/tetratelabs/wazero/api"
+	internalsys "github.com/tetratelabs/wazero/internal/sys"
 	. "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -132,9 +133,11 @@ func processFDEvent(mod api.Module, eventType byte, inBuf []byte) Errno {
 	// Choose the best error, which falls back to unsupported, until we support
 	// files.
 	errno := ErrnoNotsup
-	if eventType == EventTypeFdRead && fsc.FdReader(fd) == nil {
-		errno = ErrnoBadf
-	} else if eventType == EventTypeFdWrite && fsc.FdWriter(fd) == nil {
+	if eventType == EventTypeFdRead {
+		if _, ok := fsc.LookupFile(fd); !ok {
+			errno = ErrnoBadf
+		}
+	} else if eventType == EventTypeFdWrite && internalsys.WriterForFile(fsc, fd) == nil {
 		errno = ErrnoBadf
 	}
 
diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go
index 80b34b31..ce2200f4 100644
--- a/internal/engine/interpreter/interpreter.go
+++ b/internal/engine/interpreter/interpreter.go
@@ -75,7 +75,7 @@ type moduleEngine struct {
 
 	// codes are the compiled functions in a module instances.
 	// The index is module instance-scoped.
-	functions []*function
+	functions []function
 
 	// parentEngine holds *engine from which this module engine is created from.
 	parentEngine *engine
@@ -177,8 +177,6 @@ type code struct {
 
 type function struct {
 	source *wasm.FunctionInstance
-	body   []*interpreterOp
-	hostFn interface{}
 	parent *code
 }
 
@@ -194,15 +192,6 @@ func functionFromUintptr(ptr uintptr) *function {
 	return *(**function)(unsafe.Pointer(wrapped))
 }
 
-func (c *code) instantiate(f *wasm.FunctionInstance) *function {
-	return &function{
-		source: f,
-		body:   c.body,
-		hostFn: c.hostFn,
-		parent: c,
-	}
-}
-
 // interpreterOp is the compilation (engine.lowerIR) result of a wazeroir.Operation.
 //
 // Not all operations result in an interpreterOp, e.g. wazeroir.OperationI32ReinterpretFromF32, and some operations are
@@ -266,7 +255,7 @@ func (e *engine) NewModuleEngine(name string, module *wasm.Module, functions []w
 	me := &moduleEngine{
 		name:         name,
 		parentEngine: e,
-		functions:    make([]*function, len(functions)),
+		functions:    make([]function, len(functions)),
 	}
 
 	imported := int(module.ImportFuncCount())
@@ -283,8 +272,7 @@ func (e *engine) NewModuleEngine(name string, module *wasm.Module, functions []w
 	for i, c := range codes {
 		offset := i + imported
 		f := &functions[offset]
-		inst := c.instantiate(f)
-		me.functions[offset] = inst
+		me.functions[offset] = function{source: f, parent: c}
 	}
 	return me, nil
 }
@@ -743,7 +731,7 @@ func (e *moduleEngine) CreateFuncElementInstance(indexes []*wasm.Index) *wasm.El
 	refs := make([]wasm.Reference, len(indexes))
 	for i, index := range indexes {
 		if index != nil {
-			refs[i] = uintptr(unsafe.Pointer(e.functions[*index]))
+			refs[i] = uintptr(unsafe.Pointer(&e.functions[*index]))
 		}
 	}
 	return &wasm.ElementInstance{
@@ -754,20 +742,14 @@ func (e *moduleEngine) CreateFuncElementInstance(indexes []*wasm.Index) *wasm.El
 
 // FunctionInstanceReference implements the same method as documented on wasm.ModuleEngine.
 func (e *moduleEngine) FunctionInstanceReference(funcIndex wasm.Index) wasm.Reference {
-	return uintptr(unsafe.Pointer(e.functions[funcIndex]))
+	return uintptr(unsafe.Pointer(&e.functions[funcIndex]))
 }
 
 // NewCallEngine implements the same method as documented on wasm.ModuleEngine.
-func (e *moduleEngine) NewCallEngine(callCtx *wasm.CallContext, f *wasm.FunctionInstance) (ce wasm.CallEngine, err error) {
+func (e *moduleEngine) NewCallEngine(_ *wasm.CallContext, f *wasm.FunctionInstance) (ce wasm.CallEngine, err error) {
 	// Note: The input parameters are pre-validated, so a compiled function is only absent on close. Updates to
 	// code on close aren't locked, neither is this read.
-	compiled := e.functions[f.Idx]
-	if compiled == nil { // Lazy check the cause as it could be because the module was already closed.
-		if err = callCtx.FailIfClosed(); err == nil {
-			panic(fmt.Errorf("BUG: %s.func[%d] was nil before close", e.name, f.Idx))
-		}
-		return
-	}
+	compiled := &e.functions[f.Idx]
 	return e.newCallEngine(f, compiled), nil
 }
 
@@ -841,8 +823,8 @@ func (ce *callEngine) recoverOnCall(v interface{}) (err error) {
 		frame := ce.popFrame()
 		def := frame.f.source.Definition
 		var sources []string
-		if frame.f.body != nil {
-			sources = frame.f.parent.source.DWARFLines.Line(frame.f.body[frame.pc].sourcePC)
+		if body := frame.f.parent.body; body != nil {
+			sources = frame.f.parent.source.DWARFLines.Line(body[frame.pc].sourcePC)
 		}
 		builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes(), sources)
 	}
@@ -854,7 +836,7 @@ func (ce *callEngine) recoverOnCall(v interface{}) (err error) {
 }
 
 func (ce *callEngine) callFunction(ctx context.Context, callCtx *wasm.CallContext, f *function) {
-	if f.hostFn != nil {
+	if f.parent.hostFn != nil {
 		ce.callGoFuncWithStack(ctx, callCtx, f)
 	} else if lsn := f.parent.listener; lsn != nil {
 		ce.callNativeFuncWithListener(ctx, callCtx, f, lsn)
@@ -905,9 +887,10 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, callCtx *wasm.CallCont
 	dataInstances := f.source.Module.DataInstances
 	elementInstances := f.source.Module.ElementInstances
 	ce.pushFrame(frame)
-	bodyLen := uint64(len(frame.f.body))
+	body := frame.f.parent.body
+	bodyLen := uint64(len(body))
 	for frame.pc < bodyLen {
-		op := frame.f.body[frame.pc]
+		op := body[frame.pc]
 		// TODO: add description of each operation/case
 		// on, for example, how many args are used,
 		// how the stack is modified, etc.
@@ -934,7 +917,7 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, callCtx *wasm.CallCont
 				frame.pc = op.us[0]
 			}
 		case wazeroir.OperationKindCall:
-			ce.callFunction(ctx, callCtx, functions[op.us[0]])
+			ce.callFunction(ctx, callCtx, &functions[op.us[0]])
 			frame.pc++
 		case wazeroir.OperationKindCallIndirect:
 			offset := ce.popValue()
@@ -1940,7 +1923,7 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, callCtx *wasm.CallCont
 			}
 			frame.pc++
 		case wazeroir.OperationKindRefFunc:
-			ce.pushValue(uint64(uintptr(unsafe.Pointer(functions[op.us[0]]))))
+			ce.pushValue(uint64(uintptr(unsafe.Pointer(&functions[op.us[0]]))))
 			frame.pc++
 		case wazeroir.OperationKindTableGet:
 			table := tables[op.us[0]]
diff --git a/internal/gojs/builtin.go b/internal/gojs/builtin.go
index ae683fd5..a19e3845 100644
--- a/internal/gojs/builtin.go
+++ b/internal/gojs/builtin.go
@@ -8,7 +8,7 @@ import (
 
 // newJsGlobal = js.Global() // js.go init
 func newJsGlobal(rt http.RoundTripper) *jsVal {
-	var fetchProperty interface{} = undefined
+	var fetchProperty interface{} = goos.Undefined
 	if rt != nil {
 		fetchProperty = goos.RefHttpFetch
 	}
@@ -19,7 +19,7 @@ func newJsGlobal(rt http.RoundTripper) *jsVal {
 			"crypto":          jsCrypto,
 			"Uint8Array":      uint8ArrayConstructor,
 			"fetch":           fetchProperty,
-			"AbortController": undefined,
+			"AbortController": goos.Undefined,
 			"Headers":         headersConstructor,
 			"process":         jsProcess,
 			"fs":              jsfs,
diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go
index 795e199a..ac701e56 100644
--- a/internal/gojs/fs.go
+++ b/internal/gojs/fs.go
@@ -13,6 +13,7 @@ import (
 	"github.com/tetratelabs/wazero/internal/gojs/goos"
 	"github.com/tetratelabs/wazero/internal/platform"
 	internalsys "github.com/tetratelabs/wazero/internal/sys"
+	"github.com/tetratelabs/wazero/internal/syscallfs"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -89,10 +90,11 @@ type jsfsOpen struct{}
 func (jsfsOpen) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	path := args[0].(string)
 	flags := toUint64(args[1]) // flags are derived from constants like oWRONLY
-	perm := toUint32(args[2])
+	perm := goos.ValueToUint32(args[2])
 	callback := args[3].(funcWrapper)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
+
 	fd, err := fsc.OpenFile(path, int(flags), fs.FileMode(perm))
 
 	return callback.invoke(ctx, mod, goos.RefJsfs, err, fd) // note: error first
@@ -112,13 +114,13 @@ func (jsfsStat) invoke(ctx context.Context, mod api.Module, args ...interface{})
 }
 
 // syscallStat is like syscall.Stat
-func syscallStat(mod api.Module, name string) (*jsSt, error) {
+func syscallStat(mod api.Module, path string) (*jsSt, error) {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
-	if fd, err := fsc.OpenFile(name, os.O_RDONLY, 0); err != nil {
+
+	if stat, err := syscallfs.StatPath(fsc.FS(), path); err != nil {
 		return nil, err
 	} else {
-		defer fsc.CloseFile(fd)
-		return syscallFstat(fsc, fd)
+		return newJsSt(stat), nil
 	}
 }
 
@@ -144,7 +146,7 @@ type jsfsFstat struct{}
 func (jsfsFstat) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	fd := toUint32(args[0])
+	fd := goos.ValueToUint32(args[0])
 	callback := args[1].(funcWrapper)
 
 	fstat, err := syscallFstat(fsc, fd)
@@ -168,21 +170,23 @@ const (
 
 // syscallFstat is like syscall.Fstat
 func syscallFstat(fsc *internalsys.FSContext, fd uint32) (*jsSt, error) {
-	if f, ok := fsc.OpenedFile(fd); !ok {
-		return nil, syscall.EBADF
-	} else if stat, err := f.File.Stat(); err != nil {
+	stat, err := internalsys.StatFile(fsc, fd)
+	if err != nil {
 		return nil, err
-	} else {
-		ret := &jsSt{}
-		ret.isDir = stat.IsDir()
-		ret.mode = getJsMode(stat.Mode())
-		ret.size = stat.Size()
-		atimeNsec, mtimeNsec, ctimeNsec := platform.StatTimes(stat)
-		ret.atimeMs = atimeNsec / 1e6
-		ret.mtimeMs = mtimeNsec / 1e6
-		ret.ctimeMs = ctimeNsec / 1e6
-		return ret, nil
 	}
+	return newJsSt(stat), nil
+}
+
+func newJsSt(stat fs.FileInfo) *jsSt {
+	ret := &jsSt{}
+	ret.isDir = stat.IsDir()
+	ret.mode = getJsMode(stat.Mode())
+	ret.size = stat.Size()
+	atimeNsec, mtimeNsec, ctimeNsec := platform.StatTimes(stat)
+	ret.atimeMs = atimeNsec / 1e6
+	ret.mtimeMs = mtimeNsec / 1e6
+	ret.ctimeMs = ctimeNsec / 1e6
+	return ret
 }
 
 // getJsMode is required because the mode property read in `GOOS=js` is
@@ -227,13 +231,10 @@ type jsfsClose struct{}
 func (jsfsClose) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	fd := toUint32(args[0])
+	fd := goos.ValueToUint32(args[0])
 	callback := args[1].(funcWrapper)
 
-	var err error
-	if ok := fsc.CloseFile(fd); !ok {
-		err = syscall.EBADF // already closed
-	}
+	err := fsc.CloseFile(fd)
 
 	return jsfsInvoke(ctx, mod, callback, err)
 }
@@ -245,13 +246,13 @@ func (jsfsClose) invoke(ctx context.Context, mod api.Module, args ...interface{}
 type jsfsRead struct{}
 
 func (jsfsRead) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := toUint32(args[0])
+	fd := goos.ValueToUint32(args[0])
 	buf, ok := args[1].(*byteArray)
 	if !ok {
 		return nil, fmt.Errorf("arg[1] is %v not a []byte", args[1])
 	}
-	offset := toUint32(args[2])
-	byteCount := toUint32(args[3])
+	offset := goos.ValueToUint32(args[2])
+	byteCount := goos.ValueToUint32(args[3])
 	fOffset := args[4] // nil unless Pread
 	callback := args[5].(funcWrapper)
 
@@ -263,13 +264,13 @@ func (jsfsRead) invoke(ctx context.Context, mod api.Module, args ...interface{})
 func syscallRead(mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	r := fsc.FdReader(fd)
-	if r == nil {
+	f, ok := fsc.LookupFile(fd)
+	if !ok {
 		err = syscall.EBADF
 	}
 
 	if offset != nil {
-		if s, ok := r.(io.Seeker); ok {
+		if s, ok := f.File.(io.Seeker); ok {
 			if _, err := s.Seek(toInt64(offset), io.SeekStart); err != nil {
 				return 0, err
 			}
@@ -278,7 +279,7 @@ func syscallRead(mod api.Module, fd uint32, offset interface{}, p []byte) (n uin
 		}
 	}
 
-	if nRead, e := r.Read(p); e == nil || e == io.EOF {
+	if nRead, e := f.File.Read(p); e == nil || e == io.EOF {
 		// fs_js.go cannot parse io.EOF so coerce it to nil.
 		// See https://github.com/golang/go/issues/43913
 		n = uint32(nRead)
@@ -296,13 +297,13 @@ func syscallRead(mod api.Module, fd uint32, offset interface{}, p []byte) (n uin
 type jsfsWrite struct{}
 
 func (jsfsWrite) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := toUint32(args[0])
+	fd := goos.ValueToUint32(args[0])
 	buf, ok := args[1].(*byteArray)
 	if !ok {
 		return nil, fmt.Errorf("arg[1] is %v not a []byte", args[1])
 	}
-	offset := toUint32(args[2])
-	byteCount := toUint32(args[3])
+	offset := goos.ValueToUint32(args[2])
+	byteCount := goos.ValueToUint32(args[3])
 	fOffset := args[4] // nil unless Pread
 	callback := args[5].(funcWrapper)
 
@@ -317,7 +318,7 @@ func (jsfsWrite) invoke(ctx context.Context, mod api.Module, args ...interface{}
 func syscallWrite(mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	if writer := fsc.FdWriter(fd); writer == nil {
+	if writer := internalsys.WriterForFile(fsc, fd); writer == nil {
 		err = syscall.EBADF
 	} else if nWritten, e := writer.Write(p); e == nil || e == io.EOF {
 		// fs_js.go cannot parse io.EOF so coerce it to nil.
@@ -346,15 +347,14 @@ func (jsfsReaddir) invoke(ctx context.Context, mod api.Module, args ...interface
 func syscallReaddir(_ context.Context, mod api.Module, name string) (*objectArray, error) {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	fd, err := fsc.OpenFile(name, os.O_RDONLY, 0)
+	// don't allocate a file descriptor
+	f, err := fsc.FS().OpenFile(name, os.O_RDONLY, 0)
 	if err != nil {
 		return nil, err
 	}
-	defer fsc.CloseFile(fd)
+	defer f.Close() //nolint
 
-	if f, ok := fsc.OpenedFile(fd); !ok {
-		return nil, syscall.EBADF
-	} else if d, ok := f.File.(fs.ReadDirFile); !ok {
+	if d, ok := f.(fs.ReadDirFile); !ok {
 		return nil, syscall.ENOTDIR
 	} else if l, err := d.ReadDir(-1); err != nil {
 		return nil, err
@@ -399,20 +399,11 @@ func (processCwd) invoke(ctx context.Context, _ api.Module, _ ...interface{}) (i
 type processChdir struct{}
 
 func (processChdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fsc := mod.(*wasm.CallContext).Sys.FS()
-
 	path := args[0].(string)
 
-	// TODO: refactor so that sys has path-based ops, also needed in WASI.
-	if fd, err := fsc.OpenFile(path, os.O_RDONLY, 0); err != nil {
-		return nil, syscall.ENOENT
-	} else if f, ok := fsc.OpenedFile(fd); !ok {
-		return nil, syscall.ENOENT
-	} else if s, err := f.File.Stat(); err != nil {
-		fsc.CloseFile(fd)
-		return nil, syscall.ENOENT
-	} else if !s.IsDir() {
-		fsc.CloseFile(fd)
+	if s, err := syscallStat(mod, path); err != nil {
+		return nil, mapJSError(err)
+	} else if !s.isDir {
 		return nil, syscall.ENOTDIR
 	} else {
 		getState(ctx).cwd = path
@@ -427,11 +418,16 @@ type jsfsMkdir struct{}
 
 func (jsfsMkdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	path := args[0].(string)
-	perm := toUint32(args[1])
+	perm := goos.ValueToUint32(args[1])
 	callback := args[2].(funcWrapper)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
-	fd, err := fsc.Mkdir(path, fs.FileMode(perm))
+
+	var fd uint32
+	var err error
+	if err = fsc.FS().Mkdir(path, fs.FileMode(perm)); err == nil {
+		fd, err = fsc.OpenFile(path, os.O_RDONLY, 0)
+	}
 
 	return callback.invoke(ctx, mod, goos.RefJsfs, err, fd) // note: error first
 }
@@ -446,7 +442,7 @@ func (jsfsRmdir) invoke(ctx context.Context, mod api.Module, args ...interface{}
 	callback := args[1].(funcWrapper)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
-	err := fsc.Rmdir(path)
+	err := fsc.FS().Rmdir(path)
 
 	return jsfsInvoke(ctx, mod, callback, err)
 }
@@ -462,7 +458,7 @@ func (jsfsRename) invoke(ctx context.Context, mod api.Module, args ...interface{
 	callback := args[2].(funcWrapper)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
-	err := fsc.Rename(from, to)
+	err := fsc.FS().Rename(from, to)
 
 	return jsfsInvoke(ctx, mod, callback, err)
 }
@@ -477,7 +473,7 @@ func (jsfsUnlink) invoke(ctx context.Context, mod api.Module, args ...interface{
 	callback := args[1].(funcWrapper)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
-	err := fsc.Unlink(path)
+	err := fsc.FS().Unlink(path)
 
 	return jsfsInvoke(ctx, mod, callback, err)
 }
@@ -494,7 +490,7 @@ func (jsfsUtimes) invoke(ctx context.Context, mod api.Module, args ...interface{
 	callback := args[3].(funcWrapper)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
-	err := fsc.Utimes(path, atimeSec*1e9, mtimeSec*1e9)
+	err := fsc.FS().Utimes(path, atimeSec*1e9, mtimeSec*1e9)
 
 	return jsfsInvoke(ctx, mod, callback, err)
 }
@@ -506,7 +502,7 @@ type jsfsChmod struct{}
 
 func (jsfsChmod) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	path := args[0].(string)
-	mode := toUint32(args[1])
+	mode := goos.ValueToUint32(args[1])
 	callback := args[2].(funcWrapper)
 
 	_, _ = path, mode // TODO
@@ -521,8 +517,8 @@ func (jsfsChmod) invoke(ctx context.Context, mod api.Module, args ...interface{}
 type jsfsFchmod struct{}
 
 func (jsfsFchmod) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := toUint32(args[0])
-	mode := toUint32(args[1])
+	fd := goos.ValueToUint32(args[0])
+	mode := goos.ValueToUint32(args[1])
 	callback := args[2].(funcWrapper)
 
 	_, _ = fd, mode // TODO
@@ -538,8 +534,8 @@ type jsfsChown struct{}
 
 func (jsfsChown) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	path := args[0].(string)
-	uid := toUint32(args[1])
-	gid := toUint32(args[2])
+	uid := goos.ValueToUint32(args[1])
+	gid := goos.ValueToUint32(args[2])
 	callback := args[3].(funcWrapper)
 
 	_, _, _ = path, uid, gid // TODO
@@ -554,9 +550,9 @@ func (jsfsChown) invoke(ctx context.Context, mod api.Module, args ...interface{}
 type jsfsFchown struct{}
 
 func (jsfsFchown) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := toUint32(args[0])
-	uid := toUint32(args[1])
-	gid := toUint32(args[2])
+	fd := goos.ValueToUint32(args[0])
+	uid := goos.ValueToUint32(args[1])
+	gid := goos.ValueToUint32(args[2])
 	callback := args[3].(funcWrapper)
 
 	_, _, _ = fd, uid, gid // TODO
@@ -572,8 +568,8 @@ type jsfsLchown struct{}
 
 func (jsfsLchown) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	path := args[0].(string)
-	uid := toUint32(args[1])
-	gid := toUint32(args[2])
+	uid := goos.ValueToUint32(args[1])
+	gid := goos.ValueToUint32(args[2])
 	callback := args[3].(funcWrapper)
 
 	_, _, _ = path, uid, gid // TODO
@@ -604,7 +600,7 @@ func (jsfsTruncate) invoke(ctx context.Context, mod api.Module, args ...interfac
 type jsfsFtruncate struct{}
 
 func (jsfsFtruncate) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := toUint32(args[0])
+	fd := goos.ValueToUint32(args[0])
 	length := toInt64(args[1])
 	callback := args[2].(funcWrapper)
 
@@ -668,7 +664,7 @@ func (jsfsSymlink) invoke(ctx context.Context, mod api.Module, args ...interface
 type jsfsFsync struct{}
 
 func (jsfsFsync) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := toUint32(args[0])
+	fd := goos.ValueToUint32(args[0])
 	callback := args[1].(funcWrapper)
 
 	_ = fd // TODO
diff --git a/internal/gojs/goos/goos.go b/internal/gojs/goos/goos.go
index 849af5f6..1adc7f11 100644
--- a/internal/gojs/goos/goos.go
+++ b/internal/gojs/goos/goos.go
@@ -251,3 +251,14 @@ func (f *stackFunc) Call(ctx context.Context, mod api.Module, wasmStack []uint64
 func NewStack(name string, mem api.Memory, sp uint32) *stack {
 	return &stack{goarch.NewStack(name, mem, sp)}
 }
+
+var Undefined = struct{ name string }{name: "undefined"}
+
+func ValueToUint32(arg interface{}) uint32 {
+	if arg == RefValueZero || arg == Undefined {
+		return 0
+	} else if u, ok := arg.(uint32); ok {
+		return u
+	}
+	return uint32(arg.(float64))
+}
diff --git a/internal/gojs/http.go b/internal/gojs/http.go
index 3a835651..3c5d65cd 100644
--- a/internal/gojs/http.go
+++ b/internal/gojs/http.go
@@ -89,7 +89,7 @@ func (s *fetchResult) get(_ context.Context, propertyKey string) interface{} {
 		return h
 	case "body":
 		// return undefined as arrayPromise is more complicated than an array.
-		return undefined
+		return goos.Undefined
 	case "status":
 		return uint32(s.res.StatusCode)
 	}
diff --git a/internal/gojs/logging/logging.go b/internal/gojs/logging/logging.go
index 20efd7fd..9ddfa413 100644
--- a/internal/gojs/logging/logging.go
+++ b/internal/gojs/logging/logging.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"io/fs"
 	"os"
-	"strconv"
 
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/gojs"
@@ -13,12 +12,12 @@ import (
 	"github.com/tetratelabs/wazero/internal/gojs/goarch"
 	"github.com/tetratelabs/wazero/internal/gojs/goos"
 	"github.com/tetratelabs/wazero/internal/logging"
+	"github.com/tetratelabs/wazero/internal/sys"
 )
 
 func Config(fnd api.FunctionDefinition) (pSampler logging.ParamSampler, pLoggers []logging.ParamLogger, rLoggers []logging.ResultLogger) {
 	switch fnd.Name() {
-	case custom.NameRuntimeWasmWrite:
-		pLoggers = []logging.ParamLogger{runtimeWasmWriteParamLogger}
+	// Don't log NameRuntimeWasmWrite as it is used in panics
 	case custom.NameSyscallValueCall:
 		pSampler = syscallValueCallParamSampler
 		pLoggers = []logging.ParamLogger{syscallValueCallParamLogger}
@@ -28,21 +27,6 @@ func Config(fnd api.FunctionDefinition) (pSampler logging.ParamSampler, pLoggers
 	return
 }
 
-func runtimeWasmWriteParamLogger(_ context.Context, mod api.Module, w logging.Writer, params []uint64) {
-	mem := mod.Memory()
-	funcName := custom.NameSyscallValueCall
-	stack := goos.NewStack(funcName, mem, uint32(params[0]))
-	fd := stack.ParamUint32(0)
-	pLen := stack.ParamUint32(2)
-
-	w.WriteString(funcName) //nolint
-	w.WriteString("(fd=")   //nolint
-	writeI32(w, fd)
-	w.WriteString(",p_len=") //nolint
-	writeI32(w, pLen)
-	w.WriteByte(')') //nolint
-}
-
 func syscallValueCallParamLogger(ctx context.Context, mod api.Module, w logging.Writer, params []uint64) {
 	vRef, m, args := syscallValueCallParams(ctx, mod, params)
 
@@ -73,10 +57,20 @@ func syscallValueCallParamLogger(ctx context.Context, mod api.Module, w logging.
 }
 
 func syscallValueCallParamSampler(ctx context.Context, mod api.Module, params []uint64) bool {
-	vRef, _, _ := syscallValueCallParams(ctx, mod, params)
+	vRef, m, args := syscallValueCallParams(ctx, mod, params)
 
 	// TODO: add more than just filesystem
-	return vRef == goos.RefJsfs
+	if vRef != goos.RefJsfs {
+		return false
+	}
+
+	// Don't amplify logs with stdio reads or writes
+	switch m {
+	case custom.NameFsWrite, custom.NameFsRead:
+		fd := goos.ValueToUint32(args[0])
+		return fd > sys.FdStderr
+	}
+	return true
 }
 
 func syscallValueCallParams(ctx context.Context, mod api.Module, params []uint64) (goos.Ref, string, []interface{}) {
@@ -168,7 +162,3 @@ var oflagToString = [...]string{
 	"SYNC",
 	"TRUNC",
 }
-
-func writeI32(w logging.Writer, v uint32) {
-	w.WriteString(strconv.FormatInt(int64(int32(v)), 10)) //nolint
-}
diff --git a/internal/gojs/runtime.go b/internal/gojs/runtime.go
index 06b6b1c0..213de99c 100644
--- a/internal/gojs/runtime.go
+++ b/internal/gojs/runtime.go
@@ -8,6 +8,7 @@ import (
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/gojs/custom"
 	"github.com/tetratelabs/wazero/internal/gojs/goarch"
+	internalsys "github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -39,12 +40,9 @@ func wasmWrite(_ context.Context, mod api.Module, stack goarch.Stack) {
 	p := stack.ParamBytes(mod.Memory(), 1 /*, 2 */)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
-	writer := fsc.FdWriter(fd)
-	if writer == nil {
-		panic(fmt.Errorf("unexpected fd %d", fd))
-	}
-
-	if _, err := writer.Write(p); err != nil {
+	if writer := internalsys.WriterForFile(fsc, fd); writer == nil {
+		panic(fmt.Errorf("fd %d invalid", fd))
+	} else if _, err := writer.Write(p); err != nil {
 		panic(fmt.Errorf("error writing p: %w", err))
 	}
 }
diff --git a/internal/gojs/state.go b/internal/gojs/state.go
index 24b06fa6..4a4fcc30 100644
--- a/internal/gojs/state.go
+++ b/internal/gojs/state.go
@@ -7,11 +7,12 @@ import (
 
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/gojs/goos"
+	"github.com/tetratelabs/wazero/internal/gojs/values"
 )
 
 func NewState(ctx context.Context) *State {
 	return &State{
-		values:                 &values{ids: map[interface{}]uint32{}},
+		values:                 values.NewValues(),
 		valueGlobal:            newJsGlobal(getRoundTripper(ctx)),
 		cwd:                    "/",
 		_nextCallbackTimeoutID: 1,
@@ -57,10 +58,7 @@ func (e *event) get(_ context.Context, propertyKey string) interface{} {
 	panic(fmt.Sprintf("TODO: event.%s", propertyKey))
 }
 
-var (
-	undefined = struct{ name string }{name: "undefined"}
-	NaN       = math.NaN()
-)
+var NaN = math.NaN()
 
 // LoadValue reads up to 8 bytes at the memory offset `addr` to return the
 // value written by storeValue.
@@ -69,7 +67,7 @@ var (
 func LoadValue(ctx context.Context, ref goos.Ref) interface{} { //nolint
 	switch ref {
 	case 0:
-		return undefined
+		return goos.Undefined
 	case goos.RefValueNaN:
 		return NaN
 	case goos.RefValueZero:
@@ -108,7 +106,7 @@ func LoadValue(ctx context.Context, ref goos.Ref) interface{} { //nolint
 		if f, ok := ref.ParseFloat(); ok { // numbers are passed through as a Ref
 			return f
 		}
-		return getState(ctx).values.get(uint32(ref))
+		return getState(ctx).values.Get(uint32(ref))
 	}
 }
 
@@ -119,7 +117,7 @@ func LoadValue(ctx context.Context, ref goos.Ref) interface{} { //nolint
 // See https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L135-L183
 func storeRef(ctx context.Context, v interface{}) goos.Ref { //nolint
 	// allow-list because we control all implementations
-	if v == undefined {
+	if v == goos.Undefined {
 		return goos.RefValueUndefined
 	} else if v == nil {
 		return goos.RefValueNull
@@ -134,16 +132,16 @@ func storeRef(ctx context.Context, v interface{}) goos.Ref { //nolint
 	} else if c, ok := v.(*jsVal); ok {
 		return c.ref // already stored
 	} else if _, ok := v.(*event); ok {
-		id := getState(ctx).values.increment(v)
+		id := getState(ctx).values.Increment(v)
 		return goos.ValueRef(id, goos.TypeFlagFunction)
 	} else if _, ok := v.(funcWrapper); ok {
-		id := getState(ctx).values.increment(v)
+		id := getState(ctx).values.Increment(v)
 		return goos.ValueRef(id, goos.TypeFlagFunction)
 	} else if _, ok := v.(jsFn); ok {
-		id := getState(ctx).values.increment(v)
+		id := getState(ctx).values.Increment(v)
 		return goos.ValueRef(id, goos.TypeFlagFunction)
 	} else if _, ok := v.(string); ok {
-		id := getState(ctx).values.increment(v)
+		id := getState(ctx).values.Increment(v)
 		return goos.ValueRef(id, goos.TypeFlagString)
 	} else if i32, ok := v.(int32); ok {
 		return toFloatRef(float64(i32))
@@ -156,7 +154,7 @@ func storeRef(ctx context.Context, v interface{}) goos.Ref { //nolint
 	} else if f64, ok := v.(float64); ok {
 		return toFloatRef(f64)
 	}
-	id := getState(ctx).values.increment(v)
+	id := getState(ctx).values.Increment(v)
 	return goos.ValueRef(id, goos.TypeFlagObject)
 }
 
@@ -168,56 +166,10 @@ func toFloatRef(f float64) goos.Ref {
 	return goos.Ref(api.EncodeF64(f))
 }
 
-type values struct {
-	// Below is needed to avoid exhausting the ID namespace finalizeRef reclaims
-	// See https://go-review.googlesource.com/c/go/+/203600
-
-	values      []interface{}          // values indexed by ID, nil
-	goRefCounts []uint32               // recount pair-indexed with values
-	ids         map[interface{}]uint32 // live values
-	idPool      []uint32               // reclaimed IDs (values[i] = nil, goRefCounts[i] nil
-}
-
-func (j *values) get(id uint32) interface{} {
-	index := id - goos.NextID
-	if index >= uint32(len(j.values)) {
-		panic(fmt.Errorf("id %d is out of range %d", id, len(j.values)))
-	}
-	return j.values[index]
-}
-
-func (j *values) increment(v interface{}) uint32 {
-	id, ok := j.ids[v]
-	if !ok {
-		if len(j.idPool) == 0 {
-			id, j.values, j.goRefCounts = uint32(len(j.values)), append(j.values, v), append(j.goRefCounts, 0)
-		} else {
-			id, j.idPool = j.idPool[len(j.idPool)-1], j.idPool[:len(j.idPool)-1]
-			j.values[id], j.goRefCounts[id] = v, 0
-		}
-		j.ids[v] = id
-	}
-	j.goRefCounts[id]++
-	return id + goos.NextID
-}
-
-func (j *values) decrement(id uint32) {
-	// Special IDs are not goos.Refcounted.
-	if id < goos.NextID {
-		return
-	}
-	id -= goos.NextID
-	j.goRefCounts[id]--
-	if j.goRefCounts[id] == 0 {
-		j.values[id] = nil
-		j.idPool = append(j.idPool, id)
-	}
-}
-
 // State holds state used by the "go" imports used by gojs.
 // Note: This is module-scoped.
 type State struct {
-	values        *values
+	values        *values.Values
 	_pendingEvent *event
 	// _lastEvent was the last _pendingEvent value
 	_lastEvent *event
@@ -260,10 +212,7 @@ func (s *State) close() {
 	// Reset all state recursively to their initial values. This allows our
 	// unit tests to check we closed everything.
 	s._scheduledTimeouts = map[uint32]chan bool{}
-	s.values.values = nil
-	s.values.goRefCounts = nil
-	s.values.ids = map[interface{}]uint32{}
-	s.values.idPool = nil
+	s.values.Reset()
 	s._pendingEvent = nil
 	s._lastEvent = nil
 	s._nextCallbackTimeoutID = 1
@@ -271,7 +220,7 @@ func (s *State) close() {
 }
 
 func toInt64(arg interface{}) int64 {
-	if arg == goos.RefValueZero || arg == undefined {
+	if arg == goos.RefValueZero || arg == goos.Undefined {
 		return 0
 	} else if u, ok := arg.(int64); ok {
 		return u
@@ -280,7 +229,7 @@ func toInt64(arg interface{}) int64 {
 }
 
 func toUint64(arg interface{}) uint64 {
-	if arg == goos.RefValueZero || arg == undefined {
+	if arg == goos.RefValueZero || arg == goos.Undefined {
 		return 0
 	} else if u, ok := arg.(uint64); ok {
 		return u
@@ -288,15 +237,6 @@ func toUint64(arg interface{}) uint64 {
 	return uint64(arg.(float64))
 }
 
-func toUint32(arg interface{}) uint32 {
-	if arg == goos.RefValueZero || arg == undefined {
-		return 0
-	} else if u, ok := arg.(uint32); ok {
-		return u
-	}
-	return uint32(arg.(float64))
-}
-
 // valueString returns the string form of JavaScript string, boolean and number types.
 func valueString(v interface{}) string { //nolint
 	if s, ok := v.(string); ok {
diff --git a/internal/gojs/syscall.go b/internal/gojs/syscall.go
index 9df39949..36e9ff06 100644
--- a/internal/gojs/syscall.go
+++ b/internal/gojs/syscall.go
@@ -26,7 +26,7 @@ func finalizeRef(ctx context.Context, _ api.Module, stack goos.Stack) {
 
 	id := uint32(r) // 32-bits of the ref are the ID
 
-	getState(ctx).values.decrement(id)
+	getState(ctx).values.Decrement(id)
 }
 
 // StringVal implements js.stringVal, which is used to load the string for
diff --git a/internal/gojs/values/values.go b/internal/gojs/values/values.go
new file mode 100644
index 00000000..53dce81a
--- /dev/null
+++ b/internal/gojs/values/values.go
@@ -0,0 +1,73 @@
+package values
+
+import (
+	"fmt"
+
+	"github.com/tetratelabs/wazero/internal/gojs/goos"
+)
+
+func NewValues() *Values {
+	ret := &Values{}
+	ret.Reset()
+	return ret
+}
+
+type Values struct {
+	// Below is needed to avoid exhausting the ID namespace finalizeRef reclaims
+	// See https://go-review.googlesource.com/c/go/+/203600
+
+	values      []interface{}          // values indexed by ID, nil
+	goRefCounts []uint32               // recount pair-indexed with values
+	ids         map[interface{}]uint32 // live values
+	idPool      []uint32               // reclaimed IDs (values[i] = nil, goRefCounts[i] nil
+}
+
+func (j *Values) Get(id uint32) interface{} {
+	index := id - goos.NextID
+	if index >= uint32(len(j.values)) {
+		panic(fmt.Errorf("id %d is out of range %d", id, len(j.values)))
+	}
+	if v := j.values[index]; v == nil {
+		panic(fmt.Errorf("value for %d was nil", id))
+	} else {
+		return v
+	}
+}
+
+func (j *Values) Increment(v interface{}) uint32 {
+	id, ok := j.ids[v]
+	if !ok {
+		if len(j.idPool) == 0 {
+			id, j.values, j.goRefCounts = uint32(len(j.values)), append(j.values, v), append(j.goRefCounts, 0)
+		} else {
+			id, j.idPool = j.idPool[len(j.idPool)-1], j.idPool[:len(j.idPool)-1]
+			j.values[id], j.goRefCounts[id] = v, 0
+		}
+		j.ids[v] = id
+	}
+	j.goRefCounts[id]++
+
+	return id + goos.NextID
+}
+
+func (j *Values) Decrement(id uint32) {
+	// Special IDs are not goos.Refcounted.
+	if id < goos.NextID {
+		return
+	}
+	id -= goos.NextID
+	j.goRefCounts[id]--
+	if j.goRefCounts[id] == 0 {
+		v := j.values[id]
+		j.values[id] = nil
+		delete(j.ids, v)
+		j.idPool = append(j.idPool, id)
+	}
+}
+
+func (j *Values) Reset() {
+	j.values = nil
+	j.goRefCounts = nil
+	j.ids = map[interface{}]uint32{}
+	j.idPool = nil
+}
diff --git a/internal/logging/logging.go b/internal/logging/logging.go
index 20341055..28aad9ab 100644
--- a/internal/logging/logging.go
+++ b/internal/logging/logging.go
@@ -53,7 +53,7 @@ type Writer interface {
 // ValueTypeString, it would read `vals[i+1]` and write the string from memory.
 type ValWriter func(ctx context.Context, mod api.Module, w Writer, i uint32, vals []uint64)
 
-func ValueLoggers(fnd api.FunctionDefinition) (paramLoggers []ParamLogger, resultLoggers []ResultLogger) {
+func Config(fnd api.FunctionDefinition) (paramLoggers []ParamLogger, resultLoggers []ResultLogger) {
 	if paramLen := uint32(len(fnd.ParamTypes())); paramLen > 0 {
 		paramLoggers = make([]ParamLogger, paramLen)
 		hasParamNames := len(fnd.ParamNames()) > 0
diff --git a/internal/sys/fs.go b/internal/sys/fs.go
index 87853977..e3d2142c 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -38,22 +38,6 @@ const (
 	modeCharDevice = uint32(fs.ModeCharDevice | 0o640)
 )
 
-// EmptyFS is exported to special-case an empty file system.
-var EmptyFS = &emptyFS{}
-
-type emptyFS struct{}
-
-// compile-time check to ensure emptyFS implements fs.FS
-var _ fs.FS = &emptyFS{}
-
-// Open implements the same method as documented on fs.FS.
-func (f *emptyFS) Open(name string) (fs.File, error) {
-	if !fs.ValidPath(name) {
-		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
-	}
-	return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
-}
-
 // An emptyRootDir is a fake "/" directory.
 type emptyRootDir struct{}
 
@@ -175,7 +159,7 @@ type ReadDir struct {
 
 type FSContext struct {
 	// fs is the root ("/") mount.
-	fs fs.FS
+	fs syscallfs.FS
 
 	// openedFiles is a map of file descriptor numbers (>=FdRoot) to open files
 	// (or directories) and defaults to empty.
@@ -190,27 +174,18 @@ var errNotDir = errors.New("not a directory")
 // Otherwise, `root` is assigned file descriptor FdRoot and the returned
 // context can open files in that file system. Any error on opening "." is
 // returned.
-func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, root fs.FS) (fsc *FSContext, err error) {
+func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, root syscallfs.FS) (fsc *FSContext, err error) {
 	fsc = &FSContext{fs: root}
 	fsc.openedFiles.Insert(stdinReader(stdin))
 	fsc.openedFiles.Insert(stdioWriter(stdout, noopStdoutStat))
 	fsc.openedFiles.Insert(stdioWriter(stderr, noopStderrStat))
 
-	if root == EmptyFS {
+	if root == syscallfs.EmptyFS {
 		return fsc, nil
 	}
 
-	// Open the root directory by using "." as "/" is not relevant in fs.FS.
-	// This not only validates the file system, but also allows us to test if
-	// 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.
-	var rootDir fs.File
-	if sfs, ok := root.(syscallfs.FS); ok {
-		rootDir, err = sfs.OpenFile(".", os.O_RDONLY, 0)
-	} else {
-		rootDir, err = root.Open(".")
-	}
+	// Test if this is a real file or not. ex. `file.(*os.File)`.
+	rootDir, err := root.OpenFile(".", os.O_RDONLY, 0)
 	if err != nil {
 		// This could fail because someone made a special-purpose file system,
 		// which only passes certain filenames and not ".".
@@ -255,19 +230,6 @@ func stdioStat(f interface{}, defaultStat stdioFileInfo) fs.FileInfo {
 	return defaultStat
 }
 
-// OpenedFile returns a file and true if it was opened or nil and false, if syscall.EBADF.
-func (c *FSContext) OpenedFile(fd uint32) (*FileEntry, bool) {
-	return c.openedFiles.Lookup(fd)
-}
-
-func (c *FSContext) StatFile(fd uint32) (fs.FileInfo, error) {
-	f, ok := c.openedFiles.Lookup(fd)
-	if !ok {
-		return nil, syscall.EBADF
-	}
-	return f.File.Stat()
-}
-
 // fileModeStat is a fake fs.FileInfo which only returns its mode.
 // This is used for character devices.
 type fileModeStat fs.FileMode
@@ -281,156 +243,37 @@ func (s fileModeStat) Sys() interface{}   { return nil }
 func (s fileModeStat) Name() string       { return "" }
 func (s fileModeStat) IsDir() bool        { return false }
 
-// Mkdir is like syscall.Mkdir and returns the file descriptor of the new
-// directory or an error.
-func (c *FSContext) Mkdir(name string, perm fs.FileMode) (newFD uint32, err error) {
-	name = c.cleanPath(name)
-	if wfs, ok := c.fs.(syscallfs.FS); ok {
-		if err = wfs.Mkdir(name, perm); err != nil {
-			return
-		}
-		// TODO: Determine how to handle when a directory already
-		// exists or is a file.
-		return c.OpenFile(name, os.O_RDONLY, perm)
-	}
-	err = syscall.ENOSYS
-	return
+// FS returns the underlying filesystem. Any files that should be added to the
+// table should be inserted via InsertFile.
+func (c *FSContext) FS() syscallfs.FS {
+	return c.fs
 }
 
-// OpenFile is like syscall.Open and returns the file descriptor of the new file or an error.
-func (c *FSContext) OpenFile(path string, flags int, perm fs.FileMode) (newFD uint32, err error) {
-	var f fs.File
-	if wfs, ok := c.fs.(syscallfs.FS); ok {
-		path = c.cleanPath(path)
-		f, err = wfs.OpenFile(path, flags, perm)
-	} else {
-		// While os.Open says it is read-only, in reality the files returned
-		// are often writable. Fail only on the flags which won't work.
-		switch {
-		case flags&os.O_APPEND != 0:
-			fallthrough
-		case flags&os.O_CREATE != 0:
-			fallthrough
-		case flags&os.O_TRUNC != 0:
-			return 0, syscall.ENOSYS
-		default:
-			// only time fs.FS is used
-			f, err = c.fs.Open(c.cleanPath(path))
-		}
-	}
-
-	if err != nil {
+// OpenFile opens the file into the table and returns its file descriptor.
+// The result must be closed by CloseFile or Close.
+func (c *FSContext) OpenFile(path string, flag int, perm fs.FileMode) (uint32, error) {
+	if f, err := c.fs.OpenFile(path, flag, perm); err != nil {
 		return 0, err
-	}
-
-	newFD = c.openedFiles.Insert(&FileEntry{Name: pathutil.Base(path), File: f})
-	return newFD, nil
-}
-
-// Rmdir is like syscall.Rmdir.
-func (c *FSContext) Rmdir(path string) (err error) {
-	if wfs, ok := c.fs.(syscallfs.FS); ok {
-		path = c.cleanPath(path)
-		return wfs.Rmdir(path)
-	}
-	err = syscall.ENOSYS
-	return
-}
-
-func (c *FSContext) StatPath(path string) (fs.FileInfo, error) {
-	fd, err := c.OpenFile(path, os.O_RDONLY, 0)
-	if err != nil {
-		return nil, err
-	}
-	defer c.CloseFile(fd)
-	return c.StatFile(fd)
-}
-
-// Rename is like syscall.Rename.
-func (c *FSContext) Rename(from, to string) (err error) {
-	if wfs, ok := c.fs.(syscallfs.FS); ok {
-		from = c.cleanPath(from)
-		to = c.cleanPath(to)
-		return wfs.Rename(from, to)
-	}
-	err = syscall.ENOSYS
-	return
-}
-
-// Unlink is like syscall.Unlink.
-func (c *FSContext) Unlink(path string) (err error) {
-	if wfs, ok := c.fs.(syscallfs.FS); ok {
-		path = c.cleanPath(path)
-		return wfs.Unlink(path)
-	}
-	err = syscall.ENOSYS
-	return
-}
-
-// Utimes is like syscall.Utimes.
-func (c *FSContext) Utimes(path string, atimeNsec, mtimeNsec int64) (err error) {
-	if wfs, ok := c.fs.(syscallfs.FS); ok {
-		path = c.cleanPath(path)
-		return wfs.Utimes(path, atimeNsec, mtimeNsec)
-	}
-	err = syscall.ENOSYS
-	return
-}
-
-func (c *FSContext) cleanPath(path string) string {
-	if len(path) == 0 {
-		return path
-	}
-	// fs.ValidFile cannot be rooted (start with '/')
-	cleaned := path
-	if path[0] == '/' {
-		cleaned = path[1:]
-	}
-	cleaned = pathutil.Clean(cleaned) // e.g. "sub/." -> "sub"
-	return cleaned
-}
-
-// FdWriter returns a valid writer for the given file descriptor or nil if syscall.EBADF.
-func (c *FSContext) FdWriter(fd uint32) io.Writer {
-	// Check to see if the file descriptor is available
-	if f, ok := c.openedFiles.Lookup(fd); !ok {
-		return nil
-	} else if writer, ok := f.File.(io.Writer); !ok {
-		// Go's syscall.Write also returns EBADF if the FD is present, but not writeable
-		return nil
 	} else {
-		return writer
+		newFD := c.openedFiles.Insert(&FileEntry{Name: pathutil.Base(path), File: f})
+		return newFD, nil
 	}
 }
 
-// FdReader returns a valid reader for the given file descriptor or nil if syscall.EBADF.
-func (c *FSContext) FdReader(fd uint32) io.Reader {
-	switch fd {
-	case FdStdout, FdStderr:
-		return nil // writer, not a readable file.
-	case FdRoot:
-		return nil // directory, not a readable file.
-	}
-
-	if f, ok := c.openedFiles.Lookup(fd); !ok {
-		return nil // TODO: could be a directory not a file.
-	} else {
-		return f.File
-	}
+// LookupFile returns a file if it is in the table.
+func (c *FSContext) LookupFile(fd uint32) (*FileEntry, bool) {
+	f, ok := c.openedFiles.Lookup(fd)
+	return f, ok
 }
 
-// CloseFile returns true if a file was opened and closed without error, or false if syscall.EBADF.
-func (c *FSContext) CloseFile(fd uint32) bool {
+// CloseFile returns any error closing the existing file.
+func (c *FSContext) CloseFile(fd uint32) error {
 	f, ok := c.openedFiles.Lookup(fd)
 	if !ok {
-		return false
+		return syscall.EBADF
 	}
 	c.openedFiles.Delete(fd)
-
-	if err := f.File.Close(); err != nil {
-		return false
-	}
-	return true
+	return f.File.Close()
 }
 
 // Close implements api.Closer
@@ -447,3 +290,23 @@ func (c *FSContext) Close(context.Context) (err error) {
 	c.openedFiles = FileTable{}
 	return
 }
+
+// StatFile is a convenience that calls FSContext.LookupFile then fs.File Stat.
+// syscall.EBADF is returned on lookup failure.
+func StatFile(fsc *FSContext, fd uint32) (stat fs.FileInfo, err error) {
+	if f, ok := fsc.LookupFile(fd); !ok {
+		err = syscall.EBADF
+	} else {
+		stat, err = f.File.Stat()
+	}
+	return
+}
+
+// WriterForFile returns a writer for the given file descriptor or nil if not
+// opened or not writeable (e.g. a directory or a file not opened for writes).
+func WriterForFile(fsc *FSContext, fd uint32) (writer io.Writer) {
+	if f, ok := fsc.LookupFile(fd); ok {
+		writer = f.File.(io.Writer)
+	}
+	return
+}
diff --git a/internal/sys/sys.go b/internal/sys/sys.go
index c678f801..2b19976e 100644
--- a/internal/sys/sys.go
+++ b/internal/sys/sys.go
@@ -8,6 +8,7 @@ import (
 	"time"
 
 	"github.com/tetratelabs/wazero/internal/platform"
+	"github.com/tetratelabs/wazero/internal/syscallfs"
 	"github.com/tetratelabs/wazero/sys"
 )
 
@@ -181,9 +182,9 @@ func NewContext(
 	}
 
 	if fs != nil {
-		sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, fs)
+		sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, syscallfs.Adapt(fs))
 	} else {
-		sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, EmptyFS)
+		sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, syscallfs.EmptyFS)
 	}
 
 	return
diff --git a/internal/syscallfs/adapter.go b/internal/syscallfs/adapter.go
new file mode 100644
index 00000000..fc129acf
--- /dev/null
+++ b/internal/syscallfs/adapter.go
@@ -0,0 +1,77 @@
+package syscallfs
+
+import (
+	"fmt"
+	"io/fs"
+	"os"
+	pathutil "path"
+	"syscall"
+)
+
+// Adapt returns a read-only FS unless the input is already one.
+func Adapt(fs fs.FS) FS {
+	if sys, ok := fs.(FS); ok {
+		return sys
+	}
+	return &adapter{fs}
+}
+
+type adapter struct{ fs fs.FS }
+
+// Open implements the same method as documented on fs.FS
+func (ro *adapter) Open(name string) (fs.File, error) {
+	panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
+}
+
+// OpenFile implements FS.OpenFile
+func (ro *adapter) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
+	if flag != 0 && flag != os.O_RDONLY {
+		return nil, syscall.ENOSYS
+	}
+
+	path = cleanPath(path)
+	f, err := ro.fs.Open(path)
+	if err != nil {
+		// wrapped is fine while FS.OpenFile emulates os.OpenFile vs syscall.OpenFile.
+		return nil, err
+	}
+	return maskForReads(f), nil
+}
+
+func cleanPath(name string) string {
+	if len(name) == 0 {
+		return name
+	}
+	// fs.ValidFile cannot be rooted (start with '/')
+	cleaned := name
+	if name[0] == '/' {
+		cleaned = name[1:]
+	}
+	cleaned = pathutil.Clean(cleaned) // e.g. "sub/." -> "sub"
+	return cleaned
+}
+
+// Mkdir implements FS.Mkdir
+func (ro *adapter) Mkdir(path string, perm fs.FileMode) error {
+	return syscall.ENOSYS
+}
+
+// Rename implements FS.Rename
+func (ro *adapter) Rename(from, to string) error {
+	return syscall.ENOSYS
+}
+
+// Rmdir implements FS.Rmdir
+func (ro *adapter) Rmdir(path string) error {
+	return syscall.ENOSYS
+}
+
+// Unlink implements FS.Unlink
+func (ro *adapter) Unlink(path string) error {
+	return syscall.ENOSYS
+}
+
+// Utimes implements FS.Utimes
+func (ro *adapter) Utimes(path string, atimeNsec, mtimeNsec int64) error {
+	return syscall.ENOSYS
+}
diff --git a/internal/syscallfs/dirfs.go b/internal/syscallfs/dirfs.go
index 1ed4f7b1..150be6a8 100644
--- a/internal/syscallfs/dirfs.go
+++ b/internal/syscallfs/dirfs.go
@@ -29,10 +29,6 @@ func (dir dirFS) Open(name string) (fs.File, error) {
 
 // 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}
-	}
-
 	f, err := os.OpenFile(path.Join(string(dir), name), flag, perm)
 	if err != nil {
 		return nil, err
@@ -46,23 +42,12 @@ func (dir dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, err
 
 // 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)
 }
 
 // Rename implements FS.Rename
 func (dir dirFS) Rename(from, to string) error {
-	if !fs.ValidPath(from) {
-		return syscall.EINVAL
-	}
-	if !fs.ValidPath(to) {
-		return syscall.EINVAL
-	}
 	if from == to {
 		return nil
 	}
@@ -71,31 +56,18 @@ func (dir dirFS) Rename(from, to string) error {
 
 // 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, atimeNsec, mtimeNsec int64) error {
-	if !fs.ValidPath(name) {
-		return syscall.EINVAL
-	}
 	return syscall.UtimesNano(path.Join(string(dir), name), []syscall.Timespec{
 		syscall.NsecToTimespec(atimeNsec),
 		syscall.NsecToTimespec(mtimeNsec),
diff --git a/internal/syscallfs/empty.go b/internal/syscallfs/empty.go
new file mode 100644
index 00000000..14fb233e
--- /dev/null
+++ b/internal/syscallfs/empty.go
@@ -0,0 +1,48 @@
+package syscallfs
+
+import (
+	"fmt"
+	"io/fs"
+	"syscall"
+)
+
+// EmptyFS is an FS that returns syscall.ENOENT for all read functions, and
+// syscall.ENOSYS otherwise.
+var EmptyFS FS = unsupported{}
+
+type unsupported struct{}
+
+// Open implements the same method as documented on fs.FS
+func (unsupported) Open(name string) (fs.File, error) {
+	panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
+}
+
+// OpenFile implements FS.OpenFile
+func (unsupported) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
+	return nil, &fs.PathError{Op: "open", Path: path, Err: syscall.ENOENT}
+}
+
+// Mkdir implements FS.Mkdir
+func (unsupported) Mkdir(path string, perm fs.FileMode) error {
+	return syscall.ENOSYS
+}
+
+// Rename implements FS.Rename
+func (unsupported) Rename(from, to string) error {
+	return syscall.ENOSYS
+}
+
+// Rmdir implements FS.Rmdir
+func (unsupported) Rmdir(path string) error {
+	return syscall.ENOSYS
+}
+
+// Unlink implements FS.Unlink
+func (unsupported) Unlink(path string) error {
+	return syscall.ENOSYS
+}
+
+// Utimes implements FS.Utimes
+func (unsupported) Utimes(path string, atimeNsec, mtimeNsec int64) error {
+	return syscall.ENOSYS
+}
diff --git a/internal/syscallfs/readfs.go b/internal/syscallfs/readfs.go
index 5d3a599c..3d9a4626 100644
--- a/internal/syscallfs/readfs.go
+++ b/internal/syscallfs/readfs.go
@@ -22,15 +22,15 @@ func (ro *readFS) Open(name string) (fs.File, error) {
 }
 
 // OpenFile implements FS.OpenFile
-func (ro *readFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
+func (ro *readFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
 	if flag == 0 || flag == os.O_RDONLY {
-		return ro.fs.OpenFile(name, flag, perm)
+		return ro.fs.OpenFile(path, flag, perm)
 	}
 	return nil, syscall.ENOSYS
 }
 
 // Mkdir implements FS.Mkdir
-func (ro *readFS) Mkdir(name string, perm fs.FileMode) error {
+func (ro *readFS) Mkdir(path string, perm fs.FileMode) error {
 	return syscall.ENOSYS
 }
 
@@ -40,16 +40,16 @@ func (ro *readFS) Rename(from, to string) error {
 }
 
 // Rmdir implements FS.Rmdir
-func (ro *readFS) Rmdir(name string) error {
+func (ro *readFS) Rmdir(path string) error {
 	return syscall.ENOSYS
 }
 
 // Unlink implements FS.Unlink
-func (ro *readFS) Unlink(name string) error {
+func (ro *readFS) Unlink(path string) error {
 	return syscall.ENOSYS
 }
 
 // Utimes implements FS.Utimes
-func (ro *readFS) Utimes(name string, atimeNsec, mtimeNsec int64) error {
-	return ro.fs.Utimes(name, atimeNsec, mtimeNsec)
+func (ro *readFS) Utimes(path string, atimeNsec, mtimeNsec int64) error {
+	return syscall.ENOSYS
 }
diff --git a/internal/syscallfs/syscallfs.go b/internal/syscallfs/syscallfs.go
index 7acbf0d0..01cedc01 100644
--- a/internal/syscallfs/syscallfs.go
+++ b/internal/syscallfs/syscallfs.go
@@ -3,6 +3,7 @@ package syscallfs
 import (
 	"io"
 	"io/fs"
+	"os"
 )
 
 // FS is a writeable fs.FS bridge backed by syscall functions needed for ABI
@@ -19,13 +20,13 @@ type FS interface {
 
 	// 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)
+	OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error)
 	// ^^ TODO: Consider 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
+	Mkdir(path string, perm fs.FileMode) error
 	// ^^ TODO: Consider syscall.Mkdir, though this implies defining and
 	// coercing flags and perms similar to what is done in os.Mkdir.
 
@@ -141,3 +142,14 @@ func maskForReads(f fs.File) fs.File {
 		panic("BUG: unhandled pattern")
 	}
 }
+
+// StatPath is a convenience that calls FS.OpenFile until there is a stat
+// method.
+func StatPath(fs FS, path string) (fs.FileInfo, error) {
+	f, err := fs.OpenFile(path, os.O_RDONLY, 0)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return f.Stat()
+}
diff --git a/internal/wasi_snapshot_preview1/logging/logging.go b/internal/wasi_snapshot_preview1/logging/logging.go
index c7c49b25..de5d1372 100644
--- a/internal/wasi_snapshot_preview1/logging/logging.go
+++ b/internal/wasi_snapshot_preview1/logging/logging.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/logging"
+	"github.com/tetratelabs/wazero/internal/sys"
 	. "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
 )
 
@@ -23,14 +24,17 @@ func IsFilesystemFunction(fnd api.FunctionDefinition) bool {
 	return false
 }
 
-func Config(fnd api.FunctionDefinition) (pLoggers []logging.ParamLogger, rLoggers []logging.ResultLogger) {
+func Config(fnd api.FunctionDefinition) (pSampler logging.ParamSampler, pLoggers []logging.ParamLogger, rLoggers []logging.ResultLogger) {
 	switch fnd.Name() {
 	case FdPrestatGetName:
 		pLoggers = []logging.ParamLogger{logging.NewParamLogger(0, "fd", logging.ValueTypeI32)}
 		rLoggers = []logging.ResultLogger{resultParamLogger("prestat", logPrestat(1).Log), logErrno}
 		return
 	case ProcExitName:
-		return logging.ValueLoggers(fnd)
+		pLoggers, rLoggers = logging.Config(fnd)
+		return
+	case FdReadName, FdWriteName:
+		pSampler = fdReadWriteSampler
 	}
 
 	for idx := uint32(0); idx < uint32(len(fnd.ParamTypes())); idx++ {
@@ -92,6 +96,12 @@ func Config(fnd api.FunctionDefinition) (pLoggers []logging.ParamLogger, rLogger
 	return
 }
 
+// Ensure we don't clutter log with reads and writes to stdio.
+func fdReadWriteSampler(_ context.Context, _ api.Module, params []uint64) bool {
+	fd := uint32(params[0])
+	return fd > sys.FdStderr
+}
+
 func isLookupFlags(fnd api.FunctionDefinition, name string) bool {
 	switch fnd.Name() {
 	case PathFilestatGetName, PathFilestatSetTimesName:


Test patch:
diff --git a/cmd/wazero/wazero_test.go b/cmd/wazero/wazero_test.go
index 743edf33..db96a847 100644
--- a/cmd/wazero/wazero_test.go
+++ b/cmd/wazero/wazero_test.go
@@ -235,8 +235,6 @@ func TestRun(t *testing.T) {
 <== (opened_fd=4,errno=ESUCCESS)
 ==> wasi_snapshot_preview1.fd_read(fd=4,iovs=1024,iovs_len=1)
 <== (nread=5,errno=ESUCCESS)
-==> wasi_snapshot_preview1.fd_write(fd=1,iovs=1024,iovs_len=1)
-<== (nwritten=5,errno=ESUCCESS)
 `,
 		},
 		{
@@ -264,8 +262,6 @@ func TestRun(t *testing.T) {
 <== (err=<nil>,n=0)
 ==> go.syscall/js.valueCall(fs.close(fd=4))
 <== (err=<nil>,ok=true)
-==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=5,fOffset=<nil>))
-<== (err=<nil>,n=5)
 `, bearMode, bearMtime),
 		},
 		{
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index 2433e72f..bcdc502f 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -67,11 +67,11 @@ func Test_fdClose(t *testing.T) {
 `, "\n"+log.String())
 
 	// Verify fdToClose is closed and removed from the opened FDs.
-	_, ok := fsc.OpenedFile(fdToClose)
+	_, ok := fsc.LookupFile(fdToClose)
 	require.False(t, ok)
 
 	// Verify fdToKeep is not closed
-	_, ok = fsc.OpenedFile(fdToKeep)
+	_, ok = fsc.LookupFile(fdToKeep)
 	require.True(t, ok)
 
 	log.Reset()
@@ -104,10 +104,10 @@ func Test_fdFdstatGet(t *testing.T) {
 	// open both paths without using WASI
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	fileFd, err := fsc.OpenFile(file, os.O_RDONLY, 0)
+	fileFD, err := fsc.OpenFile(file, os.O_RDONLY, 0)
 	require.NoError(t, err)
 
-	dirFd, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
+	dirFD, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
 	require.NoError(t, err)
 
 	tests := []struct {
@@ -175,7 +175,7 @@ func Test_fdFdstatGet(t *testing.T) {
 		},
 		{
 			name: "file",
-			fd:   fileFd,
+			fd:   fileFD,
 			expectedMemory: []byte{
 				4, 0, // fs_filetype
 				0, 0, 0, 0, 0, 0, // fs_flags
@@ -189,7 +189,7 @@ func Test_fdFdstatGet(t *testing.T) {
 		},
 		{
 			name: "dir",
-			fd:   dirFd,
+			fd:   dirFD,
 			expectedMemory: []byte{
 				3, 0, // fs_filetype
 				0, 0, 0, 0, 0, 0, // fs_flags
@@ -212,7 +212,7 @@ func Test_fdFdstatGet(t *testing.T) {
 		},
 		{
 			name:          "resultFdstat exceeds the maximum valid address by 1",
-			fd:            dirFd,
+			fd:            dirFD,
 			resultFdstat:  memorySize - 24 + 1,
 			expectedErrno: ErrnoFault,
 			expectedLog: `
@@ -273,10 +273,10 @@ func Test_fdFilestatGet(t *testing.T) {
 	// open both paths without using WASI
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	fileFd, err := fsc.OpenFile(file, os.O_RDONLY, 0)
+	fileFD, err := fsc.OpenFile(file, os.O_RDONLY, 0)
 	require.NoError(t, err)
 
-	dirFd, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
+	dirFD, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
 	require.NoError(t, err)
 
 	tests := []struct {
@@ -363,7 +363,7 @@ func Test_fdFilestatGet(t *testing.T) {
 		},
 		{
 			name: "file",
-			fd:   fileFd,
+			fd:   fileFD,
 			expectedMemory: []byte{
 				0, 0, 0, 0, 0, 0, 0, 0, // dev
 				0, 0, 0, 0, 0, 0, 0, 0, // ino
@@ -381,7 +381,7 @@ func Test_fdFilestatGet(t *testing.T) {
 		},
 		{
 			name: "dir",
-			fd:   dirFd,
+			fd:   dirFD,
 			expectedMemory: []byte{
 				0, 0, 0, 0, 0, 0, 0, 0, // dev
 				0, 0, 0, 0, 0, 0, 0, 0, // ino
@@ -408,7 +408,7 @@ func Test_fdFilestatGet(t *testing.T) {
 		},
 		{
 			name:           "resultFilestat exceeds the maximum valid address by 1",
-			fd:             dirFd,
+			fd:             dirFD,
 			resultFilestat: memorySize - 64 + 1,
 			expectedErrno:  ErrnoFault,
 			expectedLog: `
@@ -1059,7 +1059,7 @@ func Test_fdRead_Errors(t *testing.T) {
 
 var (
 	fdReadDirFs = fstest.MapFS{
-		"notdir":   {},
+		"file":     {},
 		"emptydir": {Mode: fs.ModeDir},
 		"dir":      {Mode: fs.ModeDir},
 		"dir/-":    {},                 // len = 24+1 = 25
@@ -1341,7 +1341,7 @@ func Test_fdReaddir(t *testing.T) {
 			defer log.Reset()
 
 			// Assign the state we are testing
-			file, ok := fsc.OpenedFile(fd)
+			file, ok := fsc.LookupFile(fd)
 			require.True(t, ok)
 			dir := tc.dir()
 			defer dir.File.Close()
@@ -1383,10 +1383,10 @@ func Test_fdReaddir_Errors(t *testing.T) {
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	dirFD, err := fsc.OpenFile("dir", os.O_RDONLY, 0)
+	fileFD, err := fsc.OpenFile("file", os.O_RDONLY, 0)
 	require.NoError(t, err)
 
-	fileFD, err := fsc.OpenFile("notdir", os.O_RDONLY, 0)
+	dirFD, err := fsc.OpenFile("dir", os.O_RDONLY, 0)
 	require.NoError(t, err)
 
 	tests := []struct {
@@ -1405,7 +1405,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
 			bufLen:        1000,
 			expectedErrno: ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=65536,buf_len=1000,cookie=0,result.bufused=0)
+==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=65536,buf_len=1000,cookie=0,result.bufused=0)
 <== errno=EFAULT
 `,
 		},
@@ -1427,19 +1427,8 @@ func Test_fdReaddir_Errors(t *testing.T) {
 			resultBufused: 1000, // arbitrary
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=24,cookie=0,result.bufused=1000)
+==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=24,cookie=0,result.bufused=1000)
 <== errno=EBADF
-`,
-		},
-		{
-			name:          "out-of-memory reading buf",
-			fd:            dirFD,
-			buf:           memLen,
-			bufLen:        1000,
-			expectedErrno: ErrnoFault,
-			expectedLog: `
-==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=65536,buf_len=1000,cookie=0,result.bufused=0)
-<== errno=EFAULT
 `,
 		},
 		{
@@ -1449,7 +1438,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
 			bufLen:        1000,
 			expectedErrno: ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=65535,buf_len=1000,cookie=0,result.bufused=0)
+==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=65535,buf_len=1000,cookie=0,result.bufused=0)
 <== errno=EFAULT
 `,
 		},
@@ -1460,19 +1449,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
 			resultBufused: 1000,
 			expectedErrno: ErrnoInval,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1,cookie=0,result.bufused=1000)
-<== errno=EINVAL
-`,
-		},
-		{
-			name: "cookie invalid when no prior state",
-			fd:   dirFD,
-			buf:  0, bufLen: 1000,
-			cookie:        1,
-			resultBufused: 2000,
-			expectedErrno: ErrnoInval,
-			expectedLog: `
-==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1000,cookie=1,result.bufused=2000)
+==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=1,cookie=0,result.bufused=1000)
 <== errno=EINVAL
 `,
 		},
@@ -1484,7 +1461,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
 			resultBufused: 2000,
 			expectedErrno: ErrnoInval,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1000,cookie=1,result.bufused=2000)
+==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=1000,cookie=1,result.bufused=2000)
 <== errno=EINVAL
 `,
 		},
@@ -1497,7 +1474,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
 			resultBufused: 2000,
 			expectedErrno: ErrnoInval,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1000,cookie=-1,result.bufused=2000)
+==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=1000,cookie=-1,result.bufused=2000)
 <== errno=EINVAL
 `,
 		},
@@ -1509,7 +1486,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
 			defer log.Reset()
 
 			// Reset the directory so that tests don't taint each other.
-			if file, ok := fsc.OpenedFile(tc.fd); ok && tc.fd == dirFD {
+			if file, ok := fsc.LookupFile(tc.fd); ok && tc.fd == dirFD {
 				dir, err := fdReadDirFs.Open("dir")
 				require.NoError(t, err)
 				defer dir.Close()
@@ -1604,7 +1581,7 @@ func Test_fdSeek(t *testing.T) {
 
 			// Since we initialized this file, we know it is a seeker (because it is a MapFile)
 			fsc := mod.(*wasm.CallContext).Sys.FS()
-			f, ok := fsc.OpenedFile(fd)
+			f, ok := fsc.LookupFile(fd)
 			require.True(t, ok)
 			seeker := f.File.(io.Seeker)
 
@@ -1782,12 +1759,10 @@ func Test_fdWrite_discard(t *testing.T) {
 	ok := mod.Memory().Write(0, initialMemory)
 	require.True(t, ok)
 
-	fd := 1 // stdout
+	fd := sys.FdStdout
 	requireErrno(t, ErrnoSuccess, mod, FdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten))
-	require.Equal(t, `
-==> wasi_snapshot_preview1.fd_write(fd=1,iovs=1,iovs_len=2)
-<== (nwritten=6,errno=ESUCCESS)
-`, "\n"+log.String())
+	// Should not amplify logging
+	require.Zero(t, len(log.Bytes()))
 
 	actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 	require.True(t, ok)
@@ -1961,17 +1936,6 @@ func Test_pathCreateDirectory_Errors(t *testing.T) {
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_create_directory(fd=3,path=OOM(65536,1))
 <== errno=EFAULT
-`,
-		},
-		{
-			name:          "path invalid",
-			fd:            sys.FdRoot,
-			pathName:      "../foo",
-			pathLen:       6,
-			expectedErrno: ErrnoInval,
-			expectedLog: `
-==> wasi_snapshot_preview1.path_create_directory(fd=3,path=../foo)
-<== errno=EINVAL
 `,
 		},
 		{
@@ -2043,10 +2007,10 @@ func Test_pathFilestatGet(t *testing.T) {
 	// open both paths without using WASI
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	fileFd, err := fsc.OpenFile(file, os.O_RDONLY, 0)
+	fileFD, err := fsc.OpenFile(file, os.O_RDONLY, 0)
 	require.NoError(t, err)
 
-	dirFd, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
+	dirFD, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
 	require.NoError(t, err)
 
 	tests := []struct {
@@ -2080,7 +2044,7 @@ func Test_pathFilestatGet(t *testing.T) {
 		},
 		{
 			name:           "file under dir",
-			fd:             dirFd, // root
+			fd:             dirFD, // root
 			memory:         initialMemoryFile,
 			pathLen:        1,
 			resultFilestat: 2,
@@ -2133,7 +2097,7 @@ func Test_pathFilestatGet(t *testing.T) {
 		},
 		{
 			name:           "bad FD - not dir",
-			fd:             fileFd,
+			fd:             fileFD,
 			memory:         initialMemoryFile,
 			pathLen:        1,
 			resultFilestat: 2,
@@ -2157,7 +2121,7 @@ func Test_pathFilestatGet(t *testing.T) {
 		},
 		{
 			name:           "path under dir doesn't exist",
-			fd:             dirFd,
+			fd:             dirFD,
 			memory:         initialMemoryNotExists,
 			pathLen:        1,
 			resultFilestat: 2,
@@ -2169,7 +2133,7 @@ func Test_pathFilestatGet(t *testing.T) {
 		},
 		{
 			name:           "path invalid",
-			fd:             dirFd,
+			fd:             dirFD,
 			memory:         []byte("?../foo"),
 			pathLen:        6,
 			resultFilestat: 7,
@@ -2321,9 +2285,9 @@ func Test_pathOpen(t *testing.T) {
 			fdflags: FD_APPEND,
 			expected: func(t *testing.T, fsc *sys.FSContext) {
 				contents := []byte("hello")
-				_, err := fsc.FdWriter(expectedOpenedFd).Write(contents)
+				_, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents)
 				require.NoError(t, err)
-				require.True(t, fsc.CloseFile(expectedOpenedFd))
+				require.NoError(t, fsc.CloseFile(expectedOpenedFd))
 
 				// verify the contents were appended
 				b := readFile(t, dir, appendName)
@@ -2353,9 +2317,9 @@ func Test_pathOpen(t *testing.T) {
 			expected: func(t *testing.T, fsc *sys.FSContext) {
 				// expect to create a new file
 				contents := []byte("hello")
-				_, err := fsc.FdWriter(expectedOpenedFd).Write(contents)
+				_, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents)
 				require.NoError(t, err)
-				require.True(t, fsc.CloseFile(expectedOpenedFd))
+				require.NoError(t, fsc.CloseFile(expectedOpenedFd))
 
 				// verify the contents were written
 				b := readFile(t, dir, "creat")
@@ -2385,9 +2349,9 @@ func Test_pathOpen(t *testing.T) {
 			expected: func(t *testing.T, fsc *sys.FSContext) {
 				// expect to create a new file
 				contents := []byte("hello")
-				_, err := fsc.FdWriter(expectedOpenedFd).Write(contents)
+				_, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents)
 				require.NoError(t, err)
-				require.True(t, fsc.CloseFile(expectedOpenedFd))
+				require.NoError(t, fsc.CloseFile(expectedOpenedFd))
 
 				// verify the contents were written
 				b := readFile(t, dir, path.Join(dirName, "O_CREAT-O_TRUNC"))
@@ -2404,7 +2368,7 @@ func Test_pathOpen(t *testing.T) {
 			oflags: O_DIRECTORY,
 			path:   func(*testing.T) string { return dirName },
 			expected: func(t *testing.T, fsc *sys.FSContext) {
-				stat, err := fsc.StatFile(expectedOpenedFd)
+				stat, err := sys.StatFile(fsc, expectedOpenedFd)
 				require.NoError(t, err)
 				require.True(t, stat.IsDir())
 			},
@@ -2419,7 +2383,7 @@ func Test_pathOpen(t *testing.T) {
 			path:   func(*testing.T) string { return dirName },
 			oflags: O_DIRECTORY,
 			expected: func(t *testing.T, fsc *sys.FSContext) {
-				stat, err := fsc.StatFile(expectedOpenedFd)
+				stat, err := sys.StatFile(fsc, expectedOpenedFd)
 				require.NoError(t, err)
 				require.True(t, stat.IsDir())
 			},
@@ -2446,9 +2410,9 @@ func Test_pathOpen(t *testing.T) {
 			oflags: O_TRUNC,
 			expected: func(t *testing.T, fsc *sys.FSContext) {
 				contents := []byte("hello")
-				_, err := fsc.FdWriter(expectedOpenedFd).Write(contents)
+				_, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents)
 				require.NoError(t, err)
-				require.True(t, fsc.CloseFile(expectedOpenedFd))
+				require.NoError(t, fsc.CloseFile(expectedOpenedFd))
 
 				// verify the contents were truncated
 				b := readFile(t, dir, "trunc")
@@ -2499,12 +2463,12 @@ func Test_pathOpen(t *testing.T) {
 
 func requireContents(t *testing.T, fsc *sys.FSContext, expectedOpenedFd uint32, fileName string, fileContents []byte) {
 	// verify the file was actually opened
-	f, ok := fsc.OpenedFile(expectedOpenedFd)
+	f, ok := fsc.LookupFile(expectedOpenedFd)
 	require.True(t, ok)
 	require.Equal(t, fileName, f.Name)
 
 	// verify the contents are readable
-	b, err := io.ReadAll(fsc.FdReader(expectedOpenedFd))
+	b, err := io.ReadAll(f.File)
 	require.NoError(t, err)
 	require.Equal(t, fileContents, b)
 }
@@ -2528,7 +2492,7 @@ func writeFile(t *testing.T, tmpDir, file string, contents []byte) {
 func Test_pathOpen_Errors(t *testing.T) {
 	validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
 	dirName := "wazero"
-	fileName := "notdir" // name length as wazero
+	fileName := "file" // name length as wazero
 	testFS := fstest.MapFS{
 		dirName:  &fstest.MapFile{Mode: os.ModeDir},
 		fileName: &fstest.MapFile{},
@@ -2563,18 +2527,6 @@ func Test_pathOpen_Errors(t *testing.T) {
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=OOM(65536,6),oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
 <== (opened_fd=,errno=EFAULT)
-`,
-		},
-		{
-			name:     "path invalid",
-			fd:       validFD,
-			pathName: "../foo",
-			pathLen:  6,
-			// fstest.MapFS returns file not found instead of invalid on invalid path
-			expectedErrno: ErrnoNoent,
-			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=../foo,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
-<== (opened_fd=,errno=ENOENT)
 `,
 		},
 		{
@@ -2619,10 +2571,10 @@ func Test_pathOpen_Errors(t *testing.T) {
 			fd:            validFD,
 			pathName:      fileName,
 			path:          validPath,
-			pathLen:       validPathLen,
+			pathLen:       uint32(len(fileName)),
 			expectedErrno: ErrnoNotdir,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=notdir,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=file,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
 <== (opened_fd=,errno=ENOTDIR)
 `,
 		},
@@ -2632,10 +2584,10 @@ func Test_pathOpen_Errors(t *testing.T) {
 			fd:            validFD,
 			pathName:      fileName,
 			path:          validPath,
-			pathLen:       validPathLen,
+			pathLen:       uint32(len(fileName)),
 			expectedErrno: ErrnoInval,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=notdir,oflags=CREAT|DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=file,oflags=CREAT|DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
 <== (opened_fd=,errno=EINVAL)
 `,
 		},
@@ -2741,17 +2693,6 @@ func Test_pathRemoveDirectory_Errors(t *testing.T) {
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=OOM(65536,1))
 <== errno=EFAULT
-`,
-		},
-		{
-			name:          "path invalid",
-			fd:            sys.FdRoot,
-			pathName:      "../foo",
-			pathLen:       6,
-			expectedErrno: ErrnoInval,
-			expectedLog: `
-==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=../foo)
-<== errno=EINVAL
 `,
 		},
 		{
@@ -2945,32 +2886,6 @@ func Test_pathRename_Errors(t *testing.T) {
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_rename(fd=3,old_path=a,new_fd=3,new_path=OOM(65536,1))
 <== errno=EFAULT
-`,
-		},
-		{
-			name:          "old path invalid",
-			oldFd:         sys.FdRoot,
-			newFd:         sys.FdRoot,
-			oldPathName:   "../foo",
-			oldPathLen:    6,
-			expectedErrno: ErrnoInval,
-			expectedLog: `
-==> wasi_snapshot_preview1.path_rename(fd=3,old_path=../foo,new_fd=3,new_path=)
-<== errno=EINVAL
-`,
-		},
-		{
-			name:          "new path invalid",
-			oldFd:         sys.FdRoot,
-			newFd:         sys.FdRoot,
-			oldPathName:   file,
-			oldPathLen:    uint32(len(file)),
-			newPathName:   "../foo",
-			newPathLen:    6,
-			expectedErrno: ErrnoInval,
-			expectedLog: `
-==> wasi_snapshot_preview1.path_rename(fd=3,old_path=../f,new_fd=3,new_path=../foo)
-<== errno=EINVAL
 `,
 		},
 		{
@@ -3120,17 +3035,6 @@ func Test_pathUnlinkFile_Errors(t *testing.T) {
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_unlink_file(fd=3,path=OOM(65536,1))
 <== errno=EFAULT
-`,
-		},
-		{
-			name:          "path invalid",
-			fd:            sys.FdRoot,
-			pathName:      "../foo",
-			pathLen:       6,
-			expectedErrno: ErrnoInval,
-			expectedLog: `
-==> wasi_snapshot_preview1.path_unlink_file(fd=3,path=../foo)
-<== errno=EINVAL
 `,
 		},
 		{
@@ -3191,6 +3095,7 @@ func requireOpenFile(t *testing.T, pathName string, data []byte) (api.Module, ui
 	testFS := fstest.MapFS{pathName[1:]: mapFile} // strip the leading slash
 	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	fsc := mod.(*wasm.CallContext).Sys.FS()
+
 	fd, err := fsc.OpenFile(pathName, os.O_RDONLY, 0)
 	require.NoError(t, err)
 	return mod, fd, log, r
@@ -3198,29 +3103,17 @@ func requireOpenFile(t *testing.T, pathName string, data []byte) (api.Module, ui
 
 // requireOpenWritableFile is temporary until we add the ability to open files for writing.
 func requireOpenWritableFile(t *testing.T, tmpDir string, pathName string) (api.Module, uint32, *bytes.Buffer, api.Closer) {
-	writeable, testFS := createWriteableFile(t, tmpDir, pathName, []byte{})
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
-	fsc := mod.(*wasm.CallContext).Sys.FS()
-	fd, err := fsc.OpenFile(pathName, os.O_RDWR, 0)
-	require.NoError(t, err)
-
-	// Swap the read-only file with a writeable one until #390
-	f, ok := fsc.OpenedFile(fd)
-	require.True(t, ok)
-	f.File.Close()
-	f.File = writeable
+	absolutePath := path.Join(tmpDir, pathName)
+	require.NoError(t, os.WriteFile(absolutePath, []byte{}, 0o600))
 
-	return mod, fd, log, r
-}
+	writeFS, err := syscallfs.NewDirFS(tmpDir)
+	require.NoError(t, err)
 
-// createWriteableFile uses real files when io.Writer tests are needed.
-func createWriteableFile(t *testing.T, tmpDir string, pathName string, data []byte) (fs.File, fs.FS) {
-	require.NotNil(t, data)
-	absolutePath := path.Join(tmpDir, pathName)
-	require.NoError(t, os.WriteFile(absolutePath, data, 0o600))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writeFS))
+	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	// open the file for writing in a custom way until #390
-	f, err := os.OpenFile(absolutePath, os.O_RDWR, 0o600)
+	fd, err := fsc.OpenFile(pathName, os.O_RDWR, 0o600)
 	require.NoError(t, err)
-	return f, os.DirFS(tmpDir)
+
+	return mod, fd, log, r
 }
diff --git a/imports/wasi_snapshot_preview1/wasi_bench_test.go b/imports/wasi_snapshot_preview1/wasi_bench_test.go
index bfebf86d..2de336bb 100644
--- a/imports/wasi_snapshot_preview1/wasi_bench_test.go
+++ b/imports/wasi_snapshot_preview1/wasi_bench_test.go
@@ -178,11 +178,11 @@ func Benchmark_fdReaddir(b *testing.B) {
 			if err != nil {
 				b.Fatal(err)
 			}
-			f, ok := fsc.OpenedFile(fd)
+			f, ok := fsc.LookupFile(fd)
 			if !ok {
 				b.Fatal("couldn't open fd ", fd)
 			}
-			defer fsc.CloseFile(fd)
+			defer fsc.CloseFile(fd) //nolint
 
 			b.ResetTimer()
 			b.ReportAllocs()
@@ -284,7 +284,7 @@ func Benchmark_pathFilestat(b *testing.B) {
 				if err != nil {
 					b.Fatal(err)
 				}
-				defer fsc.CloseFile(fd)
+				defer fsc.CloseFile(fd) //nolint
 			}
 
 			fn := mod.ExportedFunction(PathFilestatGetName)
diff --git a/internal/engine/interpreter/interpreter_test.go b/internal/engine/interpreter/interpreter_test.go
index ff58ac08..4689c362 100644
--- a/internal/engine/interpreter/interpreter_test.go
+++ b/internal/engine/interpreter/interpreter_test.go
@@ -92,7 +92,7 @@ func (e engineTester) NewEngine(enabledFeatures api.CoreFeatures) wasm.Engine {
 // CompiledFunctionPointerValue implements enginetest.EngineTester CompiledFunctionPointerValue.
 func (e engineTester) CompiledFunctionPointerValue(me wasm.ModuleEngine, funcIndex wasm.Index) uint64 {
 	internal := me.(*moduleEngine)
-	return uint64(uintptr(unsafe.Pointer(internal.functions[funcIndex])))
+	return uint64(uintptr(unsafe.Pointer(&internal.functions[funcIndex])))
 }
 
 func TestInterpreter_MemoryGrowInRecursiveCall(t *testing.T) {
@@ -365,7 +365,7 @@ func TestInterpreter_NonTrappingFloatToIntConversion(t *testing.T) {
 					ce := &callEngine{}
 					f := &function{
 						source: &wasm.FunctionInstance{Module: &wasm.ModuleInstance{Engine: &moduleEngine{}}},
-						body:   body,
+						parent: &code{body: body},
 					}
 					ce.callNativeFunc(testCtx, &wasm.CallContext{}, f)
 
@@ -428,11 +428,11 @@ func TestInterpreter_CallEngine_callNativeFunc_signExtend(t *testing.T) {
 				ce := &callEngine{}
 				f := &function{
 					source: &wasm.FunctionInstance{Module: &wasm.ModuleInstance{Engine: &moduleEngine{}}},
-					body: []*interpreterOp{
+					parent: &code{body: []*interpreterOp{
 						{kind: wazeroir.OperationKindConstI32, us: []uint64{uint64(uint32(tc.in))}},
 						{kind: translateToIROperationKind(tc.opcode)},
 						{kind: wazeroir.OperationKindBr, us: []uint64{math.MaxUint64}},
-					},
+					}},
 				}
 				ce.callNativeFunc(testCtx, &wasm.CallContext{}, f)
 				require.Equal(t, tc.expected, int32(uint32(ce.popValue())))
@@ -482,11 +482,11 @@ func TestInterpreter_CallEngine_callNativeFunc_signExtend(t *testing.T) {
 				ce := &callEngine{}
 				f := &function{
 					source: &wasm.FunctionInstance{Module: &wasm.ModuleInstance{Engine: &moduleEngine{}}},
-					body: []*interpreterOp{
+					parent: &code{body: []*interpreterOp{
 						{kind: wazeroir.OperationKindConstI64, us: []uint64{uint64(tc.in)}},
 						{kind: translateToIROperationKind(tc.opcode)},
 						{kind: wazeroir.OperationKindBr, us: []uint64{math.MaxUint64}},
-					},
+					}},
 				}
 				ce.callNativeFunc(testCtx, &wasm.CallContext{}, f)
 				require.Equal(t, tc.expected, int64(ce.popValue()))
diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go
index 94827b22..cf1a64d9 100644
--- a/internal/gojs/compiler_test.go
+++ b/internal/gojs/compiler_test.go
@@ -22,6 +22,7 @@ import (
 	gojs "github.com/tetratelabs/wazero/imports/go"
 	internalgojs "github.com/tetratelabs/wazero/internal/gojs"
 	"github.com/tetratelabs/wazero/internal/gojs/run"
+	"github.com/tetratelabs/wazero/internal/syscallfs"
 )
 
 func compileAndRun(ctx context.Context, arg string, config wazero.ModuleConfig) (stdout, stderr string, err error) {
@@ -65,12 +66,12 @@ var testBin []byte
 // testCtx is configured in TestMain to re-use wazero's compilation cache.
 var (
 	testCtx context.Context
-	testFS  = fstest.MapFS{
+	testFS  = syscallfs.Adapt(fstest.MapFS{
 		"empty.txt":    {},
 		"test.txt":     {Data: []byte("animals\n"), Mode: 0o644},
 		"sub":          {Mode: fs.ModeDir | 0o755},
 		"sub/test.txt": {Data: []byte("greet sub dir\n"), Mode: 0o444},
-	}
+	})
 	rt wazero.Runtime
 )
 
diff --git a/internal/gojs/values/values_test.go b/internal/gojs/values/values_test.go
new file mode 100644
index 00000000..552683e2
--- /dev/null
+++ b/internal/gojs/values/values_test.go
@@ -0,0 +1,51 @@
+package values
+
+import (
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/gojs/goos"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func Test_Values(t *testing.T) {
+	t.Parallel()
+
+	vs := NewValues()
+
+	err := require.CapturePanic(func() {
+		_ = vs.Get(goos.NextID)
+	})
+	require.EqualError(t, err, "id 18 is out of range 0")
+
+	v1 := "foo"
+	id1 := vs.Increment(v1)
+	v2 := "bar"
+	id2 := vs.Increment(v2)
+
+	require.Equal(t, goos.NextID, id1)
+	require.Equal(t, v1, vs.Get(id1))
+
+	// Second value should be at a sequential position
+	require.Equal(t, id1+1, id2)
+	require.Equal(t, v2, vs.Get(id2))
+
+	// Incrementing the ref count should return the same ID
+	require.Equal(t, id1, vs.Increment(v1))
+	require.Equal(t, v1, vs.Get(id1))
+
+	// Decrement and we should still get the value
+	vs.Decrement(id1)
+	require.Equal(t, v1, vs.Get(id1))
+
+	// Decrement again, and we should panic, as go should never attempt to
+	// get a value it already decremented to zero.
+	vs.Decrement(id1)
+	err = require.CapturePanic(func() {
+		_ = vs.Get(id1)
+	})
+	require.EqualError(t, err, "value for 18 was nil")
+
+	// Since the ID is no longer in use, we should be able to revive it.
+	require.Equal(t, id1, vs.Increment(v1))
+	require.Equal(t, v1, vs.Get(id1))
+}
diff --git a/internal/integration_test/engine/adhoc_test.go b/internal/integration_test/engine/adhoc_test.go
index 7872dcc8..814477e6 100644
--- a/internal/integration_test/engine/adhoc_test.go
+++ b/internal/integration_test/engine/adhoc_test.go
@@ -11,6 +11,7 @@ import (
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/platform"
+	"github.com/tetratelabs/wazero/internal/testing/proxy"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wasm/binary"
@@ -42,6 +43,7 @@ var tests = map[string]func(t *testing.T, r wazero.Runtime){
 	"import functions with reference type in signature": testReftypeImports,
 	"overflow integer addition":                         testOverflow,
 	"un-signed extend global":                           testGlobalExtend,
+	"user-defined primitive in host func":               testUserDefinedPrimitiveHostFunc,
 }
 
 func TestEngineCompiler(t *testing.T) {
@@ -85,6 +87,36 @@ var (
 	globalExtendWasm []byte
 )
 
+func testUserDefinedPrimitiveHostFunc(t *testing.T, r wazero.Runtime) {
+	type u32 uint32
+	type u64 uint64
+	type f32 float32
+	type f64 float64
+
+	const fn = "fn"
+	hostCompiled, err := r.NewHostModuleBuilder("").NewFunctionBuilder().
+		WithFunc(func(u1 u32, u2 u64, f1 f32, f2 f64) u64 {
+			return u64(u1) + u2 + u64(math.Float32bits(float32(f1))) + u64(math.Float64bits(float64(f2)))
+		}).Export(fn).Compile(testCtx)
+	require.NoError(t, err)
+
+	_, err = r.InstantiateModule(testCtx, hostCompiled, wazero.NewModuleConfig())
+	require.NoError(t, err)
+
+	proxyBin := proxy.NewModuleBinary("", hostCompiled)
+
+	mod, err := r.InstantiateModuleFromBinary(testCtx, proxyBin)
+	require.NoError(t, err)
+
+	f := mod.ExportedFunction(fn)
+	require.NotNil(t, f)
+
+	const u1, u2, f1, f2 = 1, 2, float32(1234.123), 5431.123
+	res, err := f.Call(context.Background(), uint64(u1), uint64(u2), uint64(math.Float32bits(f1)), math.Float64bits(f2))
+	require.NoError(t, err)
+	require.Equal(t, res[0], uint64(u1)+uint64(u2)+uint64(math.Float32bits(f1))+math.Float64bits(f2))
+}
+
 func testReftypeImports(t *testing.T, r wazero.Runtime) {
 	type dog struct {
 		name string
diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go
index 7ef2890b..f80f87bd 100644
--- a/internal/sys/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -7,10 +7,10 @@ import (
 	"io"
 	"io/fs"
 	"os"
-	"path"
 	"testing"
 	"testing/fstest"
 
+	"github.com/tetratelabs/wazero/internal/syscallfs"
 	testfs "github.com/tetratelabs/wazero/internal/testing/fs"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
@@ -31,26 +31,31 @@ func TestNewFSContext(t *testing.T) {
 	embedFS, err := fs.Sub(testdata, "testdata")
 	require.NoError(t, err)
 
+	dirfs, err := syscallfs.NewDirFS(".")
+	require.NoError(t, err)
+
 	// Test various usual configuration for the file system.
 	tests := []struct {
-		name         string
-		fs           fs.FS
-		expectOsFile bool
+		name string
+		fs   syscallfs.FS
 	}{
 		{
 			name: "embed.FS",
-			fs:   embedFS,
+			fs:   syscallfs.Adapt(embedFS),
 		},
 		{
-			name: "os.DirFS",
+			name: "syscallfs.NewDirFS",
 			// Don't use "testdata" because it may not be present in
 			// cross-architecture (a.k.a. scratch) build containers.
-			fs:           os.DirFS("."),
-			expectOsFile: true,
+			fs: dirfs,
+		},
+		{
+			name: "syscallfs.NewReadFS",
+			fs:   syscallfs.NewReadFS(dirfs),
 		},
 		{
 			name: "fstest.MapFS",
-			fs:   fstest.MapFS{},
+			fs:   syscallfs.Adapt(fstest.MapFS{}),
 		},
 	}
 
@@ -67,17 +72,11 @@ func TestNewFSContext(t *testing.T) {
 			require.NotNil(t, rootFile)
 			require.Equal(t, "/", rootFile.Name)
 
-			_, osFile := rootFile.File.(*os.File)
-			require.Equal(t, tc.expectOsFile, osFile)
-
-			f0, err := fsc.OpenFile("/", 0, 0)
-			require.NoError(t, err)
-
 			// Verify that each call to OpenFile returns a different file
 			// descriptor.
 			f1, err := fsc.OpenFile("/", 0, 0)
 			require.NoError(t, err)
-			require.NotEqual(t, f0, f1)
+			require.NotEqual(t, FdRoot, f1)
 
 			// Verify that file descriptors are reused.
 			//
@@ -87,119 +86,38 @@ func TestNewFSContext(t *testing.T) {
 			// test to ensure that our implementation properly reuses descriptor
 			// numbers but if we were to change the reuse strategy, this test
 			// would likely break and need to be updated.
-			require.True(t, fsc.CloseFile(f0))
+			require.NoError(t, fsc.CloseFile(f1))
 			f2, err := fsc.OpenFile("/", 0, 0)
 			require.NoError(t, err)
-			require.Equal(t, f0, f2)
+			require.Equal(t, f1, f2)
 		})
 	}
 }
 
-func TestEmptyFS(t *testing.T) {
-	testFS := EmptyFS
-
-	t.Run("validates path", func(t *testing.T) {
-		f, err := testFS.Open("/foo.txt")
-		require.Nil(t, f)
-		require.EqualError(t, err, "open /foo.txt: invalid argument")
-	})
-
-	t.Run("path not found", func(t *testing.T) {
-		f, err := testFS.Open("foo.txt")
-		require.Nil(t, f)
-		require.EqualError(t, err, "open foo.txt: file does not exist")
-	})
-}
-
 func TestEmptyFSContext(t *testing.T) {
-	testFS, err := NewFSContext(nil, nil, nil, EmptyFS)
+	testFS, err := NewFSContext(nil, nil, nil, syscallfs.EmptyFS)
 	require.NoError(t, err)
 
-	expected := &FSContext{fs: EmptyFS}
+	expected := &FSContext{fs: syscallfs.EmptyFS}
 	expected.openedFiles.Insert(noopStdin)
 	expected.openedFiles.Insert(noopStdout)
 	expected.openedFiles.Insert(noopStderr)
 
-	t.Run("OpenFile doesn't affect state", func(t *testing.T) {
-		fd, err := testFS.OpenFile("foo.txt", os.O_RDONLY, 0)
-		require.Zero(t, fd)
-		require.EqualError(t, err, "open foo.txt: file does not exist")
-
-		// Ensure this didn't modify state
-		require.Equal(t, expected, testFS)
-	})
-
 	t.Run("Close closes", func(t *testing.T) {
 		err := testFS.Close(testCtx)
 		require.NoError(t, err)
 
 		// Closes opened files
-		require.Equal(t, &FSContext{fs: EmptyFS}, testFS)
+		require.Equal(t, &FSContext{fs: syscallfs.EmptyFS}, testFS)
 	})
 }
 
-func TestContext_File(t *testing.T) {
-	embedFS, err := fs.Sub(testdata, "testdata")
-	require.NoError(t, err)
+func TestContext_Close(t *testing.T) {
+	testFS := syscallfs.Adapt(testfs.FS{"foo": &testfs.File{}})
 
-	fsc, err := NewFSContext(nil, nil, nil, embedFS)
+	fsc, err := NewFSContext(nil, nil, nil, testFS)
 	require.NoError(t, err)
-	defer fsc.Close(testCtx)
-
-	tests := []struct {
-		name     string
-		expected string
-	}{
-		{
-			name: "empty.txt",
-		},
-		{
-			name:     "test.txt",
-			expected: "animals\n",
-		},
-		{
-			name:     "sub/test.txt",
-			expected: "greet sub dir\n",
-		},
-		{
-			name:     "sub/sub/test.txt",
-			expected: "greet sub sub dir\n",
-		},
-	}
-
-	for _, tt := range tests {
-		tc := tt
-
-		t.Run(tc.name, func(b *testing.T) {
-			fd, err := fsc.OpenFile(tc.name, os.O_RDONLY, 0)
-			require.NoError(t, err)
-			defer fsc.CloseFile(fd)
-
-			f, ok := fsc.OpenedFile(fd)
-			require.True(t, ok)
 
-			stat, err := f.File.Stat()
-			require.NoError(t, err)
-
-			// Ensure the name is the basename and matches the stat name.
-			require.Equal(t, path.Base(tc.name), f.Name)
-			require.Equal(t, f.Name, stat.Name())
-
-			buf := make([]byte, stat.Size())
-			size, err := f.File.Read(buf)
-			if err != nil {
-				require.Equal(t, io.EOF, err)
-			}
-			require.Equal(t, stat.Size(), int64(size))
-
-			require.Equal(t, tc.expected, string(buf[:size]))
-		})
-	}
-}
-
-func TestContext_Close(t *testing.T) {
-	fsc, err := NewFSContext(nil, nil, nil, testfs.FS{"foo": &testfs.File{}})
-	require.NoError(t, err)
 	// Verify base case
 	require.Equal(t, 1+FdRoot, uint32(fsc.openedFiles.Len()))
 
@@ -219,7 +137,10 @@ func TestContext_Close(t *testing.T) {
 
 func TestContext_Close_Error(t *testing.T) {
 	file := &testfs.File{CloseErr: errors.New("error closing")}
-	fsc, err := NewFSContext(nil, nil, nil, testfs.FS{"foo": file})
+
+	testFS := syscallfs.Adapt(testfs.FS{"foo": file})
+
+	fsc, err := NewFSContext(nil, nil, nil, testFS)
 	require.NoError(t, err)
 
 	// open another file
diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go
index 81f38a08..e4e8114d 100644
--- a/internal/sys/sys_test.go
+++ b/internal/sys/sys_test.go
@@ -6,15 +6,16 @@ import (
 	"time"
 
 	"github.com/tetratelabs/wazero/internal/platform"
+	"github.com/tetratelabs/wazero/internal/syscallfs"
 	testfs "github.com/tetratelabs/wazero/internal/testing/fs"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/sys"
 )
 
 func TestContext_FS(t *testing.T) {
-	sysCtx := DefaultContext(testfs.FS{})
+	sysCtx := DefaultContext(syscallfs.EmptyFS)
 
-	fsc, err := NewFSContext(nil, nil, nil, testfs.FS{})
+	fsc, err := NewFSContext(nil, nil, nil, syscallfs.EmptyFS)
 	require.NoError(t, err)
 
 	require.Equal(t, fsc, sysCtx.FS())
@@ -50,7 +51,8 @@ func TestDefaultSysContext(t *testing.T) {
 	require.Equal(t, &ns, sysCtx.nanosleep)
 	require.Equal(t, platform.NewFakeRandSource(), sysCtx.RandSource())
 
-	expectedFS, _ := NewFSContext(nil, nil, nil, testfs.FS{})
+	testFS := syscallfs.Adapt(testfs.FS{})
+	expectedFS, _ := NewFSContext(nil, nil, nil, testFS)
 
 	expectedOpenedFiles := FileTable{}
 	expectedOpenedFiles.Insert(noopStdin)
diff --git a/internal/syscallfs/adapter_test.go b/internal/syscallfs/adapter_test.go
new file mode 100644
index 00000000..bc4d5abf
--- /dev/null
+++ b/internal/syscallfs/adapter_test.go
@@ -0,0 +1,99 @@
+package syscallfs
+
+import (
+	"errors"
+	"io/fs"
+	"os"
+	pathutil "path"
+	"syscall"
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func TestAdapt_MkDir(t *testing.T) {
+	dir := t.TempDir()
+
+	testFS := Adapt(os.DirFS(dir))
+
+	err := testFS.Mkdir("mkdir", fs.ModeDir)
+	require.Equal(t, syscall.ENOSYS, err)
+}
+
+func TestAdapt_Rename(t *testing.T) {
+	tmpDir := t.TempDir()
+	testFS := Adapt(os.DirFS(tmpDir))
+
+	file1 := "file1"
+	file1Path := pathutil.Join(tmpDir, file1)
+	file1Contents := []byte{1}
+	err := os.WriteFile(file1Path, file1Contents, 0o600)
+	require.NoError(t, err)
+
+	file2 := "file2"
+	file2Path := pathutil.Join(tmpDir, file2)
+	file2Contents := []byte{2}
+	err = os.WriteFile(file2Path, file2Contents, 0o600)
+	require.NoError(t, err)
+
+	err = testFS.Rename(file1, file2)
+	require.Equal(t, syscall.ENOSYS, err)
+}
+
+func TestAdapt_Rmdir(t *testing.T) {
+	dir := t.TempDir()
+
+	testFS := Adapt(os.DirFS(dir))
+
+	path := "rmdir"
+	realPath := pathutil.Join(dir, path)
+	require.NoError(t, os.Mkdir(realPath, 0o700))
+
+	err := testFS.Rmdir(path)
+	require.Equal(t, syscall.ENOSYS, err)
+}
+
+func TestAdapt_Unlink(t *testing.T) {
+	dir := t.TempDir()
+
+	testFS := Adapt(os.DirFS(dir))
+
+	path := "unlink"
+	realPath := pathutil.Join(dir, path)
+	require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
+
+	err := testFS.Unlink(path)
+	require.Equal(t, syscall.ENOSYS, err)
+}
+
+func TestAdapt_Utimes(t *testing.T) {
+	dir := t.TempDir()
+
+	testFS := Adapt(os.DirFS(dir))
+
+	path := "utimes"
+	realPath := pathutil.Join(dir, path)
+	require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
+
+	err := testFS.Utimes(path, 1, 1)
+	require.Equal(t, syscall.ENOSYS, err)
+}
+
+func TestAdapt_Open_Read(t *testing.T) {
+	tmpDir := t.TempDir()
+
+	// Create a subdirectory, so we can test reads outside the FS root.
+	tmpDir = pathutil.Join(tmpDir, t.Name())
+	require.NoError(t, os.Mkdir(tmpDir, 0o700))
+
+	testFS := Adapt(os.DirFS(tmpDir))
+
+	testFS_Open_Read(t, tmpDir, testFS)
+
+	t.Run("path outside root invalid", func(t *testing.T) {
+		_, err := testFS.OpenFile("../foo", os.O_RDONLY, 0)
+
+		// fs.FS doesn't allow relative path lookups
+		require.True(t, errors.Is(err, fs.ErrInvalid))
+	})
+}
diff --git a/internal/syscallfs/dirfs_test.go b/internal/syscallfs/dirfs_test.go
index 4c09e590..86178ab5 100644
--- a/internal/syscallfs/dirfs_test.go
+++ b/internal/syscallfs/dirfs_test.go
@@ -4,7 +4,7 @@ import (
 	"errors"
 	"io/fs"
 	"os"
-	"path"
+	pathutil "path"
 	"runtime"
 	"syscall"
 	"testing"
@@ -18,7 +18,7 @@ func TestDirFS_MkDir(t *testing.T) {
 	testFS := dirFS(dir)
 
 	name := "mkdir"
-	realPath := path.Join(dir, name)
+	realPath := pathutil.Join(dir, name)
 
 	t.Run("doesn't exist", func(t *testing.T) {
 		require.NoError(t, testFS.Mkdir(name, fs.ModeDir))
@@ -48,7 +48,7 @@ func TestDirFS_Rename(t *testing.T) {
 		testFS := dirFS(tmpDir)
 
 		file1 := "file1"
-		file1Path := path.Join(tmpDir, file1)
+		file1Path := pathutil.Join(tmpDir, file1)
 		err := os.WriteFile(file1Path, []byte{1}, 0o600)
 		require.NoError(t, err)
 
@@ -60,13 +60,13 @@ func TestDirFS_Rename(t *testing.T) {
 		testFS := dirFS(tmpDir)
 
 		file1 := "file1"
-		file1Path := path.Join(tmpDir, file1)
+		file1Path := pathutil.Join(tmpDir, file1)
 		file1Contents := []byte{1}
 		err := os.WriteFile(file1Path, file1Contents, 0o600)
 		require.NoError(t, err)
 
 		file2 := "file2"
-		file2Path := path.Join(tmpDir, file2)
+		file2Path := pathutil.Join(tmpDir, file2)
 		err = testFS.Rename(file1, file2)
 		require.NoError(t, err)
 
@@ -83,11 +83,11 @@ func TestDirFS_Rename(t *testing.T) {
 		testFS := dirFS(tmpDir)
 
 		dir1 := "dir1"
-		dir1Path := path.Join(tmpDir, dir1)
+		dir1Path := pathutil.Join(tmpDir, dir1)
 		require.NoError(t, os.Mkdir(dir1Path, 0o700))
 
 		dir2 := "dir2"
-		dir2Path := path.Join(tmpDir, dir2)
+		dir2Path := pathutil.Join(tmpDir, dir2)
 		err := testFS.Rename(dir1, dir2)
 		require.NoError(t, err)
 
@@ -104,11 +104,11 @@ func TestDirFS_Rename(t *testing.T) {
 		testFS := dirFS(tmpDir)
 
 		dir1 := "dir1"
-		dir1Path := path.Join(tmpDir, dir1)
+		dir1Path := pathutil.Join(tmpDir, dir1)
 		require.NoError(t, os.Mkdir(dir1Path, 0o700))
 
 		dir2 := "dir2"
-		dir2Path := path.Join(tmpDir, dir2)
+		dir2Path := pathutil.Join(tmpDir, dir2)
 
 		// write a file to that path
 		err := os.WriteFile(dir2Path, []byte{2}, 0o600)
@@ -131,13 +131,13 @@ func TestDirFS_Rename(t *testing.T) {
 		testFS := dirFS(tmpDir)
 
 		file1 := "file1"
-		file1Path := path.Join(tmpDir, file1)
+		file1Path := pathutil.Join(tmpDir, file1)
 		file1Contents := []byte{1}
 		err := os.WriteFile(file1Path, file1Contents, 0o600)
 		require.NoError(t, err)
 
 		dir1 := "dir1"
-		dir1Path := path.Join(tmpDir, dir1)
+		dir1Path := pathutil.Join(tmpDir, dir1)
 		require.NoError(t, os.Mkdir(dir1Path, 0o700))
 
 		err = testFS.Rename(file1, dir1)
@@ -148,18 +148,18 @@ func TestDirFS_Rename(t *testing.T) {
 		testFS := dirFS(tmpDir)
 
 		dir1 := "dir1"
-		dir1Path := path.Join(tmpDir, dir1)
+		dir1Path := pathutil.Join(tmpDir, dir1)
 		require.NoError(t, os.Mkdir(dir1Path, 0o700))
 
 		// add a file to that directory
 		file1 := "file1"
-		file1Path := path.Join(dir1Path, file1)
+		file1Path := pathutil.Join(dir1Path, file1)
 		file1Contents := []byte{1}
 		err := os.WriteFile(file1Path, file1Contents, 0o600)
 		require.NoError(t, err)
 
 		dir2 := "dir2"
-		dir2Path := path.Join(tmpDir, dir2)
+		dir2Path := pathutil.Join(tmpDir, dir2)
 		require.NoError(t, os.Mkdir(dir2Path, 0o700))
 
 		err = testFS.Rename(dir1, dir2)
@@ -175,7 +175,7 @@ func TestDirFS_Rename(t *testing.T) {
 		require.Equal(t, syscall.ENOENT, errors.Unwrap(err))
 
 		// Show the file inside that directory moved
-		s, err := os.Stat(path.Join(dir2Path, file1))
+		s, err := os.Stat(pathutil.Join(dir2Path, file1))
 		require.NoError(t, err)
 		require.False(t, s.IsDir())
 	})
@@ -184,13 +184,13 @@ func TestDirFS_Rename(t *testing.T) {
 		testFS := dirFS(tmpDir)
 
 		file1 := "file1"
-		file1Path := path.Join(tmpDir, file1)
+		file1Path := pathutil.Join(tmpDir, file1)
 		file1Contents := []byte{1}
 		err := os.WriteFile(file1Path, file1Contents, 0o600)
 		require.NoError(t, err)
 
 		file2 := "file2"
-		file2Path := path.Join(tmpDir, file2)
+		file2Path := pathutil.Join(tmpDir, file2)
 		file2Contents := []byte{2}
 		err = os.WriteFile(file2Path, file2Contents, 0o600)
 		require.NoError(t, err)
@@ -212,7 +212,7 @@ func TestDirFS_Rename(t *testing.T) {
 		testFS := dirFS(tmpDir)
 
 		dir1 := "dir1"
-		dir1Path := path.Join(tmpDir, dir1)
+		dir1Path := pathutil.Join(tmpDir, dir1)
 		require.NoError(t, os.Mkdir(dir1Path, 0o700))
 
 		err := testFS.Rename(dir1, dir1)
@@ -227,7 +227,7 @@ func TestDirFS_Rename(t *testing.T) {
 		testFS := dirFS(tmpDir)
 
 		file1 := "file1"
-		file1Path := path.Join(tmpDir, file1)
+		file1Path := pathutil.Join(tmpDir, file1)
 		file1Contents := []byte{1}
 		err := os.WriteFile(file1Path, file1Contents, 0o600)
 		require.NoError(t, err)
@@ -247,7 +247,7 @@ func TestDirFS_Rmdir(t *testing.T) {
 	testFS := dirFS(dir)
 
 	name := "rmdir"
-	realPath := path.Join(dir, name)
+	realPath := pathutil.Join(dir, name)
 
 	t.Run("doesn't exist", func(t *testing.T) {
 		err := testFS.Rmdir(name)
@@ -256,7 +256,7 @@ func TestDirFS_Rmdir(t *testing.T) {
 
 	t.Run("dir not empty", func(t *testing.T) {
 		require.NoError(t, os.Mkdir(realPath, 0o700))
-		fileInDir := path.Join(realPath, "file")
+		fileInDir := pathutil.Join(realPath, "file")
 		require.NoError(t, os.WriteFile(fileInDir, []byte{}, 0o600))
 
 		err := testFS.Rmdir(name)
@@ -287,7 +287,7 @@ func TestDirFS_Unlink(t *testing.T) {
 	testFS := dirFS(dir)
 
 	name := "unlink"
-	realPath := path.Join(dir, name)
+	realPath := pathutil.Join(dir, name)
 
 	t.Run("doesn't exist", func(t *testing.T) {
 		err := testFS.Unlink(name)
@@ -323,7 +323,18 @@ func TestDirFS_Utimes(t *testing.T) {
 func TestDirFS_Open_Read(t *testing.T) {
 	tmpDir := t.TempDir()
 
-	testFS := dirFS(tmpDir)
+	// Create a subdirectory, so we can test reads outside the FS root.
+	tmpDir = pathutil.Join(tmpDir, t.Name())
+	require.NoError(t, os.Mkdir(tmpDir, 0o700))
+
+	testFS := Adapt(dirFS(tmpDir))
 
 	testFS_Open_Read(t, tmpDir, testFS)
+
+	t.Run("path outside root valid", func(t *testing.T) {
+		_, err := testFS.OpenFile("../foo", os.O_RDONLY, 0)
+
+		// syscall.FS allows relative path lookups
+		require.True(t, errors.Is(err, fs.ErrNotExist))
+	})
 }
diff --git a/internal/syscallfs/readfs_test.go b/internal/syscallfs/readfs_test.go
index e08d4b9f..34c22a11 100644
--- a/internal/syscallfs/readfs_test.go
+++ b/internal/syscallfs/readfs_test.go
@@ -3,7 +3,7 @@ package syscallfs
 import (
 	"io/fs"
 	"os"
-	"path"
+	pathutil "path"
 	"syscall"
 	"testing"
 
@@ -24,13 +24,13 @@ func TestReadFS_Rename(t *testing.T) {
 	testFS := NewReadFS(dirFS(tmpDir))
 
 	file1 := "file1"
-	file1Path := path.Join(tmpDir, file1)
+	file1Path := pathutil.Join(tmpDir, file1)
 	file1Contents := []byte{1}
 	err := os.WriteFile(file1Path, file1Contents, 0o600)
 	require.NoError(t, err)
 
 	file2 := "file2"
-	file2Path := path.Join(tmpDir, file2)
+	file2Path := pathutil.Join(tmpDir, file2)
 	file2Contents := []byte{2}
 	err = os.WriteFile(file2Path, file2Contents, 0o600)
 	require.NoError(t, err)
@@ -44,11 +44,11 @@ func TestReadFS_Rmdir(t *testing.T) {
 
 	testFS := NewReadFS(dirFS(dir))
 
-	name := "rmdir"
-	realPath := path.Join(dir, name)
+	path := "rmdir"
+	realPath := pathutil.Join(dir, path)
 	require.NoError(t, os.Mkdir(realPath, 0o700))
 
-	err := testFS.Rmdir(name)
+	err := testFS.Rmdir(path)
 	require.Equal(t, syscall.ENOSYS, err)
 }
 
@@ -57,20 +57,25 @@ func TestReadFS_Unlink(t *testing.T) {
 
 	testFS := NewReadFS(dirFS(dir))
 
-	name := "unlink"
-	realPath := path.Join(dir, name)
+	path := "unlink"
+	realPath := pathutil.Join(dir, path)
 	require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
 
-	err := testFS.Unlink(name)
+	err := testFS.Unlink(path)
 	require.Equal(t, syscall.ENOSYS, err)
 }
 
 func TestReadFS_Utimes(t *testing.T) {
-	tmpDir := t.TempDir()
+	dir := t.TempDir()
 
-	testFS := NewReadFS(dirFS(tmpDir))
+	testFS := NewReadFS(dirFS(dir))
 
-	testFS_Utimes(t, tmpDir, testFS)
+	path := "utimes"
+	realPath := pathutil.Join(dir, path)
+	require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
+
+	err := testFS.Utimes(path, 1, 1)
+	require.Equal(t, syscall.ENOSYS, err)
 }
 
 func TestReadFS_Open_Read(t *testing.T) {
diff --git a/internal/wasm/call_context_test.go b/internal/wasm/call_context_test.go
index ce10b47f..719e4299 100644
--- a/internal/wasm/call_context_test.go
+++ b/internal/wasm/call_context_test.go
@@ -155,14 +155,14 @@ func TestCallContext_Close(t *testing.T) {
 
 		// We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go).
 		// One side effect of Context.Close is that it clears the openedFiles map. Verify our base case.
-		_, ok := fsCtx.OpenedFile(3)
+		_, ok := fsCtx.LookupFile(3)
 		require.True(t, ok, "sysCtx.openedFiles was empty")
 
 		// Closing should not err.
 		require.NoError(t, m.Close(testCtx))
 
 		// Verify our intended side-effect
-		_, ok = fsCtx.OpenedFile(3)
+		_, ok = fsCtx.LookupFile(3)
 		require.False(t, ok, "expected no opened files")
 
 		// Verify no error closing again.
@@ -184,7 +184,7 @@ func TestCallContext_Close(t *testing.T) {
 		require.EqualError(t, m.Close(testCtx), "error closing")
 
 		// Verify our intended side-effect
-		_, ok := fsCtx.OpenedFile(3)
+		_, ok := fsCtx.LookupFile(3)
 		require.False(t, ok, "expected no opened files")
 	})
 }
@@ -251,14 +251,14 @@ func TestCallContext_CallDynamic(t *testing.T) {
 
 		// We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go).
 		// One side effect of Context.Close is that it clears the openedFiles map. Verify our base case.
-		_, ok := fsCtx.OpenedFile(3)
+		_, ok := fsCtx.LookupFile(3)
 		require.True(t, ok, "sysCtx.openedFiles was empty")
 
 		// Closing should not err.
 		require.NoError(t, m.Close(testCtx))
 
 		// Verify our intended side-effect
-		_, ok = fsCtx.OpenedFile(3)
+		_, ok = fsCtx.LookupFile(3)
 		require.False(t, ok, "expected no opened files")
 
 		// Verify no error closing again.
@@ -271,7 +271,8 @@ func TestCallContext_CallDynamic(t *testing.T) {
 		sysCtx := sys.DefaultContext(testFS)
 		fsCtx := sysCtx.FS()
 
-		_, err := fsCtx.OpenFile("/foo", os.O_RDONLY, 0)
+		path := "/foo"
+		_, err := fsCtx.OpenFile(path, os.O_RDONLY, 0)
 		require.NoError(t, err)
 
 		m, err := s.Instantiate(context.Background(), ns, &Module{}, t.Name(), sysCtx)
@@ -280,7 +281,7 @@ func TestCallContext_CallDynamic(t *testing.T) {
 		require.EqualError(t, m.Close(testCtx), "error closing")
 
 		// Verify our intended side-effect
-		_, ok := fsCtx.OpenedFile(3)
+		_, ok := fsCtx.LookupFile(3)
 		require.False(t, ok, "expected no opened files")
 	})
 }