Repository: tetratelabs/wazero
Pull request #394 (2022-03-18 08:03:16)

wasi: replaces existing filesystem apis with fs.FS
This changes `SysConfig.WithFS` and `SysConfig.WithWorkDirFS` to accept an `fs.FS` for the root ("/") and working directory (".") file systems. This allows end users to leverage standard libraries that adhere to this, including OS, HTTP and embedded filesystems.

For example, here's how you can allow WebAssembly modules to read
"/work/home/a.txt" as "/a.txt" or "./a.txt":
```go
wasi, err := r.InstantiateModule(wazero.WASISnapshotPreview1())
defer wasi.Close()

sysConfig := wazero.NewSysConfig().WithFS(os.DirFS("/work/home"))
module, err := wazero.StartWASICommandWithConfig(r, compiled, sysConfig)
defer module.Close()
...
```

Note: Further development will happen after this, for example to allow creation of new directories, though priority of that is demand-based. If you need further FS features, you should ask on https://github.com/tetratelabs/wazero/issues/390 with your use-case!


codefromthecrypt: @nullpo-head would you mind pushing a commit with `make build.examples` from linux? darwin output is massive
codefromthecrypt: @nullpo-head would you mind pushing a commit with `make build.examples` from linux? darwin output is massive
nullpo-head: > @nullpo-head would you mind pushing a commit with `make build.examples` from linux? darwin output is massive

Do you mean files such as "cat.wasm", right? I tried but the size of the output was the same. I guess the target platform is wasm, so the output result of TinyGo doesn't change much on different platform?
codefromthecrypt: > Do you mean files such as "cat.wasm", right? I tried but the size of the output was the same. I guess the target platform is wasm, so the output result of TinyGo doesn't change much on different platform?

OK yeah I think once it made a lot of difference. I rolled back the println on error as that increased the size by 140KB
codefromthecrypt: lol there's no /tmp on scratch @mathetake  :) I'll try mounting a volume..

```
 --- FAIL: TestSnapshotPreview1_FdWrite (0.00s)
    --- FAIL: TestSnapshotPreview1_FdWrite/SnapshotPreview1.FdWrite (0.00s)
        wasi_test.go:2180: TempDir: stat /tmp: no such file or directory
    --- FAIL: TestSnapshotPreview1_FdWrite/fd_write (0.00s)
        wasi_test.go:2180: TempDir: stat /tmp: no such file or directory
--- FAIL: TestSnapshotPreview1_FdWrite_Errors (0.00s)
    wasi_test.go:2180: TempDir: stat /tmp: no such file or directory
```
codefromthecrypt: I'll look into the windows stuff tomorrow. I remember some of this ordering issue in func-e tests, just not exactly how we unraveled it.

```
--- FAIL: TestSnapshotPreview1_FdWrite (0.00s)
    --- FAIL: TestSnapshotPreview1_FdWrite/SnapshotPreview1.FdWrite (0.00s)
        testing.go:967: TempDir RemoveAll cleanup: remove C:\Users\RUNNER~1\AppData\Local\Temp\TestSnapshotPreview1_FdWriteSnapshotPreview1.FdWrite603158801\001\arbitrary: The process cannot access the file because it is being used by another process.
    --- FAIL: TestSnapshotPreview1_FdWrite/fd_write (0.00s)
        testing.go:967: TempDir RemoveAll cleanup: remove C:\Users\RUNNER~1\AppData\Local\Temp\TestSnapshotPreview1_FdWritefd_write2424343077\001\arbitrary: The process cannot access the file because it is being used by another process.
--- FAIL: TestSnapshotPreview1_FdWrite_Errors (0.00s)
    testing.go:967: TempDir RemoveAll cleanup: remove C:\Users\RUNNER~1\AppData\Local\Temp\TestSnapshotPreview1_FdWrite_Errors4036609832\001\arbitrary: The process cannot access the file because it is being used by another process.
```
codefromthecrypt: probably need a t.Cleanup() when opening test file descriptors as not all tests close them. This also hints maybe we should make mod.Close() actually close file descriptors when WASI
codefromthecrypt: actually I will redo this in the next couple days because how opened file descriptors are kept is wrong. they should always be owned by the importing module.
codefromthecrypt: So there are a couple next steps before merge. I'm going to do these in the same PR so that the most important features are retained, and when there's a new API it is safe. That windows tests are failing are a good incentive to fix this now, and we couldn't fix the safety issues before as we were relying on non-standard types (memfs)

1. Add a feature for PWD (ex allowing the directory named "." to resolve).
  * The two most common "preopen" on GitHub are working directory "." and "/"
  * Other subpaths can be achieved by composing a rootFS or working dir
2. Now that we are using standard types, move file descriptors to internalwasm.Module.
  * Right now, there are scope problems as FDs are shared across modules. this doesn't make sense in sandboxing.
  * Trying to manage them with a scoped object isn't correct as WASI can be imported twice
  * Moveover we may have a different WASI impl which means a different memory.
  * By moving FDs to internalwasm.Module, they are like memory or anything else and can be cleaned up coherently.
3. Add a toehold test for sanity checking file system works
  * Even though we have unit tests on what memory is written
codefromthecrypt: FYI I tried to retrofit memFS to fs.FS timeboxed to a couple hours, but it is more messy than removing it. I'm not spending any more time on memFS
codefromthecrypt: ok this is rebased. next steps before ready for review:

1. fix whatever's going on in the write tests in windows
2. Test_Cat is the only thing actually using this. Add tests to ensure working directory and root directory paths work
3. scrub for TODOs and fix here, leave them or open a new issue.
codefromthecrypt: opened https://github.com/tetratelabs/wazero/issues/408 to follow-up at some point and add another kind of integration test, now that it is easy to compare against an `fs.FS`
codefromthecrypt: cc @pims this is an API break, but a good one as we've gotten rid of an incompatible FS and formalized the API. Hope this doesn't drift you too much!

