Repository: tetratelabs/wazero
Pull request #1054 (2023-01-23 00:33:54)

fuzz: adds target on checking memory state
Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>

mathetake: now running this locally for an hour or so, and if it ends, I will make this ready
mathetake: seems like found a bug in memory.fill

```
Failed WebAssembly Text:
(module
  (type (;0;) (func))
  (func (;0;) (type 0)
    (local v128 f32)
    global.get 1
    i32.eqz
    if  ;; label = @1
      unreachable
    end
    global.get 1
    i32.const 1
    i32.sub
    global.set 1
    table.size 1
    table.size 0
    i32.const -1
    i32.rotr
    table.size 1
    memory.fill
  )
  (func (;1;) (type 0)
    global.get 1
    i32.eqz
    if  ;; label = @1
      unreachable
    end
    global.get 1
    i32.const 1
    i32.sub
    global.set 1
  )
  (table (;0;) 128 376 externref)
  (table (;1;) 74 241 externref)
  (memory (;0;) 1 2)
  (global (;0;) (mut funcref) ref.func 1)
  (global (;1;) (mut i32) i32.const 1000)
  (export "" (func 0))
  (export "1" (func 1))
  (export "2" (table 0))
  (export "3" (table 1))
  (export "4" (memory 0))
  (export "5" (global 0))
  (start 1)
  (elem (;0;) (table 1) (i32.const 0) externref)
)
```
mathetake: minimized by hand :) 

```
(module
  (type (;0;) (func))
  (func (;0;) (type 0)
    table.size 0
    table.size 0
    table.size 0
    memory.fill
  )
  (table (;1;) 1000 1000 externref)
  (memory (;0;) 1 2)
  (start 0)
)
```

mathetake: ok it's nothing to do with table

```
(module
  (type (;0;) (func))
  (func (;0;) (type 0)
    i32.const 1000
    i32.const 1000
    i32.const 1000
    memory.fill
  )
  (memory (;0;) 1 2)
  (start 0)
)
```
mathetake: extracted to #1055, and keep this draftt until we solve it
mathetake: good news is that it seems like there's no other bugs found

Patch:
diff --git a/Makefile b/Makefile
index bf4351a0..b6f9d5fb 100644
--- a/Makefile
+++ b/Makefile
@@ -254,4 +254,5 @@ fuzz_timeout_seconds ?= 10
 .PHONY: fuzz
 fuzz:
 	@cd internal/integration_test/fuzz && cargo fuzz run basic -- -max_total_time=$(fuzz_timeout_seconds)
+	@cd internal/integration_test/fuzz && cargo fuzz run memory_no_diff -- -max_total_time=$(fuzz_timeout_seconds)
 	@cd internal/integration_test/fuzz && cargo fuzz run validation -- -max_total_time=$(fuzz_timeout_seconds)
diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go
index 71734108..e7eaf903 100644
--- a/cmd/wazero/wazero.go
+++ b/cmd/wazero/wazero.go
@@ -120,8 +120,8 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
 
 	var mounts sliceFlag
 	flags.Var(&mounts, "mount",
-		"filesystem path to expose to the binary in the form of <host path>[:<wasm path>][:ro]. "+
-			"This may be specified multiple times. When <wasm path> is unset, the host path is used. "+
+		"filesystem path to expose to the binary in the form of <path>[:<wasm path>][:ro]. "+
+			"This may be specified multiple times. When <wasm path> is unset, <path> is used. "+
 			"For read-only mounts, append the suffix ':ro'.")
 
 	hostLogging := hostLoggingFlag(flags)
@@ -196,7 +196,7 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
 		conf = conf.WithEnv(env[i], env[i+1])
 	}
 	if rootFS != nil {
-		conf = conf.WithFS(rootFS)
+		conf = conf.WithFS(&sysfs.FSHolder{FS: rootFS})
 	}
 
 	code, err := rt.CompileModule(ctx, wasm)
