Repository: tetratelabs/wazero
Pull request #1010 (2023-01-05 10:44:31)

Moves FreeBSD to compilation test only.
FreeBSD is not natively supported by GitHub actions or Docker. To test it, we had to run vagrant which is both time-consuming (~7minutes), and frequently fails to setup, resulting in false negatives. Meanwhile the code related to FreeBSD is quite limited and doesn't change.

This changes our approach to testing FreeBSD to the same as how we deal with 386, which is to do compilation tests only.



Patch:
diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml
index 68676d4e..f584a34a 100644
--- a/.github/workflows/commit.yaml
+++ b/.github/workflows/commit.yaml
@@ -87,39 +87,6 @@ jobs:
           CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
         run: bash <(curl -s https://codecov.io/bash)
 
-  test_freebsd:  # See /Vagrantfile for notes
-    name: amd64, FreeBSD, Go-${{ matrix.go-version }}
-    runs-on: macos-12
-    strategy:
-      fail-fast: false  # don't fail fast as sometimes failures are arch/OS specific
-      matrix:  # Use versions consistent with wazero's Go support policy.
-        go-version:
-          - "1.19"  # Current Go version
-          - "1.17"  # Floor Go version of wazero (current - 2)
-
-    steps:
-
-      - uses: actions/checkout@v3
-
-      - uses: actions/setup-go@v3
-        with:  # Note: This isn't really used, it is just to match versions.
-          go-version: ${{ matrix.go-version }}
-
-      - uses: actions/cache@v3
-        with:
-          path: ~/.vagrant.d/boxes
-          key: spectest-${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum','Vagrantfile') }}-${{ matrix.spec-version }}
-
-      - name: Install Vagrant
-        run: brew install --cask vagrant
-
-      - name: Configure Vagrant
-        run: |  # This ensures the same version of Go is used inside Vagrant.
-          GOVERSION=$(go env GOVERSION) GOARCH=$(go env GOARCH) vagrant up
-
-      - name: make test
-        run: vagrant ssh -c "cd wazero && make test"
-
   test_scratch:
     name: ${{ matrix.arch }}, Linux (scratch), Go-${{ matrix.go-version }}
     runs-on: ubuntu-20.04
diff --git a/Makefile b/Makefile
index 7bf78841..606369d3 100644
--- a/Makefile
+++ b/Makefile
@@ -225,6 +225,9 @@ check:
 # 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 ./...
+# Ensure we build on FreeBSD amd64 for Trivy:
+#	gh release view -R aquasecurity/trivy --json assets --jq 'first(.assets[] | select(.name| test("FreeBSD-64bit.*tar.gz")) | {url, downloadCount})'
+	@GOARCH=amd64 GOOS=freebsd go build ./...
 	@$(MAKE) lint golangci_lint_goarch=arm64
 	@$(MAKE) lint golangci_lint_goarch=amd64
 	@$(MAKE) format
diff --git a/README.md b/README.md
index 7d0edb27..2eac5c6a 100644
--- a/README.md
+++ b/README.md
@@ -96,14 +96,14 @@ systems are ones we test, but that doesn't necessarily mean other operating
 system versions won't work.
 
 We currently test Linux (Ubuntu and scratch), MacOS and Windows as packaged by
-[GitHub Actions][11], as well FreeBSD via Vagrant/VirtualBox.
+[GitHub Actions][11], as well compilation of 32-bit Linux and 64-bit FreeBSD.
 
 * Interpreter
   * Linux is tested on amd64 (native) as well arm64 and riscv64 via emulation.
-  * FreeBSD, MacOS and Windows are only tested on amd64.
+  * MacOS and Windows are only tested on amd64.
 * Compiler
   * Linux is tested on amd64 (native) as well arm64 via emulation.
-  * FreeBSD, MacOS and Windows are only tested on amd64.
+  * MacOS and Windows are only tested on amd64.
 
 wazero has no dependencies and doesn't require CGO. This means it can also be
 embedded in an application that doesn't use an operating system. This is a main
diff --git a/Vagrantfile b/Vagrantfile
deleted file mode 100644
index 4b83bfdb..00000000
--- a/Vagrantfile
+++ /dev/null
@@ -1,47 +0,0 @@
-# Vagrant file for FreeBSD
-#
-# Here's an example:
-#   GOVERSION=$(go env GOVERSION) GOARCH=$(go env GOARCH) vagrant up
-#   vagrant rsync
-#   vagrant ssh -c "cd wazero; go test ./..."
-#
-# Notes on FreeBSD:
-# * GitHub Actions doesn’t support FreeBSD, and may never.
-# * We could use Travis to run FreeBSD, but it would split our CI config.
-# * Using Vagrant directly is easier to debug than vmactions/freebsd-vm.
-# * GitHub Actions only supports virtualization on MacOS.
-# * GitHub Actions removed vagrant from the image starting with macos-11.
-# * Since VirtualBox doesn't work on arm64, freebsd/arm64 is untestable.
-
-Vagrant.configure("2") do |config|
-  config.vm.box = "generic/freebsd13"
-  config.vm.synced_folder ".", "/home/vagrant/wazero",
-    type: "rsync",
-    rsync__exclude: ".git/"
-
-  config.vm.provider "virtualbox" do |v|
-    v.memory = 1024
-    v.cpus = 1
-    # To prevent any DNS-related provisioning errors:
-    # https://serverfault.com/questions/453185/vagrant-virtualbox-dns-10-0-2-3-not-working/506206
-    v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
-  end
-
-  # Similar to `GOVERSION=$(go env GOVERSION) GOARCH=$(go env GOARCH) vagrant provision`
-  config.vm.provision "install-golang", type: "shell", run: "once" do |sh|
-    sh.env = {
-      'GOVERSION': ENV['GOVERSION'],
-      'GOARCH': ENV['GOARCH'],
-    }
-    sh.inline = <<~GOINSTALL
-      set -eux -o pipefail
-      curl -fsSL "https://dl.google.com/go/${GOVERSION}.freebsd-${GOARCH}.tar.gz" | tar Cxz /usr/local
-      cat >> /usr/local/etc/profile <<EOF
-export GOROOT=/usr/local/go
-export PATH=/usr/local/go/bin:$PATH
-EOF
-    GOINSTALL
-  end
-end
-
-
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index 2bd28431..f01feda8 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -7,7 +7,7 @@ import (
 	"io/fs"
 	"math"
 	"os"
-	"path"
+	pathutil "path"
 
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/platform"
@@ -233,7 +233,12 @@ func fdFilestatGetFunc(mod api.Module, fd, resultBuf uint32) Errno {
 		return ErrnoFault
 	}
 
-	stat, err := sys.StatFile(fsc, fd)
+	f, ok := fsc.LookupFile(fd)
+	if !ok {
+		return ErrnoBadf
+	}
+
+	stat, err := f.Stat()
 	if err != nil {
 		return ToErrno(err)
 	}
@@ -353,16 +358,9 @@ func fdPrestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 	fd, resultPrestat := uint32(params[0]), uint32(params[1])
 
-	// Currently, we only pre-open the root file descriptor.
-	if fd != sys.FdRoot {
-		return ErrnoBadf
-	}
-
-	var name string
-	if f, ok := fsc.LookupFile(fd); !ok {
-		return ErrnoBadf // closed
-	} else {
-		name = f.Name
+	name, errno := preopenPath(fsc, fd)
+	if errno != ErrnoSuccess {
+		return errno
 	}
 
 	// Upper 32-bits are zero because...
@@ -414,16 +412,9 @@ func fdPrestatDirNameFn(_ context.Context, mod api.Module, params []uint64) Errn
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 	fd, path, pathLen := uint32(params[0]), uint32(params[1]), uint32(params[2])
 
-	// Currently, we only pre-open the root file descriptor.
-	if fd != sys.FdRoot {
-		return ErrnoBadf
-	}
-
-	var name string
-	if f, ok := fsc.LookupFile(fd); !ok {
-		return ErrnoBadf // closed
-	} else {
-		name = f.Name
+	name, errno := preopenPath(fsc, fd)
+	if errno != ErrnoSuccess {
+		return errno
 	}
 
 	// Some runtimes may have another semantics. See /RATIONALE.md
@@ -931,10 +922,6 @@ func fdSeekFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	whence := uint32(params[2])
 	resultNewoffset := uint32(params[3])
 
-	if fd == sys.FdRoot {
-		return ErrnoBadf // cannot seek a directory
-	}
-
 	var seeker io.Seeker
 	// Check to see if the file descriptor is available
 	if f, ok := fsc.LookupFile(fd); !ok {
@@ -1113,11 +1100,11 @@ var pathCreateDirectory = newHostFunc(
 func pathCreateDirectoryFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	dirfd := uint32(params[0])
+	dirFD := uint32(params[0])
 	path := uint32(params[1])
 	pathLen := uint32(params[2])
 
-	pathName, errno := atPath(fsc, mod.Memory(), dirfd, path, pathLen)
+	pathName, errno := atPath(fsc, mod.Memory(), dirFD, path, pathLen)
 	if errno != ErrnoSuccess {
 		return errno
 	}
@@ -1166,40 +1153,24 @@ var pathFilestatGet = newHostFunc(
 func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	dirfd := uint32(params[0])
+	dirFD := uint32(params[0])
 
 	// TODO: flags is a lookupflags and it only has one bit: symlink_follow
 	// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#lookupflags
 	_ /* flags */ = uint32(params[1])
 
-	pathOffset := uint32(params[2])
+	path := uint32(params[2])
 	pathLen := uint32(params[3])
 
-	resultBuf := uint32(params[4])
-
-	// open_at isn't supported in fs.FS, so we check the path can't escape,
-	// then join it with its parent
-	b, ok := mod.Memory().Read(pathOffset, pathLen)
-	if !ok {
-		return ErrnoNametoolong
-	}
-	pathName := string(b)
-
-	// os.File implements ReadDirFile, so we have to check with stat.
-	stat, err := sys.StatFile(fsc, dirfd)
-	if err != nil {
-		return ToErrno(err)
+	pathName, errno := atPath(fsc, mod.Memory(), dirFD, path, pathLen)
+	if errno != ErrnoSuccess {
+		return errno
 	}
 
-	if !stat.IsDir() {
-		return ErrnoNotdir
-	} else { // prepend the name
-		// TODO: consolidate "at" logic with path_open as same issues occur.
-		pathName = path.Join(stat.Name(), pathName)
-	}
+	resultBuf := uint32(params[4])
 
 	// Stat the file without allocating a file descriptor
-	stat, err = syscallfs.StatPath(fsc.FS(), pathName)
+	stat, err := syscallfs.StatPath(fsc.FS(), pathName)
 	if err != nil {
 		return ToErrno(err)
 	}
@@ -1331,7 +1302,9 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
 
 	// Check any flags that require the file to evaluate.
 	if isDir {
-		if stat, err := sys.StatFile(fsc, newFD); err != nil || !stat.IsDir() {
+		if f, ok := fsc.LookupFile(newFD); !ok {
+			return ErrnoBadf // unexpected
+		} else if !f.IsDir() {
 			_ = fsc.CloseFile(newFD)
 			return ErrnoNotdir
 		}
@@ -1344,32 +1317,47 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	return ErrnoSuccess
 }
 
-// Note: We don't handle AT_FDCWD, as that's resolved in the compiler.
-// There's no working directory function in WASI, so CWD cannot be handled
-// here in any way except assuming it is "/".
+// atPath returns the pre-open specific path after verifying it is a directory.
+//
+// # Notes
+//
+// Languages including Zig and Rust use only pre-opens for the FD because
+// wasi-libc `__wasilibc_find_relpath` will only return a preopen. That said,
+// our wasi.c example shows other languages act differently and can use dirFD
+// of a non-preopen.
+//
+// We don't handle AT_FDCWD, as that's resolved in the compiler. There's no
+// working directory function in WASI, so most assume CWD is "/". Notably, Zig
+// has different behavior which assumes it is whatever the first pre-open name
+// is.
 //
-// See https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/sources/at_fdcwd.c#L24-L26
+// See https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/sources/at_fdcwd.c
 // See https://linux.die.net/man/2/openat
-func atPath(fsc *sys.FSContext, mem api.Memory, dirFd, path, pathLen uint32) (string, Errno) {
-	if dirFd != sys.FdRoot { //nolint
-		// TODO: Research if dirFd is always a pre-open. If so, it should
-		// always be rootFd (3), until we support multiple pre-opens.
-		//
-		// Otherwise, the dirFd could be a file created dynamically, and mean
-		// paths for Open may need to be built up. For example, if dirFd
-		// represents "/tmp/foo" and path="bar", this should open
-		// "/tmp/foo/bar" not "/bar".
+func atPath(fsc *sys.FSContext, mem api.Memory, dirFD, path, pathLen uint32) (string, Errno) {
+	b, ok := mem.Read(path, pathLen)
+	if !ok {
+		return "", ErrnoFault
 	}
+	pathName := string(b)
 
-	if _, ok := fsc.LookupFile(dirFd); !ok {
+	if f, ok := fsc.LookupFile(dirFD); !ok {
 		return "", ErrnoBadf // closed
+	} else if f.IsDir() {
+		return pathutil.Join(f.Name, pathName), ErrnoSuccess
+	} else {
+		return "", ErrnoNotdir
 	}
+}
 
-	b, ok := mem.Read(path, pathLen)
-	if !ok {
-		return "", ErrnoFault
+func preopenPath(fsc *sys.FSContext, dirFD uint32) (string, Errno) {
+	if f, ok := fsc.LookupFile(dirFD); !ok {
+		return "", ErrnoBadf // closed
+	} else if !f.IsPreopen {
+		return "", ErrnoInval
+	} else {
+		// TODO: multiple pre-opens
+		return fsc.FS().Path(), ErrnoSuccess
 	}
-	return string(b), ErrnoSuccess
 }
 
 func openFlags(oflags, fdflags uint16) (openFlags int, isDir bool) {
@@ -1436,11 +1424,11 @@ var pathRemoveDirectory = newHostFunc(
 func pathRemoveDirectoryFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	dirfd := uint32(params[0])
+	dirFD := uint32(params[0])
 	path := uint32(params[1])
 	pathLen := uint32(params[2])
 
-	pathName, errno := atPath(fsc, mod.Memory(), dirfd, path, pathLen)
+	pathName, errno := atPath(fsc, mod.Memory(), dirFD, path, pathLen)
 	if errno != ErrnoSuccess {
 		return errno
 	}
@@ -1486,20 +1474,20 @@ var pathRename = newHostFunc(
 func pathRenameFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	oldDirFd := uint32(params[0])
+	olddirFD := uint32(params[0])
 	oldPath := uint32(params[1])
 	oldPathLen := uint32(params[2])
 
-	newDirFd := uint32(params[3])
+	newdirFD := uint32(params[3])
 	newPath := uint32(params[4])
 	newPathLen := uint32(params[5])
 
-	oldPathName, errno := atPath(fsc, mod.Memory(), oldDirFd, oldPath, oldPathLen)
+	oldPathName, errno := atPath(fsc, mod.Memory(), olddirFD, oldPath, oldPathLen)
 	if errno != ErrnoSuccess {
 		return errno
 	}
 
-	newPathName, errno := atPath(fsc, mod.Memory(), newDirFd, newPath, newPathLen)
+	newPathName, errno := atPath(fsc, mod.Memory(), newdirFD, newPath, newPathLen)
 	if errno != ErrnoSuccess {
 		return errno
 	}
@@ -1551,11 +1539,11 @@ var pathUnlinkFile = newHostFunc(
 func pathUnlinkFileFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	dirfd := uint32(params[0])
+	dirFD := uint32(params[0])
 	path := uint32(params[1])
 	pathLen := uint32(params[2])
 
-	pathName, errno := atPath(fsc, mod.Memory(), dirfd, path, pathLen)
+	pathName, errno := atPath(fsc, mod.Memory(), dirFD, path, pathLen)
 	if errno != ErrnoSuccess {
 		return errno
 	}
diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go
index ac701e56..7d5a8f0d 100644
--- a/internal/gojs/fs.go
+++ b/internal/gojs/fs.go
@@ -170,7 +170,12 @@ const (
 
 // syscallFstat is like syscall.Fstat
 func syscallFstat(fsc *internalsys.FSContext, fd uint32) (*jsSt, error) {
-	stat, err := internalsys.StatFile(fsc, fd)
+	f, ok := fsc.LookupFile(fd)
+	if !ok {
+		return nil, syscall.EBADF
+	}
+
+	stat, err := f.Stat()
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/sys/fs.go b/internal/sys/fs.go
index e3d2142c..cfea4ee5 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -2,12 +2,10 @@ package sys
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"io"
 	"io/fs"
 	"os"
-	pathutil "path"
 	"syscall"
 	"time"
 
@@ -19,18 +17,18 @@ const (
 	FdStdin uint32 = iota
 	FdStdout
 	FdStderr
-	// FdRoot is the file descriptor of the root ("/") filesystem.
+	// FdPreopen is the file descriptor of the first pre-opened directory.
 	//
 	// # Why file descriptor 3?
 	//
-	// While not specified, the most common WASI implementation, wasi-libc, expects
-	// POSIX style file descriptor allocation, where the lowest available number is
-	// used to open the next file. Since 1 and 2 are taken by stdout and stderr,
-	// `root` is assigned 3.
+	// While not specified, the most common WASI implementation, wasi-libc,
+	// expects POSIX style file descriptor allocation, where the lowest
+	// available number is used to open the next file. Since 1 and 2 are taken
+	// by stdout and stderr, the next is 3.
 	//   - https://github.com/WebAssembly/WASI/issues/122
 	//   - https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_14
 	//   - https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-16/libc-bottom-half/sources/preopens.c#L215
-	FdRoot
+	FdPreopen
 )
 
 const (
@@ -38,27 +36,6 @@ const (
 	modeCharDevice = uint32(fs.ModeCharDevice | 0o640)
 )
 
-// An emptyRootDir is a fake "/" directory.
-type emptyRootDir struct{}
-
-var _ fs.ReadDirFile = emptyRootDir{}
-
-func (emptyRootDir) Stat() (fs.FileInfo, error) { return emptyRootDir{}, nil }
-func (emptyRootDir) Read([]byte) (int, error) {
-	return 0, &fs.PathError{Op: "read", Path: "/", Err: errors.New("is a directory")}
-}
-func (emptyRootDir) Close() error                       { return nil }
-func (emptyRootDir) ReadDir(int) ([]fs.DirEntry, error) { return nil, nil }
-
-var _ fs.FileInfo = emptyRootDir{}
-
-func (emptyRootDir) Name() string       { return "/" }
-func (emptyRootDir) Size() int64        { return 0 }
-func (emptyRootDir) Mode() fs.FileMode  { return fs.ModeDir | 0o555 }
-func (emptyRootDir) ModTime() time.Time { return time.Unix(0, 0) }
-func (emptyRootDir) IsDir() bool        { return true }
-func (emptyRootDir) Sys() interface{}   { return nil }
-
 type stdioFileWriter struct {
 	w io.Writer
 	s fs.FileInfo
@@ -130,15 +107,60 @@ func (stdioFileInfo) ModTime() time.Time  { return time.Unix(0, 0) }
 func (stdioFileInfo) IsDir() bool         { return false }
 func (stdioFileInfo) Sys() interface{}    { return nil }
 
+type lazyDir struct {
+	fs syscallfs.FS
+	f  fs.File
+}
+
+// Stat implements fs.File
+func (r *lazyDir) Stat() (fs.FileInfo, error) {
+	if f, err := r.file(); err != nil {
+		return nil, err
+	} else {
+		return f.Stat()
+	}
+}
+
+func (r *lazyDir) file() (f fs.File, err error) {
+	if f = r.f; r.f != nil {
+		return
+	}
+	r.f, err = r.fs.OpenFile(".", os.O_RDONLY, 0)
+	f = r.f
+	return
+}
+
+// Read implements fs.File
+func (r *lazyDir) Read(p []byte) (n int, err error) {
+	if f, err := r.file(); err != nil {
+		return 0, err
+	} else {
+		return f.Read(p)
+	}
+}
+
+// Close implements fs.File
+func (r *lazyDir) Close() error {
+	if f, err := r.file(); err != nil {
+		return nil
+	} else {
+		return f.Close()
+	}
+}
+
 // FileEntry maps a path to an open file in a file system.
 type FileEntry struct {
-	// Name is the basename of the file, at the time it was opened. When the
-	// file is root "/" (fd = FdRoot), this is "/".
+	// Name is the name of the directory up to its pre-open.
 	//
-	// Note: This must match fs.FileInfo.
+	// Note: This is empty when a pre-open and can drift on rename.
 	Name string
 
-	// File is always non-nil, even when root "/" (fd = FdRoot).
+	// IsPreopen is a directory that is lazily opened.
+	IsPreopen bool
+
+	isDirectory bool
+
+	// File is always non-nil.
 	File fs.File
 
 	// ReadDir is present when this File is a fs.ReadDirFile and `ReadDir`
@@ -146,6 +168,24 @@ type FileEntry struct {
 	ReadDir *ReadDir
 }
 
+// IsDir returns true if the file is a directory.
+func (f *FileEntry) IsDir() bool {
+	if f.IsPreopen || f.isDirectory {
+		return true
+	}
+	_, _ = f.Stat() // Maybe the file hasn't had stat yet.
+	return f.isDirectory
+}
+
+// Stat returns the underlying stat of this file.
+func (f *FileEntry) Stat() (stat fs.FileInfo, err error) {
+	stat, err = f.File.Stat()
+	if err == nil && stat.IsDir() {
+		f.isDirectory = true
+	}
+	return stat, err
+}
+
 // ReadDir is the status of a prior fs.ReadDirFile call.
 type ReadDir struct {
 	// CountRead is the total count of files read including Entries.
@@ -161,49 +201,31 @@ type FSContext struct {
 	// fs is the root ("/") mount.
 	fs syscallfs.FS
 
-	// openedFiles is a map of file descriptor numbers (>=FdRoot) to open files
+	// openedFiles is a map of file descriptor numbers (>=FdPreopen) to open files
 	// (or directories) and defaults to empty.
 	// TODO: This is unguarded, so not goroutine-safe!
 	openedFiles FileTable
 }
 
-var errNotDir = errors.New("not a directory")
-
-// NewFSContext creates a FSContext, using the `root` parameter for any paths
-// beginning at "/". If the input is EmptyFS, there is no root filesystem.
-// 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 syscallfs.FS) (fsc *FSContext, err error) {
-	fsc = &FSContext{fs: root}
+// NewFSContext creates a FSContext with stdio streams and an optional
+// pre-opened filesystem.
+//
+// If `preopened` is not syscallfs.EmptyFS, it is inserted into the file
+// descriptor table as FdPreopen.
+func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, preopened syscallfs.FS) (fsc *FSContext, err error) {
+	fsc = &FSContext{fs: preopened}
 	fsc.openedFiles.Insert(stdinReader(stdin))
 	fsc.openedFiles.Insert(stdioWriter(stdout, noopStdoutStat))
 	fsc.openedFiles.Insert(stdioWriter(stderr, noopStderrStat))
 
-	if root == syscallfs.EmptyFS {
+	if preopened == syscallfs.EmptyFS {
 		return fsc, nil
 	}
 
-	// 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 ".".
-		rootDir = emptyRootDir{}
-		err = nil
-	}
-
-	// Verify the directory existed and was a directory at the time the context
-	// was created.
-	var stat fs.FileInfo
-	if stat, err = rootDir.Stat(); err != nil {
-		return // err if we couldn't determine if the root was a directory.
-	} else if !stat.IsDir() {
-		err = &fs.PathError{Op: "ReadDir", Path: stat.Name(), Err: errNotDir}
-		return
-	}
-
-	fsc.openedFiles.Insert(&FileEntry{Name: "/", File: rootDir})
+	fsc.openedFiles.Insert(&FileEntry{
+		IsPreopen: true,
+		File:      &lazyDir{fs: preopened},
+	})
 	return fsc, nil
 }
 
@@ -212,7 +234,7 @@ func stdinReader(r io.Reader) *FileEntry {
 		r = eofReader{}
 	}
 	s := stdioStat(r, noopStdinStat)
-	return &FileEntry{Name: noopStdinStat.Name(), File: &stdioFileReader{r: r, s: s}}
+	return &FileEntry{File: &stdioFileReader{r: r, s: s}}
 }
 
 func stdioWriter(w io.Writer, defaultStat stdioFileInfo) *FileEntry {
@@ -220,7 +242,7 @@ func stdioWriter(w io.Writer, defaultStat stdioFileInfo) *FileEntry {
 		w = io.Discard
 	}
 	s := stdioStat(w, defaultStat)
-	return &FileEntry{Name: defaultStat.Name(), File: &stdioFileWriter{w: w, s: s}}
+	return &FileEntry{File: &stdioFileWriter{w: w, s: s}}
 }
 
 func stdioStat(f interface{}, defaultStat stdioFileInfo) fs.FileInfo {
@@ -255,7 +277,10 @@ func (c *FSContext) OpenFile(path string, flag int, perm fs.FileMode) (uint32, e
 	if f, err := c.fs.OpenFile(path, flag, perm); err != nil {
 		return 0, err
 	} else {
-		newFD := c.openedFiles.Insert(&FileEntry{Name: pathutil.Base(path), File: f})
+		if path == "/" || path == "." {
+			path = ""
+		}
+		newFD := c.openedFiles.Insert(&FileEntry{Name: path, File: f})
 		return newFD, nil
 	}
 }
@@ -291,17 +316,6 @@ func (c *FSContext) Close(context.Context) (err error) {
 	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) {
diff --git a/internal/syscallfs/adapter.go b/internal/syscallfs/adapter.go
index fc129acf..6ebd95d9 100644
--- a/internal/syscallfs/adapter.go
+++ b/internal/syscallfs/adapter.go
@@ -16,13 +16,20 @@ func Adapt(fs fs.FS) FS {
 	return &adapter{fs}
 }
 
-type adapter struct{ fs fs.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))
 }
 
+// Path implements FS.Path
+func (ro *adapter) Path() string {
+	return "/"
+}
+
 // 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 {
diff --git a/internal/syscallfs/dirfs.go b/internal/syscallfs/dirfs.go
index 150be6a8..1aa63189 100644
--- a/internal/syscallfs/dirfs.go
+++ b/internal/syscallfs/dirfs.go
@@ -27,6 +27,11 @@ func (dir dirFS) Open(name string) (fs.File, error) {
 	panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
 }
 
+// Path implements FS.Path
+func (dir dirFS) Path() string {
+	return "/"
+}
+
 // OpenFile implements FS.OpenFile
 func (dir dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
 	f, err := os.OpenFile(path.Join(string(dir), name), flag, perm)
diff --git a/internal/syscallfs/empty.go b/internal/syscallfs/empty.go
index 14fb233e..25797946 100644
--- a/internal/syscallfs/empty.go
+++ b/internal/syscallfs/empty.go
@@ -8,41 +8,46 @@ import (
 
 // EmptyFS is an FS that returns syscall.ENOENT for all read functions, and
 // syscall.ENOSYS otherwise.
-var EmptyFS FS = unsupported{}
+var EmptyFS FS = empty{}
 
-type unsupported struct{}
+type empty struct{}
 
 // Open implements the same method as documented on fs.FS
-func (unsupported) Open(name string) (fs.File, error) {
+func (empty) Open(name string) (fs.File, error) {
 	panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
 }
 
+// Path implements FS.Path
+func (empty) Path() string {
+	return "/"
+}
+
 // OpenFile implements FS.OpenFile
-func (unsupported) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
+func (empty) 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 {
+func (empty) Mkdir(path string, perm fs.FileMode) error {
 	return syscall.ENOSYS
 }
 
 // Rename implements FS.Rename
-func (unsupported) Rename(from, to string) error {
+func (empty) Rename(from, to string) error {
 	return syscall.ENOSYS
 }
 
 // Rmdir implements FS.Rmdir
-func (unsupported) Rmdir(path string) error {
+func (empty) Rmdir(path string) error {
 	return syscall.ENOSYS
 }
 
 // Unlink implements FS.Unlink
-func (unsupported) Unlink(path string) error {
+func (empty) Unlink(path string) error {
 	return syscall.ENOSYS
 }
 
 // Utimes implements FS.Utimes
-func (unsupported) Utimes(path string, atimeNsec, mtimeNsec int64) error {
+func (empty) Utimes(path string, atimeNsec, mtimeNsec int64) error {
 	return syscall.ENOSYS
 }
diff --git a/internal/syscallfs/readfs.go b/internal/syscallfs/readfs.go
index 3d9a4626..cffffc48 100644
--- a/internal/syscallfs/readfs.go
+++ b/internal/syscallfs/readfs.go
@@ -17,39 +17,44 @@ func NewReadFS(fs FS) FS {
 type readFS struct{ fs FS }
 
 // Open implements the same method as documented on fs.FS
-func (ro *readFS) Open(name string) (fs.File, error) {
+func (r *readFS) Open(name string) (fs.File, error) {
 	panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
 }
 
+// Path implements FS.Path
+func (r *readFS) Path() string {
+	return "/"
+}
+
 // OpenFile implements FS.OpenFile
-func (ro *readFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
+func (r *readFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
 	if flag == 0 || flag == os.O_RDONLY {
-		return ro.fs.OpenFile(path, flag, perm)
+		return r.fs.OpenFile(path, flag, perm)
 	}
 	return nil, syscall.ENOSYS
 }
 
 // Mkdir implements FS.Mkdir
-func (ro *readFS) Mkdir(path string, perm fs.FileMode) error {
+func (r *readFS) Mkdir(path string, perm fs.FileMode) error {
 	return syscall.ENOSYS
 }
 
 // Rename implements FS.Rename
-func (ro *readFS) Rename(from, to string) error {
+func (r *readFS) Rename(from, to string) error {
 	return syscall.ENOSYS
 }
 
 // Rmdir implements FS.Rmdir
-func (ro *readFS) Rmdir(path string) error {
+func (r *readFS) Rmdir(path string) error {
 	return syscall.ENOSYS
 }
 
 // Unlink implements FS.Unlink
-func (ro *readFS) Unlink(path string) error {
+func (r *readFS) Unlink(path string) error {
 	return syscall.ENOSYS
 }
 
 // Utimes implements FS.Utimes
-func (ro *readFS) Utimes(path string, atimeNsec, mtimeNsec int64) error {
+func (r *readFS) Utimes(path string, atimeNsec, mtimeNsec int64) error {
 	return syscall.ENOSYS
 }
diff --git a/internal/syscallfs/syscallfs.go b/internal/syscallfs/syscallfs.go
index 01cedc01..c9d2a702 100644
--- a/internal/syscallfs/syscallfs.go
+++ b/internal/syscallfs/syscallfs.go
@@ -13,6 +13,25 @@ import (
 //
 // See https://github.com/golang/go/issues/45757
 type FS interface {
+	// Path is the name of the path the guest should use this filesystem for,
+	// or root ("/") if unknown.
+	//
+	// This value allows the guest to avoid making file-system calls when they
+	// won't succeed. e.g. if "/tmp" is returned and the guest requests
+	// "/etc/passwd". This approach is used in compilers that use WASI
+	// pre-opens.
+	//
+	// # Notes
+	//   - Go compiled with runtime.GOOS=js do not pay attention to this value.
+	//     Hence, you need to normalize the filesystem with NewRootFS to ensure
+	//     paths requested resolve as expected.
+	//   - Working directories are typically tracked in wasm, though possible
+	//     some relative paths are requested. For example, TinyGo may attempt
+	//     to resolve a path "../.." in unit tests.
+	//   - Zig uses the first path name it sees as the initial working
+	//     directory of the process.
+	Path() string
+
 	// Open is only defined to match the signature of fs.FS until we remove it.
 	// Once we are done bridging, we will remove this function. Meanwhile,
 	// using it will panic to ensure internal code doesn't depend on it.


Test patch:
diff --git a/.github/workflows/spectest.yaml b/.github/workflows/spectest.yaml
index f4debf27..719ca8b8 100644
--- a/.github/workflows/spectest.yaml
+++ b/.github/workflows/spectest.yaml
@@ -46,41 +46,6 @@ jobs:
 
       - run: make spectest.${{ matrix.spec-version }}
 
-  test_freebsd:  # See /Vagrantfile for notes
-    name: ${{ matrix.spec-version }} - freebsd/amd64, Go-${{ matrix.go-version }}
-    runs-on: macos-12
-    strategy:
-      fail-fast: false  # don't fail fast as sometimes failures are arch/OS specific
-      matrix:  # Use versions consistent with wazero's Go support policy.
-        go-version:
-          - "1.19"  # Current Go version
-          - "1.17"  # Floor Go version of wazero (current - 2)
-        spec-version:
-          - "v1"
-          - "v2"
-
-    steps:
-      - uses: actions/setup-go@v3
-        with:  # Note: This isn't really used, it is just to match versions.
-          go-version: ${{ matrix.go-version }}
-
-      - uses: actions/checkout@v3
-
-      - uses: actions/cache@v3
-        with:
-          path: ~/.vagrant.d/boxes
-          key: spectest-${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum','Vagrantfile') }}-${{ matrix.spec-version }}
-
-      - name: Install Vagrant
-        run: brew install --cask vagrant
-
-      - name: Configure Vagrant
-        run: |  # This ensures the same version of Go is used inside Vagrant.
-          GOVERSION=$(go env GOVERSION) GOARCH=$(go env GOARCH) vagrant up
-
-      - name: make spectest.${{ matrix.spec-version }}
-        run: vagrant ssh -c "cd wazero && make spectest.${{ matrix.spec-version }}"
-
   test_scratch:
     name: ${{ matrix.spec-version }} / ${{ matrix.arch }}, Go-${{ matrix.go-version }}
     runs-on: ubuntu-20.04
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index bcdc502f..c23fb2da 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -161,7 +161,7 @@ func Test_fdFdstatGet(t *testing.T) {
 		},
 		{
 			name: "root",
-			fd:   sys.FdRoot,
+			fd:   sys.FdPreopen,
 			expectedMemory: []byte{
 				3, 0, // fs_filetype
 				0, 0, 0, 0, 0, 0, // fs_flags
@@ -345,7 +345,7 @@ func Test_fdFilestatGet(t *testing.T) {
 		},
 		{
 			name: "root",
-			fd:   sys.FdRoot,
+			fd:   sys.FdPreopen,
 			expectedMemory: []byte{
 				0, 0, 0, 0, 0, 0, 0, 0, // dev
 				0, 0, 0, 0, 0, 0, 0, 0, // ino
@@ -720,7 +720,7 @@ func Test_fdPread_Errors(t *testing.T) {
 func Test_fdPrestatGet(t *testing.T) {
 	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fstest.MapFS{}))
 	defer r.Close(testCtx)
-	fd := sys.FdRoot // only pre-opened directory currently supported.
+	dirFD := sys.FdPreopen
 
 	resultPrestat := uint32(1) // arbitrary offset
 	expectedMemory := []byte{
@@ -734,7 +734,7 @@ func Test_fdPrestatGet(t *testing.T) {
 
 	maskMemory(t, mod, len(expectedMemory))
 
-	requireErrno(t, ErrnoSuccess, mod, FdPrestatGetName, uint64(fd), uint64(resultPrestat))
+	requireErrno(t, ErrnoSuccess, mod, FdPrestatGetName, uint64(dirFD), uint64(resultPrestat))
 	require.Equal(t, `
 ==> wasi_snapshot_preview1.fd_prestat_get(fd=3)
 <== (prestat={pr_name_len=1},errno=ESUCCESS)
@@ -746,9 +746,8 @@ func Test_fdPrestatGet(t *testing.T) {
 }
 
 func Test_fdPrestatGet_Errors(t *testing.T) {
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fstest.MapFS{}))
+	mod, dirFD, log, r := requireOpenFile(t, "/tmp", nil)
 	defer r.Close(testCtx)
-	fd := sys.FdRoot // only pre-opened directory currently supported.
 
 	memorySize := mod.Memory().Size()
 	tests := []struct {
@@ -759,18 +758,28 @@ func Test_fdPrestatGet_Errors(t *testing.T) {
 		expectedLog   string
 	}{
 		{
-			name:          "invalid FD",
+			name:          "unopened FD",
 			fd:            42, // arbitrary invalid FD
 			resultPrestat: 0,  // valid offset
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_prestat_get(fd=42)
 <== (prestat=,errno=EBADF)
+`,
+		},
+		{
+			name:          "not pre-opened FD",
+			fd:            dirFD,
+			resultPrestat: 0, // valid offset
+			expectedErrno: ErrnoInval,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_prestat_get(fd=4)
+<== (prestat=,errno=EINVAL)
 `,
 		},
 		{
 			name:          "out-of-memory resultPrestat",
-			fd:            fd,
+			fd:            sys.FdPreopen,
 			resultPrestat: memorySize,
 			expectedErrno: ErrnoFault,
 			expectedLog: `
@@ -778,7 +787,6 @@ func Test_fdPrestatGet_Errors(t *testing.T) {
 <== (prestat=,errno=EFAULT)
 `,
 		},
-		// TODO: non pre-opened file == api.ErrnoBadf
 	}
 
 	for _, tt := range tests {
@@ -796,7 +804,7 @@ func Test_fdPrestatGet_Errors(t *testing.T) {
 func Test_fdPrestatDirName(t *testing.T) {
 	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fstest.MapFS{}))
 	defer r.Close(testCtx)
-	fd := sys.FdRoot // only pre-opened directory currently supported.
+	dirFD := sys.FdPreopen
 
 	path := uint32(1)    // arbitrary offset
 	pathLen := uint32(0) // shorter than len("/") to prove truncation is ok
@@ -806,7 +814,7 @@ func Test_fdPrestatDirName(t *testing.T) {
 
 	maskMemory(t, mod, len(expectedMemory))
 
-	requireErrno(t, ErrnoSuccess, mod, FdPrestatDirNameName, uint64(fd), uint64(path), uint64(pathLen))
+	requireErrno(t, ErrnoSuccess, mod, FdPrestatDirNameName, uint64(dirFD), uint64(path), uint64(pathLen))
 	require.Equal(t, `
 ==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=3)
 <== (path=,errno=ESUCCESS)
@@ -818,9 +826,8 @@ func Test_fdPrestatDirName(t *testing.T) {
 }
 
 func Test_fdPrestatDirName_Errors(t *testing.T) {
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fstest.MapFS{}))
+	mod, dirFD, log, r := requireOpenFile(t, "/tmp", nil)
 	defer r.Close(testCtx)
-	fd := sys.FdRoot // only pre-opened directory currently supported.
 
 	memorySize := mod.Memory().Size()
 	maskMemory(t, mod, 10)
@@ -838,7 +845,7 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 	}{
 		{
 			name:          "out-of-memory path",
-			fd:            fd,
+			fd:            sys.FdPreopen,
 			path:          memorySize,
 			pathLen:       pathLen,
 			expectedErrno: ErrnoFault,
@@ -849,7 +856,7 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 		},
 		{
 			name:          "path exceeds the maximum valid address by 1",
-			fd:            fd,
+			fd:            sys.FdPreopen,
 			path:          memorySize - pathLen + 1,
 			pathLen:       pathLen,
 			expectedErrno: ErrnoFault,
@@ -860,7 +867,7 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 		},
 		{
 			name:          "pathLen exceeds the length of the dir name",
-			fd:            fd,
+			fd:            sys.FdPreopen,
 			path:          validAddress,
 			pathLen:       pathLen + 1,
 			expectedErrno: ErrnoNametoolong,
@@ -870,7 +877,7 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 `,
 		},
 		{
-			name:          "invalid fd",
+			name:          "unopened FD",
 			fd:            42, // arbitrary invalid fd
 			path:          validAddress,
 			pathLen:       pathLen,
@@ -880,7 +887,17 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 <== (path=,errno=EBADF)
 `,
 		},
-		// TODO: non pre-opened file == ErrnoBadf
+		{
+			name:          "not pre-opened FD",
+			fd:            dirFD,
+			path:          validAddress,
+			pathLen:       pathLen,
+			expectedErrno: ErrnoInval,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=4)
+<== (path=,errno=EINVAL)
+`,
+		},
 	}
 
 	for _, tt := range tests {
@@ -1879,11 +1896,11 @@ func Test_pathCreateDirectory(t *testing.T) {
 	ok := mod.Memory().Write(0, append([]byte{'?'}, pathName...))
 	require.True(t, ok)
 
-	dirFD := sys.FdRoot
+	preopenedFD := sys.FdPreopen
 	name := 1
 	nameLen := len(pathName)
 
-	requireErrno(t, ErrnoSuccess, mod, PathCreateDirectoryName, uint64(dirFD), uint64(name), uint64(nameLen))
+	requireErrno(t, ErrnoSuccess, mod, PathCreateDirectoryName, uint64(preopenedFD), uint64(name), uint64(nameLen))
 	require.Equal(t, `
 ==> wasi_snapshot_preview1.path_create_directory(fd=3,path=wazero)
 <== errno=ESUCCESS
@@ -1907,6 +1924,7 @@ func Test_pathCreateDirectory_Errors(t *testing.T) {
 	file := "file"
 	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
+	fileFD := requireOpenFD(t, mod, file)
 
 	dir := "dir"
 	err = os.Mkdir(path.Join(tmpDir, dir), 0o700)
@@ -1919,17 +1937,29 @@ func Test_pathCreateDirectory_Errors(t *testing.T) {
 		expectedLog       string
 	}{
 		{
-			name:          "invalid fd",
+			name:          "unopened FD",
 			fd:            42, // arbitrary invalid fd
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_create_directory(fd=42,path=)
 <== errno=EBADF
+`,
+		},
+		{
+			name:          "FD not a directory",
+			fd:            fileFD,
+			pathName:      file,
+			path:          0,
+			pathLen:       uint32(len(file)),
+			expectedErrno: ErrnoNotdir,
+			expectedLog: `
+==> wasi_snapshot_preview1.path_create_directory(fd=4,path=file)
+<== errno=ENOTDIR
 `,
 		},
 		{
 			name:          "out-of-memory reading path",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			path:          mod.Memory().Size(),
 			pathLen:       1,
 			expectedErrno: ErrnoFault,
@@ -1940,7 +1970,7 @@ func Test_pathCreateDirectory_Errors(t *testing.T) {
 		},
 		{
 			name:          "out-of-memory reading pathLen",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			path:          0,
 			pathLen:       mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is OOM for path
 			expectedErrno: ErrnoFault,
@@ -1951,7 +1981,7 @@ func Test_pathCreateDirectory_Errors(t *testing.T) {
 		},
 		{
 			name:          "file exists",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			pathName:      file,
 			path:          0,
 			pathLen:       uint32(len(file)),
@@ -1963,7 +1993,7 @@ func Test_pathCreateDirectory_Errors(t *testing.T) {
 		},
 		{
 			name:          "dir exists",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			pathName:      dir,
 			path:          0,
 			pathLen:       uint32(len(dir)),
@@ -1989,29 +2019,23 @@ func Test_pathCreateDirectory_Errors(t *testing.T) {
 }
 
 func Test_pathFilestatGet(t *testing.T) {
-	file, dir := "a", "b"
+	file, dir, fileInDir := "a", "b", "a/b"
 	testFS := fstest.MapFS{
-		file:             {Data: make([]byte, 10), ModTime: time.Unix(1667482413, 0)},
-		dir:              {Mode: fs.ModeDir, ModTime: time.Unix(1667482413, 0)},
-		dir + "/" + file: {Data: make([]byte, 20), ModTime: time.Unix(1667482413, 0)},
+		file:      {Data: make([]byte, 10), ModTime: time.Unix(1667482413, 0)},
+		dir:       {Mode: fs.ModeDir, ModTime: time.Unix(1667482413, 0)},
+		fileInDir: {Data: make([]byte, 20), ModTime: time.Unix(1667482413, 0)},
 	}
 
 	initialMemoryFile := append([]byte{'?'}, file...)
 	initialMemoryDir := append([]byte{'?'}, dir...)
+	initialMemoryFileInDir := append([]byte{'?'}, fileInDir...)
 	initialMemoryNotExists := []byte{'?', '?'}
 
 	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 	memorySize := mod.Memory().Size()
 
-	// open both paths without using WASI
-	fsc := mod.(*wasm.CallContext).Sys.FS()
-
-	fileFD, err := fsc.OpenFile(file, os.O_RDONLY, 0)
-	require.NoError(t, err)
-
-	dirFD, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
-	require.NoError(t, err)
+	fileFD := requireOpenFD(t, mod, file)
 
 	tests := []struct {
 		name                        string
@@ -2022,7 +2046,7 @@ func Test_pathFilestatGet(t *testing.T) {
 	}{
 		{
 			name:           "file under root",
-			fd:             sys.FdRoot,
+			fd:             sys.FdPreopen,
 			memory:         initialMemoryFile,
 			pathLen:        1,
 			resultFilestat: 2,
@@ -2044,12 +2068,12 @@ func Test_pathFilestatGet(t *testing.T) {
 		},
 		{
 			name:           "file under dir",
-			fd:             dirFD, // root
-			memory:         initialMemoryFile,
-			pathLen:        1,
-			resultFilestat: 2,
+			fd:             sys.FdPreopen, // root
+			memory:         initialMemoryFileInDir,
+			pathLen:        uint32(len(initialMemoryFileInDir)) - 1,
+			resultFilestat: uint32(len(initialMemoryFileInDir)),
 			expectedMemory: append(
-				initialMemoryFile,
+				initialMemoryFileInDir,
 				0, 0, 0, 0, 0, 0, 0, 0, // dev
 				0, 0, 0, 0, 0, 0, 0, 0, // ino
 				4, 0, 0, 0, 0, 0, 0, 0, // filetype + padding
@@ -2060,13 +2084,13 @@ func Test_pathFilestatGet(t *testing.T) {
 				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // ctim
 			),
 			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=5,flags=,path=a)
+==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=,path=a/b)
 <== (filestat={filetype=REGULAR_FILE,size=20,mtim=1667482413000000000},errno=ESUCCESS)
 `,
 		},
 		{
 			name:           "dir under root",
-			fd:             sys.FdRoot,
+			fd:             sys.FdPreopen,
 			memory:         initialMemoryDir,
 			pathLen:        1,
 			resultFilestat: 2,
@@ -2087,7 +2111,7 @@ func Test_pathFilestatGet(t *testing.T) {
 `,
 		},
 		{
-			name:          "bad FD - not opened",
+			name:          "unopened FD",
 			fd:            math.MaxUint32,
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
@@ -2096,10 +2120,10 @@ func Test_pathFilestatGet(t *testing.T) {
 `,
 		},
 		{
-			name:           "bad FD - not dir",
+			name:           "FD not a directory",
 			fd:             fileFD,
 			memory:         initialMemoryFile,
-			pathLen:        1,
+			pathLen:        uint32(len(file)),
 			resultFilestat: 2,
 			expectedErrno:  ErrnoNotdir,
 			expectedLog: `
@@ -2109,7 +2133,7 @@ func Test_pathFilestatGet(t *testing.T) {
 		},
 		{
 			name:           "path under root doesn't exist",
-			fd:             sys.FdRoot,
+			fd:             sys.FdPreopen,
 			memory:         initialMemoryNotExists,
 			pathLen:        1,
 			resultFilestat: 2,
@@ -2117,46 +2141,22 @@ func Test_pathFilestatGet(t *testing.T) {
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=,path=?)
 <== (filestat=,errno=ENOENT)
-`,
-		},
-		{
-			name:           "path under dir doesn't exist",
-			fd:             dirFD,
-			memory:         initialMemoryNotExists,
-			pathLen:        1,
-			resultFilestat: 2,
-			expectedErrno:  ErrnoNoent,
-			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=5,flags=,path=?)
-<== (filestat=,errno=ENOENT)
-`,
-		},
-		{
-			name:           "path invalid",
-			fd:             dirFD,
-			memory:         []byte("?../foo"),
-			pathLen:        6,
-			resultFilestat: 7,
-			expectedErrno:  ErrnoNoent,
-			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=5,flags=,path=../foo)
-<== (filestat=,errno=ENOENT)
 `,
 		},
 		{
 			name:          "path is out of memory",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			memory:        initialMemoryFile,
 			pathLen:       memorySize,
-			expectedErrno: ErrnoNametoolong,
+			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=,path=OOM(1,65536))
-<== (filestat=,errno=ENAMETOOLONG)
+<== (filestat=,errno=EFAULT)
 `,
 		},
 		{
 			name:           "resultFilestat exceeds the maximum valid address by 1",
-			fd:             sys.FdRoot,
+			fd:             sys.FdPreopen,
 			memory:         initialMemoryFile,
 			pathLen:        1,
 			resultFilestat: memorySize - 64 + 1,
@@ -2231,7 +2231,7 @@ func Test_pathOpen(t *testing.T) {
 	dirFileContents := []byte("def")
 	writeFile(t, dir, dirFileName, dirFileContents)
 
-	expectedOpenedFd := sys.FdRoot + 1
+	expectedOpenedFd := sys.FdPreopen + 1
 
 	tests := []struct {
 		name          string
@@ -2368,9 +2368,9 @@ 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 := sys.StatFile(fsc, expectedOpenedFd)
-				require.NoError(t, err)
-				require.True(t, stat.IsDir())
+				f, ok := fsc.LookupFile(expectedOpenedFd)
+				require.True(t, ok)
+				require.True(t, f.IsDir())
 			},
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=dir,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
@@ -2383,9 +2383,9 @@ 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 := sys.StatFile(fsc, expectedOpenedFd)
-				require.NoError(t, err)
-				require.True(t, stat.IsDir())
+				f, ok := fsc.LookupFile(expectedOpenedFd)
+				require.True(t, ok)
+				require.True(t, f.IsDir())
 			},
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=dir,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
@@ -2437,7 +2437,7 @@ func Test_pathOpen(t *testing.T) {
 			path := uint32(0)
 			pathLen := uint32(len(pathName))
 			resultOpenedFd := pathLen
-			dirfd := sys.FdRoot
+			dirfd := sys.FdPreopen
 
 			// TODO: dirflags is a lookupflags and it only has one bit: symlink_follow
 			// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#lookupflags
@@ -2461,6 +2461,14 @@ func Test_pathOpen(t *testing.T) {
 	}
 }
 
+func requireOpenFD(t *testing.T, mod api.Module, path string) uint32 {
+	fsc := mod.(*wasm.CallContext).Sys.FS()
+
+	fd, err := fsc.OpenFile(path, os.O_RDONLY, 0)
+	require.NoError(t, err)
+	return fd
+}
+
 func requireContents(t *testing.T, fsc *sys.FSContext, expectedOpenedFd uint32, fileName string, fileContents []byte) {
 	// verify the file was actually opened
 	f, ok := fsc.LookupFile(expectedOpenedFd)
@@ -2490,18 +2498,23 @@ 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 := "file" // name length as wazero
-	testFS := fstest.MapFS{
-		dirName:  &fstest.MapFile{Mode: os.ModeDir},
-		fileName: &fstest.MapFile{},
-	}
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
+	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
+	fs, err := syscallfs.NewDirFS(tmpDir)
+	require.NoError(t, err)
+
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
 	defer r.Close(testCtx)
 
-	validPath := uint32(0)    // arbitrary offset
-	validPathLen := uint32(6) // the length of dirName
+	preopenedFD := sys.FdPreopen
+
+	file := "file"
+	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
+	require.NoError(t, err)
+	fileFD := requireOpenFD(t, mod, file)
+
+	dir := "dir"
+	err = os.Mkdir(path.Join(tmpDir, dir), 0o700)
+	require.NoError(t, err)
 
 	tests := []struct {
 		name, pathName                            string
@@ -2510,29 +2523,41 @@ func Test_pathOpen_Errors(t *testing.T) {
 		expectedLog                               string
 	}{
 		{
-			name:          "invalid fd",
+			name:          "unopened FD",
 			fd:            42, // arbitrary invalid fd
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_open(fd=42,dirflags=,path=,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
 <== (opened_fd=,errno=EBADF)
+`,
+		},
+		{
+			name:          "FD not a directory",
+			fd:            fileFD,
+			pathName:      file,
+			path:          0,
+			pathLen:       uint32(len(file)),
+			expectedErrno: ErrnoNotdir,
+			expectedLog: `
+==> wasi_snapshot_preview1.path_open(fd=4,dirflags=,path=file,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+<== (opened_fd=,errno=ENOTDIR)
 `,
 		},
 		{
 			name:          "out-of-memory reading path",
-			fd:            validFD,
+			fd:            sys.FdPreopen,
 			path:          mod.Memory().Size(),
-			pathLen:       validPathLen,
+			pathLen:       uint32(len(file)),
 			expectedErrno: ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=OOM(65536,6),oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=OOM(65536,4),oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
 <== (opened_fd=,errno=EFAULT)
 `,
 		},
 		{
 			name:          "out-of-memory reading pathLen",
-			fd:            validFD,
-			path:          validPath,
+			fd:            sys.FdPreopen,
+			path:          0,
 			pathLen:       mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is OOM for path
 			expectedErrno: ErrnoFault,
 			expectedLog: `
@@ -2542,36 +2567,36 @@ func Test_pathOpen_Errors(t *testing.T) {
 		},
 		{
 			name:          "no such file exists",
-			fd:            validFD,
-			pathName:      dirName,
-			path:          validPath,
-			pathLen:       validPathLen - 1, // this make the path "wazer", which doesn't exit
+			fd:            sys.FdPreopen,
+			pathName:      dir,
+			path:          0,
+			pathLen:       uint32(len(dir)) - 1,
 			expectedErrno: ErrnoNoent,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=wazer,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=di,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
 <== (opened_fd=,errno=ENOENT)
 `,
 		},
 		{
 			name:           "out-of-memory writing resultOpenedFd",
-			fd:             validFD,
-			pathName:       dirName,
-			path:           validPath,
-			pathLen:        validPathLen,
+			fd:             preopenedFD,
+			pathName:       dir,
+			path:           0,
+			pathLen:        uint32(len(dir)),
 			resultOpenedFd: mod.Memory().Size(), // path and pathLen correctly point to the right path, but where to write the opened FD is outside memory.
 			expectedErrno:  ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=wazero,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=dir,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
 <== (opened_fd=,errno=EFAULT)
 `,
 		},
 		{
 			name:          "O_DIRECTORY, but not a directory",
 			oflags:        uint32(O_DIRECTORY),
-			fd:            validFD,
-			pathName:      fileName,
-			path:          validPath,
-			pathLen:       uint32(len(fileName)),
+			fd:            sys.FdPreopen,
+			pathName:      file,
+			path:          0,
+			pathLen:       uint32(len(file)),
 			expectedErrno: ErrnoNotdir,
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=file,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
@@ -2581,10 +2606,10 @@ func Test_pathOpen_Errors(t *testing.T) {
 		{
 			name:          "oflags=directory and create invalid",
 			oflags:        uint32(O_DIRECTORY | O_CREAT),
-			fd:            validFD,
-			pathName:      fileName,
-			path:          validPath,
-			pathLen:       uint32(len(fileName)),
+			fd:            sys.FdPreopen,
+			pathName:      file,
+			path:          0,
+			pathLen:       uint32(len(file)),
 			expectedErrno: ErrnoInval,
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=file,oflags=CREAT|DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
@@ -2598,7 +2623,7 @@ func Test_pathOpen_Errors(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			defer log.Reset()
 
-			mod.Memory().Write(validPath, []byte(tc.pathName))
+			mod.Memory().Write(tc.path, []byte(tc.pathName))
 
 			requireErrno(t, tc.expectedErrno, mod, PathOpenName, uint64(tc.fd), uint64(0), uint64(tc.path),
 				uint64(tc.pathLen), uint64(tc.oflags), 0, 0, 0, uint64(tc.resultOpenedFd))
@@ -2634,7 +2659,7 @@ func Test_pathRemoveDirectory(t *testing.T) {
 	err = os.Mkdir(realPath, 0o700)
 	require.NoError(t, err)
 
-	dirFD := sys.FdRoot
+	dirFD := sys.FdPreopen
 	name := 1
 	nameLen := len(pathName)
 
@@ -2660,6 +2685,7 @@ func Test_pathRemoveDirectory_Errors(t *testing.T) {
 	file := "file"
 	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
+	fileFD := requireOpenFD(t, mod, file)
 
 	dirNotEmpty := "notempty"
 	err = os.Mkdir(path.Join(tmpDir, dirNotEmpty), 0o700)
@@ -2676,17 +2702,29 @@ func Test_pathRemoveDirectory_Errors(t *testing.T) {
 		expectedLog       string
 	}{
 		{
-			name:          "invalid fd",
+			name:          "unopened FD",
 			fd:            42, // arbitrary invalid fd
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_remove_directory(fd=42,path=)
 <== errno=EBADF
+`,
+		},
+		{
+			name:          "FD not a directory",
+			fd:            fileFD,
+			pathName:      file,
+			path:          0,
+			pathLen:       uint32(len(file)),
+			expectedErrno: ErrnoNotdir,
+			expectedLog: `
+==> wasi_snapshot_preview1.path_remove_directory(fd=4,path=file)
+<== errno=ENOTDIR
 `,
 		},
 		{
 			name:          "out-of-memory reading path",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			path:          mod.Memory().Size(),
 			pathLen:       1,
 			expectedErrno: ErrnoFault,
@@ -2697,7 +2735,7 @@ func Test_pathRemoveDirectory_Errors(t *testing.T) {
 		},
 		{
 			name:          "out-of-memory reading pathLen",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			path:          0,
 			pathLen:       mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is OOM for path
 			expectedErrno: ErrnoFault,
@@ -2708,7 +2746,7 @@ func Test_pathRemoveDirectory_Errors(t *testing.T) {
 		},
 		{
 			name:          "no such file exists",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			pathName:      file,
 			path:          0,
 			pathLen:       uint32(len(file) - 1),
@@ -2720,7 +2758,7 @@ func Test_pathRemoveDirectory_Errors(t *testing.T) {
 		},
 		{
 			name:          "file not dir",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			pathName:      file,
 			path:          0,
 			pathLen:       uint32(len(file)),
@@ -2732,7 +2770,7 @@ func Test_pathRemoveDirectory_Errors(t *testing.T) {
 		},
 		{
 			name:          "dir not empty",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			pathName:      dirNotEmpty,
 			path:          0,
 			pathLen:       uint32(len(dirNotEmpty)),
@@ -2783,7 +2821,7 @@ func Test_pathRename(t *testing.T) {
 	defer r.Close(testCtx)
 
 	// set up the initial memory to include the old path name starting at an offset.
-	oldDirFD := sys.FdRoot
+	oldDirFD := sys.FdPreopen
 	oldPathName := "wazero"
 	realOldPath := path.Join(tmpDir, oldPathName)
 	oldPath := uint32(0)
@@ -2795,7 +2833,7 @@ func Test_pathRename(t *testing.T) {
 	err = os.WriteFile(realOldPath, []byte{}, 0o600)
 	require.NoError(t, err)
 
-	newDirFD := sys.FdRoot
+	newDirFD := sys.FdPreopen
 	newPathName := "wahzero"
 	realNewPath := path.Join(tmpDir, newPathName)
 	newPath := uint32(16)
@@ -2830,7 +2868,19 @@ func Test_pathRename_Errors(t *testing.T) {
 	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
 
-	dir := "dir"
+	// We have to test FD validation with a path not under test. Otherwise,
+	// Windows may fail for the wrong reason, like:
+	//	The process cannot access the file because it is being used by another process.
+	file1 := "file1"
+	err = os.WriteFile(path.Join(tmpDir, file1), []byte{}, 0o700)
+	require.NoError(t, err)
+	fileFD := requireOpenFD(t, mod, file1)
+
+	dirNotEmpty := "notempty"
+	err = os.Mkdir(path.Join(tmpDir, dirNotEmpty), 0o700)
+	require.NoError(t, err)
+
+	dir := path.Join(dirNotEmpty, "dir")
 	err = os.Mkdir(path.Join(tmpDir, dir), 0o700)
 	require.NoError(t, err)
 
@@ -2842,9 +2892,9 @@ func Test_pathRename_Errors(t *testing.T) {
 		expectedLog                    string
 	}{
 		{
-			name:          "invalid old fd",
+			name:          "unopened old fd",
 			oldFd:         42, // arbitrary invalid fd
-			newFd:         sys.FdRoot,
+			newFd:         sys.FdPreopen,
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_rename(fd=42,old_path=,new_fd=3,new_path=)
@@ -2852,19 +2902,39 @@ func Test_pathRename_Errors(t *testing.T) {
 `,
 		},
 		{
-			name:          "invalid new fd",
-			oldFd:         sys.FdRoot,
+			name:          "old FD not a directory",
+			oldFd:         fileFD,
+			newFd:         sys.FdPreopen,
+			expectedErrno: ErrnoNotdir,
+			expectedLog: `
+==> wasi_snapshot_preview1.path_rename(fd=4,old_path=,new_fd=3,new_path=)
+<== errno=ENOTDIR
+`,
+		},
+		{
+			name:          "unopened new fd",
+			oldFd:         sys.FdPreopen,
 			newFd:         42, // arbitrary invalid fd
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_rename(fd=3,old_path=,new_fd=42,new_path=)
 <== errno=EBADF
+`,
+		},
+		{
+			name:          "new FD not a directory",
+			oldFd:         sys.FdPreopen,
+			newFd:         fileFD,
+			expectedErrno: ErrnoNotdir,
+			expectedLog: `
+==> wasi_snapshot_preview1.path_rename(fd=3,old_path=,new_fd=4,new_path=)
+<== errno=ENOTDIR
 `,
 		},
 		{
 			name:          "out-of-memory reading old path",
-			oldFd:         sys.FdRoot,
-			newFd:         sys.FdRoot,
+			oldFd:         sys.FdPreopen,
+			newFd:         sys.FdPreopen,
 			oldPath:       mod.Memory().Size(),
 			oldPathLen:    1,
 			expectedErrno: ErrnoFault,
@@ -2875,8 +2945,8 @@ func Test_pathRename_Errors(t *testing.T) {
 		},
 		{
 			name:          "out-of-memory reading new path",
-			oldFd:         sys.FdRoot,
-			newFd:         sys.FdRoot,
+			oldFd:         sys.FdPreopen,
+			newFd:         sys.FdPreopen,
 			oldPath:       0,
 			oldPathName:   "a",
 			oldPathLen:    1,
@@ -2890,8 +2960,8 @@ func Test_pathRename_Errors(t *testing.T) {
 		},
 		{
 			name:          "out-of-memory reading old pathLen",
-			oldFd:         sys.FdRoot,
-			newFd:         sys.FdRoot,
+			oldFd:         sys.FdPreopen,
+			newFd:         sys.FdPreopen,
 			oldPath:       0,
 			oldPathLen:    mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is OOM for path
 			expectedErrno: ErrnoFault,
@@ -2902,8 +2972,8 @@ func Test_pathRename_Errors(t *testing.T) {
 		},
 		{
 			name:          "out-of-memory reading new pathLen",
-			oldFd:         sys.FdRoot,
-			newFd:         sys.FdRoot,
+			oldFd:         sys.FdPreopen,
+			newFd:         sys.FdPreopen,
 			oldPathName:   file,
 			oldPathLen:    uint32(len(file)),
 			newPath:       0,
@@ -2916,8 +2986,8 @@ func Test_pathRename_Errors(t *testing.T) {
 		},
 		{
 			name:          "no such file exists",
-			oldFd:         sys.FdRoot,
-			newFd:         sys.FdRoot,
+			oldFd:         sys.FdPreopen,
+			newFd:         sys.FdPreopen,
 			oldPathName:   file,
 			oldPathLen:    uint32(len(file)) - 1,
 			newPath:       16,
@@ -2931,8 +3001,8 @@ func Test_pathRename_Errors(t *testing.T) {
 		},
 		{
 			name:          "dir not file",
-			oldFd:         sys.FdRoot,
-			newFd:         sys.FdRoot,
+			oldFd:         sys.FdPreopen,
+			newFd:         sys.FdPreopen,
 			oldPathName:   file,
 			oldPathLen:    uint32(len(file)),
 			newPath:       16,
@@ -2940,7 +3010,7 @@ func Test_pathRename_Errors(t *testing.T) {
 			newPathLen:    uint32(len(dir)),
 			expectedErrno: ErrnoIsdir,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_rename(fd=3,old_path=file,new_fd=3,new_path=dir)
+==> wasi_snapshot_preview1.path_rename(fd=3,old_path=file,new_fd=3,new_path=notempty/dir)
 <== errno=EISDIR
 `,
 		},
@@ -2980,7 +3050,7 @@ func Test_pathUnlinkFile(t *testing.T) {
 	err = os.WriteFile(realPath, []byte{}, 0o600)
 	require.NoError(t, err)
 
-	dirFD := sys.FdRoot
+	dirFD := sys.FdPreopen
 	name := 1
 	nameLen := len(pathName)
 
@@ -3006,6 +3076,7 @@ func Test_pathUnlinkFile_Errors(t *testing.T) {
 	file := "file"
 	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
+	fileFD := requireOpenFD(t, mod, file)
 
 	dir := "dir"
 	err = os.Mkdir(path.Join(tmpDir, dir), 0o700)
@@ -3018,17 +3089,26 @@ func Test_pathUnlinkFile_Errors(t *testing.T) {
 		expectedLog       string
 	}{
 		{
-			name:          "invalid fd",
+			name:          "unopened FD",
 			fd:            42, // arbitrary invalid fd
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_unlink_file(fd=42,path=)
 <== errno=EBADF
+`,
+		},
+		{
+			name:          "FD not a directory",
+			fd:            fileFD,
+			expectedErrno: ErrnoNotdir,
+			expectedLog: `
+==> wasi_snapshot_preview1.path_unlink_file(fd=4,path=)
+<== errno=ENOTDIR
 `,
 		},
 		{
 			name:          "out-of-memory reading path",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			path:          mod.Memory().Size(),
 			pathLen:       1,
 			expectedErrno: ErrnoFault,
@@ -3039,7 +3119,7 @@ func Test_pathUnlinkFile_Errors(t *testing.T) {
 		},
 		{
 			name:          "out-of-memory reading pathLen",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			path:          0,
 			pathLen:       mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is OOM for path
 			expectedErrno: ErrnoFault,
@@ -3050,7 +3130,7 @@ func Test_pathUnlinkFile_Errors(t *testing.T) {
 		},
 		{
 			name:          "no such file exists",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			pathName:      file,
 			path:          0,
 			pathLen:       uint32(len(file) - 1),
@@ -3062,7 +3142,7 @@ func Test_pathUnlinkFile_Errors(t *testing.T) {
 		},
 		{
 			name:          "dir not file",
-			fd:            sys.FdRoot,
+			fd:            sys.FdPreopen,
 			pathName:      dir,
 			path:          0,
 			pathLen:       uint32(len(dir)),
diff --git a/imports/wasi_snapshot_preview1/wasi_bench_test.go b/imports/wasi_snapshot_preview1/wasi_bench_test.go
index 2de336bb..b54450b1 100644
--- a/imports/wasi_snapshot_preview1/wasi_bench_test.go
+++ b/imports/wasi_snapshot_preview1/wasi_bench_test.go
@@ -243,7 +243,7 @@ func Benchmark_pathFilestat(b *testing.B) {
 			name: "embed.FS fd=root",
 			fs:   embedFS,
 			path: "zig",
-			fd:   sys.FdRoot,
+			fd:   sys.FdPreopen,
 		},
 		{
 			name: "embed.FS fd=directory",
@@ -254,7 +254,7 @@ func Benchmark_pathFilestat(b *testing.B) {
 			name: "os.DirFS fd=root",
 			fs:   os.DirFS("testdata"),
 			path: "zig",
-			fd:   sys.FdRoot,
+			fd:   sys.FdPreopen,
 		},
 		{
 			name: "os.DirFS fd=directory",
@@ -277,8 +277,8 @@ func Benchmark_pathFilestat(b *testing.B) {
 
 			// If the benchmark's file descriptor isn't root, open the file
 			// under a pre-determined directory: zig
-			fd := sys.FdRoot
-			if bc.fd != sys.FdRoot {
+			fd := sys.FdPreopen
+			if bc.fd != sys.FdPreopen {
 				fsc := mod.(*wasm.CallContext).Sys.FS()
 				fd, err = fsc.OpenFile("zig", os.O_RDONLY, 0)
 				if err != nil {
diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go
index f80f87bd..1018b92b 100644
--- a/internal/sys/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -19,9 +19,9 @@ import (
 var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
 
 var (
-	noopStdin  = &FileEntry{Name: noopStdinStat.Name(), File: &stdioFileReader{r: eofReader{}, s: noopStdinStat}}
-	noopStdout = &FileEntry{Name: noopStdoutStat.Name(), File: &stdioFileWriter{w: io.Discard, s: noopStdoutStat}}
-	noopStderr = &FileEntry{Name: noopStderrStat.Name(), File: &stdioFileWriter{w: io.Discard, s: noopStderrStat}}
+	noopStdin  = &FileEntry{File: &stdioFileReader{r: eofReader{}, s: noopStdinStat}}
+	noopStdout = &FileEntry{File: &stdioFileWriter{w: io.Discard, s: noopStdoutStat}}
+	noopStderr = &FileEntry{File: &stdioFileWriter{w: io.Discard, s: noopStderrStat}}
 )
 
 //go:embed testdata
@@ -67,16 +67,17 @@ func TestNewFSContext(t *testing.T) {
 			require.NoError(t, err)
 			defer fsc.Close(testCtx)
 
-			rootFile, _ := fsc.openedFiles.Lookup(FdRoot)
+			preopenedDir, _ := fsc.openedFiles.Lookup(FdPreopen)
+			preopenedPath := fsc.fs.Path()
 			require.Equal(t, tc.fs, fsc.fs)
-			require.NotNil(t, rootFile)
-			require.Equal(t, "/", rootFile.Name)
+			require.NotNil(t, preopenedDir)
+			require.Equal(t, "", preopenedDir.Name)
 
 			// Verify that each call to OpenFile returns a different file
 			// descriptor.
-			f1, err := fsc.OpenFile("/", 0, 0)
+			f1, err := fsc.OpenFile(preopenedPath, 0, 0)
 			require.NoError(t, err)
-			require.NotEqual(t, FdRoot, f1)
+			require.NotEqual(t, FdPreopen, f1)
 
 			// Verify that file descriptors are reused.
 			//
@@ -87,7 +88,7 @@ func TestNewFSContext(t *testing.T) {
 			// numbers but if we were to change the reuse strategy, this test
 			// would likely break and need to be updated.
 			require.NoError(t, fsc.CloseFile(f1))
-			f2, err := fsc.OpenFile("/", 0, 0)
+			f2, err := fsc.OpenFile(preopenedPath, 0, 0)
 			require.NoError(t, err)
 			require.Equal(t, f1, f2)
 		})
@@ -119,11 +120,11 @@ func TestContext_Close(t *testing.T) {
 	require.NoError(t, err)
 
 	// Verify base case
-	require.Equal(t, 1+FdRoot, uint32(fsc.openedFiles.Len()))
+	require.Equal(t, 1+FdPreopen, uint32(fsc.openedFiles.Len()))
 
 	_, err = fsc.OpenFile("foo", os.O_RDONLY, 0)
 	require.NoError(t, err)
-	require.Equal(t, 2+FdRoot, uint32(fsc.openedFiles.Len()))
+	require.Equal(t, 2+FdPreopen, uint32(fsc.openedFiles.Len()))
 
 	// Closing should not err.
 	require.NoError(t, fsc.Close(testCtx))
diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go
index e4e8114d..e13ea0e6 100644
--- a/internal/sys/sys_test.go
+++ b/internal/sys/sys_test.go
@@ -58,7 +58,7 @@ func TestDefaultSysContext(t *testing.T) {
 	expectedOpenedFiles.Insert(noopStdin)
 	expectedOpenedFiles.Insert(noopStdout)
 	expectedOpenedFiles.Insert(noopStderr)
-	expectedOpenedFiles.Insert(&FileEntry{Name: "/", File: emptyRootDir{}})
+	expectedOpenedFiles.Insert(&FileEntry{IsPreopen: true, Name: "", File: &lazyDir{fs: testFS}})
 
 	require.Equal(t, expectedOpenedFiles, expectedFS.openedFiles)
 	require.Equal(t, expectedFS, sysCtx.FS())