Repository: tetratelabs/wazero
Pull request #1009 (2023-01-05 07:20:07)

Clarifies at semantics and preopen semantics in WASI
This adds FS.Path which holds the pre-open path currently only used in WASI. It also fixes a TODO where we didn't know for sure if the FD parameter for `path_` functions must always be a pre-open. The TL;DR; is that usually it is, but it may not be (e.g. in our zig-cc example we can see any directory FD, not just pre-opens).

Finally, this fixes a bug in our path resolution where we mistook paths like "foo/foo" for "foo" because we only considered basenames instead of the full path from the pre-open root.

This also makes pre-open directory lookup lazy because I noticed in Trivy specifically, this is unnecessary for us to do eagerly, as they change the FS at runtime per-call. In other words, any value from init time is invalid later.

codefromthecrypt: I will sort out the windows error prior to merge
codefromthecrypt: 🤞 windows should work in CI now (works on my laptop).

Patch:
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/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())