@@ -229,6 +229,7 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
 
 func validateMounts(mounts sliceFlag, stdErr logging.Writer, exit func(code int)) sysfs.FS {
 	fs := make([]sysfs.FS, 0, len(mounts))
+	guestPaths := make([]string, 0, len(mounts))
 	for _, mount := range mounts {
 		if len(mount) == 0 {
 			fmt.Fprintln(stdErr, "invalid mount: empty string")
@@ -254,25 +255,30 @@ func validateMounts(mounts sliceFlag, stdErr logging.Writer, exit func(code int)
 		if guest == "" {
 			guest = "/"
 		}
+
+		// Eagerly validate the mounts as we know they should be on the host.
 		if abs, err := filepath.Abs(host); err != nil {
-			fmt.Fprintf(stdErr, "invalid mount: host path %q invalid: %v\n", host, err)
+			fmt.Fprintf(stdErr, "invalid mount: path %q invalid: %v\n", host, err)
 			exit(1)
 		} else {
 			host = abs
 		}
 
-		next, err := sysfs.NewDirFS(host, guest)
-		if err != nil {
-			fmt.Fprintf(stdErr, "invalid mount: %v\n", err)
+		if stat, err := os.Stat(host); err != nil {
+			fmt.Fprintf(stdErr, "invalid mount: path %q error: %v\n", host, err)
 			exit(1)
-		} else {
-			if readOnly {
-				next = sysfs.NewReadFS(next)
-			}
-			fs = append(fs, next)
+		} else if !stat.IsDir() {
+			fmt.Fprintf(stdErr, "invalid mount: path %q is not a directory\n", host)
+		}
+
+		next := sysfs.NewDirFS(host)
+		if readOnly {
+			next = sysfs.NewReadFS(next)
 		}
+		fs = append(fs, next)
+		guestPaths = append(guestPaths, guest)
 	}
-	if fs, err := sysfs.NewRootFS(fs...); err != nil {
+	if fs, err := sysfs.NewRootFS(fs, guestPaths); err != nil {
 		fmt.Fprintf(stdErr, "invalid mounts %v: %v\n", fs, err)
 		exit(1)
 		return nil
diff --git a/experimental/writefs/writefs.go b/experimental/writefs/writefs.go
index b48d67c2..289ce14d 100644
--- a/experimental/writefs/writefs.go
+++ b/experimental/writefs/writefs.go
@@ -34,7 +34,7 @@ import (
 //
 // Do not attempt to use the result as a fs.FS, as it will panic. This is a
 // bridge to a future filesystem abstraction made for wazero.
-func NewDirFS(hostDir string) (fs.FS, error) {
+func NewDirFS(dir string) fs.FS {
 	// sysfs.DirFS is intentionally internal as it is still evolving
-	return sysfs.NewDirFS(hostDir, "/")
+	return &sysfs.FSHolder{FS: sysfs.NewDirFS(dir)}
 }
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index a77244f6..de3652a1 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -1366,7 +1366,7 @@ func preopenPath(fsc *sys.FSContext, dirFD uint32) (string, Errno) {
 		return "", ErrnoBadf
 	} else {
 		// TODO: multiple pre-opens
-		return fsc.FS().GuestDir(), ErrnoSuccess
+		return "/", ErrnoSuccess
 	}
 }
 
diff --git a/internal/sys/sys.go b/internal/sys/sys.go
index 0fa9b252..052deb0d 100644
--- a/internal/sys/sys.go
+++ b/internal/sys/sys.go
@@ -108,7 +108,7 @@ func (eofReader) Read([]byte) (int, error) {
 	return 0, io.EOF
 }
 
-// DefaultContext returns Context with no values set except a possibly nil fs.FS
+// DefaultContext returns Context with no values set except a possible nil fs.FS
 func DefaultContext(fs fs.FS) *Context {
 	if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, fs); err != nil {
 		panic(fmt.Errorf("BUG: DefaultContext should never error: %w", err))
@@ -182,7 +182,7 @@ func NewContext(
 	}
 
 	if fs != nil {
-		sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, sysfs.Adapt(fs, "/"))
+		sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, sysfs.Adapt(fs))
 	} else {
 		sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, sysfs.UnimplementedFS{})
 	}
diff --git a/internal/sysfs/adapter.go b/internal/sysfs/adapter.go
index 7433ec3b..18630d0a 100644
--- a/internal/sysfs/adapter.go
+++ b/internal/sysfs/adapter.go
@@ -14,27 +14,21 @@ import (
 // flags as there is no parameter to pass them through with. Moreover, fs.FS
 // documentation does not require the file to be present. In summary, we can't
 // enforce flag behavior.
-func Adapt(fs fs.FS, guestDir string) FS {
-	if sys, ok := fs.(FS); ok {
-		return sys
+func Adapt(fs fs.FS) FS {
+	if sys, ok := fs.(*FSHolder); ok {
+		return sys.FS
 	}
-	return &adapter{fs: fs, guestDir: guestDir}
+	return &adapter{fs: fs}
 }
 
 type adapter struct {
 	UnimplementedFS
-	fs       fs.FS
-	guestDir string
+	fs fs.FS
 }
 
 // String implements fmt.Stringer
 func (a *adapter) String() string {
-	return fmt.Sprintf("%v:%s:ro", a.fs, a.guestDir)
-}
-
-// GuestDir implements FS.GuestDir
-func (a *adapter) GuestDir() string {
-	return a.guestDir
+	return fmt.Sprintf("%v", a.fs)
 }
 
 // OpenFile implements FS.OpenFile
diff --git a/internal/sysfs/dirfs.go b/internal/sysfs/dirfs.go
index 7ec4353a..76175b44 100644
--- a/internal/sysfs/dirfs.go
+++ b/internal/sysfs/dirfs.go
@@ -1,26 +1,16 @@
 package sysfs
 
 import (
-	"errors"
-	"fmt"
 	"io/fs"
 	"os"
 	"syscall"
 )
 
-func NewDirFS(hostDir, guestDir string) (FS, error) {
-	ret := &dirFS{
-		hostDir:        hostDir,
-		guestDir:       guestDir,
-		cleanedHostDir: ensureTrailingPathSeparator(hostDir),
+func NewDirFS(dir string) FS {
+	return &dirFS{
+		dir:        dir,
+		cleanedDir: ensureTrailingPathSeparator(dir),
 	}
-
-	if stat, err := os.Stat(hostDir); err != nil {
-		return nil, fmt.Errorf("%s invalid: %v", ret, errors.Unwrap(err))
-	} else if !stat.IsDir() {
-		return nil, fmt.Errorf("%s invalid: %s is not a directory", ret, hostDir)
-	}
-	return ret, nil
 }
 
 func ensureTrailingPathSeparator(dir string) string {
@@ -32,20 +22,15 @@ func ensureTrailingPathSeparator(dir string) string {
 
 type dirFS struct {
 	UnimplementedFS
-	hostDir, guestDir string
-	// cleanedHostDir is for easier OS-specific concatenation, as it always has
+	dir string
+	// cleanedDir is for easier OS-specific concatenation, as it always has
 	// a trailing path separator.
-	cleanedHostDir string
+	cleanedDir string
 }
 
 // String implements fmt.Stringer
 func (d *dirFS) String() string {
-	return d.hostDir + ":" + d.guestDir
-}
-
-// GuestDir implements FS.GuestDir
-func (d *dirFS) GuestDir() string {
-	return d.guestDir
+	return d.dir
 }
 
 // OpenFile implements FS.OpenFile
@@ -93,10 +78,12 @@ func (d *dirFS) Utimes(name string, atimeNsec, mtimeNsec int64) error {
 }
 
 func (d *dirFS) join(name string) string {
-	if name == "." {
-		return d.cleanedHostDir
+	switch name {
+	case "", ".", "/":
+		// cleanedDir includes an unnecessary delimiter for the root path.
+		return d.cleanedDir[:len(d.cleanedDir)-1]
 	}
 	// TODO: Enforce similar to safefilepath.FromFS(name), but be careful as
 	// relative path inputs are allowed. e.g. dir or name == ../
-	return d.cleanedHostDir + name
+	return d.cleanedDir + name
 }
diff --git a/internal/sysfs/readfs.go b/internal/sysfs/readfs.go
index ee09c688..7dbc1f39 100644
--- a/internal/sysfs/readfs.go
+++ b/internal/sysfs/readfs.go
@@ -29,12 +29,7 @@ type readFS struct {
 
 // String implements fmt.Stringer
 func (r *readFS) String() string {
-	return r.fs.String() + ":ro"
-}
-
-// GuestDir implements FS.GuestDir
-func (r *readFS) GuestDir() string {
-	return r.fs.GuestDir()
+	return r.fs.String()
 }
 
 // OpenFile implements FS.OpenFile
diff --git a/internal/sysfs/rootfs.go b/internal/sysfs/rootfs.go
index 3c767a43..8d67dba3 100644
--- a/internal/sysfs/rootfs.go
+++ b/internal/sysfs/rootfs.go
@@ -9,12 +9,12 @@ import (
 	"time"
 )
 
-func NewRootFS(fs ...FS) (FS, error) {
+func NewRootFS(fs []FS, guestPaths []string) (FS, error) {
 	switch len(fs) {
 	case 0:
 		return UnimplementedFS{}, nil
 	case 1:
-		if fs[0].GuestDir() == "/" {
+		if StripPrefixesAndTrailingSlash(guestPaths[0]) == "" {
 			return fs[0], nil
 		}
 	}
@@ -22,31 +22,32 @@ func NewRootFS(fs ...FS) (FS, error) {
 	// Last is the highest precedence, so we iterate backwards to keep runtime
 	// code simpler.
 	ret := &CompositeFS{
-		prefixes:     make([]string, len(fs)),
-		rootPrefixes: map[string]int{},
-		fs:           make([]FS, len(fs)),
-		rootIndex:    -1,
+		string:         stringFS(fs, guestPaths),
+		guestPaths:     make([]string, len(fs)),
+		rootGuestPaths: map[string]int{},
+		fs:             make([]FS, len(fs)),
+		rootIndex:      -1,
 	}
+
 	j := 0
 	for i := len(fs) - 1; i >= 0; i-- {
+		guestPath := guestPaths[i]
 		// Clean the prefix in the same way path matches will.
-		path := fs[i].GuestDir()
-		pathI, pathLen := stripPrefixesAndTrailingSlash(path)
-		prefix := path[pathI:pathLen]
-		if prefix == "" {
+		cleaned := StripPrefixesAndTrailingSlash(guestPath)
+		if cleaned == "" {
 			if ret.rootIndex != -1 {
-				return nil, fmt.Errorf("multiple root filesystems are invalid: %s", fsString(fs))
+				return nil, fmt.Errorf("multiple root filesystems are invalid: %s", ret.string)
 			}
 			ret.rootIndex = j
-		} else if strings.HasPrefix(prefix, "..") {
+		} else if strings.HasPrefix(cleaned, "..") {
 			// ../ mounts are special cased and aren't returned in a directory
 			// listing, so we can ignore them for now.
-		} else if strings.Contains(prefix, "/") {
-			return nil, fmt.Errorf("only single-level guest paths allowed: %s", fsString(fs))
+		} else if strings.Contains(cleaned, "/") {
+			return nil, fmt.Errorf("only single-level guest paths allowed: %s", ret.string)
 		} else {
-			ret.rootPrefixes[prefix] = j
+			ret.rootGuestPaths[cleaned] = j
 		}
-		ret.prefixes[j] = prefix
+		ret.guestPaths[j] = cleaned
 		ret.fs[j] = fs[i]
 		j++
 	}
@@ -54,7 +55,7 @@ func NewRootFS(fs ...FS) (FS, error) {
 	// Ensure there is always a root match to keep runtime logic simpler.
 	if ret.rootIndex == -1 {
 		ret.rootIndex = len(fs)
-		ret.prefixes = append(ret.prefixes, "")
+		ret.guestPaths = append(ret.guestPaths, "")
 		ret.fs = append(ret.fs, fakeRootFS{})
 	}
 	return ret, nil
@@ -62,11 +63,14 @@ func NewRootFS(fs ...FS) (FS, error) {
 
 type CompositeFS struct {
 	UnimplementedFS
-	// prefixes to match in precedence order
-	prefixes []string
-	// rootPrefixes are prefixes that exist directly under root, such as "tmp".
-	rootPrefixes map[string]int
-	// fs is index-correlated with prefixes
+	// string is cached for convenience.
+	string string
+	// guestPaths to match in precedence order, descending.
+	guestPaths []string
+	// rootGuestPaths are guestPaths that exist directly under root, such as
+	// "tmp".
+	rootGuestPaths map[string]int
+	// fs is index-correlated with guestPaths
 	fs []FS
 	// rootIndex is the index in fs that is the root filesystem
 	rootIndex int
@@ -74,15 +78,28 @@ type CompositeFS struct {
 
 // String implements fmt.Stringer
 func (c *CompositeFS) String() string {
-	// return the string in its initial order
-	return fsString(c.Unwrap())
+	return c.string
+}
+
+func stringFS(fs []FS, guestPaths []string) string {
+	var ret strings.Builder
+	ret.WriteString("[")
+	writeMount(&ret, fs[0], guestPaths[0])
+	for i, f := range fs[1:] {
+		ret.WriteString(" ")
+		writeMount(&ret, f, guestPaths[i+1])
+	}
+	ret.WriteString("]")
+	return ret.String()
 }
 
-func fsString(fs []FS) string {
-	if len(fs) == 1 {
-		return fmt.Sprintf("%v", fs[0])
+func writeMount(ret *strings.Builder, f FS, guestPath string) {
+	ret.WriteString(f.String())
+	ret.WriteString(":")
+	ret.WriteString(guestPath)
+	if _, ok := f.(*readFS); ok {
+		ret.WriteString(":ro")
 	}
-	return fmt.Sprintf("%v", fs)
 }
 
 // Unwrap returns the underlying filesystems in original order.
@@ -109,7 +126,7 @@ func (c *CompositeFS) OpenFile(path string, flag int, perm fs.FileMode) (f fs.Fi
 	if matchIndex == c.rootIndex {
 		switch path {
 		case ".", "/", "":
-			if len(c.rootPrefixes) > 0 {
+			if len(c.rootGuestPaths) > 0 {
 				f = &openRootDir{c: c, f: f.(fs.ReadDirFile)}
 			}
 		}
@@ -141,8 +158,8 @@ func (d *openRootDir) readDir() (err error) {
 		return
 	}
 
-	remaining := make(map[string]int, len(d.c.rootPrefixes))
-	for k, v := range d.c.rootPrefixes {
+	remaining := make(map[string]int, len(d.c.rootGuestPaths))
+	for k, v := range d.c.rootGuestPaths {
 		remaining[k] = v
 	}
 
@@ -250,12 +267,12 @@ func (c *CompositeFS) Utimes(path string, atimeNsec, mtimeNsec int64) error {
 
 // chooseFS chooses the best fs and the relative path to use for the input.
 func (c *CompositeFS) chooseFS(path string) (matchIndex int, relativePath string) {
-	// c.prefixes are already in precedence order. The first longest match wins
+	// c.guestPaths are already in precedence order. The first longest match wins
 	// so that pre-open "tmp" wins vs "" regardless of order.
 	matchIndex = -1
 	matchPrefixLen := 0
 	pathI, pathLen := stripPrefixesAndTrailingSlash(path)
-	for i, prefix := range c.prefixes {
+	for i, prefix := range c.guestPaths {
 		if eq, match := hasPathPrefix(path, pathI, pathLen, prefix); eq {
 			// When the input equals the prefix, there cannot be a longer match
 			// later. The relative path is the FS root, so return empty string.
@@ -337,6 +354,11 @@ func pathContainsPrefix(path string, pathI, prefixLen int, prefix string) bool {
 	return true // e.g. prefix=foo, path=foo or foo/bar
 }
 
+func StripPrefixesAndTrailingSlash(path string) string {
+	pathI, pathLen := stripPrefixesAndTrailingSlash(path)
+	return path[pathI:pathLen]
+}
+
 // stripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the
 // result index begins with another string. A result of "." coerces to the
 // empty string "" because the current directory is handled by the guest.
diff --git a/internal/sysfs/sysfs.go b/internal/sysfs/sysfs.go
index 5227ae8e..cc4eaa90 100644
--- a/internal/sysfs/sysfs.go
+++ b/internal/sysfs/sysfs.go
@@ -6,12 +6,24 @@
 package sysfs
 
 import (
+	"fmt"
 	"io"
 	"io/fs"
 	"os"
 	"syscall"
 )
 
+// FSHolder implements fs.FS in order to pass an FS until configuration
+// supports it natively.
+type FSHolder struct {
+	FS FS
+}
+
+// Open implements the same method as documented on fs.FS
+func (*FSHolder) Open(name string) (fs.File, error) {
+	panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
+}
+
 // FS is a writeable fs.FS bridge backed by syscall functions needed for ABI
 // including WASI and runtime.GOOS=js.
 //
@@ -20,43 +32,15 @@ import (
 //
 // See https://github.com/golang/go/issues/45757
 type FS interface {
-	// String should return a human-readable format of the filesystem:
-	//   - If read-only, $host:$guestDir:ro
-	//   - If read-write, $host:$guestDir
+	// String should return a human-readable format of the filesystem
 	//
 	// For example, if this filesystem is backed by the real directory
-	// "/tmp/wasm" and the GuestDir is "/", the expected value is
-	// "/var/tmp:/tmp".
+	// "/tmp/wasm", the expected value is "/tmp/wasm".
 	//
 	// When the host filesystem isn't a real filesystem, substitute a symbolic,
-	// human-readable name. e.g. "virtual:/"
+	// human-readable name. e.g. "virtual"
 	String() string
 
-	// GuestDir is the name of the path the guest should use this filesystem
-	// for, or root ("/") for any files.
-	//
-	// 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
-	//   - Implementations must always return the same value.
-	//   - 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.
-	GuestDir() 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.
-	Open(name string) (fs.File, error)
-
 	// OpenFile is similar to os.OpenFile, except the path is relative to this
 	// file system, and syscall.Errno are returned instead of a os.PathError.
 	//
diff --git a/internal/sysfs/unsupported.go b/internal/sysfs/unsupported.go
index fd386434..42cad374 100644
--- a/internal/sysfs/unsupported.go
+++ b/internal/sysfs/unsupported.go
@@ -1,7 +1,6 @@
 package sysfs
 
 import (
-	"fmt"
 	"io/fs"
 	"syscall"
 )
@@ -15,16 +14,6 @@ func (UnimplementedFS) String() string {
 	return "Unimplemented:/"
 }
 
-// Open implements the same method as documented on fs.FS
-func (UnimplementedFS) Open(name string) (fs.File, error) {
-	panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
-}
-
-// GuestDir implements FS.GuestDir
-func (UnimplementedFS) GuestDir() string {
-	return "/"
-}
-
 // OpenFile implements FS.OpenFile
 func (UnimplementedFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
 	return nil, syscall.ENOSYS


Test patch:
diff --git a/experimental/writefs/writefs_example_test.go b/experimental/writefs/writefs_example_test.go
index 6a31352a..2d73d2df 100644
--- a/experimental/writefs/writefs_example_test.go
+++ b/experimental/writefs/writefs_example_test.go
@@ -2,7 +2,6 @@ package writefs_test
 
 import (
 	_ "embed"
-	"log"
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/experimental/writefs"
@@ -13,9 +12,6 @@ var config wazero.ModuleConfig //nolint
 // This shows how to use writefs.NewDirFS to map paths relative to "/work/appA",
 // as "/". Unlike os.DirFS, these paths will be writable.
 func Example_dirFS() {
-	fs, err := writefs.NewDirFS("/work/appA")
-	if err != nil {
-		log.Panicln(err)
-	}
+	fs := writefs.NewDirFS("/work/appA")
 	config = wazero.NewModuleConfig().WithFS(fs)
 }
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index 65c30839..93f68c30 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -14,6 +14,7 @@ import (
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/experimental/writefs"
 	"github.com/tetratelabs/wazero/internal/fstest"
 	"github.com/tetratelabs/wazero/internal/leb128"
 	"github.com/tetratelabs/wazero/internal/sys"
@@ -719,10 +720,9 @@ func Test_fdPread_Errors(t *testing.T) {
 }
 
 func Test_fdPrestatGet(t *testing.T) {
-	testfs, err := sysfs.NewDirFS(t.TempDir(), "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(t.TempDir())
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testfs))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 	dirFD := sys.FdPreopen
 
@@ -806,10 +806,9 @@ func Test_fdPrestatGet_Errors(t *testing.T) {
 }
 
 func Test_fdPrestatDirName(t *testing.T) {
-	testfs, err := sysfs.NewDirFS(t.TempDir(), "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(t.TempDir())
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testfs))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 	dirFD := sys.FdPreopen
 
@@ -2283,10 +2282,9 @@ func Test_fdWrite_Errors(t *testing.T) {
 
 func Test_pathCreateDirectory(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	fs, err := sysfs.NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(tmpDir)
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 
 	// set up the initial memory to include the path name starting at an offset.
@@ -2314,14 +2312,13 @@ func Test_pathCreateDirectory(t *testing.T) {
 
 func Test_pathCreateDirectory_Errors(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	fs, err := sysfs.NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(tmpDir)
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 
 	file := "file"
-	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
+	err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
 	fileFD := requireOpenFD(t, mod, file)
 
@@ -2602,8 +2599,7 @@ func Test_pathLink(t *testing.T) {
 
 func Test_pathOpen(t *testing.T) {
 	dir := t.TempDir() // open before loop to ensure no locking problems.
-	writeFS, err := sysfs.NewDirFS(dir, "/")
-	require.NoError(t, err)
+	writeFS := sysfs.NewDirFS(dir)
 	readFS := sysfs.NewReadFS(writeFS)
 
 	fileName := "file"
@@ -2629,7 +2625,7 @@ func Test_pathOpen(t *testing.T) {
 
 	tests := []struct {
 		name          string
-		fs            fs.FS
+		fs            sysfs.FS
 		path          func(t *testing.T) string
 		oflags        uint16
 		fdflags       uint16
@@ -2823,7 +2819,8 @@ func Test_pathOpen(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(tc.fs))
+			mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().
+				WithFS(&sysfs.FSHolder{FS: tc.fs}))
 			defer r.Close(testCtx)
 			pathName := tc.path(t)
 			mod.Memory().Write(0, []byte(pathName))
@@ -2893,16 +2890,15 @@ func writeFile(t *testing.T, tmpDir, file string, contents []byte) {
 
 func Test_pathOpen_Errors(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	fs, err := sysfs.NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(tmpDir)
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 
 	preopenedFD := sys.FdPreopen
 
 	file := "file"
-	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
+	err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
 	fileFD := requireOpenFD(t, mod, file)
 
@@ -3037,10 +3033,9 @@ func Test_pathReadlink(t *testing.T) {
 
 func Test_pathRemoveDirectory(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	fs, err := sysfs.NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(tmpDir)
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 
 	// set up the initial memory to include the path name starting at an offset.
@@ -3050,7 +3045,7 @@ func Test_pathRemoveDirectory(t *testing.T) {
 	require.True(t, ok)
 
 	// create the directory
-	err = os.Mkdir(realPath, 0o700)
+	err := os.Mkdir(realPath, 0o700)
 	require.NoError(t, err)
 
 	dirFD := sys.FdPreopen
@@ -3070,14 +3065,13 @@ func Test_pathRemoveDirectory(t *testing.T) {
 
 func Test_pathRemoveDirectory_Errors(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	fs, err := sysfs.NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(tmpDir)
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 
 	file := "file"
-	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
+	err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
 	fileFD := requireOpenFD(t, mod, file)
 
@@ -3208,10 +3202,9 @@ func Test_pathSymlink(t *testing.T) {
 
 func Test_pathRename(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	fs, err := sysfs.NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(tmpDir)
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 
 	// set up the initial memory to include the old path name starting at an offset.
@@ -3224,7 +3217,7 @@ func Test_pathRename(t *testing.T) {
 	require.True(t, ok)
 
 	// create the file
-	err = os.WriteFile(realOldPath, []byte{}, 0o600)
+	err := os.WriteFile(realOldPath, []byte{}, 0o600)
 	require.NoError(t, err)
 
 	newDirFD := sys.FdPreopen
@@ -3252,14 +3245,13 @@ func Test_pathRename(t *testing.T) {
 
 func Test_pathRename_Errors(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	fs, err := sysfs.NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(tmpDir)
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 
 	file := "file"
-	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
+	err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
 
 	// We have to test FD validation with a path not under test. Otherwise,
@@ -3428,10 +3420,9 @@ func Test_pathRename_Errors(t *testing.T) {
 
 func Test_pathUnlinkFile(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	fs, err := sysfs.NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(tmpDir)
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 
 	// set up the initial memory to include the path name starting at an offset.
@@ -3441,7 +3432,7 @@ func Test_pathUnlinkFile(t *testing.T) {
 	require.True(t, ok)
 
 	// create the file
-	err = os.WriteFile(realPath, []byte{}, 0o600)
+	err := os.WriteFile(realPath, []byte{}, 0o600)
 	require.NoError(t, err)
 
 	dirFD := sys.FdPreopen
@@ -3461,14 +3452,13 @@ func Test_pathUnlinkFile(t *testing.T) {
 
 func Test_pathUnlinkFile_Errors(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
-	fs, err := sysfs.NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(tmpDir)
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
 	defer r.Close(testCtx)
 
 	file := "file"
-	err = os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
+	err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
 	require.NoError(t, err)
 	fileFD := requireOpenFD(t, mod, file)
 
@@ -3572,16 +3562,15 @@ func requireOpenFile(t *testing.T, tmpDir string, pathName string, data []byte,
 		require.NoError(t, os.WriteFile(realPath, data, 0o600))
 	}
 
-	writeFS, err := sysfs.NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := sysfs.NewDirFS(tmpDir)
 
-	testFS := writeFS
 	if readOnly {
 		oflags = os.O_RDONLY
 		testFS = sysfs.NewReadFS(testFS)
 	}
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().
+		WithFS(&sysfs.FSHolder{FS: testFS}))
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
 	fd, err := fsc.OpenFile(pathName, oflags, 0)
diff --git a/imports/wasi_snapshot_preview1/wasi_stdlib_test.go b/imports/wasi_snapshot_preview1/wasi_stdlib_test.go
index 32a5a3ba..919e2a40 100644
--- a/imports/wasi_snapshot_preview1/wasi_stdlib_test.go
+++ b/imports/wasi_snapshot_preview1/wasi_stdlib_test.go
@@ -11,7 +11,6 @@ import (
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
-	"github.com/tetratelabs/wazero/internal/sysfs"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/sys"
 )
@@ -51,11 +50,11 @@ func Test_fdReaddir_ls(t *testing.T) {
 func testFdReaddirLs(t *testing.T, bin []byte) {
 	// TODO: make a subfs
 	moduleConfig := wazero.NewModuleConfig().
-		WithFS(sysfs.Adapt(fstest.MapFS{
+		WithFS(fstest.MapFS{
 			"-":   {},
 			"a-":  {Mode: fs.ModeDir},
 			"ab-": {},
-		}, "/"))
+		})
 
 	t.Run("empty directory", func(t *testing.T) {
 		console := compileAndRun(t, moduleConfig.WithArgs("wasi", "ls", "./a-"), bin)
@@ -87,7 +86,7 @@ ENOTDIR
 		for i := 0; i < count; i++ {
 			testFS[strconv.Itoa(i)] = &fstest.MapFile{}
 		}
-		config := wazero.NewModuleConfig().WithFS(sysfs.Adapt(testFS, "/")).WithArgs("wasi", "ls", ".")
+		config := wazero.NewModuleConfig().WithFS(testFS).WithArgs("wasi", "ls", ".")
 		console := compileAndRun(t, config, bin)
 
 		lines := strings.Split(console, "\n")
@@ -112,7 +111,7 @@ func Test_fdReaddir_stat(t *testing.T) {
 func testFdReaddirStat(t *testing.T, bin []byte) {
 	moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "stat")
 
-	console := compileAndRun(t, moduleConfig.WithFS(sysfs.Adapt(fstest.MapFS{}, "/")), bin)
+	console := compileAndRun(t, moduleConfig.WithFS(fstest.MapFS{}), bin)
 
 	// TODO: switch this to a real stat test
 	require.Equal(t, `
diff --git a/internal/gojs/fs_test.go b/internal/gojs/fs_test.go
index 204a58e1..61ef81a2 100644
--- a/internal/gojs/fs_test.go
+++ b/internal/gojs/fs_test.go
@@ -9,7 +9,6 @@ import (
 	"github.com/tetratelabs/wazero/experimental/writefs"
 	"github.com/tetratelabs/wazero/internal/fstest"
 	"github.com/tetratelabs/wazero/internal/platform"
-	"github.com/tetratelabs/wazero/internal/sysfs"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
@@ -45,10 +44,9 @@ func Test_testfs(t *testing.T) {
 	require.NoError(t, os.Mkdir(testfsDir, 0o700))
 	require.NoError(t, fstest.WriteTestFiles(testfsDir))
 
-	rootFS, err := sysfs.NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(tmpDir)
 
-	stdout, stderr, err := compileAndRun(testCtx, "testfs", wazero.NewModuleConfig().WithFS(rootFS))
+	stdout, stderr, err := compileAndRun(testCtx, "testfs", wazero.NewModuleConfig().WithFS(testFS))
 
 	require.Zero(t, stderr)
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
@@ -58,13 +56,12 @@ func Test_testfs(t *testing.T) {
 func Test_writefs(t *testing.T) {
 	t.Parallel()
 	tmpDir := t.TempDir()
-	fs, err := writefs.NewDirFS(tmpDir)
-	require.NoError(t, err)
+	testFS := writefs.NewDirFS(tmpDir)
 
 	// test expects to write under /tmp
 	require.NoError(t, os.Mkdir(path.Join(tmpDir, "tmp"), 0o700))
 
-	stdout, stderr, err := compileAndRun(testCtx, "writefs", wazero.NewModuleConfig().WithFS(fs))
+	stdout, stderr, err := compileAndRun(testCtx, "writefs", wazero.NewModuleConfig().WithFS(testFS))
 
 	require.Zero(t, stderr)
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
diff --git a/internal/integration_test/fuzz/fuzz/Cargo.toml b/internal/integration_test/fuzz/fuzz/Cargo.toml
index 67936d45..50f7b0c9 100644
--- a/internal/integration_test/fuzz/fuzz/Cargo.toml
+++ b/internal/integration_test/fuzz/fuzz/Cargo.toml
@@ -13,6 +13,12 @@ libfuzzer-sys = "0.4.3"
 wasm-smith = "0.11.4"
 wasmprinter = "0.2.39"
 
+[[bin]]
+name = "memory_no_diff"
+path = "fuzz_targets/memory_no_diff.rs"
+test = false
+doc = false
+
 [[bin]]
 name = "validation"
 path = "fuzz_targets/validation.rs"
diff --git a/internal/integration_test/fuzz/fuzz/fuzz_targets/basic.rs b/internal/integration_test/fuzz/fuzz/fuzz_targets/basic.rs
index 18446a45..374fa88c 100644
--- a/internal/integration_test/fuzz/fuzz/fuzz_targets/basic.rs
+++ b/internal/integration_test/fuzz/fuzz/fuzz_targets/basic.rs
@@ -4,6 +4,8 @@ use libfuzzer_sys::arbitrary::{Result, Unstructured};
 use libfuzzer_sys::fuzz_target;
 use wasm_smith::SwarmConfig;
 
+mod wazero_abi;
+
 fuzz_target!(|data: &[u8]| {
     drop(run(data));
 });
@@ -50,24 +52,15 @@ fn run(data: &[u8]) -> Result<()> {
 
     // Pass the randomly generated module to the wazero library.
     unsafe {
-        require_no_diff(
+        wazero_abi::require_no_diff(
             module_bytes.as_ptr(),
             module_bytes.len(),
             wat_bytes.as_ptr(),
             wat_bytes.len(),
+            false,
         );
     }
 
     // We always return Ok as inside require_no_diff, we cause panic if the binary is interesting.
     Ok(())
 }
-
-extern "C" {
-    // require_no_diff is implemented in Go, and accepts the pointer to the binary and its size.
-    fn require_no_diff(
-        binary_ptr: *const u8,
-        binary_size: usize,
-        wat_ptr: *const u8,
-        wat_size: usize,
-    );
-}
diff --git a/internal/integration_test/fuzz/fuzz/fuzz_targets/memory_no_diff.rs b/internal/integration_test/fuzz/fuzz/fuzz_targets/memory_no_diff.rs
new file mode 100644
index 00000000..5ccb23f9
--- /dev/null
+++ b/internal/integration_test/fuzz/fuzz/fuzz_targets/memory_no_diff.rs
@@ -0,0 +1,68 @@
+//! memory_diff fuzzing test is almost the same as basic.rs
+//! but this also ensure that there's no difference in the memory buffer
+//! state after the execution.
+
+#![no_main]
+use libfuzzer_sys::arbitrary::{Result, Unstructured};
+use libfuzzer_sys::fuzz_target;
+use wasm_smith::SwarmConfig;
+mod wazero_abi;
+
+fuzz_target!(|data: &[u8]| {
+    drop(run(data));
+});
+
+fn run(data: &[u8]) -> Result<()> {
+    // Create the random source.
+    let mut u = Unstructured::new(data);
+
+    // Generate the configuration.
+    let mut config: SwarmConfig = u.arbitrary()?;
+
+    // 64-bit memory won't be supported by wazero.
+    config.memory64_enabled = false;
+    // For exactly one memory exists.
+    config.max_memories = 1;
+    config.min_memories = 1;
+    // If we don't set the limit, we will soon reach the OOM and the fuzzing will be killed by OS.
+    config.max_memory_pages = 10;
+    config.memory_max_size_required = true;
+    // Don't test too large tables.
+    config.max_tables = 2;
+    config.max_table_elements = 1_000;
+    config.table_max_size_required = true;
+
+    // max_instructions is set to 100 by default which seems a little bit smaller.
+    config.max_instructions = 5000;
+
+    // Without canonicalization of NaNs, the results cannot be matched among engines.
+    config.canonicalize_nans = true;
+
+    // Export all the things so that we can invoke them.
+    config.export_everything = true;
+
+    // Ensures that at least one function exists.
+    config.min_funcs = 1;
+    config.max_funcs = config.max_funcs.max(1);
+
+    // Generate the random module via wasm-smith.
+    let mut module = wasm_smith::Module::new(config.clone(), &mut u)?;
+    module.ensure_termination(1000);
+    let module_bytes = module.to_bytes();
+
+    let wat_bytes = wasmprinter::print_bytes(&module_bytes).unwrap();
+
+    // Pass the randomly generated module to the wazero library.
+    unsafe {
+        wazero_abi::require_no_diff(
+            module_bytes.as_ptr(),
+            module_bytes.len(),
+            wat_bytes.as_ptr(),
+            wat_bytes.len(),
+            true,
+        );
+    }
+
+    // We always return Ok as inside require_no_diff, we cause panic if the binary is interesting.
+    Ok(())
+}
diff --git a/internal/integration_test/fuzz/fuzz/fuzz_targets/validation.rs b/internal/integration_test/fuzz/fuzz/fuzz_targets/validation.rs
index 4e8c5277..f0cd5417 100644
--- a/internal/integration_test/fuzz/fuzz/fuzz_targets/validation.rs
+++ b/internal/integration_test/fuzz/fuzz/fuzz_targets/validation.rs
@@ -3,6 +3,8 @@
 use libfuzzer_sys::arbitrary::{Result, Unstructured};
 use libfuzzer_sys::fuzz_target;
 
+mod wazero_abi;
+
 fuzz_target!(|data: &[u8]| {
     drop(run(data));
 });
@@ -16,14 +18,9 @@ fn run(data: &[u8]) -> Result<()> {
     let module_bytes = module.to_bytes();
 
     unsafe {
-        validate(module_bytes.as_ptr(), module_bytes.len());
+        wazero_abi::validate(module_bytes.as_ptr(), module_bytes.len());
     }
 
     // We always return Ok as inside validate, we cause panic if the binary is interesting.
     Ok(())
 }
-
-extern "C" {
-    // validate is implemented in Go, and accepts the pointer to the binary and its size.
-    fn validate(binary_ptr: *const u8, binary_size: usize);
-}
diff --git a/internal/integration_test/fuzz/fuzz/fuzz_targets/wazero_abi.rs b/internal/integration_test/fuzz/fuzz/fuzz_targets/wazero_abi.rs
new file mode 100644
index 00000000..49bc8dab
--- /dev/null
+++ b/internal/integration_test/fuzz/fuzz/fuzz_targets/wazero_abi.rs
@@ -0,0 +1,15 @@
+//! This module provides the functions implemented by wazero via CGo.
+
+extern "C" {
+    // require_no_diff is implemented in Go, and accepts the pointer to the binary and its size.
+    pub fn require_no_diff(
+        binary_ptr: *const u8,
+        binary_size: usize,
+        wat_ptr: *const u8,
+        wat_size: usize,
+        check_memory: bool,
+    );
+
+    // validate is implemented in Go, and accepts the pointer to the binary and its size.
+    pub fn validate(binary_ptr: *const u8, binary_size: usize);
+}
diff --git a/internal/integration_test/fuzz/wazerolib/nodiff.go b/internal/integration_test/fuzz/wazerolib/nodiff.go
index 916a3484..1102853c 100644
--- a/internal/integration_test/fuzz/wazerolib/nodiff.go
+++ b/internal/integration_test/fuzz/wazerolib/nodiff.go
@@ -20,7 +20,7 @@ import (
 // And if there's diff, this also saves the problematic binary and wat into testdata directory.
 //
 //export require_no_diff
-func require_no_diff(binaryPtr uintptr, binarySize int, watPtr uintptr, watSize int) {
+func require_no_diff(binaryPtr uintptr, binarySize int, watPtr uintptr, watSize int, checkMemory bool) {
 	wasmBin := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
 		Data: binaryPtr,
 		Len:  binarySize,
@@ -41,7 +41,7 @@ func require_no_diff(binaryPtr uintptr, binarySize int, watPtr uintptr, watSize
 		}
 	}()
 
-	requireNoDiff(wasmBin, func(err error) {
+	requireNoDiff(wasmBin, checkMemory, func(err error) {
 		if err != nil {
 			panic(err)
 		}
@@ -76,7 +76,7 @@ func extractInternalWasmModuleFromCompiledModule(c wazero.CompiledModule) (*wasm
 }
 
 // requireNoDiff ensures that the behavior is the same between the compiler and the interpreter for any given binary.
-func requireNoDiff(wasmBin []byte, requireNoError func(err error)) {
+func requireNoDiff(wasmBin []byte, checkMemory bool, requireNoError func(err error)) {
 	// Choose the context to use for function calls.
 	ctx := context.Background()
 
@@ -111,6 +111,15 @@ func requireNoDiff(wasmBin []byte, requireNoError func(err error)) {
 	if okToInvoke {
 		err = ensureInvocationResultMatch(compilerMod, interpreterMod, interpreterCompiled.ExportedFunctions())
 		requireNoError(err)
+
+		compilerMem, _ := compilerMod.Memory().(*wasm.MemoryInstance)
+		interpreterMem, _ := interpreterMod.Memory().(*wasm.MemoryInstance)
+		if checkMemory && compilerMem != nil && interpreterMem != nil {
+			if !bytes.Equal(compilerMem.Buffer, interpreterMem.Buffer) {
+				requireNoError(fmt.Errorf("memory state mimsmatch\ncompiler: %v\ninterpreter: %v",
+					compilerMem.Buffer, interpreterMem.Buffer))
+			}
+		}
 	}
 }
 
diff --git a/internal/integration_test/fuzz/wazerolib/nodiff_test.go b/internal/integration_test/fuzz/wazerolib/nodiff_test.go
index 85ffcd04..ed63af98 100644
--- a/internal/integration_test/fuzz/wazerolib/nodiff_test.go
+++ b/internal/integration_test/fuzz/wazerolib/nodiff_test.go
@@ -16,5 +16,5 @@ func TestReRunFailedRequireNoDiffCase(t *testing.T) {
 		t.Skip(err)
 	}
 
-	requireNoDiff(wasmBin, func(err error) { require.NoError(t, err) })
+	requireNoDiff(wasmBin, true, func(err error) { require.NoError(t, err) })
 }
diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go
index 74873206..fa442bb0 100644
--- a/internal/sys/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -31,8 +31,7 @@ func TestNewFSContext(t *testing.T) {
 	embedFS, err := fs.Sub(testdata, "testdata")
 	require.NoError(t, err)
 
-	dirfs, err := sysfs.NewDirFS(".", "/")
-	require.NoError(t, err)
+	dirfs := sysfs.NewDirFS(".")
 
 	// Test various usual configuration for the file system.
 	tests := []struct {
@@ -41,7 +40,7 @@ func TestNewFSContext(t *testing.T) {
 	}{
 		{
 			name: "embed.FS",
-			fs:   sysfs.Adapt(embedFS, "/"),
+			fs:   sysfs.Adapt(embedFS),
 		},
 		{
 			name: "sysfs.NewDirFS",
@@ -55,7 +54,7 @@ func TestNewFSContext(t *testing.T) {
 		},
 		{
 			name: "fstest.MapFS",
-			fs:   sysfs.Adapt(fstest.MapFS{}, "/"),
+			fs:   sysfs.Adapt(fstest.MapFS{}),
 		},
 	}
 
@@ -68,7 +67,7 @@ func TestNewFSContext(t *testing.T) {
 			defer fsc.Close(testCtx)
 
 			preopenedDir, _ := fsc.openedFiles.Lookup(FdPreopen)
-			preopenedPath := fsc.fs.GuestDir()
+			preopenedPath := "/"
 			require.Equal(t, tc.fs, fsc.fs)
 			require.NotNil(t, preopenedDir)
 			require.Equal(t, "", preopenedDir.Name)
@@ -114,7 +113,7 @@ func TestUnimplementedFSContext(t *testing.T) {
 }
 
 func TestContext_Close(t *testing.T) {
-	testFS := sysfs.Adapt(testfs.FS{"foo": &testfs.File{}}, "/")
+	testFS := sysfs.Adapt(testfs.FS{"foo": &testfs.File{}})
 
 	fsc, err := NewFSContext(nil, nil, nil, testFS)
 	require.NoError(t, err)
@@ -139,7 +138,7 @@ func TestContext_Close(t *testing.T) {
 func TestContext_Close_Error(t *testing.T) {
 	file := &testfs.File{CloseErr: errors.New("error closing")}
 
-	testFS := sysfs.Adapt(testfs.FS{"foo": file}, "/")
+	testFS := sysfs.Adapt(testfs.FS{"foo": file})
 
 	fsc, err := NewFSContext(nil, nil, nil, testFS)
 	require.NoError(t, err)
diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go
index c24c7fdf..d26b9d96 100644
--- a/internal/sys/sys_test.go
+++ b/internal/sys/sys_test.go
@@ -13,7 +13,7 @@ import (
 )
 
 func TestContext_FS(t *testing.T) {
-	sysCtx := DefaultContext(sysfs.UnimplementedFS{})
+	sysCtx := DefaultContext(nil)
 
 	fsc, err := NewFSContext(nil, nil, nil, sysfs.UnimplementedFS{})
 	require.NoError(t, err)
@@ -51,7 +51,7 @@ func TestDefaultSysContext(t *testing.T) {
 	require.Equal(t, &ns, sysCtx.nanosleep)
 	require.Equal(t, platform.NewFakeRandSource(), sysCtx.RandSource())
 
-	testFS := sysfs.Adapt(testfs.FS{}, "/")
+	testFS := sysfs.Adapt(testfs.FS{})
 	expectedFS, _ := NewFSContext(nil, nil, nil, testFS)
 
 	expectedOpenedFiles := FileTable{}
diff --git a/internal/sysfs/adapter_test.go b/internal/sysfs/adapter_test.go
index df6fcfb8..8971caed 100644
--- a/internal/sysfs/adapter_test.go
+++ b/internal/sysfs/adapter_test.go
@@ -14,12 +14,12 @@ import (
 )
 
 func TestAdapt_String(t *testing.T) {
-	testFS := Adapt(os.DirFS("."), "/tmp")
-	require.Equal(t, ".:/tmp:ro", testFS.String())
+	testFS := Adapt(os.DirFS("."))
+	require.Equal(t, ".", testFS.String())
 }
 
 func TestAdapt_MkDir(t *testing.T) {
-	testFS := Adapt(os.DirFS(t.TempDir()), "/")
+	testFS := Adapt(os.DirFS(t.TempDir()))
 
 	err := testFS.Mkdir("mkdir", fs.ModeDir)
 	require.Equal(t, syscall.ENOSYS, err)
@@ -27,7 +27,7 @@ func TestAdapt_MkDir(t *testing.T) {
 
 func TestAdapt_Rename(t *testing.T) {
 	tmpDir := t.TempDir()
-	testFS := Adapt(os.DirFS(tmpDir), "/")
+	testFS := Adapt(os.DirFS(tmpDir))
 
 	file1 := "file1"
 	file1Path := pathutil.Join(tmpDir, file1)
@@ -47,7 +47,7 @@ func TestAdapt_Rename(t *testing.T) {
 
 func TestAdapt_Rmdir(t *testing.T) {
 	tmpDir := t.TempDir()
-	testFS := Adapt(os.DirFS(tmpDir), "/")
+	testFS := Adapt(os.DirFS(tmpDir))
 
 	path := "rmdir"
 	realPath := pathutil.Join(tmpDir, path)
@@ -59,7 +59,7 @@ func TestAdapt_Rmdir(t *testing.T) {
 
 func TestAdapt_Unlink(t *testing.T) {
 	tmpDir := t.TempDir()
-	testFS := Adapt(os.DirFS(tmpDir), "/")
+	testFS := Adapt(os.DirFS(tmpDir))
 
 	path := "unlink"
 	realPath := pathutil.Join(tmpDir, path)
@@ -71,7 +71,7 @@ func TestAdapt_Unlink(t *testing.T) {
 
 func TestAdapt_Utimes(t *testing.T) {
 	tmpDir := t.TempDir()
-	testFS := Adapt(os.DirFS(tmpDir), "/")
+	testFS := Adapt(os.DirFS(tmpDir))
 
 	path := "utimes"
 	realPath := pathutil.Join(tmpDir, path)
@@ -86,7 +86,7 @@ func TestAdapt_Open_Read(t *testing.T) {
 	tmpDir := t.TempDir()
 	tmpDir = pathutil.Join(tmpDir, t.Name())
 	require.NoError(t, os.Mkdir(tmpDir, 0o700))
-	testFS := Adapt(os.DirFS(tmpDir), "/")
+	testFS := Adapt(os.DirFS(tmpDir))
 
 	testOpen_Read(t, tmpDir, testFS)
 
@@ -120,7 +120,7 @@ func (dir hackFS) Open(name string) (fs.File, error) {
 // fs.FS contract.
 func TestAdapt_HackedWrites(t *testing.T) {
 	tmpDir := t.TempDir()
-	testFS := Adapt(hackFS(tmpDir), "/")
+	testFS := Adapt(hackFS(tmpDir))
 
 	testOpen_O_RDWR(t, tmpDir, testFS)
 }
@@ -156,7 +156,7 @@ func TestAdapt_TestFS(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			// Adapt a normal fs.FS to sysfs.FS
-			testFS := Adapt(tc.fs, "/")
+			testFS := Adapt(tc.fs)
 
 			// Adapt it back to fs.FS and run the tests
 			require.NoError(t, fstest.TestFS(&testFSAdapter{testFS}))
diff --git a/internal/sysfs/dirfs_test.go b/internal/sysfs/dirfs_test.go
index 0e5b13ae..f27b808e 100644
--- a/internal/sysfs/dirfs_test.go
+++ b/internal/sysfs/dirfs_test.go
@@ -2,7 +2,6 @@ package sysfs
 
 import (
 	"errors"
-	"fmt"
 	"io/fs"
 	"os"
 	pathutil "path"
@@ -15,45 +14,39 @@ import (
 )
 
 func TestNewDirFS(t *testing.T) {
-	testFS, err := NewDirFS(".", "/tmp")
-	require.NoError(t, err)
-
-	// String doesn't include the fake name
-	require.Equal(t, ".:/tmp", testFS.String())
+	testFS := NewDirFS(".")
 
 	// Guest can look up /
 	f, err := testFS.OpenFile("/", os.O_RDONLY, 0)
 	require.NoError(t, err)
 	require.NoError(t, f.Close())
 
-	t.Run("not found", func(t *testing.T) {
-		_, err := NewDirFS("a", "/tmp")
-
-		// get the os-dependent error string. This is needed for windows, e.g.
-		// "no such file or directory" vs "The system cannot find the file specified."
-		_, statErr := os.Stat("a")
-		expectedErrString := errors.Unwrap(statErr).Error()
-
-		require.EqualError(t, err, "a:/tmp invalid: "+expectedErrString)
+	t.Run("host path not found", func(t *testing.T) {
+		testFS := NewDirFS("a")
+		_, err = testFS.OpenFile(".", os.O_RDONLY, 0)
+		require.Equal(t, syscall.ENOENT, err)
 	})
-	t.Run("not a directory", func(t *testing.T) {
-		f := os.Args[0] // should be safe in scratch tests which don't have the source mounted.
-		_, err := NewDirFS(f, "/tmp")
-		require.EqualError(t, err, fmt.Sprintf("%[1]s:/tmp invalid: %[1]s is not a directory", f))
+	t.Run("host path not a directory", func(t *testing.T) {
+		arg0 := os.Args[0] // should be safe in scratch tests which don't have the source mounted.
+
+		testFS := NewDirFS(arg0)
+		d, err := testFS.OpenFile(".", os.O_RDONLY, 0)
+		require.NoError(t, err)
+		_, err = d.(fs.ReadDirFile).ReadDir(-1)
+		require.Equal(t, syscall.ENOTDIR, errors.Unwrap(err))
 	})
 }
 
 func TestDirFS_String(t *testing.T) {
-	testFS, err := NewDirFS(".", "/tmp")
-	require.NoError(t, err)
+	testFS := NewDirFS(".")
 
-	require.Equal(t, ".:/tmp", testFS.String())
+	// String has the name of the path entered
+	require.Equal(t, ".", testFS.String())
 }
 
 func TestDirFS_MkDir(t *testing.T) {
 	tmpDir := t.TempDir()
-	testFS, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := NewDirFS(tmpDir)
 
 	name := "mkdir"
 	realPath := pathutil.Join(tmpDir, name)
@@ -83,12 +76,11 @@ func TestDirFS_MkDir(t *testing.T) {
 func TestDirFS_Rename(t *testing.T) {
 	t.Run("from doesn't exist", func(t *testing.T) {
 		tmpDir := t.TempDir()
-		testFS, err := NewDirFS(tmpDir, "/")
-		require.NoError(t, err)
+		testFS := NewDirFS(tmpDir)
 
 		file1 := "file1"
 		file1Path := pathutil.Join(tmpDir, file1)
-		err = os.WriteFile(file1Path, []byte{1}, 0o600)
+		err := os.WriteFile(file1Path, []byte{1}, 0o600)
 		require.NoError(t, err)
 
 		err = testFS.Rename("file2", file1)
@@ -96,13 +88,12 @@ func TestDirFS_Rename(t *testing.T) {
 	})
 	t.Run("file to non-exist", func(t *testing.T) {
 		tmpDir := t.TempDir()
-		testFS, err := NewDirFS(tmpDir, "/")
-		require.NoError(t, err)
+		testFS := NewDirFS(tmpDir)
 
 		file1 := "file1"
 		file1Path := pathutil.Join(tmpDir, file1)
 		file1Contents := []byte{1}
-		err = os.WriteFile(file1Path, file1Contents, 0o600)
+		err := os.WriteFile(file1Path, file1Contents, 0o600)
 		require.NoError(t, err)
 
 		file2 := "file2"
@@ -120,8 +111,7 @@ func TestDirFS_Rename(t *testing.T) {
 	})
 	t.Run("dir to non-exist", func(t *testing.T) {
 		tmpDir := t.TempDir()
-		testFS, err := NewDirFS(tmpDir, "/")
-		require.NoError(t, err)
+		testFS := NewDirFS(tmpDir)
 
 		dir1 := "dir1"
 		dir1Path := pathutil.Join(tmpDir, dir1)
@@ -129,7 +119,7 @@ func TestDirFS_Rename(t *testing.T) {
 
 		dir2 := "dir2"
 		dir2Path := pathutil.Join(tmpDir, dir2)
-		err = testFS.Rename(dir1, dir2)
+		err := testFS.Rename(dir1, dir2)
 		require.NoError(t, err)
 
 		// Show the prior path no longer exists
@@ -142,8 +132,7 @@ func TestDirFS_Rename(t *testing.T) {
 	})
 	t.Run("dir to file", func(t *testing.T) {
 		tmpDir := t.TempDir()
-		testFS, err := NewDirFS(tmpDir, "/")
-		require.NoError(t, err)
+		testFS := NewDirFS(tmpDir)
 
 		dir1 := "dir1"
 		dir1Path := pathutil.Join(tmpDir, dir1)
@@ -153,7 +142,7 @@ func TestDirFS_Rename(t *testing.T) {
 		dir2Path := pathutil.Join(tmpDir, dir2)
 
 		// write a file to that path
-		err = os.WriteFile(dir2Path, []byte{2}, 0o600)
+		err := os.WriteFile(dir2Path, []byte{2}, 0o600)
 		require.NoError(t, err)
 
 		err = testFS.Rename(dir1, dir2)
@@ -170,13 +159,12 @@ func TestDirFS_Rename(t *testing.T) {
 	})
 	t.Run("file to dir", func(t *testing.T) {
 		tmpDir := t.TempDir()
-		testFS, err := NewDirFS(tmpDir, "/")
-		require.NoError(t, err)
+		testFS := NewDirFS(tmpDir)
 
 		file1 := "file1"
 		file1Path := pathutil.Join(tmpDir, file1)
 		file1Contents := []byte{1}
-		err = os.WriteFile(file1Path, file1Contents, 0o600)
+		err := os.WriteFile(file1Path, file1Contents, 0o600)
 		require.NoError(t, err)
 
 		dir1 := "dir1"
@@ -188,8 +176,7 @@ func TestDirFS_Rename(t *testing.T) {
 	})
 	t.Run("dir to dir", func(t *testing.T) {
 		tmpDir := t.TempDir()
-		testFS, err := NewDirFS(tmpDir, "/")
-		require.NoError(t, err)
+		testFS := NewDirFS(tmpDir)
 
 		dir1 := "dir1"
 		dir1Path := pathutil.Join(tmpDir, dir1)
@@ -199,7 +186,7 @@ func TestDirFS_Rename(t *testing.T) {
 		file1 := "file1"
 		file1Path := pathutil.Join(dir1Path, file1)
 		file1Contents := []byte{1}
-		err = os.WriteFile(file1Path, file1Contents, 0o600)
+		err := os.WriteFile(file1Path, file1Contents, 0o600)
 		require.NoError(t, err)
 
 		dir2 := "dir2"
@@ -225,13 +212,12 @@ func TestDirFS_Rename(t *testing.T) {
 	})
 	t.Run("file to file", func(t *testing.T) {
 		tmpDir := t.TempDir()
-		testFS, err := NewDirFS(tmpDir, "/")
-		require.NoError(t, err)
+		testFS := NewDirFS(tmpDir)
 
 		file1 := "file1"
 		file1Path := pathutil.Join(tmpDir, file1)
 		file1Contents := []byte{1}
-		err = os.WriteFile(file1Path, file1Contents, 0o600)
+		err := os.WriteFile(file1Path, file1Contents, 0o600)
 		require.NoError(t, err)
 
 		file2 := "file2"
@@ -254,14 +240,13 @@ func TestDirFS_Rename(t *testing.T) {
 	})
 	t.Run("dir to itself", func(t *testing.T) {
 		tmpDir := t.TempDir()
-		testFS, err := NewDirFS(tmpDir, "/")
-		require.NoError(t, err)
+		testFS := NewDirFS(tmpDir)
 
 		dir1 := "dir1"
 		dir1Path := pathutil.Join(tmpDir, dir1)
 		require.NoError(t, os.Mkdir(dir1Path, 0o700))
 
-		err = testFS.Rename(dir1, dir1)
+		err := testFS.Rename(dir1, dir1)
 		require.NoError(t, err)
 
 		s, err := os.Stat(dir1Path)
@@ -270,13 +255,12 @@ func TestDirFS_Rename(t *testing.T) {
 	})
 	t.Run("file to itself", func(t *testing.T) {
 		tmpDir := t.TempDir()
-		testFS, err := NewDirFS(tmpDir, "/")
-		require.NoError(t, err)
+		testFS := NewDirFS(tmpDir)
 
 		file1 := "file1"
 		file1Path := pathutil.Join(tmpDir, file1)
 		file1Contents := []byte{1}
-		err = os.WriteFile(file1Path, file1Contents, 0o600)
+		err := os.WriteFile(file1Path, file1Contents, 0o600)
 		require.NoError(t, err)
 
 		err = testFS.Rename(file1, file1)
@@ -290,9 +274,7 @@ func TestDirFS_Rename(t *testing.T) {
 
 func TestDirFS_Rmdir(t *testing.T) {
 	tmpDir := t.TempDir()
-
-	testFS, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := NewDirFS(tmpDir)
 
 	name := "rmdir"
 	realPath := pathutil.Join(tmpDir, name)
@@ -331,9 +313,7 @@ func TestDirFS_Rmdir(t *testing.T) {
 
 func TestDirFS_Unlink(t *testing.T) {
 	tmpDir := t.TempDir()
-
-	testFS, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := NewDirFS(tmpDir)
 
 	name := "unlink"
 	realPath := pathutil.Join(tmpDir, name)
@@ -363,9 +343,7 @@ func TestDirFS_Unlink(t *testing.T) {
 
 func TestDirFS_Utimes(t *testing.T) {
 	tmpDir := t.TempDir()
-
-	testFS, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := NewDirFS(tmpDir)
 
 	testUtimes(t, tmpDir, testFS)
 }
@@ -377,8 +355,7 @@ func TestDirFS_Open(t *testing.T) {
 	tmpDir = pathutil.Join(tmpDir, t.Name())
 	require.NoError(t, os.Mkdir(tmpDir, 0o700))
 
-	testFS, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := NewDirFS(tmpDir)
 
 	testOpen_Read(t, tmpDir, testFS)
 
@@ -400,8 +377,7 @@ func TestDirFS_TestFS(t *testing.T) {
 	require.NoError(t, fstest.WriteTestFiles(tmpDir))
 
 	// Create a writeable filesystem
-	testFS, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := NewDirFS(tmpDir)
 
 	// Run TestFS via the adapter
 	require.NoError(t, fstest.TestFS(&testFSAdapter{testFS}))
diff --git a/internal/sysfs/readfs_test.go b/internal/sysfs/readfs_test.go
index 8ee12fcd..6beeb90d 100644
--- a/internal/sysfs/readfs_test.go
+++ b/internal/sysfs/readfs_test.go
@@ -15,46 +15,41 @@ func TestNewReadFS(t *testing.T) {
 	tmpDir := t.TempDir()
 
 	// Doesn't double-wrap file systems that are already read-only
-	adapted := Adapt(os.DirFS(tmpDir), "/")
+	adapted := Adapt(os.DirFS(tmpDir))
 	require.Equal(t, adapted, NewReadFS(adapted))
 	require.Equal(t, UnimplementedFS{}, NewReadFS(UnimplementedFS{}))
 
 	// Wraps a writeable file system
-	writeable, err := NewDirFS(tmpDir, "/tmp")
-	require.NoError(t, err)
+	writeable := NewDirFS(tmpDir)
 	readFS := NewReadFS(writeable)
 	require.NotEqual(t, writeable, readFS)
-	require.Equal(t, writeable.GuestDir(), readFS.GuestDir())
 }
 
 func TestReadFS_String(t *testing.T) {
-	writeable, err := NewDirFS(".", "/tmp")
-	require.NoError(t, err)
+	writeable := NewDirFS("/tmp")
 
 	readFS := NewReadFS(writeable)
 	require.NotEqual(t, writeable, readFS)
-	require.Equal(t, ".:/tmp:ro", readFS.String())
+	require.Equal(t, "/tmp", readFS.String())
 }
 
 func TestReadFS_MkDir(t *testing.T) {
-	writeable, err := NewDirFS(t.TempDir(), "/")
-	require.NoError(t, err)
+	writeable := NewDirFS(t.TempDir())
 	testFS := NewReadFS(writeable)
 
-	err = testFS.Mkdir("mkdir", fs.ModeDir)
+	err := testFS.Mkdir("mkdir", fs.ModeDir)
 	require.Equal(t, syscall.ENOSYS, err)
 }
 
 func TestReadFS_Rename(t *testing.T) {
 	tmpDir := t.TempDir()
-	writeable, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	writeable := NewDirFS(tmpDir)
 	testFS := NewReadFS(writeable)
 
 	file1 := "file1"
 	file1Path := pathutil.Join(tmpDir, file1)
 	file1Contents := []byte{1}
-	err = os.WriteFile(file1Path, file1Contents, 0o600)
+	err := os.WriteFile(file1Path, file1Contents, 0o600)
 	require.NoError(t, err)
 
 	file2 := "file2"
@@ -69,50 +64,46 @@ func TestReadFS_Rename(t *testing.T) {
 
 func TestReadFS_Rmdir(t *testing.T) {
 	tmpDir := t.TempDir()
-	writeable, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	writeable := NewDirFS(tmpDir)
 	testFS := NewReadFS(writeable)
 
 	path := "rmdir"
 	realPath := pathutil.Join(tmpDir, path)
 	require.NoError(t, os.Mkdir(realPath, 0o700))
 
-	err = testFS.Rmdir(path)
+	err := testFS.Rmdir(path)
 	require.Equal(t, syscall.ENOSYS, err)
 }
 
 func TestReadFS_Unlink(t *testing.T) {
 	tmpDir := t.TempDir()
-	writeable, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	writeable := NewDirFS(tmpDir)
 	testFS := NewReadFS(writeable)
 
 	path := "unlink"
 	realPath := pathutil.Join(tmpDir, path)
 	require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
 
-	err = testFS.Unlink(path)
+	err := testFS.Unlink(path)
 	require.Equal(t, syscall.ENOSYS, err)
 }
 
 func TestReadFS_Utimes(t *testing.T) {
 	tmpDir := t.TempDir()
-	writeable, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	writeable := NewDirFS(tmpDir)
 	testFS := NewReadFS(writeable)
 
 	path := "utimes"
 	realPath := pathutil.Join(tmpDir, path)
 	require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
 
-	err = testFS.Utimes(path, 1, 1)
+	err := testFS.Utimes(path, 1, 1)
 	require.Equal(t, syscall.ENOSYS, err)
 }
 
 func TestReadFS_Open_Read(t *testing.T) {
 	tmpDir := t.TempDir()
-	writeable, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	writeable := NewDirFS(tmpDir)
 	testFS := NewReadFS(writeable)
 
 	testOpen_Read(t, tmpDir, testFS)
@@ -126,8 +117,7 @@ func TestReadFS_TestFS(t *testing.T) {
 	require.NoError(t, fstest.WriteTestFiles(tmpDir))
 
 	// Create a writeable filesystem
-	testFS, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := NewDirFS(tmpDir)
 
 	// Wrap it as read-only
 	testFS = NewReadFS(testFS)
diff --git a/internal/sysfs/rootfs_test.go b/internal/sysfs/rootfs_test.go
index 9c373adf..aa721c97 100644
--- a/internal/sysfs/rootfs_test.go
+++ b/internal/sysfs/rootfs_test.go
@@ -19,30 +19,28 @@ import (
 
 func TestNewRootFS(t *testing.T) {
 	t.Run("empty", func(t *testing.T) {
-		rootFS, err := NewRootFS()
+		rootFS, err := NewRootFS(nil, nil)
 		require.NoError(t, err)
 
 		require.Equal(t, UnimplementedFS{}, rootFS)
 	})
 	t.Run("only root", func(t *testing.T) {
-		testFS, err := NewDirFS(t.TempDir(), "/")
-		require.NoError(t, err)
+		testFS := NewDirFS(t.TempDir())
 
-		rootFS, err := NewRootFS(testFS)
+		rootFS, err := NewRootFS([]FS{testFS}, []string{""})
 		require.NoError(t, err)
 
 		// Should not be a composite filesystem
 		require.Equal(t, testFS, rootFS)
 	})
 	t.Run("only non root", func(t *testing.T) {
-		testFS, err := NewDirFS(".", "/tmp")
-		require.NoError(t, err)
+		testFS := NewDirFS(".")
 
-		rootFS, err := NewRootFS(testFS)
+		rootFS, err := NewRootFS([]FS{testFS}, []string{"/tmp"})
 		require.NoError(t, err)
 
-		// String doesn't include the fake name
-		require.Equal(t, ".:/tmp", rootFS.String())
+		// String is human-readable
+		require.Equal(t, "[.:/tmp]", rootFS.String())
 
 		// Guest can look up /tmp
 		f, err := rootFS.OpenFile("/tmp", os.O_RDONLY, 0)
@@ -59,32 +57,28 @@ func TestNewRootFS(t *testing.T) {
 		require.True(t, dirents[0].IsDir())
 	})
 	t.Run("multiple roots unsupported", func(t *testing.T) {
-		testFS, err := NewDirFS(".", "/")
-		require.NoError(t, err)
+		testFS := NewDirFS(".")
 
-		_, err = NewRootFS(testFS, testFS)
+		_, err := NewRootFS([]FS{testFS, testFS}, []string{"/", "/"})
 		require.EqualError(t, err, "multiple root filesystems are invalid: [.:/ .:/]")
 	})
 	t.Run("virtual paths unsupported", func(t *testing.T) {
-		testFS, err := NewDirFS(".", "/usr/bin")
-		require.NoError(t, err)
+		testFS := NewDirFS(".")
 
-		_, err = NewRootFS(testFS)
-		require.EqualError(t, err, "only single-level guest paths allowed: .:/usr/bin")
+		_, err := NewRootFS([]FS{testFS}, []string{"usr/bin"})
+		require.EqualError(t, err, "only single-level guest paths allowed: [.:usr/bin]")
 	})
 	t.Run("multiple matches", func(t *testing.T) {
 		tmpDir1 := t.TempDir()
-		testFS1, err := NewDirFS(tmpDir1, "/")
-		require.NoError(t, err)
+		testFS1 := NewDirFS(tmpDir1)
 		require.NoError(t, os.Mkdir(pathutil.Join(tmpDir1, "tmp"), 0o700))
 		require.NoError(t, os.WriteFile(pathutil.Join(tmpDir1, "a"), []byte{1}, 0o600))
 
 		tmpDir2 := t.TempDir()
-		testFS2, err := NewDirFS(tmpDir2, "/tmp")
-		require.NoError(t, err)
+		testFS2 := NewDirFS(tmpDir2)
 		require.NoError(t, os.WriteFile(pathutil.Join(tmpDir2, "a"), []byte{2}, 0o600))
 
-		rootFS, err := NewRootFS(testFS2, testFS1)
+		rootFS, err := NewRootFS([]FS{testFS2, testFS1}, []string{"tmp", ""})
 		require.NoError(t, err)
 
 		// unwrapping returns in original order
@@ -128,13 +122,10 @@ func readDirNames(t *testing.T, f fs.File) []string {
 }
 
 func TestRootFS_String(t *testing.T) {
-	tmpFS, err := NewDirFS(".", "/tmp")
-	require.NoError(t, err)
-
-	rootFS, err := NewDirFS(".", "/")
-	require.NoError(t, err)
+	tmpFS := NewDirFS(".")
+	rootFS := NewDirFS(".")
 
-	testFS, err := NewRootFS(rootFS, tmpFS)
+	testFS, err := NewRootFS([]FS{rootFS, tmpFS}, []string{"/", "/tmp"})
 	require.NoError(t, err)
 
 	require.Equal(t, "[.:/ .:/tmp]", testFS.String())
@@ -147,8 +138,7 @@ func TestRootFS_Open(t *testing.T) {
 	tmpDir = pathutil.Join(tmpDir, t.Name())
 	require.NoError(t, os.Mkdir(tmpDir, 0o700))
 
-	testFS, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	testFS := NewDirFS(tmpDir)
 
 	testOpen_Read(t, tmpDir, testFS)
 
@@ -174,14 +164,12 @@ func TestRootFS_TestFS(t *testing.T) {
 	require.NoError(t, os.Rename(pathutil.Join(tmpDir1, "dir"), pathutil.Join(tmpDir2, "dir")))
 
 	// Create a root mount
-	testFS1, err := NewDirFS(tmpDir1, "/")
-	require.NoError(t, err)
+	testFS1 := NewDirFS(tmpDir1)
 
 	// Create a dir mount
-	testFS2, err := NewDirFS(pathutil.Join(tmpDir2, "dir"), "/dir")
-	require.NoError(t, err)
+	testFS2 := NewDirFS(pathutil.Join(tmpDir2, "dir"))
 
-	testFS, err := NewRootFS(testFS1, testFS2)
+	testFS, err := NewRootFS([]FS{testFS1, testFS2}, []string{"/", "/dir"})
 	require.NoError(t, err)
 
 	// Run TestFS via the adapter
@@ -192,6 +180,7 @@ func TestRootFS_examples(t *testing.T) {
 	tests := []struct {
 		name                 string
 		fs                   []FS
+		guestPaths           []string
 		expected, unexpected []string
 	}{
 		// e.g. from Go project root:
@@ -200,9 +189,10 @@ func TestRootFS_examples(t *testing.T) {
 		{
 			name: "go test text/template",
 			fs: []FS{
-				&adapter{fs: testfs.FS{"go-example-stdout-ExampleTemplate-0.txt": &testfs.File{}}, guestDir: "/tmp"},
-				&adapter{fs: testfs.FS{"testdata/file1.tmpl": &testfs.File{}}, guestDir: "."},
+				&adapter{fs: testfs.FS{"go-example-stdout-ExampleTemplate-0.txt": &testfs.File{}}},
+				&adapter{fs: testfs.FS{"testdata/file1.tmpl": &testfs.File{}}},
 			},
+			guestPaths: []string{"/tmp", "/"},
 			expected:   []string{"/tmp/go-example-stdout-ExampleTemplate-0.txt", "testdata/file1.tmpl"},
 			unexpected: []string{"DOES NOT EXIST"},
 		},
@@ -212,10 +202,11 @@ func TestRootFS_examples(t *testing.T) {
 		{
 			name: "tinygo test compress/flate",
 			fs: []FS{
-				&adapter{fs: testfs.FS{}, guestDir: "/"},
-				&adapter{fs: testfs.FS{"testdata/e.txt": &testfs.File{}}, guestDir: "../"},
-				&adapter{fs: testfs.FS{"testdata/Isaac.Newton-Opticks.txt": &testfs.File{}}, guestDir: "../../"},
+				&adapter{fs: testfs.FS{}},
+				&adapter{fs: testfs.FS{"testdata/e.txt": &testfs.File{}}},
+				&adapter{fs: testfs.FS{"testdata/Isaac.Newton-Opticks.txt": &testfs.File{}}},
 			},
+			guestPaths: []string{"/", "../", "../../"},
 			expected:   []string{"../testdata/e.txt", "../../testdata/Isaac.Newton-Opticks.txt"},
 			unexpected: []string{"../../testdata/e.txt"},
 		},
@@ -225,9 +216,10 @@ func TestRootFS_examples(t *testing.T) {
 		{
 			name: "go test net",
 			fs: []FS{
-				&adapter{fs: testfs.FS{"services": &testfs.File{}}, guestDir: "/etc"},
-				&adapter{fs: testfs.FS{"testdata/aliases": &testfs.File{}}, guestDir: "/"},
+				&adapter{fs: testfs.FS{"services": &testfs.File{}}},
+				&adapter{fs: testfs.FS{"testdata/aliases": &testfs.File{}}},
 			},
+			guestPaths: []string{"/etc", "/"},
 			expected:   []string{"/etc/services", "testdata/aliases"},
 			unexpected: []string{"services"},
 		},
@@ -241,8 +233,9 @@ func TestRootFS_examples(t *testing.T) {
 				&adapter{fs: gofstest.MapFS{ // to allow resolution of "."
 					"pybuilddir.txt": &gofstest.MapFile{},
 					"opt/wasi-python/lib/python3.11/__phello__/__init__.py": &gofstest.MapFile{},
-				}, guestDir: "/"},
+				}},
 			},
+			guestPaths: []string{"/"},
 			expected: []string{
 				".",
 				"pybuilddir.txt",
@@ -255,9 +248,10 @@ func TestRootFS_examples(t *testing.T) {
 		{
 			name: "zig",
 			fs: []FS{
-				&adapter{fs: testfs.FS{"zig-cache": &testfs.File{}}, guestDir: "/"},
-				&adapter{fs: testfs.FS{"qSQRrUkgJX9L20mr": &testfs.File{}}, guestDir: "/tmp"},
+				&adapter{fs: testfs.FS{"zig-cache": &testfs.File{}}},
+				&adapter{fs: testfs.FS{"qSQRrUkgJX9L20mr": &testfs.File{}}},
 			},
+			guestPaths: []string{"/", "/tmp"},
 			expected:   []string{"zig-cache", "/tmp/qSQRrUkgJX9L20mr"},
 			unexpected: []string{"/qSQRrUkgJX9L20mr"},
 		},
@@ -267,7 +261,7 @@ func TestRootFS_examples(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			root, err := NewRootFS(tc.fs...)
+			root, err := NewRootFS(tc.fs, tc.guestPaths)
 			require.NoError(t, err)
 
 			for _, p := range tc.expected {
diff --git a/internal/sysfs/sysfs_test.go b/internal/sysfs/sysfs_test.go
index 389b231f..fe4017d3 100644
--- a/internal/sysfs/sysfs_test.go
+++ b/internal/sysfs/sysfs_test.go
@@ -414,8 +414,7 @@ func TestReaderAtOffset_Unsupported(t *testing.T) {
 
 func TestWriterAtOffset(t *testing.T) {
 	tmpDir := t.TempDir()
-	dirFS, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	dirFS := NewDirFS(tmpDir)
 
 	// fs.FS doesn't support writes, and there is no other built-in
 	// implementation except os.File.
@@ -481,8 +480,7 @@ func TestWriterAtOffset(t *testing.T) {
 
 func TestWriterAtOffset_empty(t *testing.T) {
 	tmpDir := t.TempDir()
-	dirFS, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	dirFS := NewDirFS(tmpDir)
 
 	// fs.FS doesn't support writes, and there is no other built-in
 	// implementation except os.File.
@@ -523,8 +521,7 @@ func TestWriterAtOffset_empty(t *testing.T) {
 
 func TestWriterAtOffset_Unsupported(t *testing.T) {
 	tmpDir := t.TempDir()
-	dirFS, err := NewDirFS(tmpDir, "/")
-	require.NoError(t, err)
+	dirFS := NewDirFS(tmpDir)
 
 	f, err := dirFS.OpenFile(readerAtFile, os.O_RDWR|os.O_CREATE, 0o600)
 	require.NoError(t, err)