Patch:
diff --git a/README.md b/README.md
index ee31afcd..8656f215 100644
--- a/README.md
+++ b/README.md
@@ -41,12 +41,22 @@ on your machine unless you explicitly allow it.
 
 System access is defined by an emerging specification called WebAssembly
 System Interface ([WASI](https://github.com/WebAssembly/WASI)). WASI defines
-how WebAssembly programs interact with the host embedding them. For example,
-WASI defines functions for reading the time, or a random number.
+how WebAssembly programs interact with the host embedding them.
 
-This repository includes several [examples](examples) that expose system
-interfaces, via the module `wazero.WASISnapshotPreview1`. These examples are
-tested and a good way to learn what's possible with wazero.
+For example, here's how you can allow WebAssembly modules to read
+"/work/home/a.txt" as "/a.txt" or "./a.txt":
+```go
+wasi, err := r.InstantiateModule(wazero.WASISnapshotPreview1())
+defer wasi.Close()
+
+sysConfig := wazero.NewSysConfig().WithFS(os.DirFS("/work/home"))
+module, err := wazero.StartWASICommandWithConfig(r, compiled, sysConfig)
+defer module.Close()
+...
+```
+
+The best way to learn this and other features you get with wazero is by trying
+[examples](examples).
 
 ## Runtime
 
diff --git a/config.go b/config.go
index f2dacfb9..3cb114a0 100644
--- a/config.go
+++ b/config.go
@@ -5,12 +5,12 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"math"
 
 	internalwasm "github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wasm/interpreter"
 	"github.com/tetratelabs/wazero/internal/wasm/jit"
-	"github.com/tetratelabs/wazero/wasi"
 )
 
 // NewRuntimeConfigJIT compiles WebAssembly modules into runtime.GOARCH-specific assembly for optimal performance.
@@ -216,21 +216,42 @@ func (c *SysConfig) WithEnv(key, value string) *SysConfig {
 
 // WithFS assigns the file system to use for any paths beginning at "/". Defaults to not found.
 //
+// Ex. This sets a read-only, embedded file-system to serve files under the root ("/") and working (".") directories:
+//
+//	//go:embed testdata/index.html
+//	var testdataIndex embed.FS
+//
+//	rooted, err := fs.Sub(testdataIndex, "testdata")
+//	require.NoError(t, err)
+//
+//	// "index.html" is accessible as both "/index.html" and "./index.html" because we didn't use WithWorkDirFS.
+//	sysConfig := wazero.NewSysConfig().WithFS(rooted)
+//
 // Note: This sets WithWorkDirFS to the same file-system unless already set.
-func (c *SysConfig) WithFS(fs wasi.FS) *SysConfig {
+func (c *SysConfig) WithFS(fs fs.FS) *SysConfig {
 	c.setFS("/", fs)
 	return c
 }
 
-// WithWorkDirFS indicates the file system to use for any paths beginning at ".". Defaults to the same as WithFS.
-func (c *SysConfig) WithWorkDirFS(fs wasi.FS) *SysConfig {
+// WithWorkDirFS indicates the file system to use for any paths beginning at "./". Defaults to the same as WithFS.
+//
+// Ex. This sets a read-only, embedded file-system as the root ("/"), and a mutable one as the working directory ("."):
+//
+//	//go:embed appA
+//	var rootFS embed.FS
+//
+//	// Files relative to this source under appA are available under "/" and files relative to "/work/appA" under ".".
+//	sysConfig := wazero.NewSysConfig().WithFS(rootFS).WithWorkDirFS(os.DirFS("/work/appA"))
+//
+// Note: os.DirFS documentation includes important notes about isolation, which also applies to fs.Sub. As of Go 1.18,
+// the built-in file-systems are not jailed (chroot). See https://github.com/golang/go/issues/42322
+func (c *SysConfig) WithWorkDirFS(fs fs.FS) *SysConfig {
 	c.setFS(".", fs)
 	return c
 }
 
-// withFS is hidden especially until #394 as existing use cases should be possible by composing file systems.
-// TODO: in #394 add examples on WithFS to accomplish this.
-func (c *SysConfig) setFS(path string, fs wasi.FS) {
+// setFS maps a path to a file-system. This is only used for base paths: "/" and ".".
+func (c *SysConfig) setFS(path string, fs fs.FS) {
 	// Check to see if this key already exists and update it.
 	entry := &internalwasm.FileEntry{Path: path, FS: fs}
 	if fd, ok := c.preopenPaths[path]; ok {
@@ -265,13 +286,13 @@ func (c *SysConfig) toSysContext() (sys *internalwasm.SysContext, err error) {
 	rootFD := uint32(0) // zero is invalid
 	setWorkDirFS := false
 	preopens := c.preopens
-	for fd, fs := range preopens {
-		if fs.FS == nil {
-			err = fmt.Errorf("FS for %s is nil", fs.Path)
+	for fd, entry := range preopens {
+		if entry.FS == nil {
+			err = fmt.Errorf("FS for %s is nil", entry.Path)
 			return
-		} else if fs.Path == "/" {
+		} else if entry.Path == "/" {
 			rootFD = fd
-		} else if fs.Path == "." {
+		} else if entry.Path == "." {
 			setWorkDirFS = true
 		}
 	}
diff --git a/internal/wasi/fs.go b/internal/wasi/fs.go
deleted file mode 100644
index 71e4d45d..00000000
--- a/internal/wasi/fs.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package internalwasi
-
-import (
-	"fmt"
-	"io"
-	"io/fs"
-	"os"
-	"runtime"
-	"strings"
-
-	"github.com/tetratelabs/wazero/wasi"
-)
-
-type DirFS string
-
-func (dir DirFS) OpenWASI(dirFlags uint32, path string, oFlags uint32, fsRights, fsRightsInheriting uint64, fdFlags uint32) (wasi.File, error) {
-	// I'm not sure how to use all these passed flags and rights yet
-	if !fs.ValidPath(path) || runtime.GOOS == "windows" && strings.ContainsAny(path, `\:`) {
-		return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrInvalid}
-	}
-	mode := fs.FileMode(0644)
-	if oFlags&wasi.O_DIR != 0 {
-		mode |= fs.ModeDir
-	}
-	f, err := os.OpenFile(string(dir)+"/"+path, posixOpenFlags(oFlags, fsRights), mode)
-	if err != nil {
-		return nil, err
-	}
-	return f, nil
-}
-
-type MemFS struct {
-	Files map[string][]byte
-}
-
-func (m *MemFS) OpenWASI(dirFlags uint32, path string, oFlags uint32, fsRights, fsRightsInheriting uint64, fdFlags uint32) (wasi.File, error) {
-	if !fs.ValidPath(path) {
-		return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrInvalid}
-	}
-
-	var buf []byte
-	if oFlags&wasi.O_CREATE == 0 {
-		bts, ok := m.Files[path]
-		if !ok {
-			return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
-		}
-
-		if oFlags&wasi.O_TRUNC == 0 {
-			buf = append(buf, bts...)
-		}
-	}
-
-	ret := &memFile{buf: buf}
-
-	if fsRights&wasi.R_FD_WRITE != 0 {
-		ret.flush = func(bts []byte) {
-			m.Files[path] = bts
-		}
-	}
-
-	return ret, nil
-}
-
-// memFile implements wasi.File
-type memFile struct {
-	buf    []byte
-	offset int64
-	flush  func(bts []byte)
-}
-
-// Read implements io.Reader
-func (f *memFile) Read(p []byte) (int, error) {
-	// In memFile, the end of the buffer is the end of the file.
-	if f.offset == int64(len(f.buf)) {
-		return 0, io.EOF
-	}
-	nread := copy(p, f.buf[f.offset:])
-	f.offset += int64(nread)
-	return nread, nil
-}
-
-// Write implements io.Writer
-func (f *memFile) Write(p []byte) (int, error) {
-	nwritten := copy(f.buf[f.offset:], p)
-	f.buf = append(f.buf, p[nwritten:]...)
-	f.offset += int64(len(p))
-	return len(p), nil
-}
-
-// Seek implements io.Seeker
-func (f *memFile) Seek(offset int64, whence int) (int64, error) {
-	switch whence {
-	case io.SeekStart:
-		f.offset = offset
-	case io.SeekCurrent:
-		f.offset += offset
-	case io.SeekEnd:
-		f.offset = int64(len(f.buf)) + offset
-	default:
-		return 0, fmt.Errorf("invalid whence: %d", whence)
-	}
-	return f.offset, nil
-}
-
-// Close implements io.Closer
-func (f *memFile) Close() error {
-	if f.flush != nil {
-		f.flush(f.buf)
-	}
-	return nil
-}
diff --git a/internal/wasi/wasi.go b/internal/wasi/wasi.go
index b21f00be..2b2bbfb6 100644
--- a/internal/wasi/wasi.go
+++ b/internal/wasi/wasi.go
@@ -6,7 +6,7 @@ import (
 	"fmt"
 	"io"
 	"io/fs"
-	"os"
+	"path"
 	"time"
 
 	internalwasm "github.com/tetratelabs/wazero/internal/wasm"
@@ -1100,16 +1100,12 @@ func (a *wasiAPI) FdDatasync(ctx wasm.Module, fd uint32) wasi.Errno {
 }
 
 // FdFdstatGet implements SnapshotPreview1.FdFdstatGet
-// TODO: Currently FdFdstatget implements nothing except returning fake fs_right_inheriting
 func (a *wasiAPI) FdFdstatGet(ctx wasm.Module, fd uint32, resultStat uint32) wasi.Errno {
 	sys := sysContext(ctx)
 
 	if _, ok := sys.OpenedFile(fd); !ok {
 		return wasi.ErrnoBadf
 	}
-	if !ctx.Memory().WriteUint64Le(resultStat+16, rightFDRead|rightFDWrite) {
-		return wasi.ErrnoFault
-	}
 	return wasi.ErrnoSuccess
 }
 
@@ -1118,7 +1114,7 @@ func (a *wasiAPI) FdPrestatGet(ctx wasm.Module, fd uint32, resultPrestat uint32)
 	sys := sysContext(ctx)
 
 	entry, ok := sys.OpenedFile(fd)
-	if !ok || entry.Path == "" {
+	if !ok {
 		return wasi.ErrnoBadf
 	}
 
@@ -1140,6 +1136,7 @@ func (a *wasiAPI) FdFdstatSetFlags(ctx wasm.Module, fd uint32, flags uint32) was
 }
 
 // FdFdstatSetRights implements SnapshotPreview1.FdFdstatSetRights
+// Note: This will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844
 func (a *wasiAPI) FdFdstatSetRights(ctx wasm.Module, fd uint32, fsRightsBase, fsRightsInheriting uint64) wasi.Errno {
 	return wasi.ErrnoNosys // stubbed for GrainLang per #271
 }
@@ -1196,13 +1193,11 @@ func (a *wasiAPI) FdRead(ctx wasm.Module, fd, iovs, iovsCount, resultSize uint32
 
 	var reader io.Reader
 
-	if fd == 0 {
+	if fd == fdStdin {
 		reader = sys.Stdin()
+	} else if f, ok := sys.OpenedFile(fd); !ok || f.File == nil {
+		return wasi.ErrnoBadf
 	} else {
-		f, ok := sys.OpenedFile(fd)
-		if !ok || f.File == nil {
-			return wasi.ErrnoBadf
-		}
 		reader = f.File
 	}
 
@@ -1249,24 +1244,24 @@ func (a *wasiAPI) FdRenumber(ctx wasm.Module, fd, to uint32) wasi.Errno {
 func (a *wasiAPI) FdSeek(ctx wasm.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) wasi.Errno {
 	sys := sysContext(ctx)
 
-	f, ok := sys.OpenedFile(fd)
-	if !ok || f.File == nil {
+	var seeker io.Seeker
+	// Check to see if the file descriptor is available
+	if f, ok := sys.OpenedFile(fd); !ok || f.File == nil {
 		return wasi.ErrnoBadf
-	}
-	seeker, ok := f.File.(io.Seeker)
-	if !ok {
+		// fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it.
+	} else if seeker, ok = f.File.(io.Seeker); !ok {
 		return wasi.ErrnoBadf
 	}
 
 	if whence > io.SeekEnd /* exceeds the largest valid whence */ {
 		return wasi.ErrnoInval
 	}
-	newOffst, err := seeker.Seek(int64(offset), int(whence))
+	newOffset, err := seeker.Seek(int64(offset), int(whence))
 	if err != nil {
 		return wasi.ErrnoIo
 	}
 
-	if !ctx.Memory().WriteUint32Le(resultNewoffset, uint32(newOffst)) {
+	if !ctx.Memory().WriteUint32Le(resultNewoffset, uint32(newOffset)) {
 		return wasi.ErrnoFault
 	}
 
@@ -1290,16 +1285,18 @@ func (a *wasiAPI) FdWrite(ctx wasm.Module, fd, iovs, iovsCount, resultSize uint3
 	var writer io.Writer
 
 	switch fd {
-	case 1:
+	case fdStdout:
 		writer = sys.Stdout()
-	case 2:
+	case fdStderr:
 		writer = sys.Stderr()
 	default:
-		f, ok := sys.OpenedFile(fd)
-		if !ok || f.File == nil {
+		// Check to see if the file descriptor is available
+		if f, ok := sys.OpenedFile(fd); !ok || f.File == nil {
+			return wasi.ErrnoBadf
+			// fs.FS doesn't declare io.Writer, but implementations such as os.File implement it.
+		} else if writer, ok = f.File.(io.Writer); !ok {
 			return wasi.ErrnoBadf
 		}
-		writer = f.File
 	}
 
 	var nwritten uint32
@@ -1349,41 +1346,8 @@ func (a *wasiAPI) PathLink(ctx wasm.Module, oldFd, oldFlags, oldPath, oldPathLen
 	return wasi.ErrnoNosys // stubbed for GrainLang per #271
 }
 
-const (
-	// WASI open flags
-	oflagCreate = 1 << iota
-	// TODO: oflagDir
-	oflagExclusive
-	oflagTrunc
-
-	// WASI FS rights
-	rightFDRead  = 1 << iota
-	rightFDWrite = 0x200
-)
-
-func posixOpenFlags(oFlags uint32, fsRights uint64) (pFlags int) {
-	// TODO: handle dirflags, which decides whether to follow symbolic links or not,
-	//       by O_NOFOLLOW. Note O_NOFOLLOW doesn't exist on Windows.
-	if fsRights&rightFDWrite != 0 {
-		if fsRights&rightFDRead != 0 {
-			pFlags |= os.O_RDWR
-		} else {
-			pFlags |= os.O_WRONLY
-		}
-	}
-	if oFlags&oflagCreate != 0 {
-		pFlags |= os.O_CREATE
-	}
-	if oFlags&oflagExclusive != 0 {
-		pFlags |= os.O_EXCL
-	}
-	if oFlags&oflagTrunc != 0 {
-		pFlags |= os.O_TRUNC
-	}
-	return
-}
-
 // PathOpen implements SnapshotPreview1.PathOpen
+// Note: Rights will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844
 func (a *wasiAPI) PathOpen(ctx wasm.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase,
 	fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno wasi.Errno) {
 	sys := sysContext(ctx)
@@ -1397,25 +1361,20 @@ func (a *wasiAPI) PathOpen(ctx wasm.Module, fd, dirflags, pathPtr, pathLen, ofla
 	if !ok {
 		return wasi.ErrnoFault
 	}
-	pathName := string(b)
-	f, err := dir.FS.OpenWASI(dirflags, pathName, oflags, fsRightsBase, fsRightsInheriting, fdflags)
-	if err != nil {
-		switch {
-		case errors.Is(err, fs.ErrNotExist):
-			return wasi.ErrnoNoent
-		case errors.Is(err, fs.ErrExist):
-			return wasi.ErrnoExist
-		default:
-			return wasi.ErrnoIo
-		}
-	}
 
-	// when ofagDir is set, the opened file must be a directory.
-	// TODO if oflags&oflagDir != 0 return wasi.ErrnoNotdir if stat != dir
+	// TODO: Consider dirflags and oflags. Also, allow non-read-only open based on config about the mount.
+	// Ex. allow os.O_RDONLY, os.O_WRONLY, or os.O_RDWR either by config flag or pattern on filename
+	// See #390
+	entry, errno := openFileEntry(dir.FS, path.Join(dir.Path, string(b)))
+	if errno != wasi.ErrnoSuccess {
+		return errno
+	}
 
-	if newFD, ok := sys.OpenFile(&internalwasm.FileEntry{Path: pathName, File: f, FS: dir.FS}); !ok {
+	if newFD, ok := sys.OpenFile(entry); !ok {
+		_ = entry.File.Close()
 		return wasi.ErrnoIo
 	} else if !ctx.Memory().WriteUint32Le(resultOpenedFd, newFD) {
+		_ = entry.File.Close()
 		return wasi.ErrnoFault
 	}
 	return wasi.ErrnoSuccess
@@ -1499,6 +1458,12 @@ func (a *wasiAPI) SockShutdown(ctx wasm.Module, fd, how uint32) wasi.Errno {
 	return wasi.ErrnoNosys // stubbed for GrainLang per #271
 }
 
+const (
+	fdStdin  = 0
+	fdStdout = 1
+	fdStderr = 2
+)
+
 // NewAPI is exported for benchmarks
 func NewAPI() *wasiAPI {
 	return &wasiAPI{
@@ -1520,6 +1485,25 @@ func sysContext(ctx wasm.Module) *internalwasm.SysContext {
 	}
 }
 
+func openFileEntry(rootFS fs.FS, pathName string) (*internalwasm.FileEntry, wasi.Errno) {
+	f, err := rootFS.Open(pathName)
+	if err != nil {
+		switch {
+		case errors.Is(err, fs.ErrNotExist):
+			return nil, wasi.ErrnoNoent
+		case errors.Is(err, fs.ErrExist):
+			return nil, wasi.ErrnoExist
+		default:
+			return nil, wasi.ErrnoIo
+		}
+	}
+
+	// TODO: verify if oflags is a directory and fail with wasi.ErrnoNotdir if not
+	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-oflags-flagsu16
+
+	return &internalwasm.FileEntry{Path: pathName, FS: rootFS, File: f}, wasi.ErrnoSuccess
+}
+
 func ValidateWASICommand(module *internalwasm.Module, moduleName string) error {
 	if start, err := requireExport(module, moduleName, FunctionStart, internalwasm.ExternTypeFunc); err != nil {
 		return err
diff --git a/internal/wasm/sys.go b/internal/wasm/sys.go
index d0185c54..62637b1c 100644
--- a/internal/wasm/sys.go
+++ b/internal/wasm/sys.go
@@ -4,19 +4,19 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"math"
 	"sync/atomic"
-
-	"github.com/tetratelabs/wazero/wasi"
 )
 
-// FileEntry temporarily uses wasi types until #394.
+// FileEntry maps a path to an open file in a file system.
 //
 // Note: This does not introduce cycles because the types here are in the package "wasi" not "internalwasi".
 type FileEntry struct {
 	Path string
-	FS   wasi.FS
-	File wasi.File
+	FS   fs.FS
+	// File when nil this is a mount like "." or "/".
+	File fs.File
 }
 
 // SysContext holds module-scoped system resources currently only used by internalwasi.
diff --git a/wasi.go b/wasi.go
index 9b6fd3c3..f746f723 100644
--- a/wasi.go
+++ b/wasi.go
@@ -9,19 +9,6 @@ import (
 	"github.com/tetratelabs/wazero/wasm"
 )
 
-// WASIDirFS returns a file system (a wasi.FS) for the tree of files rooted at
-// the directory dir. It's similar to os.DirFS, except that it implements
-// wasi.FS instead of the fs.FS interface.
-func WASIDirFS(dir string) wasi.FS {
-	return internalwasi.DirFS(dir)
-}
-
-func WASIMemFS() wasi.FS {
-	return &internalwasi.MemFS{
-		Files: map[string][]byte{},
-	}
-}
-
 // WASISnapshotPreview1 are functions importable as the module name wasi.ModuleSnapshotPreview1
 func WASISnapshotPreview1() *Module {
 	_, fns := internalwasi.SnapshotPreview1Functions()
diff --git a/wasi/wasi.go b/wasi/wasi.go
index a72a70e3..1fbf0aae 100644
--- a/wasi/wasi.go
+++ b/wasi/wasi.go
@@ -3,7 +3,6 @@ package wasi
 
 import (
 	"fmt"
-	"io"
 )
 
 const (
@@ -12,40 +11,6 @@ const (
 	ModuleSnapshotPreview1 = "wasi_snapshot_preview1"
 )
 
-// TODO: rename these according to other naming conventions
-const (
-	// WASI open flags
-
-	O_CREATE = 1 << iota
-	O_DIR
-	O_EXCL
-	O_TRUNC
-
-	// WASI fs rights
-
-	R_FD_READ = 1 << iota
-	R_FD_SEEK
-	R_FD_FDSTAT_SET_FLAGS
-	R_FD_SYNC
-	R_FD_TELL
-	R_FD_WRITE
-)
-
-// File combines file I/O interfaces supported by ModuleSnapshotPreview1.
-type File interface {
-	io.Reader
-	io.Writer
-	io.Seeker
-	io.Closer
-}
-
-// FS is an interface for a preopened directory.
-type FS interface {
-	// OpenWASI is a general method to open a file, similar to
-	// os.OpenFile, but with WASI flags and rights instead of POSIX.
-	OpenWASI(dirFlags uint32, path string, oFlags uint32, fsRights, fsRightsInheriting uint64, fdFlags uint32) (File, error)
-}
-
 // Errno are the error codes returned by WASI functions.
 //
 // Note: This is not always an error, as ErrnoSuccess is a valid code.


Test patch:
diff --git a/config_test.go b/config_test.go
index 12c6cbc4..7bd7d628 100644
--- a/config_test.go
+++ b/config_test.go
@@ -4,6 +4,7 @@ import (
 	"io"
 	"math"
 	"testing"
+	"testing/fstest"
 
 	"github.com/stretchr/testify/require"
 
@@ -58,8 +59,8 @@ func TestRuntimeConfig_Features(t *testing.T) {
 }
 
 func TestSysConfig_toSysContext(t *testing.T) {
-	memFS := WASIMemFS()
-	memFS2 := WASIMemFS()
+	testFS := fstest.MapFS{}
+	testFS2 := fstest.MapFS{}
 
 	tests := []struct {
 		name     string
@@ -186,7 +187,7 @@ func TestSysConfig_toSysContext(t *testing.T) {
 		},
 		{
 			name:  "WithFS",
-			input: NewSysConfig().WithFS(memFS),
+			input: NewSysConfig().WithFS(testFS),
 			expected: requireSysContext(t,
 				math.MaxUint32, // max
 				nil,            // args
@@ -195,14 +196,14 @@ func TestSysConfig_toSysContext(t *testing.T) {
 				nil,            // stdout
 				nil,            // stderr
 				map[uint32]*internalwasm.FileEntry{ // openedFiles
-					3: {Path: "/", FS: memFS},
-					4: {Path: ".", FS: memFS},
+					3: {Path: "/", FS: testFS},
+					4: {Path: ".", FS: testFS},
 				},
 			),
 		},
 		{
 			name:  "WithFS - overwrites",
-			input: NewSysConfig().WithFS(memFS).WithFS(memFS2),
+			input: NewSysConfig().WithFS(testFS).WithFS(testFS2),
 			expected: requireSysContext(t,
 				math.MaxUint32, // max
 				nil,            // args
@@ -211,14 +212,14 @@ func TestSysConfig_toSysContext(t *testing.T) {
 				nil,            // stdout
 				nil,            // stderr
 				map[uint32]*internalwasm.FileEntry{ // openedFiles
-					3: {Path: "/", FS: memFS2},
-					4: {Path: ".", FS: memFS2},
+					3: {Path: "/", FS: testFS2},
+					4: {Path: ".", FS: testFS2},
 				},
 			),
 		},
 		{
 			name:  "WithWorkDirFS",
-			input: NewSysConfig().WithWorkDirFS(memFS),
+			input: NewSysConfig().WithWorkDirFS(testFS),
 			expected: requireSysContext(t,
 				math.MaxUint32, // max
 				nil,            // args
@@ -227,13 +228,13 @@ func TestSysConfig_toSysContext(t *testing.T) {
 				nil,            // stdout
 				nil,            // stderr
 				map[uint32]*internalwasm.FileEntry{ // openedFiles
-					3: {Path: ".", FS: memFS},
+					3: {Path: ".", FS: testFS},
 				},
 			),
 		},
 		{
 			name:  "WithFS and WithWorkDirFS",
-			input: NewSysConfig().WithFS(memFS).WithWorkDirFS(memFS2),
+			input: NewSysConfig().WithFS(testFS).WithWorkDirFS(testFS2),
 			expected: requireSysContext(t,
 				math.MaxUint32, // max
 				nil,            // args
@@ -242,14 +243,14 @@ func TestSysConfig_toSysContext(t *testing.T) {
 				nil,            // stdout
 				nil,            // stderr
 				map[uint32]*internalwasm.FileEntry{ // openedFiles
-					3: {Path: "/", FS: memFS},
-					4: {Path: ".", FS: memFS2},
+					3: {Path: "/", FS: testFS},
+					4: {Path: ".", FS: testFS2},
 				},
 			),
 		},
 		{
 			name:  "WithWorkDirFS and WithFS",
-			input: NewSysConfig().WithWorkDirFS(memFS).WithFS(memFS2),
+			input: NewSysConfig().WithWorkDirFS(testFS).WithFS(testFS2),
 			expected: requireSysContext(t,
 				math.MaxUint32, // max
 				nil,            // args
@@ -258,8 +259,8 @@ func TestSysConfig_toSysContext(t *testing.T) {
 				nil,            // stdout
 				nil,            // stderr
 				map[uint32]*internalwasm.FileEntry{ // openedFiles
-					3: {Path: ".", FS: memFS},
-					4: {Path: "/", FS: memFS2},
+					3: {Path: ".", FS: testFS},
+					4: {Path: "/", FS: testFS2},
 				},
 			),
 		},
diff --git a/examples/file_system_test.go b/examples/file_system_test.go
index bc30b842..54bc4b8c 100644
--- a/examples/file_system_test.go
+++ b/examples/file_system_test.go
@@ -2,16 +2,20 @@ package examples
 
 import (
 	"bytes"
+	"embed"
 	_ "embed"
-	"io"
+	"io/fs"
 	"testing"
 
 	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/wazero"
-	"github.com/tetratelabs/wazero/wasi"
 )
 
+// catFS is an embedded filesystem limited to cat.go
+//go:embed testdata/cat.go
+var catFS embed.FS
+
 // catGo is the TinyGo source
 //go:embed testdata/cat.go
 var catGo []byte
@@ -29,18 +33,13 @@ func Test_Cat(t *testing.T) {
 
 	// First, configure where the WebAssembly Module (Wasm) console outputs to (stdout).
 	stdoutBuf := bytes.NewBuffer(nil)
-	sysConfig := wazero.NewSysConfig().WithStdout(stdoutBuf)
 
-	// Next, configure a sandboxed filesystem to include one file.
-	file := "cat.go" // arbitrary file
-	memFS := wazero.WASIMemFS()
-	err := writeFile(memFS, file, catGo)
+	// Since wazero uses fs.FS, we can use standard libraries to do things like trim the leading path.
+	rooted, err := fs.Sub(catFS, "testdata")
 	require.NoError(t, err)
-	sysConfig.WithWorkDirFS(memFS)
 
-	// Since this runs a main function (_start in WASI), configure the arguments.
-	// Remember, arg[0] is the program name!
-	sysConfig.WithArgs("cat", file)
+	// Combine the above into our baseline config, overriding defaults (which discard stdout and have no file system).
+	sysConfig := wazero.NewSysConfig().WithStdout(stdoutBuf).WithFS(rooted)
 
 	// Compile the `cat` module.
 	compiled, err := r.CompileModule(catWasm)
@@ -52,24 +51,12 @@ func Test_Cat(t *testing.T) {
 	defer wasi.Close()
 
 	// StartWASICommand runs the "_start" function which is what TinyGo compiles "main" to.
-	cat, err := wazero.StartWASICommandWithConfig(r, compiled, sysConfig)
-
+	// * Set the program name (arg[0]) to "cat" and add args to write "cat.go" to stdout twice.
+	// * We use both "/cat.go" and "./cat.go" because WithFS by default maps the workdir "." to "/".
+	cat, err := wazero.StartWASICommandWithConfig(r, compiled, sysConfig.WithArgs("cat", "/cat.go", "./cat.go"))
 	require.NoError(t, err)
 	defer cat.Close()
 
-	// To ensure it worked, verify stdout from WebAssembly had what we expected.
-	require.Equal(t, string(catGo), stdoutBuf.String())
-}
-
-func writeFile(fs wasi.FS, path string, data []byte) error {
-	f, err := fs.OpenWASI(0, path, wasi.O_CREATE|wasi.O_TRUNC, wasi.R_FD_WRITE, 0, 0)
-	if err != nil {
-		return err
-	}
-
-	if _, err := io.Copy(f, bytes.NewBuffer(data)); err != nil {
-		return err
-	}
-
-	return f.Close()
+	// We expect the WebAssembly function wrote "cat.go" twice!
+	require.Equal(t, append(catGo, catGo...), stdoutBuf.Bytes())
 }
diff --git a/internal/wasi/fs_test.go b/internal/wasi/fs_test.go
deleted file mode 100644
index 53faa887..00000000
--- a/internal/wasi/fs_test.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package internalwasi
-
-import (
-	"testing"
-	"testing/iotest"
-
-	"github.com/stretchr/testify/require"
-)
-
-// TestMemFile_Read_Seek tests the behavior of Read and Seek by iotest.TestReader.
-// See iotest.TestReader
-func TestMemFile_Read_Seek(t *testing.T) {
-	expectedFileContent := []byte("wazero") // arbitrary contents
-	memFile := &memFile{
-		buf: expectedFileContent,
-	}
-	// TestReader tests that io.Reader correctly reads the expected contents.
-	// It also tests io.Seeker when it's implemented, which memFile does.
-	err := iotest.TestReader(memFile, expectedFileContent)
-	require.NoError(t, err)
-}
diff --git a/internal/wasi/wasi_test.go b/internal/wasi/wasi_test.go
index 41df2b8a..3cffc584 100644
--- a/internal/wasi/wasi_test.go
+++ b/internal/wasi/wasi_test.go
@@ -7,9 +7,13 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"math"
 	"math/rand"
+	"os"
+	"path"
 	"testing"
+	"testing/fstest"
 
 	"github.com/stretchr/testify/require"
 
@@ -46,7 +50,7 @@ func TestSnapshotPreview1_ArgsGet(t *testing.T) {
 
 		// Invoke ArgsGet directly and check the memory side effects.
 		errno := a.ArgsGet(mod, argv, argvBuf)
-		require.Equal(t, wasi.ErrnoSuccess, errno)
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -58,7 +62,8 @@ func TestSnapshotPreview1_ArgsGet(t *testing.T) {
 
 		results, err := fn.Call(mod, uint64(argv), uint64(argvBuf))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -112,7 +117,7 @@ func TestSnapshotPreview1_ArgsGet_Errors(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			errno := a.ArgsGet(mod, tc.argv, tc.argvBuf)
 			require.NoError(t, err)
-			require.Equal(t, wasi.ErrnoFault, errno)
+			require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno))
 		})
 	}
 }
@@ -140,7 +145,7 @@ func TestSnapshotPreview1_ArgsSizesGet(t *testing.T) {
 
 		// Invoke ArgsSizesGet directly and check the memory side effects.
 		errno := a.ArgsSizesGet(mod, resultArgc, resultArgvBufSize)
-		require.Equal(t, wasi.ErrnoSuccess, errno)
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -152,7 +157,8 @@ func TestSnapshotPreview1_ArgsSizesGet(t *testing.T) {
 
 		results, err := fn.Call(mod, uint64(resultArgc), uint64(resultArgvBufSize))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -203,8 +209,7 @@ func TestSnapshotPreview1_ArgsSizesGet_Errors(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			errno := a.ArgsSizesGet(mod, tc.argc, tc.argvBufSize)
-			require.NoError(t, err)
-			require.Equal(t, wasi.ErrnoFault, errno)
+			require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno))
 		})
 	}
 }
@@ -234,7 +239,7 @@ func TestSnapshotPreview1_EnvironGet(t *testing.T) {
 
 		// Invoke EnvironGet directly and check the memory side effects.
 		errno := a.EnvironGet(mod, resultEnviron, resultEnvironBuf)
-		require.Equal(t, wasi.ErrnoSuccess, errno)
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -246,7 +251,8 @@ func TestSnapshotPreview1_EnvironGet(t *testing.T) {
 
 		results, err := fn.Call(mod, uint64(resultEnviron), uint64(resultEnvironBuf))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -299,8 +305,7 @@ func TestSnapshotPreview1_EnvironGet_Errors(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			errno := a.EnvironGet(mod, tc.environ, tc.environBuf)
-			require.NoError(t, err)
-			require.Equal(t, wasi.ErrnoFault, errno)
+			require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno))
 		})
 	}
 }
@@ -328,7 +333,7 @@ func TestSnapshotPreview1_EnvironSizesGet(t *testing.T) {
 
 		// Invoke EnvironSizesGet directly and check the memory side effects.
 		errno := a.EnvironSizesGet(mod, resultEnvironc, resultEnvironBufSize)
-		require.Equal(t, wasi.ErrnoSuccess, errno)
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -340,7 +345,8 @@ func TestSnapshotPreview1_EnvironSizesGet(t *testing.T) {
 
 		results, err := fn.Call(mod, uint64(resultEnvironc), uint64(resultEnvironBufSize))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -391,8 +397,7 @@ func TestSnapshotPreview1_EnvironSizesGet_Errors(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			errno := a.EnvironSizesGet(mod, tc.environc, tc.environBufSize)
-			require.NoError(t, err)
-			require.Equal(t, wasi.ErrnoFault, errno)
+			require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno))
 		})
 	}
 }
@@ -413,7 +418,8 @@ func TestSnapshotPreview1_ClockResGet(t *testing.T) {
 	t.Run(FunctionClockResGet, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -440,7 +446,7 @@ func TestSnapshotPreview1_ClockTimeGet(t *testing.T) {
 
 		// invoke ClockTimeGet directly and check the memory side effects!
 		errno := a.ClockTimeGet(mod, 0 /* TODO: id */, 0 /* TODO: precision */, resultTimestamp)
-		require.Equal(t, wasi.ErrnoSuccess, errno)
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -452,7 +458,8 @@ func TestSnapshotPreview1_ClockTimeGet(t *testing.T) {
 
 		results, err := fn.Call(mod, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(resultTimestamp))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -496,7 +503,8 @@ func TestSnapshotPreview1_ClockTimeGet_Errors(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			results, err := fn.Call(mod, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(tc.resultTimestamp))
 			require.NoError(t, err)
-			require.Equal(t, uint64(wasi.ErrnoFault), results[0]) // results[0] is the errno
+			errno := wasi.Errno(results[0]) // results[0] is the errno
+			require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno))
 		})
 	}
 }
@@ -511,13 +519,15 @@ func TestSnapshotPreview1_FdAdvise(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdAdvise", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdAdvise(mod, 0, 0, 0, 0))
+		errno := a.FdAdvise(mod, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdAdvise, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -531,13 +541,15 @@ func TestSnapshotPreview1_FdAllocate(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdAllocate", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdAllocate(mod, 0, 0, 0))
+		errno := a.FdAllocate(mod, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdAllocate, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -547,16 +559,18 @@ func TestSnapshotPreview1_FdClose(t *testing.T) {
 
 	setupFD := func() (publicwasm.Module, publicwasm.Function, *wasiAPI) {
 		ctx := context.Background()
-		memFs := &MemFS{}
+
+		// fd_close needs to close an open file descriptor. Open two files so that we can tell which is closed.
+		path1, path2 := "a", "b"
+		testFs := fstest.MapFS{path1: {Data: make([]byte, 0)}, path2: {Data: make([]byte, 0)}}
+		entry1, errno := openFileEntry(testFs, path1)
+		require.Zero(t, errno, wasi.ErrnoName(errno))
+		entry2, errno := openFileEntry(testFs, path2)
+		require.Zero(t, errno, wasi.ErrnoName(errno))
+
 		sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{
-			fdToClose: {
-				Path: "/tmp",
-				FS:   memFs,
-			},
-			fdToKeep: {
-				Path: "path to keep",
-				FS:   memFs,
-			},
+			fdToClose: entry1,
+			fdToKeep:  entry2,
 		})
 		require.NoError(t, err)
 
@@ -579,7 +593,7 @@ func TestSnapshotPreview1_FdClose(t *testing.T) {
 		defer mod.Close()
 
 		errno := api.FdClose(mod, fdToClose)
-		require.Equal(t, wasi.ErrnoSuccess, errno)
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		verify(mod)
 	})
@@ -587,9 +601,10 @@ func TestSnapshotPreview1_FdClose(t *testing.T) {
 		mod, fn, _ := setupFD()
 		defer mod.Close()
 
-		ret, err := fn.Call(mod, uint64(fdToClose))
+		results, err := fn.Call(mod, uint64(fdToClose))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		verify(mod)
 	})
@@ -612,13 +627,15 @@ func TestSnapshotPreview1_FdDatasync(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdDatasync", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdDatasync(mod, 0))
+		errno := a.FdDatasync(mod, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdDatasync, func(t *testing.T) {
 		results, err := fn.Call(mod, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -634,13 +651,15 @@ func TestSnapshotPreview1_FdFdstatSetFlags(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdFdstatSetFlags", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdFdstatSetFlags(mod, 0, 0))
+		errno := a.FdFdstatSetFlags(mod, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdFdstatSetFlags, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -654,13 +673,15 @@ func TestSnapshotPreview1_FdFdstatSetRights(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdFdstatSetRights", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdFdstatSetRights(mod, 0, 0, 0))
+		errno := a.FdFdstatSetRights(mod, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdFdstatSetRights, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -674,13 +695,15 @@ func TestSnapshotPreview1_FdFilestatGet(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdFilestatGet", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdFilestatGet(mod, 0, 0))
+		errno := a.FdFilestatGet(mod, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdFilestatGet, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -694,13 +717,15 @@ func TestSnapshotPreview1_FdFilestatSetSize(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdFilestatSetSize", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdFilestatSetSize(mod, 0, 0))
+		errno := a.FdFilestatSetSize(mod, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdFilestatSetSize, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -714,13 +739,15 @@ func TestSnapshotPreview1_FdFilestatSetTimes(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdFilestatSetTimes", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdFilestatSetTimes(mod, 0, 0, 0, 0))
+		errno := a.FdFilestatSetTimes(mod, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdFilestatSetTimes, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -734,13 +761,15 @@ func TestSnapshotPreview1_FdPread(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdPread", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdPread(mod, 0, 0, 0, 0, 0))
+		errno := a.FdPread(mod, 0, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdPread, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -748,7 +777,8 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) {
 	fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
 
 	ctx := context.Background()
-	sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}})
+	pathName := "/tmp"
+	sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: pathName}})
 	require.NoError(t, err)
 
 	a, mod, fn := instantiateModule(t, ctx, FunctionFdPrestatGet, ImportFdPrestatGet, moduleName, sys)
@@ -756,11 +786,11 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) {
 
 	resultPrestat := uint32(1) // arbitrary offset
 	expectedMemory := []byte{
-		'?',     // resultPrstat after this
+		'?',     // resultPrestat after this
 		0,       // 8-bit tag indicating `prestat_dir`, the only available tag
 		0, 0, 0, // 3-byte padding
 		// the result path length field after this
-		4, 0, 0, 0, // = 4, which is len("/tmp")
+		byte(len(pathName)), 0, 0, 0, // = in little endian encoding
 		'?',
 	}
 
@@ -768,7 +798,7 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) {
 		maskMemory(t, mod, len(expectedMemory))
 
 		errno := a.FdPrestatGet(mod, fd, resultPrestat)
-		require.Equal(t, wasi.ErrnoSuccess, errno)
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -778,9 +808,10 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) {
 	t.Run(FunctionFdPrestatDirName, func(t *testing.T) {
 		maskMemory(t, mod, len(expectedMemory))
 
-		ret, err := fn.Call(mod, uint64(fd), uint64(resultPrestat))
+		results, err := fn.Call(mod, uint64(fd), uint64(resultPrestat))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -827,7 +858,7 @@ func TestSnapshotPreview1_FdPrestatGet_Errors(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			errno := a.FdPrestatGet(mod, tc.fd, tc.resultPrestat)
-			require.Equal(t, tc.expectedErrno, errno)
+			require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno))
 		})
 	}
 }
@@ -854,7 +885,7 @@ func TestSnapshotPreview1_FdPrestatDirName(t *testing.T) {
 		maskMemory(t, mod, len(expectedMemory))
 
 		errno := a.FdPrestatDirName(mod, fd, path, pathLen)
-		require.Equal(t, wasi.ErrnoSuccess, errno)
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -864,9 +895,10 @@ func TestSnapshotPreview1_FdPrestatDirName(t *testing.T) {
 	t.Run(FunctionFdPrestatDirName, func(t *testing.T) {
 		maskMemory(t, mod, len(expectedMemory))
 
-		ret, err := fn.Call(mod, uint64(fd), uint64(path), uint64(pathLen))
+		results, err := fn.Call(mod, uint64(fd), uint64(path), uint64(pathLen))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 		require.True(t, ok)
@@ -931,7 +963,7 @@ func TestSnapshotPreview1_FdPrestatDirName_Errors(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			errno := a.FdPrestatDirName(mod, tc.fd, tc.path, tc.pathLen)
-			require.Equal(t, tc.expectedErrno, errno)
+			require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno))
 		})
 	}
 }
@@ -946,13 +978,15 @@ func TestSnapshotPreview1_FdPwrite(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdPwrite", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdPwrite(mod, 0, 0, 0, 0, 0))
+		errno := a.FdPwrite(mod, 0, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdPwrite, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -992,9 +1026,9 @@ func TestSnapshotPreview1_FdRead(t *testing.T) {
 		}},
 		{FunctionFdRead, func(_ *wasiAPI, mod *wasm.ModuleContext, fn publicwasm.Function) fdReadFn {
 			return func(ctx publicwasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno {
-				ret, err := fn.Call(mod, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize))
+				results, err := fn.Call(mod, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize))
 				require.NoError(t, err)
-				return wasi.Errno(ret[0])
+				return wasi.Errno(results[0])
 			}
 		}},
 	}
@@ -1003,9 +1037,9 @@ func TestSnapshotPreview1_FdRead(t *testing.T) {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
 			// Create a fresh file to read the contents from
-			file, memFS := createFile(t, "test_path", []byte("wazero"))
+			file, testFS := createFile(t, "test_path", []byte("wazero"))
 			sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{
-				fd: {Path: "test_path", FS: memFS, File: file},
+				fd: {Path: "test_path", FS: testFS, File: file},
 			})
 			require.NoError(t, err)
 
@@ -1018,7 +1052,7 @@ func TestSnapshotPreview1_FdRead(t *testing.T) {
 			require.True(t, ok)
 
 			errno := tc.fdRead(a, mod, fn)(mod, fd, iovs, iovsCount, resultSize)
-			require.Equal(t, wasi.ErrnoSuccess, errno)
+			require.Zero(t, errno, wasi.ErrnoName(errno))
 
 			actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 			require.True(t, ok)
@@ -1028,12 +1062,12 @@ func TestSnapshotPreview1_FdRead(t *testing.T) {
 }
 
 func TestSnapshotPreview1_FdRead_Errors(t *testing.T) {
-	validFD := uint32(3)                                // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
-	file, memFS := createFile(t, "test_path", []byte{}) // file with empty contents
+	validFD := uint32(3)                                 // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
+	file, testFS := createFile(t, "test_path", []byte{}) // file with empty contents
 
 	ctx := context.Background()
 	sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{
-		validFD: {Path: "test_path", FS: memFS, File: file},
+		validFD: {Path: "test_path", FS: testFS, File: file},
 	})
 	require.NoError(t, err)
 
@@ -1115,7 +1149,7 @@ func TestSnapshotPreview1_FdRead_Errors(t *testing.T) {
 			require.True(t, memoryWriteOK)
 
 			errno := a.FdRead(mod, tc.fd, tc.iovs+offset, tc.iovsCount+offset, tc.resultSize+offset)
-			require.Equal(t, tc.expectedErrno, errno)
+			require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno))
 		})
 	}
 }
@@ -1130,13 +1164,15 @@ func TestSnapshotPreview1_FdReaddir(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdReaddir", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdReaddir(mod, 0, 0, 0, 0, 0))
+		errno := a.FdReaddir(mod, 0, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdReaddir, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1150,24 +1186,26 @@ func TestSnapshotPreview1_FdRenumber(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdRenumber", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdRenumber(mod, 0, 0))
+		errno := a.FdRenumber(mod, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdRenumber, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
 func TestSnapshotPreview1_FdSeek(t *testing.T) {
-	fd := uint32(3)                                             // arbitrary fd after 0, 1, and 2, that are stdin/out/err
-	resultNewoffset := uint32(1)                                // arbitrary offset in `ctx.Memory` for the new offset value
-	file, memFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents
+	fd := uint32(3)                                              // arbitrary fd after 0, 1, and 2, that are stdin/out/err
+	resultNewoffset := uint32(1)                                 // arbitrary offset in `ctx.Memory` for the new offset value
+	file, testFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents
 
 	ctx := context.Background()
 	sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{
-		fd: {Path: "test_path", FS: memFS, File: file},
+		fd: {Path: "test_path", FS: testFS, File: file},
 	})
 	require.NoError(t, err)
 
@@ -1185,9 +1223,9 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) {
 		}},
 		{FunctionFdSeek, func() fdSeekFn {
 			return func(ctx publicwasm.Module, fd uint32, offset uint64, whence, resultNewoffset uint32) wasi.Errno {
-				ret, err := fn.Call(mod, uint64(fd), offset, uint64(whence), uint64(resultNewoffset))
+				results, err := fn.Call(mod, uint64(fd), offset, uint64(whence), uint64(resultNewoffset))
 				require.NoError(t, err)
-				return wasi.Errno(ret[0])
+				return wasi.Errno(results[0])
 			}
 		}},
 	}
@@ -1241,16 +1279,27 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) {
 				tc := tt
 				t.Run(tc.name, func(t *testing.T) {
 					maskMemory(t, mod, len(tc.expectedMemory))
-					file.offset = 1 // set the initial offset of the file to 1
+
+					// Since we initialized this file, we know it is a seeker (because it is a MapFile)
+					f, ok := sys.OpenedFile(fd)
+					require.True(t, ok)
+					seeker := f.File.(io.Seeker)
+
+					// set the initial offset of the file to 1
+					offset, err := seeker.Seek(1, io.SeekStart)
+					require.NoError(t, err)
+					require.Equal(t, int64(1), offset)
 
 					errno := sf.fdSeek()(mod, fd, uint64(tc.offset), uint32(tc.whence), resultNewoffset)
-					require.Equal(t, wasi.ErrnoSuccess, errno)
+					require.Zero(t, errno, wasi.ErrnoName(errno))
 
 					actual, ok := mod.Memory().Read(0, uint32(len(tc.expectedMemory)))
 					require.True(t, ok)
 					require.Equal(t, tc.expectedMemory, actual)
 
-					require.Equal(t, tc.expectedOffset, file.offset) // test that the offset of file is actually updated.
+					offset, err = seeker.Seek(0, io.SeekCurrent)
+					require.NoError(t, err)
+					require.Equal(t, tc.expectedOffset, offset) // test that the offset of file is actually updated.
 				})
 			}
 		})
@@ -1258,11 +1307,11 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) {
 }
 
 func TestSnapshotPreview1_FdSeek_Errors(t *testing.T) {
-	validFD := uint32(3)                                        // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
-	file, memFS := createFile(t, "test_path", []byte("wazero")) // arbitrary valid file with non-empty contents
+	validFD := uint32(3)                                         // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
+	file, testFS := createFile(t, "test_path", []byte("wazero")) // arbitrary valid file with non-empty contents
 	ctx := context.Background()
 	sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{
-		validFD: {Path: "test_path", FS: memFS, File: file},
+		validFD: {Path: "test_path", FS: testFS, File: file},
 	})
 	require.NoError(t, err)
 
@@ -1301,7 +1350,7 @@ func TestSnapshotPreview1_FdSeek_Errors(t *testing.T) {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
 			errno := a.FdSeek(mod, tc.fd, tc.offset, tc.whence, tc.resultNewoffset)
-			require.Equal(t, tc.expectedErrno, errno)
+			require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno))
 		})
 	}
 
@@ -1317,13 +1366,15 @@ func TestSnapshotPreview1_FdSync(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdSync", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdSync(mod, 0))
+		errno := a.FdSync(mod, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdSync, func(t *testing.T) {
 		results, err := fn.Call(mod, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1337,13 +1388,15 @@ func TestSnapshotPreview1_FdTell(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.FdTell", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.FdTell(mod, 0, 0))
+		errno := a.FdTell(mod, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionFdTell, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1383,20 +1436,23 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) {
 		}},
 		{FunctionFdWrite, func(_ *wasiAPI, mod *wasm.ModuleContext, fn publicwasm.Function) fdWriteFn {
 			return func(ctx publicwasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno {
-				ret, err := fn.Call(mod, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize))
+				results, err := fn.Call(mod, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize))
 				require.NoError(t, err)
-				return wasi.Errno(ret[0])
+				return wasi.Errno(results[0])
 			}
 		}},
 	}
 
+	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
+
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
 			// Create a fresh file to write the contents to
-			file, memFS := createFile(t, "test_path", []byte{})
+			pathName := "test_path"
+			file, testFS := createWriteableFile(t, tmpDir, pathName, []byte{})
 			sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{
-				fd: {Path: "test_path", FS: memFS, File: file},
+				fd: {Path: pathName, FS: testFS, File: file},
 			})
 			require.NoError(t, err)
 
@@ -1408,22 +1464,31 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) {
 			require.True(t, ok)
 
 			errno := tc.fdWrite(a, mod, fn)(mod, fd, iovs, iovsCount, resultSize)
-			require.Equal(t, wasi.ErrnoSuccess, errno)
+			require.Zero(t, errno, wasi.ErrnoName(errno))
 
 			actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 			require.True(t, ok)
 			require.Equal(t, expectedMemory, actual)
-			require.Equal(t, []byte("wazero"), file.buf) // verify the file was actually written
+
+			// Since we initialized this file, we know we can read it by path
+			buf, err := os.ReadFile(path.Join(tmpDir, pathName))
+			require.NoError(t, err)
+
+			require.Equal(t, []byte("wazero"), buf) // verify the file was actually written
 		})
 	}
 }
 
 func TestSnapshotPreview1_FdWrite_Errors(t *testing.T) {
-	validFD := uint32(3)                                // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
-	file, memFS := createFile(t, "test_path", []byte{}) // file with empty contents
+	validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
+
+	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
+	pathName := "test_path"
+	file, testFS := createWriteableFile(t, tmpDir, pathName, []byte{})
+
 	ctx := context.Background()
 	sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{
-		validFD: {Path: "test_path", FS: memFS, File: file},
+		validFD: {Path: pathName, FS: testFS, File: file},
 	})
 	require.NoError(t, err)
 
@@ -1488,22 +1553,11 @@ func TestSnapshotPreview1_FdWrite_Errors(t *testing.T) {
 			mod.Memory().(*wasm.MemoryInstance).Buffer = tc.memory
 
 			errno := a.FdWrite(mod, tc.fd, iovs, iovsCount, tc.resultSize)
-			require.Equal(t, tc.expectedErrno, errno)
+			require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno))
 		})
 	}
 }
 
-func createFile(t *testing.T, path string, contents []byte) (*memFile, *MemFS) {
-	memFS := &MemFS{Files: map[string][]byte{}}
-	f, err := memFS.OpenWASI(0, path, wasi.O_CREATE|wasi.O_TRUNC, wasi.R_FD_WRITE, 0, 0)
-	require.NoError(t, err)
-
-	mf := f.(*memFile)
-	mf.buf = append([]byte{}, contents...)
-
-	return mf, memFS
-}
-
 // TestSnapshotPreview1_PathCreateDirectory only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_PathCreateDirectory(t *testing.T) {
 	ctx := context.Background()
@@ -1514,13 +1568,15 @@ func TestSnapshotPreview1_PathCreateDirectory(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.PathCreateDirectory", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.PathCreateDirectory(mod, 0, 0, 0))
+		errno := a.PathCreateDirectory(mod, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionPathCreateDirectory, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1534,13 +1590,15 @@ func TestSnapshotPreview1_PathFilestatGet(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.PathFilestatGet", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.PathFilestatGet(mod, 0, 0, 0, 0, 0))
+		errno := a.PathFilestatGet(mod, 0, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionPathFilestatGet, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1554,13 +1612,15 @@ func TestSnapshotPreview1_PathFilestatSetTimes(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.PathFilestatSetTimes", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.PathFilestatSetTimes(mod, 0, 0, 0, 0, 0, 0, 0))
+		errno := a.PathFilestatSetTimes(mod, 0, 0, 0, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionPathFilestatSetTimes, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1574,31 +1634,32 @@ func TestSnapshotPreview1_PathLink(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.PathLink", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.PathLink(mod, 0, 0, 0, 0, 0, 0, 0))
+		errno := a.PathLink(mod, 0, 0, 0, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionPathLink, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
 func TestSnapshotPreview1_PathOpen(t *testing.T) {
-	workdirFD := uint32(3)                    // arbitrary fd after 0, 1, and 2, that are stdin/out/err
-	dirflags := uint32(0)                     // arbitrary dirflags
-	path := uint32(1)                         // arbitrary offset
-	pathLen := uint32(6)                      // The length of path
-	oflags := uint32(0)                       // arbitrary oflags
-	fsRightsBase := uint64(rightFDRead)       // arbitrary right
-	fsRightsInheriting := uint64(rightFDRead) // arbitrary right
+	workdirFD := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
+	dirflags := uint32(0)  // arbitrary dirflags
+	oflags := uint32(0)    // arbitrary oflags
 	fdFlags := uint32(0)
-	resultOpenedFd := uint32(8)
-	initialMemory := []byte{
-		'?',                          // `path` is after this
-		'w', 'a', 'z', 'e', 'r', 'o', // path
-	}
+
+	// Setup the initial memory to include the path name starting at an offset.
+	pathName := "wazero"
+	path := uint32(1)
+	pathLen := uint32(len(pathName))
+	initialMemory := append([]byte{'?'}, pathName...)
+
 	expectedFD := byte(workdirFD + 1)
+	resultOpenedFd := uint32(len(initialMemory) + 1)
 	expectedMemory := append(
 		initialMemory,
 		'?', // `resultOpenedFd` is after this
@@ -1608,88 +1669,58 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) {
 
 	ctx := context.Background()
 
-	// TestSnapshotPreview1_PathOpen uses a matrix because setting up test files is complicated and has to be clean each time.
-	type pathOpenFn func(ctx publicwasm.Module, fd, dirflags, path, pathLen, oflags uint32,
-		fsRightsBase, fsRightsInheriting uint64,
-		fdFlags, resultOpenedFd uint32) wasi.Errno
-	pathOpenFns := []struct {
-		name     string
-		pathOpen func(*wasiAPI, *wasm.ModuleContext, publicwasm.Function) pathOpenFn
-	}{
-		{"SnapshotPreview1.PathOpen", func(a *wasiAPI, _ *wasm.ModuleContext, _ publicwasm.Function) pathOpenFn {
-			return a.PathOpen
-		}},
-		{FunctionPathOpen, func(_ *wasiAPI, mod *wasm.ModuleContext, fn publicwasm.Function) pathOpenFn {
-			return func(ctx publicwasm.Module, fd, dirflags, path, pathLen, oflags uint32,
-				fsRightsBase, fsRightsInheriting uint64,
-				fdFlags, resultOpenedFd uint32) wasi.Errno {
-				ret, err := fn.Call(mod, uint64(fd), uint64(dirflags), uint64(path), uint64(pathLen), uint64(oflags), uint64(fsRightsBase), uint64(fsRightsInheriting), uint64(fdFlags), uint64(resultOpenedFd))
-				require.NoError(t, err)
-				return wasi.Errno(ret[0])
-			}
-		}},
-	}
+	// rights are ignored per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844
+	fsRightsBase, fsRightsInheriting := uint64(1), uint64(2)
 
-	tests := []struct {
-		name         string
-		fd           uint32
-		expectedPath string
-	}{
-		{
-			name:         "simple file open",
-			fd:           workdirFD,
-			expectedPath: "wazero",
-		},
+	setup := func() (*wasiAPI, *wasm.ModuleContext, publicwasm.Function) {
+		testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}}
+		sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{
+			workdirFD: {Path: ".", FS: testFS},
+		})
+		require.NoError(t, err)
+		a, mod, fn := instantiateModule(t, ctx, FunctionPathOpen, ImportPathOpen, moduleName, sys)
+		maskMemory(t, mod, len(expectedMemory))
+		ok := mod.Memory().Write(0, initialMemory)
+		require.True(t, ok)
+		return a, mod, fn
 	}
 
-	for _, pathOpenFn := range pathOpenFns {
-		pf := pathOpenFn
-		t.Run(pf.name, func(t *testing.T) {
-			for _, tt := range tests {
-				tc := tt
-				t.Run(tc.name, func(t *testing.T) {
-					// Create a memFS for testing that has "./wazero" file.
-					memFS := &MemFS{Files: map[string][]byte{"wazero": {}}}
-					sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{
-						workdirFD: {Path: ".", FS: memFS},
-					})
-					require.NoError(t, err)
-
-					a, mod, fn := instantiateModule(t, ctx, FunctionPathOpen, ImportPathOpen, moduleName, sys)
-					defer mod.Close()
+	verify := func(errno wasi.Errno, mod *wasm.ModuleContext) {
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
-					maskMemory(t, mod, len(expectedMemory))
-					ok := mod.Memory().Write(0, initialMemory)
-					require.True(t, ok)
+		actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
+		require.True(t, ok)
+		require.Equal(t, expectedMemory, actual)
 
-					errno := pf.pathOpen(a, mod, fn)(mod, tc.fd, dirflags, path, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdFlags, resultOpenedFd)
-					require.Equal(t, wasi.ErrnoSuccess, errno)
+		// verify the file was actually opened
+		f, ok := sysContext(mod).OpenedFile(uint32(expectedFD))
+		require.True(t, ok)
+		require.Equal(t, pathName, f.Path)
+	}
 
-					actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
-					require.True(t, ok)
-					require.Equal(t, expectedMemory, actual)
+	t.Run("SnapshotPreview1.PathOpen", func(t *testing.T) {
+		a, mod, _ := setup()
+		errno := a.PathOpen(mod, workdirFD, dirflags, path, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdFlags, resultOpenedFd)
+		verify(errno, mod)
+	})
 
-					// verify the file was actually opened
-					f, ok := sys.OpenedFile(uint32(expectedFD))
-					require.True(t, ok)
-					require.Equal(t, tc.expectedPath, f.Path)
-				})
-			}
-		})
-	}
+	t.Run(FunctionPathOpen, func(t *testing.T) {
+		_, mod, fn := setup()
+		results, err := fn.Call(mod, uint64(workdirFD), uint64(dirflags), uint64(path), uint64(pathLen), uint64(oflags), fsRightsBase, fsRightsInheriting, uint64(fdFlags), uint64(resultOpenedFd))
+		require.NoError(t, err)
+		errno := wasi.Errno(results[0])
+		verify(errno, mod)
+	})
 }
 
 func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) {
 	validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
-	// Create a memFS for testing that has "./wazero" file.
-	memFS := &MemFS{
-		Files: map[string][]byte{
-			"wazero": []byte(""),
-		},
-	}
+	pathName := "wazero"
+	testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}}
+
 	ctx := context.Background()
 	sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{
-		validFD: {Path: ".", FS: memFS},
+		validFD: {Path: ".", FS: testFS},
 	})
 	require.NoError(t, err)
 
@@ -1698,9 +1729,7 @@ func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) {
 
 	validPath := uint32(0)    // arbitrary offset
 	validPathLen := uint32(6) // the length of "wazero"
-	mod.Memory().Write(validPath, []byte{
-		'w', 'a', 'z', 'e', 'r', 'o', // write to offset 0 (= validPath)
-	}) // wazero is the path to the file in the memFS
+	mod.Memory().Write(validPath, []byte(pathName))
 
 	tests := []struct {
 		name                                      string
@@ -1747,7 +1776,7 @@ func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
 			errno := a.PathOpen(mod, tc.fd, 0, tc.path, tc.pathLen, tc.oflags, 0, 0, 0, tc.resultOpenedFd)
-			require.Equal(t, tc.expectedErrno, errno)
+			require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno))
 		})
 	}
 }
@@ -1762,13 +1791,15 @@ func TestSnapshotPreview1_PathReadlink(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.PathLink", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.PathReadlink(mod, 0, 0, 0, 0, 0, 0))
+		errno := a.PathReadlink(mod, 0, 0, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionPathReadlink, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1782,13 +1813,15 @@ func TestSnapshotPreview1_PathRemoveDirectory(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.PathRemoveDirectory", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.PathRemoveDirectory(mod, 0, 0, 0))
+		errno := a.PathRemoveDirectory(mod, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionPathRemoveDirectory, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1802,13 +1835,15 @@ func TestSnapshotPreview1_PathRename(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.PathRename", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.PathRename(mod, 0, 0, 0, 0, 0, 0))
+		errno := a.PathRename(mod, 0, 0, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionPathRename, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1822,13 +1857,15 @@ func TestSnapshotPreview1_PathSymlink(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.PathSymlink", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.PathSymlink(mod, 0, 0, 0, 0, 0))
+		errno := a.PathSymlink(mod, 0, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionPathSymlink, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1842,13 +1879,15 @@ func TestSnapshotPreview1_PathUnlinkFile(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.PathUnlinkFile", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.PathUnlinkFile(mod, 0, 0, 0))
+		errno := a.PathUnlinkFile(mod, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionPathUnlinkFile, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1862,13 +1901,15 @@ func TestSnapshotPreview1_PollOneoff(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.PollOneoff", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.PollOneoff(mod, 0, 0, 0, 0))
+		errno := a.PollOneoff(mod, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionPollOneoff, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1920,13 +1961,15 @@ func TestSnapshotPreview1_ProcRaise(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.ProcRaise", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.ProcRaise(mod, 0))
+		errno := a.ProcRaise(mod, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionProcRaise, func(t *testing.T) {
 		results, err := fn.Call(mod, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1940,13 +1983,15 @@ func TestSnapshotPreview1_SchedYield(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.SchedYield", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.SchedYield(mod))
+		errno := a.SchedYield(mod)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionSchedYield, func(t *testing.T) {
 		results, err := fn.Call(mod)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -1980,7 +2025,7 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) {
 
 		// Invoke RandomGet directly and check the memory side effects!
 		errno := a.RandomGet(mod, offset, length)
-		require.Equal(t, wasi.ErrnoSuccess, errno)
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, offset+length+1)
 		require.True(t, ok)
@@ -1992,7 +2037,8 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) {
 
 		results, err := fn.Call(mod, uint64(offset), uint64(length))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Zero(t, errno, wasi.ErrnoName(errno))
 
 		actual, ok := mod.Memory().Read(0, offset+length+1)
 		require.True(t, ok)
@@ -2035,7 +2081,7 @@ func TestSnapshotPreview1_RandomGet_Errors(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			errno := a.RandomGet(mod, tc.offset, tc.length)
-			require.Equal(t, wasi.ErrnoFault, errno)
+			require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno))
 		})
 	}
 }
@@ -2053,7 +2099,7 @@ func TestSnapshotPreview1_RandomGet_SourceError(t *testing.T) {
 	}
 
 	errno := a.RandomGet(mod, uint32(1), uint32(5)) // arbitrary offset and length
-	require.Equal(t, wasi.ErrnoIo, errno)
+	require.Equal(t, wasi.ErrnoIo, errno, wasi.ErrnoName(errno))
 }
 
 // TestSnapshotPreview1_SockRecv only tests it is stubbed for GrainLang per #271
@@ -2066,13 +2112,15 @@ func TestSnapshotPreview1_SockRecv(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.SockRecv", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.SockRecv(mod, 0, 0, 0, 0, 0, 0))
+		errno := a.SockRecv(mod, 0, 0, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionSockRecv, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -2086,13 +2134,15 @@ func TestSnapshotPreview1_SockSend(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.SockSend", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.SockSend(mod, 0, 0, 0, 0, 0))
+		errno := a.SockSend(mod, 0, 0, 0, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionSockSend, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0, 0, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
@@ -2106,18 +2156,27 @@ func TestSnapshotPreview1_SockShutdown(t *testing.T) {
 	defer mod.Close()
 
 	t.Run("SnapshotPreview1.SockShutdown", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, a.SockShutdown(mod, 0, 0))
+		errno := a.SockShutdown(mod, 0, 0)
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 
 	t.Run(FunctionSockShutdown, func(t *testing.T) {
 		results, err := fn.Call(mod, 0, 0)
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+		errno := wasi.Errno(results[0]) // results[0] is the errno
+		require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno))
 	})
 }
 
 const testMemoryPageSize = 1
 
+// maskMemory sets the first memory in the store to '?' * size, so tests can see what's written.
+func maskMemory(t *testing.T, mod publicwasm.Module, size int) {
+	for i := uint32(0); i < uint32(size); i++ {
+		require.True(t, mod.Memory().WriteByte(i, '?'))
+	}
+}
+
 func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImport, moduleName string, sys *wasm.SysContext) (*wasiAPI, *wasm.ModuleContext, publicwasm.Function) {
 	enabledFeatures := wasm.Features20191205
 	store := wasm.NewStore(interpreter.NewEngine(), enabledFeatures)
@@ -2150,13 +2209,29 @@ func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImpo
 	return a, mod, fn
 }
 
-// maskMemory sets the first memory in the store to '?' * size, so tests can see what's written.
-func maskMemory(t *testing.T, mod publicwasm.Module, size int) {
-	for i := uint32(0); i < uint32(size); i++ {
-		require.True(t, mod.Memory().WriteByte(i, '?'))
+func newSysContext(args, environ []string, openedFiles map[uint32]*wasm.FileEntry) (sys *wasm.SysContext, err error) {
+	return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, openedFiles)
+}
+
+func createFile(t *testing.T, pathName string, data []byte) (fs.File, fs.FS) {
+	mapFile := &fstest.MapFile{Data: data}
+	if data == nil {
+		mapFile.Mode = os.ModeDir
 	}
+	mapFS := fstest.MapFS{pathName: mapFile}
+	f, err := mapFS.Open(pathName)
+	require.NoError(t, err)
+	return f, mapFS
 }
 
-func newSysContext(args, environ []string, openedFiles map[uint32]*wasm.FileEntry) (sys *wasm.SysContext, err error) {
-	return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, openedFiles)
+// 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))
+
+	// open the file for writing in a custom way until #390
+	f, err := os.OpenFile(absolutePath, os.O_RDWR, 0o600)
+	require.NoError(t, err)
+	return f, os.DirFS(tmpDir)
 }
diff --git a/internal/wasm/sys_test.go b/internal/wasm/sys_test.go
index 5bd958ad..2d72557d 100644
--- a/internal/wasm/sys_test.go
+++ b/internal/wasm/sys_test.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"path"
 	"testing"
+	"testing/fstest"
 
 	"github.com/stretchr/testify/require"
 )
@@ -160,7 +161,7 @@ func TestSysContext_Close(t *testing.T) {
 	t.Run("open files", func(t *testing.T) {
 		tempDir := t.TempDir()
 		pathName := "test"
-		file, _ := createWriteableFile(t, tempDir, pathName, make([]byte, 0))
+		file, testFS := createWriteableFile(t, tempDir, pathName, make([]byte, 0))
 
 		sys, err := NewSysContext(
 			0,   // max
@@ -170,8 +171,9 @@ func TestSysContext_Close(t *testing.T) {
 			nil, // stdout
 			nil, // stderr
 			map[uint32]*FileEntry{ // openedFiles
-				3: {Path: "."},
-				4: {Path: path.Join(".", pathName), File: file},
+				3: {Path: "/", FS: testFS},
+				4: {Path: ".", FS: testFS},
+				5: {Path: path.Join(".", pathName), File: file, FS: testFS},
 			},
 		)
 		require.NoError(t, err)
@@ -181,20 +183,67 @@ func TestSysContext_Close(t *testing.T) {
 		require.Empty(t, sys.openedFiles)
 
 		// Verify it was actually closed, by trying to close it again.
-		err = file.Close()
+		err = file.(*os.File).Close()
 		require.Contains(t, err.Error(), "file already closed")
 
 		// No problem closing config again because the descriptors were removed, so they won't be called again.
 		require.NoError(t, sys.Close())
 	})
 
-	// TODO: fs but never used (ex file == nil)
-	// TODO: externally closed
+	t.Run("FS never used", func(t *testing.T) {
+		testFS := fstest.MapFS{}
+		sys, err := NewSysContext(
+			0,   // max
+			nil, // args
+			nil, // environ
+			nil, // stdin
+			nil, // stdout
+			nil, // stderr
+			map[uint32]*FileEntry{ // no openedFiles
+				3: {Path: "/", FS: testFS},
+				4: {Path: ".", FS: testFS},
+			},
+		)
+		require.NoError(t, err)
+
+		// Even if there are no open files, the descriptors for the file-system mappings should be removed.
+		require.NoError(t, sys.Close())
+		require.Empty(t, sys.openedFiles)
+	})
+
+	t.Run("open file externally closed", func(t *testing.T) {
+		tempDir := t.TempDir()
+		pathName := "test"
+		file, testFS := createWriteableFile(t, tempDir, pathName, make([]byte, 0))
+
+		sys, err := NewSysContext(
+			0,   // max
+			nil, // args
+			nil, // environ
+			nil, // stdin
+			nil, // stdout
+			nil, // stderr
+			map[uint32]*FileEntry{ // openedFiles
+				3: {Path: "/", FS: testFS},
+				4: {Path: ".", FS: testFS},
+				5: {Path: path.Join(".", pathName), File: file, FS: testFS},
+			},
+		)
+		require.NoError(t, err)
+
+		// Close the file externally
+		file.Close()
+
+		// Closing should err as it expected to be open
+		require.Contains(t, sys.Close().Error(), "file already closed")
+
+		// However, cleanup should still occur.
+		require.Empty(t, sys.openedFiles)
+	})
 }
 
 // createWriteableFile uses real files when io.Writer tests are needed.
-// TODO: temporarily *os.File until #394
-func createWriteableFile(t *testing.T, tmpDir string, pathName string, data []byte) (*os.File, fs.FS) {
+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))
diff --git a/wasi_test.go b/wasi_test.go
index f10b1ded..324d3a23 100644
--- a/wasi_test.go
+++ b/wasi_test.go
@@ -70,6 +70,6 @@ func TestStartWASICommandWithConfig(t *testing.T) {
 		require.Equal(t, append([]byte(tc), 0), stdout.Bytes())
 
 		stdout.Reset()
-		mod.Close()
+		require.NoError(t, mod.Close())
 	}
 }