Repository: tetratelabs/wazero
Pull request #1433 (2023-05-03 19:46:11)

experimental: expose source offset to Listener
This change provides a user of `experimental.StackIterator` the ability to retrieve the offset in the Code section of each function call in the frame of the stack. It enables capabilities such as source-mapping to be implemented by the user outside of wazero (removing the need for #1403).

It works by retrieving the return address of each stack frame and using the `irOperationSourceOffsetsInWasmBinary` attached to the function associated with that stack frame.

Though it works in my tests and on the few programs I used, I feel quite out of my depth here; I'd love some guidance and feedback! :) 

Question 1 – This source offsets mapping is only produced by the compiler when DWARF sections are present in the module. In practice, it's a reasonable limitation for us since we currently are only interested in using this information to correlate with DWARF data anyway. However, we may want to look at other applications in the future. It also meant that I had to provide a minimal DWARF info section for `dwarf/debug` not to trip up and the compiler to emit the offsets map. Would you be interested in adding either a flag to enable the mapping generation regardless of the presence of DWARF sections?

Question 2 – because the stack iterator is used in the`Before` listener hook, the topmost frame of the call stack is always the called function that triggered the listener. As such, I think the source offset should be the offset of the first byte of that function in the code section. In that case, I have observed that the pc is just lower than the first entry of `irOperationSourceOffsetsInWasmBinary` (hence the change in `getSourceOffsetInWasmBinary`). I am struggling to understand why? Returning the first entry of that collection seems to give the expected results, but I don't rationalize it.

I appreciate your consideration!

pelletier: Thanks for the review! 

Expanded the documentation for `SourceOffset` and added it to the existing `StackIterator` example (and fixed it along the way...)

> Also some bikeshedding about returning -1 instead of zero, for example for host functions.
> and the length of the code section I think will not be 64bits :)

That's fair. `getSourceOffsetInWasmBinary` (and equivalent in the interpreter) uses `uint64`. It suffers from the same issue of returning 0 if not found (which I think would be a valid offset if called with the PC at the beginning very first function of the Code section?) and using a rather large type. I opted to use `uint64` for SourceOffset for consistency, as it seems to be the type used for offsets in the Code section in Wazero.

To signal a "not found" case, I thought about either using another sentinel value (like `MaxUint64`) or returning an additional `error` value in that case but eventually decided to lean into zero-values for less ceremony. Aside from that, I don't have much argument either way.

I'm happy to change the signature of `SourceOffset` to any of the above and do the appropriate casts, and/or change `getSourceOffsetInWasmBinary` & friends to have them not use `uint64` either. Let me know your preference!
codefromthecrypt: I personally prefer -1 to zero for saying not found (e.g. strings.Index) as it feels more idiomatic in Go, I'm not aware of a lot of use of Max or zero for the same thing. It is a bike shed, and to get through them, usually blaming decision on another precedent is the easy way out. I would blame other code that look for an index of something in go returning -1 as the reason.
evacchi: ps: obviously there are still a few failures on CI that should be addressed
pelletier: Thank you everyone for the reviews! I have addressed all the comments (unless I misunderstood the -1 vs 0 decision).

Patch:
diff --git a/experimental/listener.go b/experimental/listener.go
index 8e27e88a..7455ec3f 100644
--- a/experimental/listener.go
+++ b/experimental/listener.go
@@ -13,14 +13,22 @@ import (
 // iteration. As a result, parameter values may be different than the ones their
 // function was called with.
 type StackIterator interface {
-	// Next moves the iterator to the next function in the stack. Returns false
-	// if it reached the bottom of the stack.
+	// Next moves the iterator to the next function in the stack. Returns
+	// false if it reached the bottom of the stack.
 	Next() bool
 	// FunctionDefinition returns the function type of the current function.
 	FunctionDefinition() api.FunctionDefinition
+	// SourceOffset computes the offset in the module Code section where the
+	// call occured (translated for native functions), or the beginning of
+	// function for the top of the stack. Returns 0 if the source offset
+	// cannot be calculated.
+	//
+	// The source offset is meant to help map the function calls to their
+	// location in the original source files.
+	SourceOffset() uint64
 	// Parameters returns api.ValueType-encoded parameters of the current
-	// function. Do not modify the content of the slice, and copy out any value
-	// you need.
+	// function. Do not modify the content of the slice, and copy out any
+	// value you need.
 	Parameters() []uint64
 }
 
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index 98353d31..fe7ea8b5 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -202,8 +202,21 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) syscall.E
 		return syscall.EBADF
 	} else if st, errno = f.Stat(); errno != 0 {
 		return errno
-	} else if f.File.AccessMode() != syscall.O_RDONLY {
-		fdflags = wasip1.FD_APPEND
+	} else {
+		var nonblock bool
+		switch file := f.File.File().(type) {
+		case *sys.StdioFileReader:
+			nonblock = file.IsNonblock()
+		case *sys.StdioFileWriter:
+			nonblock = file.IsNonblock()
+		default:
+			if f.File.AccessMode() != syscall.O_RDONLY {
+				fdflags |= wasip1.FD_APPEND
+			}
+		}
+		if nonblock {
+			fdflags |= wasip1.FD_NONBLOCK
+		}
 	}
 
 	var fsRightsBase uint32
@@ -276,11 +289,25 @@ func fdFdstatSetFlagsFn(_ context.Context, mod api.Module, params []uint64) sysc
 	fd, wasiFlag := int32(params[0]), uint16(params[1])
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	// We can only support APPEND flag.
-	if wasip1.FD_DSYNC&wasiFlag != 0 || wasip1.FD_NONBLOCK&wasiFlag != 0 || wasip1.FD_RSYNC&wasiFlag != 0 || wasip1.FD_SYNC&wasiFlag != 0 {
+	// Currently we only support APPEND and NONBLOCK.
+	if wasip1.FD_DSYNC&wasiFlag != 0 || wasip1.FD_RSYNC&wasiFlag != 0 || wasip1.FD_SYNC&wasiFlag != 0 {
 		return syscall.EINVAL
 	}
 
+	// Currently we only support non-blocking mode for standard I/O streams.
+	// Non-blocking mode is rarely supported for regular files, and we don't
+	// yet have support for sockets so we make a special case.
+	f, ok := fsc.LookupFile(fd)
+	if ok {
+		nonblock := wasip1.FD_NONBLOCK&wasiFlag != 0
+		switch file := f.File.File().(type) {
+		case *sys.StdioFileReader:
+			return platform.UnwrapOSError(file.SetNonblock(nonblock))
+		case *sys.StdioFileWriter:
+			return platform.UnwrapOSError(file.SetNonblock(nonblock))
+		}
+	}
+
 	var flag int
 	if wasip1.FD_APPEND&wasiFlag != 0 {
 		flag = syscall.O_APPEND
@@ -898,12 +925,15 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) syscall.Err
 // dotDirents returns "." and "..", where "." because wasi-testsuite does inode
 // validation.
 func dotDirents(f *sys.FileEntry) ([]*platform.Dirent, syscall.Errno) {
-	dotIno, ft, errno := f.CachedStat()
-	if errno != 0 {
+	if isDir, errno := f.File.IsDir(); errno != 0 {
 		return nil, errno
-	} else if ft.Type() != fs.ModeDir {
+	} else if !isDir {
 		return nil, syscall.ENOTDIR
 	}
+	dotIno, errno := f.Inode()
+	if errno != 0 {
+		return nil, errno
+	}
 	dotDotIno := uint64(0)
 	if !f.IsPreopen && f.Name != "." {
 		if st, errno := f.FS.Stat(path.Dir(f.Name)); errno != 0 {
@@ -1066,9 +1096,9 @@ func writeDirent(buf []byte, dNext uint64, ino uint64, dNamlen uint32, dType fs.
 func openedDir(fsc *sys.FSContext, fd int32) (fs.File, *sys.ReadDir, syscall.Errno) {
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return nil, nil, syscall.EBADF
-	} else if _, ft, errno := f.CachedStat(); errno != 0 {
+	} else if isDir, errno := f.File.IsDir(); errno != 0 {
 		return nil, nil, errno
-	} else if ft.Type() != fs.ModeDir {
+	} else if !isDir {
 		// fd_readdir docs don't indicate whether to return syscall.ENOTDIR or
 		// syscall.EBADF. It has been noticed that rust will crash on syscall.ENOTDIR,
 		// and POSIX C ref seems to not return this, so we don't either.
@@ -1152,29 +1182,11 @@ func fdSeekFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno
 	whence := uint32(params[2])
 	resultNewoffset := uint32(params[3])
 
-	var seeker io.Seeker
-	// Check to see if the file descriptor is available
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return syscall.EBADF
-		// fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it.
-	} else if _, ft, errno := f.CachedStat(); errno != 0 {
+	} else if newOffset, errno := f.File.Seek(int64(offset), int(whence)); errno != 0 {
 		return errno
-	} else if ft.Type() == fs.ModeDir {
-		return syscall.EBADF
-	} else if seeker, ok = f.File.File().(io.Seeker); !ok {
-		return syscall.EBADF
-	}
-
-	if whence > io.SeekEnd /* exceeds the largest valid whence */ {
-		return syscall.EINVAL
-	}
-
-	newOffset, err := seeker.Seek(int64(offset), int(whence))
-	if err != nil {
-		return platform.UnwrapOSError(err)
-	}
-
-	if !mod.Memory().WriteUint64Le(resultNewoffset, uint64(newOffset)) {
+	} else if !mod.Memory().WriteUint64Le(resultNewoffset, uint64(newOffset)) {
 		return syscall.EFAULT
 	}
 	return 0
@@ -1650,10 +1662,10 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
 	if isDir {
 		if f, ok := fsc.LookupFile(newFD); !ok {
 			return syscall.EBADF // unexpected
-		} else if _, ft, errno := f.CachedStat(); errno != 0 {
+		} else if isDir, errno := f.File.IsDir(); errno != 0 {
 			_ = fsc.CloseFile(newFD)
 			return errno
-		} else if ft.Type() != fs.ModeDir {
+		} else if !isDir {
 			_ = fsc.CloseFile(newFD)
 			return syscall.ENOTDIR
 		}
@@ -1710,9 +1722,9 @@ func atPath(fsc *sys.FSContext, mem api.Memory, fd int32, p, pathLen uint32) (sy
 
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return nil, "", syscall.EBADF // closed or invalid
-	} else if _, ft, errno := f.CachedStat(); errno != 0 {
+	} else if isDir, errno := f.File.IsDir(); errno != 0 {
 		return nil, "", errno
-	} else if ft.Type() != fs.ModeDir {
+	} else if !isDir {
 		return nil, "", syscall.ENOTDIR
 	} else if f.IsPreopen { // don't append the pre-open name
 		return f.FS, pathName, 0
@@ -1953,9 +1965,9 @@ func pathSymlinkFn(_ context.Context, mod api.Module, params []uint64) syscall.E
 	dir, ok := fsc.LookupFile(fd)
 	if !ok {
 		return syscall.EBADF // closed
-	} else if _, ft, errno := dir.CachedStat(); errno != 0 {
+	} else if isDir, errno := dir.File.IsDir(); errno != 0 {
 		return errno
-	} else if ft.Type() != fs.ModeDir {
+	} else if !isDir {
 		return syscall.ENOTDIR
 	}
 
diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go
index d44ed5e0..fcc9c6ea 100644
--- a/internal/engine/compiler/engine.go
+++ b/internal/engine/compiler/engine.go
@@ -880,6 +880,12 @@ func (f *function) getSourceOffsetInWasmBinary(pc uint64) uint64 {
 		}
 		return srcMap.irOperationOffsetsInNativeBinary[i] >= pcOffsetInNativeBinary
 	})
+	if index == 0 && len(srcMap.irOperationSourceOffsetsInWasmBinary) > 0 {
+		// When pc is the beginning of the function, the next IR
+		// operation (returned by sort.Search) is the first of the
+		// offset map.
+		return srcMap.irOperationSourceOffsetsInWasmBinary[0]
+	}
 
 	if index == n || index == 0 { // This case, somehow pc is not found in the source offset map.
 		return 0
@@ -1099,13 +1105,15 @@ type stackIterator struct {
 	stack   []uint64
 	fn      *function
 	base    int
+	pc      uint64
 	started bool
 }
 
-func (si *stackIterator) reset(stack []uint64, fn *function, base int) {
+func (si *stackIterator) reset(stack []uint64, fn *function, base int, pc uint64) {
 	si.stack = stack
 	si.fn = fn
 	si.base = base
+	si.pc = pc
 	si.started = false
 }
 
@@ -1116,7 +1124,7 @@ func (si *stackIterator) clear() {
 	si.started = false
 }
 
-// Next implements experimental.StackIterator.
+// Next implements the same method as documtend on experimental.StackIterator.
 func (si *stackIterator) Next() bool {
 	if !si.started {
 		si.started = true
@@ -1128,6 +1136,7 @@ func (si *stackIterator) Next() bool {
 	}
 
 	frame := si.base + callFrameOffset(si.fn.funcType)
+	si.pc = si.stack[frame+0]
 	si.base = int(si.stack[frame+1] >> 3)
 	// *function lives in the third field of callFrame struct. This must be
 	// aligned with the definition of callFrame struct.
@@ -1135,19 +1144,34 @@ func (si *stackIterator) Next() bool {
 	return si.fn != nil
 }
 
-// FunctionDefinition implements experimental.StackIterator.
+// SourceOffset implements the same method as documented on
+// experimental.StackIterator.
+func (si *stackIterator) SourceOffset() uint64 {
+	p := si.fn.parent
+
+	if len(p.sourceOffsetMap.irOperationSourceOffsetsInWasmBinary) == 0 {
+		return 0 // source not available
+	}
+
+	return si.fn.getSourceOffsetInWasmBinary(si.pc)
+}
+
+// FunctionDefinition implements the same method as documented on
+// experimental.StackIterator.
 func (si *stackIterator) FunctionDefinition() api.FunctionDefinition {
 	return si.fn.definition()
 }
 
-// Parameters implements experimental.StackIterator.
+// Parameters implements the same method as documented on
+// experimental.StackIterator.
 func (si *stackIterator) Parameters() []uint64 {
 	return si.stack[si.base : si.base+si.fn.funcType.ParamNumInUint64]
 }
 
 func (ce *callEngine) builtinFunctionFunctionListenerBefore(ctx context.Context, mod api.Module, fn *function) {
 	base := int(ce.stackBasePointerInBytes >> 3)
-	ce.stackIterator.reset(ce.stack, fn, base)
+	pc := uint64(ce.returnAddress)
+	ce.stackIterator.reset(ce.stack, fn, base, pc)
 
 	params := ce.stack[base : base+fn.funcType.ParamNumInUint64]
 	listerCtx := fn.parent.listener.Before(ctx, mod, fn.definition(), params, &ce.stackIterator)
diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go
index 2411a313..b2586716 100644
--- a/internal/engine/interpreter/interpreter.go
+++ b/internal/engine/interpreter/interpreter.go
@@ -222,10 +222,12 @@ type stackIterator struct {
 	frames  []*callFrame
 	started bool
 	fn      *function
+	pc      uint64
 }
 
 func (si *stackIterator) reset(stack []uint64, frames []*callFrame, f *function) {
 	si.fn = f
+	si.pc = 0
 	si.stack = stack
 	si.frames = frames
 	si.started = false
@@ -238,7 +240,7 @@ func (si *stackIterator) clear() {
 	si.fn = nil
 }
 
-// Next implements experimental.StackIterator.
+// Next implements the same method as documented on experimental.StackIterator.
 func (si *stackIterator) Next() bool {
 	if !si.started {
 		si.started = true
@@ -252,16 +254,30 @@ func (si *stackIterator) Next() bool {
 	frame := si.frames[len(si.frames)-1]
 	si.stack = si.stack[:frame.base]
 	si.fn = frame.f
+	si.pc = frame.pc
 	si.frames = si.frames[:len(si.frames)-1]
 	return true
 }
 
-// FunctionDefinition implements experimental.StackIterator.
+// FunctionDefinition implements the same method as documented on
+// experimental.StackIterator.
 func (si *stackIterator) FunctionDefinition() api.FunctionDefinition {
 	return si.fn.definition()
 }
 
-// Parameters implements experimental.StackIterator.
+// SourceOffset implements the same method as documented on
+// experimental.StackIterator.
+func (si *stackIterator) SourceOffset() uint64 {
+	offsetsMap := si.fn.parent.offsetsInWasmBinary
+	pc := si.pc
+	if pc < uint64(len(offsetsMap)) {
+		return offsetsMap[pc]
+	}
+	return 0
+}
+
+// Parameters implements the same method as documented on
+// experimental.StackIterator.
 func (si *stackIterator) Parameters() []uint64 {
 	paramsCount := si.fn.funcType.ParamNumInUint64
 	top := len(si.stack)
diff --git a/internal/platform/file.go b/internal/platform/file.go
index a2fc2408..2dee892b 100644
--- a/internal/platform/file.go
+++ b/internal/platform/file.go
@@ -58,6 +58,19 @@ type File interface {
 	//   - Windows allows you to stat a closed directory.
 	Stat() (Stat_t, syscall.Errno)
 
+	// IsDir returns true if this file is a directory or an error there was an
+	// error retrieving this information.
+	//
+	// # Errors
+	//
+	// A zero syscall.Errno is success. The below are expected otherwise:
+	//   - syscall.ENOSYS: the implementation does not support this function.
+	//
+	// # Notes
+	//
+	//   - Some implementations implement this with a cached call to Stat.
+	IsDir() (bool, syscall.Errno)
+
 	// Read attempts to read all bytes in the file into `p`, and returns the
 	// count read even on error.
 	//
@@ -95,6 +108,33 @@ type File interface {
 	//     read the file completely, the caller must repeat until `n` is zero.
 	Pread(p []byte, off int64) (n int, errno syscall.Errno)
 
+	// Seek attempts to set the next offset for Read or Write and returns the
+	// resulting absolute offset or an error.
+	//
+	// # Parameters
+	//
+	// The `offset` parameters is interpreted in terms of `whence`:
+	//   - io.SeekStart: relative to the start of the file, e.g. offset=0 sets
+	//     the next Read or Write to the beginning of the file.
+	//   - io.SeekCurrent: relative to the current offset, e.g. offset=16 sets
+	//     the next Read or Write 16 bytes past the prior.
+	//   - io.SeekEnd: relative to the end of the file, e.g. offset=-1 sets the
+	//     next Read or Write to the last byte in the file.
+	//
+	// # Errors
+	//
+	// A zero syscall.Errno is success. The below are expected otherwise:
+	//   - syscall.ENOSYS: the implementation does not support this function.
+	//   - syscall.EBADF: the file or directory was closed or not readable.
+	//   - syscall.EINVAL: the offset was negative.
+	//   - syscall.EISDIR: the file was a directory.
+	//
+	// # Notes
+	//
+	//   - This is like io.Seeker and `fseek` in POSIX, preferring semantics
+	//     of io.Seeker. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/fseek.html
+	Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno)
+
 	// Write attempts to write all bytes in `p` to the file, and returns the
 	// count written even on error.
 	//
@@ -103,6 +143,7 @@ type File interface {
 	// A zero syscall.Errno is success. The below are expected otherwise:
 	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EBADF: the file or directory was closed or not writeable.
+	//   - syscall.EISDIR: the file was a directory.
 	//
 	// # Notes
 	//
@@ -119,6 +160,7 @@ type File interface {
 	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EBADF: the file or directory was closed or not writeable.
 	//   - syscall.EINVAL: the offset was negative.
+	//   - syscall.EISDIR: the file was a directory.
 	//
 	// # Notes
 	//
@@ -255,6 +297,11 @@ func (UnimplementedFile) Stat() (Stat_t, syscall.Errno) {
 	return Stat_t{}, syscall.ENOSYS
 }
 
+// IsDir implements File.IsDir
+func (UnimplementedFile) IsDir() (bool, syscall.Errno) {
+	return false, syscall.ENOSYS
+}
+
 // Read implements File.Read
 func (UnimplementedFile) Read([]byte) (int, syscall.Errno) {
 	return 0, syscall.ENOSYS
@@ -265,6 +312,11 @@ func (UnimplementedFile) Pread([]byte, int64) (int, syscall.Errno) {
 	return 0, syscall.ENOSYS
 }
 
+// Seek implements File.Seek
+func (UnimplementedFile) Seek(int64, int) (int64, syscall.Errno) {
+	return 0, syscall.ENOSYS
+}
+
 // Write implements File.Write
 func (UnimplementedFile) Write([]byte) (int, syscall.Errno) {
 	return 0, syscall.ENOSYS
@@ -317,6 +369,25 @@ type fsFile struct {
 	path       string
 	accessMode int
 	file       fs.File
+
+	// cachedStat includes fields that won't change while a file is open.
+	cachedSt *cachedStat
+}
+
+type cachedStat struct {
+	// fileType is the same as what's documented on Dirent.
+	fileType fs.FileMode
+}
+
+// cachedStat returns the cacheable parts of platform.Stat_t or an error if
+// they couldn't be retrieved.
+func (f *fsFile) cachedStat() (fileType fs.FileMode, errno syscall.Errno) {
+	if f.cachedSt == nil {
+		if _, errno = f.Stat(); errno != 0 {
+			return
+		}
+	}
+	return f.cachedSt.fileType, 0
 }
 
 // Path implements File.Path
@@ -329,10 +400,23 @@ func (f *fsFile) AccessMode() int {
 	return f.accessMode
 }
 
+// IsDir implements File.IsDir
+func (f *fsFile) IsDir() (bool, syscall.Errno) {
+	if ft, errno := f.cachedStat(); errno != 0 {
+		return false, errno
+	} else if ft.Type() == fs.ModeDir {
+		return true, 0
+	}
+	return false, 0
+}
+
 // Stat implements File.Stat
 func (f *fsFile) Stat() (Stat_t, syscall.Errno) {
 	st, errno := statFile(f.file)
-	if errno == syscall.EIO {
+	switch errno {
+	case 0:
+		f.cachedSt = &cachedStat{fileType: st.Mode & fs.ModeType}
+	case syscall.EIO:
 		errno = syscall.EBADF
 	}
 	return st, errno
@@ -344,9 +428,12 @@ func (f *fsFile) Read(p []byte) (n int, errno syscall.Errno) {
 		return 0, 0 // less overhead on zero-length reads.
 	}
 
-	if f.accessMode == syscall.O_WRONLY {
+	if errno = f.isDirErrno(); errno != 0 {
+		return
+	} else if f.accessMode == syscall.O_WRONLY {
 		return 0, syscall.EBADF
 	}
+
 	if w, ok := f.File().(io.Reader); ok {
 		n, err := w.Read(p)
 		return n, UnwrapOSError(err)
@@ -360,7 +447,9 @@ func (f *fsFile) Pread(p []byte, off int64) (n int, errno syscall.Errno) {
 		return 0, 0 // less overhead on zero-length reads.
 	}
 
-	if f.accessMode == syscall.O_WRONLY {
+	if errno = f.isDirErrno(); errno != 0 {
+		return
+	} else if f.accessMode == syscall.O_WRONLY {
 		return 0, syscall.EBADF
 	}
 
@@ -395,14 +484,31 @@ func (f *fsFile) Pread(p []byte, off int64) (n int, errno syscall.Errno) {
 	return 0, syscall.ENOSYS // unsupported
 }
 
+// Seek implements File.Seek
+func (f *fsFile) Seek(offset int64, whence int) (int64, syscall.Errno) {
+	if errno := f.isDirErrno(); errno != 0 {
+		return 0, errno
+	} else if uint(whence) > io.SeekEnd {
+		return 0, syscall.EINVAL // negative or exceeds the largest valid whence
+	}
+
+	if seeker, ok := f.file.(io.Seeker); ok {
+		newOffset, err := seeker.Seek(offset, whence)
+		return newOffset, UnwrapOSError(err)
+	}
+	return 0, syscall.ENOSYS
+}
+
 // Write implements File.Write
 func (f *fsFile) Write(p []byte) (n int, errno syscall.Errno) {
-	if len(p) == 0 {
-		return 0, 0 // less overhead on zero-length writes.
+	if errno = f.isDirErrno(); errno != 0 {
+		return
+	} else if f.accessMode == syscall.O_RDONLY {
+		return 0, syscall.EBADF
 	}
 
-	if f.accessMode == syscall.O_RDONLY {
-		return 0, syscall.EBADF
+	if len(p) == 0 {
+		return 0, 0 // less overhead on zero-length writes.
 	}
 	if w, ok := f.File().(io.Writer); ok {
 		n, err := w.Write(p)
@@ -413,13 +519,16 @@ func (f *fsFile) Write(p []byte) (n int, errno syscall.Errno) {
 
 // Pwrite implements File.Pwrite
 func (f *fsFile) Pwrite(p []byte, off int64) (n int, errno syscall.Errno) {
+	if errno = f.isDirErrno(); errno != 0 {
+		return
+	} else if f.accessMode == syscall.O_RDONLY {
+		return 0, syscall.EBADF
+	}
+
 	if len(p) == 0 {
 		return 0, 0 // less overhead on zero-length writes.
 	}
 
-	if f.accessMode == syscall.O_RDONLY {
-		return 0, syscall.EBADF
-	}
 	if w, ok := f.File().(io.WriterAt); ok {
 		n, err := w.WriteAt(p, off)
 		return n, UnwrapOSError(err)
@@ -429,20 +538,27 @@ func (f *fsFile) Pwrite(p []byte, off int64) (n int, errno syscall.Errno) {
 
 // Truncate implements File.Truncate
 func (f *fsFile) Truncate(size int64) syscall.Errno {
+	if errno := f.isDirErrno(); errno != 0 {
+		return errno
+	} else if f.accessMode == syscall.O_RDONLY {
+		return syscall.EBADF
+	}
+
 	if tf, ok := f.file.(truncateFile); ok {
-		errno := UnwrapOSError(tf.Truncate(size))
-		if errno == 0 {
-			return 0
-		}
+		return UnwrapOSError(tf.Truncate(size))
+	}
+	return syscall.ENOSYS
+}
 
-		// Operating systems return different syscall.Errno instead of EISDIR
-		// double-check on any err until we can assure this per OS.
-		if isOpenDir(f) {
-			return syscall.EISDIR
-		}
+// isDirErrno returns syscall.EISDIR, if the file is a directory, or any error
+// calling IsDir.
+func (f *fsFile) isDirErrno() syscall.Errno {
+	if isDir, errno := f.IsDir(); errno != 0 {
 		return errno
+	} else if isDir {
+		return syscall.EISDIR
 	}
-	return syscall.ENOSYS
+	return 0
 }
 
 // Sync implements File.Sync
@@ -533,10 +649,3 @@ type (
 	// truncateFile is implemented by os.File in file_posix.go
 	truncateFile interface{ Truncate(size int64) error }
 )
-
-func isOpenDir(f File) bool {
-	if st, statErrno := f.Stat(); statErrno == 0 && st.Mode.IsDir() {
-		return true
-	}
-	return false
-}
diff --git a/internal/platform/nonblock_unix.go b/internal/platform/nonblock_unix.go
new file mode 100644
index 00000000..d022f025
--- /dev/null
+++ b/internal/platform/nonblock_unix.go
@@ -0,0 +1,9 @@
+//go:build !windows
+
+package platform
+
+import "syscall"
+
+func SetNonblock(fd uintptr, enable bool) error {
+	return syscall.SetNonblock(int(fd), enable)
+}
diff --git a/internal/platform/nonblock_windows.go b/internal/platform/nonblock_windows.go
new file mode 100644
index 00000000..8d0b669b
--- /dev/null
+++ b/internal/platform/nonblock_windows.go
@@ -0,0 +1,9 @@
+//go:build windows
+
+package platform
+
+import "syscall"
+
+func SetNonblock(fd uintptr, enable bool) error {
+	return syscall.SetNonblock(syscall.Handle(fd), enable)
+}
diff --git a/internal/sys/fs.go b/internal/sys/fs.go
index 66122c89..69c69e3d 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -33,30 +33,53 @@ const (
 
 const modeDevice = uint32(fs.ModeDevice | 0o640)
 
-type stdioFileWriter struct {
+type StdioFileWriter struct {
 	w io.Writer
 	s fs.FileInfo
+	// Known state of the non-blocking mode. Defaults to false which may not
+	// match the underlying state of the file descriptor if it was opened in
+	// non-blocking mode whe instantiating the module.
+	nonblock bool
 }
 
 // Stat implements fs.File
-func (w *stdioFileWriter) Stat() (fs.FileInfo, error) { return w.s, nil }
+func (w *StdioFileWriter) Stat() (fs.FileInfo, error) { return w.s, nil }
 
 // Read implements fs.File
-func (w *stdioFileWriter) Read([]byte) (n int, err error) {
+func (w *StdioFileWriter) Read([]byte) (n int, err error) {
 	return // emulate os.Stdout which returns zero
 }
 
 // Write implements io.Writer
-func (w *stdioFileWriter) Write(p []byte) (n int, err error) {
+func (w *StdioFileWriter) Write(p []byte) (n int, err error) {
 	return w.w.Write(p)
 }
 
 // Close implements fs.File
-func (w *stdioFileWriter) Close() error {
+func (w *StdioFileWriter) Close() error {
 	// Don't actually close the underlying file, as we didn't open it!
 	return nil
 }
 
+// IsNonblock returns the current known state of non-blocking mode on the
+// underlying file.
+func (w *StdioFileWriter) IsNonblock() bool {
+	return w.nonblock
+}
+
+// SetNonblock sets the file to non-blocking mode if the reader has access to
+// the underlying file descriptor.
+func (w *StdioFileWriter) SetNonblock(enable bool) error {
+	if f, ok := w.w.(*os.File); ok {
+		if err := platform.SetNonblock(f.Fd(), enable); err != nil {
+			return err
+		}
+		w.nonblock = enable
+		return nil
+	}
+	return syscall.ENOSYS
+}
+
 // StdioFilePoller is a strategy for polling a StdioFileReader for a given duration.
 // It returns true if the reader has data ready to be read, false and/or an error otherwise.
 type StdioFilePoller interface {
@@ -97,6 +120,8 @@ type StdioFileReader struct {
 	r    io.Reader
 	s    fs.FileInfo
 	poll StdioFilePoller
+	// See StdioFileWriter.
+	nonblock bool
 }
 
 // NewStdioFileReader is a constructor for StdioFileReader.
@@ -127,6 +152,25 @@ func (r *StdioFileReader) Close() error {
 	return nil
 }
 
+// IsNonblock returns the current known state of non-blocking mode on the
+// underlying file.
+func (r *StdioFileReader) IsNonblock() bool {
+	return r.nonblock
+}
+
+// SetNonblock sets the file to non-blocking mode if the reader has access to
+// the underlying file descriptor.
+func (r *StdioFileReader) SetNonblock(enable bool) error {
+	if f, ok := r.r.(*os.File); ok {
+		if err := platform.SetNonblock(f.Fd(), enable); err != nil {
+			return err
+		}
+		r.nonblock = enable
+		return nil
+	}
+	return syscall.ENOSYS
+}
+
 var (
 	noopStdinStat  = stdioFileInfo{0, modeDevice}
 	noopStdoutStat = stdioFileInfo{1, modeDevice}
@@ -170,6 +214,11 @@ func (r *lazyDir) AccessMode() int {
 	return syscall.O_RDONLY
 }
 
+// IsDir implements the same method as documented on platform.File
+func (r *lazyDir) IsDir() (bool, syscall.Errno) {
+	return true, 0
+}
+
 // Stat implements the same method as documented on platform.File
 func (r *lazyDir) Stat() (platform.Stat_t, syscall.Errno) {
 	if f, ok := r.file(); !ok {
@@ -189,6 +238,11 @@ func (r *lazyDir) Pread([]byte, int64) (int, syscall.Errno) {
 	return 0, syscall.EISDIR
 }
 
+// Seek implements File.Seek
+func (r *lazyDir) Seek(int64, int) (int64, syscall.Errno) {
+	return 0, syscall.EISDIR
+}
+
 // Write implements the same method as documented on platform.File
 func (r *lazyDir) Write([]byte) (int, syscall.Errno) {
 	return 0, syscall.EBADF
@@ -315,20 +369,17 @@ type FileEntry struct {
 type cachedStat struct {
 	// Ino is the file serial number, or zero if not available.
 	Ino uint64
-
-	// Type is the same as what's documented on platform.Dirent.
-	Type fs.FileMode
 }
 
-// CachedStat returns the cacheable parts of platform.Stat_t or an error if
-// they couldn't be retrieved.
-func (f *FileEntry) CachedStat() (ino uint64, fileType fs.FileMode, errno syscall.Errno) {
+// Inode returns the cached inode from of platform.Stat_t or an error if it
+// couldn't be retrieved.
+func (f *FileEntry) Inode() (ino uint64, errno syscall.Errno) {
 	if f.cachedStat == nil {
 		if _, errno = f.Stat(); errno != 0 {
 			return
 		}
 	}
-	return f.cachedStat.Ino, f.cachedStat.Type, 0
+	return f.cachedStat.Ino, 0
 }
 
 // Stat returns the underlying stat of this file.
@@ -343,7 +394,7 @@ func (f *FileEntry) Stat() (st platform.Stat_t, errno syscall.Errno) {
 	}
 
 	if errno == 0 {
-		f.cachedStat = &cachedStat{Ino: st.Ino, Type: st.Mode & fs.ModeType}
+		f.cachedStat = &cachedStat{Ino: st.Ino}
 	}
 	return
 }
@@ -455,7 +506,7 @@ func stdioWriter(w io.Writer, defaultStat stdioFileInfo) (*FileEntry, error) {
 		return nil, err
 	}
 	return &FileEntry{
-		Name: s.Name(), File: platform.NewFsFile("", syscall.O_WRONLY, &stdioFileWriter{w: w, s: s}),
+		Name: s.Name(), File: platform.NewFsFile("", syscall.O_WRONLY, &StdioFileWriter{w: w, s: s}),
 	}, nil
 }
 
@@ -503,10 +554,10 @@ func (c *FSContext) ReOpenDir(fd int32) (*FileEntry, syscall.Errno) {
 	f, ok := c.openedFiles.Lookup(fd)
 	if !ok {
 		return nil, syscall.EBADF
-	} else if _, ft, errno := f.CachedStat(); errno != 0 {
+	} else if isDir, errno := f.File.IsDir(); errno != 0 {
 		return nil, errno
-	} else if ft.Type() != fs.ModeDir {
-		return nil, syscall.EISDIR
+	} else if !isDir {
+		return nil, syscall.ENOTDIR
 	}
 
 	if errno := c.reopen(f); errno != 0 {
@@ -539,9 +590,9 @@ func (c *FSContext) ChangeOpenFlag(fd int32, flag int) syscall.Errno {
 	f, ok := c.LookupFile(fd)
 	if !ok {
 		return syscall.EBADF
-	} else if _, ft, errno := f.CachedStat(); errno != 0 {
+	} else if isDir, errno := f.File.IsDir(); errno != 0 {
 		return errno
-	} else if ft.Type() == fs.ModeDir {
+	} else if isDir {
 		return syscall.EISDIR
 	}
 


Test patch:
diff --git a/experimental/listener_example_test.go b/experimental/listener_example_test.go
index cef6cd9c..a468497b 100644
--- a/experimental/listener_example_test.go
+++ b/experimental/listener_example_test.go
@@ -89,19 +89,28 @@ func Example_stackIterator() {
 	it := &fakeStackIterator{}
 
 	for it.Next() {
-		fmt.Println("function:", it.FunctionDefinition().DebugName(), "args", it.Args())
+		fmt.Println("function:", it.FunctionDefinition().DebugName())
+		fmt.Println("\tparameters:", it.Parameters())
+		fmt.Println("\tsource offset:", it.SourceOffset())
 	}
 
 	// Output:
-	// function: fn0 args [1 2 3]
-	// function: fn1 args []
-	// function: fn2 args [4]
+	// function: fn0
+	// 	parameters: [1 2 3]
+	// 	source offset: 1234
+	// function: fn1
+	// 	parameters: []
+	// 	source offset: 7286
+	// function: fn2
+	// 	parameters: [4]
+	// 	source offset: 935891
 }
 
 type fakeStackIterator struct {
 	iteration int
 	def       api.FunctionDefinition
 	args      []uint64
+	offset    uint64
 }
 
 func (s *fakeStackIterator) Next() bool {
@@ -109,12 +118,15 @@ func (s *fakeStackIterator) Next() bool {
 	case 0:
 		s.def = &mockFunctionDefinition{debugName: "fn0"}
 		s.args = []uint64{1, 2, 3}
+		s.offset = 1234
 	case 1:
 		s.def = &mockFunctionDefinition{debugName: "fn1"}
 		s.args = []uint64{}
+		s.offset = 7286
 	case 2:
 		s.def = &mockFunctionDefinition{debugName: "fn2"}
 		s.args = []uint64{4}
+		s.offset = 935891
 	case 3:
 		return false
 	}
@@ -126,10 +138,16 @@ func (s *fakeStackIterator) FunctionDefinition() api.FunctionDefinition {
 	return s.def
 }
 
-func (s *fakeStackIterator) Args() []uint64 {
+func (s *fakeStackIterator) Parameters() []uint64 {
 	return s.args
 }
 
+func (s *fakeStackIterator) SourceOffset() uint64 {
+	return s.offset
+}
+
+var _ experimental.StackIterator = &fakeStackIterator{}
+
 type mockFunctionDefinition struct {
 	debugName string
 	*wasm.FunctionDefinition
diff --git a/experimental/wazerotest/wazerotest.go b/experimental/wazerotest/wazerotest.go
index 953a31b4..dba53302 100644
--- a/experimental/wazerotest/wazerotest.go
+++ b/experimental/wazerotest/wazerotest.go
@@ -647,9 +647,10 @@ func (def memoryDefinition) Max() (uint32, bool) {
 
 // StackFrame represents a frame on the call stack.
 type StackFrame struct {
-	Function api.Function
-	Params   []uint64
-	Results  []uint64
+	Function     api.Function
+	Params       []uint64
+	Results      []uint64
+	SourceOffset uint64
 }
 
 type stackIterator struct {
@@ -671,6 +672,10 @@ func (si *stackIterator) Parameters() []uint64 {
 	return si.stack[si.index].Params
 }
 
+func (si *stackIterator) SourceOffset() uint64 {
+	return si.stack[si.index].SourceOffset
+}
+
 func (si *stackIterator) reset() {
 	si.index = 0
 }
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index 02b49ad2..9c2d713c 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -218,6 +218,17 @@ func Test_fdDatasync(t *testing.T) {
 	}
 }
 
+func openPipe(t *testing.T) (*os.File, *os.File) {
+	r, w, err := os.Pipe()
+	require.NoError(t, err)
+	return r, w
+}
+
+func closePipe(r, w *os.File) {
+	r.Close()
+	w.Close()
+}
+
 func Test_fdFdstatGet(t *testing.T) {
 	file, dir := "animals.txt", "sub"
 	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fstest.FS))
@@ -261,13 +272,13 @@ func Test_fdFdstatGet(t *testing.T) {
 			fd:   sys.FdStdout,
 			expectedMemory: []byte{
 				1, 0, // fs_filetype
-				1, 0, 0, 0, 0, 0, // fs_flags
+				0, 0, 0, 0, 0, 0, // fs_flags
 				0xff, 0x1, 0xe0, 0x8, 0x0, 0x0, 0x0, 0x0, // fs_rights_base
 				0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
 			},
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_fdstat_get(fd=1)
-<== (stat={filetype=BLOCK_DEVICE,fdflags=APPEND,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
+<== (stat={filetype=BLOCK_DEVICE,fdflags=,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
 `,
 		},
 		{
@@ -275,13 +286,13 @@ func Test_fdFdstatGet(t *testing.T) {
 			fd:   sys.FdStderr,
 			expectedMemory: []byte{
 				1, 0, // fs_filetype
-				1, 0, 0, 0, 0, 0, // fs_flags
+				0, 0, 0, 0, 0, 0, // fs_flags
 				0xff, 0x1, 0xe0, 0x8, 0x0, 0x0, 0x0, 0x0, // fs_rights_base
 				0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
 			},
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_fdstat_get(fd=2)
-<== (stat={filetype=BLOCK_DEVICE,fdflags=APPEND,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
+<== (stat={filetype=BLOCK_DEVICE,fdflags=,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
 `,
 		},
 		{
@@ -365,6 +376,98 @@ func Test_fdFdstatGet(t *testing.T) {
 	}
 }
 
+func Test_fdFdstatGet_StdioNonblock(t *testing.T) {
+	stdinR, stdinW := openPipe(t)
+	defer closePipe(stdinR, stdinW)
+
+	stdoutR, stdoutW := openPipe(t)
+	defer closePipe(stdoutR, stdoutW)
+
+	stderrR, stderrW := openPipe(t)
+	defer closePipe(stderrR, stderrW)
+
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().
+		WithStdin(stdinR).
+		WithStdout(stdoutW).
+		WithStderr(stderrW))
+	defer r.Close(testCtx)
+
+	stdin, stdout, stderr := uint64(0), uint64(1), uint64(2)
+	requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stdin, uint64(wasip1.FD_NONBLOCK))
+	requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stdout, uint64(wasip1.FD_NONBLOCK))
+	requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stderr, uint64(wasip1.FD_NONBLOCK))
+	log.Reset()
+
+	tests := []struct {
+		name           string
+		fd             int32
+		resultFdstat   uint32
+		expectedMemory []byte
+		expectedErrno  wasip1.Errno
+		expectedLog    string
+	}{
+		{
+			name: "stdin",
+			fd:   sys.FdStdin,
+			expectedMemory: []byte{
+				0, 0, // fs_filetype
+				4, 0, 0, 0, 0, 0, // fs_flags
+				0xff, 0x1, 0xe0, 0x8, 0x0, 0x0, 0x0, 0x0, // fs_rights_base
+				0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
+			},
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=0)
+<== (stat={filetype=UNKNOWN,fdflags=NONBLOCK,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
+`,
+		},
+		{
+			name: "stdout",
+			fd:   sys.FdStdout,
+			expectedMemory: []byte{
+				0, 0, // fs_filetype
+				4, 0, 0, 0, 0, 0, // fs_flags
+				0xff, 0x1, 0xe0, 0x8, 0x0, 0x0, 0x0, 0x0, // fs_rights_base
+				0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
+			},
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=1)
+<== (stat={filetype=UNKNOWN,fdflags=NONBLOCK,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
+`,
+		},
+		{
+			name: "stderr",
+			fd:   sys.FdStderr,
+			expectedMemory: []byte{
+				0, 0, // fs_filetype
+				4, 0, 0, 0, 0, 0, // fs_flags
+				0xff, 0x1, 0xe0, 0x8, 0x0, 0x0, 0x0, 0x0, // fs_rights_base
+				0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
+			},
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=2)
+<== (stat={filetype=UNKNOWN,fdflags=NONBLOCK,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
+`,
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			defer log.Reset()
+
+			maskMemory(t, mod, len(tc.expectedMemory))
+
+			requireErrnoResult(t, tc.expectedErrno, mod, wasip1.FdFdstatGetName, uint64(tc.fd), uint64(tc.resultFdstat))
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
+
+			actual, ok := mod.Memory().Read(0, uint32(len(tc.expectedMemory)))
+			require.True(t, ok)
+			require.Equal(t, tc.expectedMemory, actual)
+		})
+	}
+}
+
 func Test_fdFdstatSetFlags(t *testing.T) {
 	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
 	const fileName = "file.txt"
@@ -373,8 +476,20 @@ func Test_fdFdstatSetFlags(t *testing.T) {
 	realPath := joinPath(tmpDir, fileName)
 	require.NoError(t, os.WriteFile(realPath, []byte("0123456789"), 0o600))
 
-	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(wazero.NewFSConfig().
-		WithDirMount(tmpDir, "/")))
+	stdinR, stdinW := openPipe(t)
+	defer closePipe(stdinR, stdinW)
+
+	stdoutR, stdoutW := openPipe(t)
+	defer closePipe(stdoutR, stdoutW)
+
+	stderrR, stderrW := openPipe(t)
+	defer closePipe(stderrR, stderrW)
+
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().
+		WithStdin(stdinR).
+		WithStdout(stdoutW).
+		WithStderr(stderrW).
+		WithFSConfig(wazero.NewFSConfig().WithDirMount(tmpDir, "/")))
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 	preopen := fsc.RootFS()
 	defer r.Close(testCtx)
@@ -445,13 +560,20 @@ func Test_fdFdstatSetFlags(t *testing.T) {
 	writeWazero()
 	requireFileContent("wazero6789" + "wazero" + "wazero")
 
+	t.Run("nonblock", func(t *testing.T) {
+		stdin, stdout, stderr := uint64(0), uint64(1), uint64(2)
+		requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stdin, uint64(wasip1.FD_NONBLOCK))
+		requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stdout, uint64(wasip1.FD_NONBLOCK))
+		requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stderr, uint64(wasip1.FD_NONBLOCK))
+	})
+
 	t.Run("errors", func(t *testing.T) {
 		requireErrnoResult(t, wasip1.ErrnoInval, mod, wasip1.FdFdstatSetFlagsName, uint64(fd), uint64(wasip1.FD_DSYNC))
-		requireErrnoResult(t, wasip1.ErrnoInval, mod, wasip1.FdFdstatSetFlagsName, uint64(fd), uint64(wasip1.FD_NONBLOCK))
 		requireErrnoResult(t, wasip1.ErrnoInval, mod, wasip1.FdFdstatSetFlagsName, uint64(fd), uint64(wasip1.FD_RSYNC))
 		requireErrnoResult(t, wasip1.ErrnoInval, mod, wasip1.FdFdstatSetFlagsName, uint64(fd), uint64(wasip1.FD_SYNC))
 		requireErrnoResult(t, wasip1.ErrnoBadf, mod, wasip1.FdFdstatSetFlagsName, uint64(12345), uint64(wasip1.FD_APPEND))
 		requireErrnoResult(t, wasip1.ErrnoIsdir, mod, wasip1.FdFdstatSetFlagsName, uint64(3) /* preopen */, uint64(wasip1.FD_APPEND))
+		requireErrnoResult(t, wasip1.ErrnoIsdir, mod, wasip1.FdFdstatSetFlagsName, uint64(3), uint64(wasip1.FD_NONBLOCK))
 	})
 }
 
@@ -2568,11 +2690,10 @@ func Test_fdSeek(t *testing.T) {
 			fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 			f, ok := fsc.LookupFile(fd)
 			require.True(t, ok)
-			seeker := f.File.File().(io.Seeker)
 
 			// set the initial offset of the file to 1
-			offset, err := seeker.Seek(1, io.SeekStart)
-			require.NoError(t, err)
+			offset, errno := f.File.Seek(1, io.SeekStart)
+			require.EqualErrno(t, 0, errno)
 			require.Equal(t, int64(1), offset)
 
 			requireErrnoResult(t, wasip1.ErrnoSuccess, mod, wasip1.FdSeekName, uint64(fd), uint64(tc.offset), uint64(tc.whence), uint64(resultNewoffset))
@@ -2582,8 +2703,8 @@ func Test_fdSeek(t *testing.T) {
 			require.True(t, ok)
 			require.Equal(t, tc.expectedMemory, actual)
 
-			offset, err = seeker.Seek(0, io.SeekCurrent)
-			require.NoError(t, err)
+			offset, errno = f.File.Seek(0, io.SeekCurrent)
+			require.EqualErrno(t, 0, errno)
 			require.Equal(t, tc.expectedOffset, offset) // test that the offset of file is actually updated.
 		})
 	}
@@ -2629,10 +2750,10 @@ func Test_fdSeek_Errors(t *testing.T) {
 		{
 			name:          "dir not file",
 			fd:            dirFD,
-			expectedErrno: wasip1.ErrnoBadf,
+			expectedErrno: wasip1.ErrnoIsdir,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_seek(fd=5,offset=0,whence=0)
-<== (newoffset=,errno=EBADF)
+<== (newoffset=,errno=EISDIR)
 `,
 		},
 		{
@@ -2726,11 +2847,10 @@ func Test_fdTell(t *testing.T) {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 	f, ok := fsc.LookupFile(fd)
 	require.True(t, ok)
-	seeker := f.File.File().(io.Seeker)
 
 	// set the initial offset of the file to 1
-	offset, err := seeker.Seek(1, io.SeekStart)
-	require.NoError(t, err)
+	offset, errno := f.File.Seek(1, io.SeekStart)
+	require.EqualErrno(t, 0, errno)
 	require.Equal(t, int64(1), offset)
 
 	requireErrnoResult(t, wasip1.ErrnoSuccess, mod, wasip1.FdTellName, uint64(fd), uint64(resultNewoffset))
@@ -2740,8 +2860,8 @@ func Test_fdTell(t *testing.T) {
 	require.True(t, ok)
 	require.Equal(t, expectedMemory, actual)
 
-	offset, err = seeker.Seek(0, io.SeekCurrent)
-	require.NoError(t, err)
+	offset, errno = f.File.Seek(0, io.SeekCurrent)
+	require.EqualErrno(t, 0, errno)
 	require.Equal(t, expectedOffset, offset) // test that the offset of file is actually updated.
 }
 
@@ -3823,9 +3943,9 @@ func Test_pathOpen(t *testing.T) {
 			expected: func(t *testing.T, fsc *sys.FSContext) {
 				f, ok := fsc.LookupFile(expectedOpenedFd)
 				require.True(t, ok)
-				_, ft, errno := f.CachedStat()
+				isDir, errno := f.File.IsDir()
 				require.EqualErrno(t, 0, errno)
-				require.Equal(t, fs.ModeDir, ft)
+				require.True(t, isDir)
 			},
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=dir,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
@@ -3840,9 +3960,9 @@ func Test_pathOpen(t *testing.T) {
 			expected: func(t *testing.T, fsc *sys.FSContext) {
 				f, ok := fsc.LookupFile(expectedOpenedFd)
 				require.True(t, ok)
-				_, ft, errno := f.CachedStat()
+				isDir, errno := f.File.IsDir()
 				require.EqualErrno(t, 0, errno)
-				require.Equal(t, fs.ModeDir, ft)
+				require.True(t, isDir)
 			},
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=dir,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go
index d8dbc041..46daa269 100644
--- a/internal/engine/compiler/engine_test.go
+++ b/internal/engine/compiler/engine_test.go
@@ -148,6 +148,10 @@ func TestCompiler_BeforeListenerStackIterator(t *testing.T) {
 	enginetest.RunTestModuleEngineBeforeListenerStackIterator(t, et)
 }
 
+func TestCompiler_BeforeListenerStackIteratorSourceOffset(t *testing.T) {
+	enginetest.RunTestModuleEngineStackIteratorOffset(t, et)
+}
+
 func TestCompiler_BeforeListenerGlobals(t *testing.T) {
 	enginetest.RunTestModuleEngineBeforeListenerGlobals(t, et)
 }
diff --git a/internal/engine/interpreter/interpreter_test.go b/internal/engine/interpreter/interpreter_test.go
index ef84b825..81a41f30 100644
--- a/internal/engine/interpreter/interpreter_test.go
+++ b/internal/engine/interpreter/interpreter_test.go
@@ -575,6 +575,10 @@ func TestCompiler_BeforeListenerStackIterator(t *testing.T) {
 	enginetest.RunTestModuleEngineBeforeListenerStackIterator(t, et)
 }
 
+func TestCompiler_BeforeListenerStackIteratorSourceOffset(t *testing.T) {
+	enginetest.RunTestModuleEngineStackIteratorOffset(t, et)
+}
+
 func TestCompiler_BeforeListenerGlobals(t *testing.T) {
 	enginetest.RunTestModuleEngineBeforeListenerGlobals(t, et)
 }
diff --git a/internal/platform/bench_test.go b/internal/platform/bench_test.go
index bb0fce6c..eb80e0b3 100644
--- a/internal/platform/bench_test.go
+++ b/internal/platform/bench_test.go
@@ -67,15 +67,16 @@ func BenchmarkFsFileRead(b *testing.B) {
 			for i := 0; i < b.N; i++ {
 				b.StopTimer()
 
-				// Reset the read position back to the beginning of the file.
-				if _, err = f.(io.Seeker).Seek(0, io.SeekStart); err != nil {
-					b.Fatal(err)
-				}
-
 				fs := NewFsFile(name, syscall.O_RDONLY, f)
+
 				var n int
 				var errno syscall.Errno
 
+				// Reset the read position back to the beginning of the file.
+				if _, errno = fs.Seek(0, io.SeekStart); errno != 0 {
+					b.Fatal(errno)
+				}
+
 				b.StartTimer()
 				if bc.pread {
 					n, errno = fs.Pread(buf, 3)
diff --git a/internal/platform/chown_unix_test.go b/internal/platform/chown_unix_test.go
index 32bd0728..c34d40cf 100644
--- a/internal/platform/chown_unix_test.go
+++ b/internal/platform/chown_unix_test.go
@@ -21,9 +21,8 @@ func TestChown(t *testing.T) {
 	dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
 	defer dirF.Close()
 
-	dirStat, err := dirF.File().Stat()
-	require.NoError(t, err)
-	dirSys := dirStat.Sys().(*syscall.Stat_t)
+	dirSt, errno := dirF.Stat()
+	require.EqualErrno(t, 0, errno)
 
 	// Similar to TestChown in os_unix_test.go, we can't expect to change
 	// owner unless root, and with another user. Instead, test gid.
@@ -32,13 +31,13 @@ func TestChown(t *testing.T) {
 	require.NoError(t, err)
 
 	t.Run("-1 parameters means leave alone", func(t *testing.T) {
-		require.Zero(t, Chown(dir, -1, -1))
-		checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
+		require.EqualErrno(t, 0, Chown(dir, -1, -1))
+		checkUidGid(t, dir, dirSt.Uid, dirSt.Gid)
 	})
 
 	t.Run("change gid, but not uid", func(t *testing.T) {
-		require.Zero(t, Chown(dir, -1, gid))
-		checkUidGid(t, dir, dirSys.Uid, uint32(gid))
+		require.EqualErrno(t, 0, Chown(dir, -1, gid))
+		checkUidGid(t, dir, dirSt.Uid, uint32(gid))
 	})
 
 	// Now, try any other groups of the current user.
@@ -46,12 +45,12 @@ func TestChown(t *testing.T) {
 		g := g
 		t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
 			// Test using our Chown
-			require.Zero(t, Chown(dir, -1, g))
-			checkUidGid(t, dir, dirSys.Uid, uint32(g))
+			require.EqualErrno(t, 0, Chown(dir, -1, g))
+			checkUidGid(t, dir, dirSt.Uid, uint32(g))
 
-			// Revert back with os.File.Chown
-			require.NoError(t, dirF.File().(*os.File).Chown(-1, gid))
-			checkUidGid(t, dir, dirSys.Uid, uint32(gid))
+			// Revert back
+			require.EqualErrno(t, 0, dirF.Chown(-1, gid))
+			checkUidGid(t, dir, dirSt.Uid, uint32(gid))
 		})
 	}
 
@@ -69,10 +68,8 @@ func TestDefaultFileChown(t *testing.T) {
 	dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
 	defer dirF.Close()
 
-	dirStat, err := dirF.File().Stat()
-	require.NoError(t, err)
-
-	dirSys := dirStat.Sys().(*syscall.Stat_t)
+	dirSt, errno := dirF.Stat()
+	require.EqualErrno(t, 0, errno)
 
 	// Similar to TestChownFile in os_unix_test.go, we can't expect to change
 	// owner unless root, and with another user. Instead, test gid.
@@ -81,13 +78,13 @@ func TestDefaultFileChown(t *testing.T) {
 	require.NoError(t, err)
 
 	t.Run("-1 parameters means leave alone", func(t *testing.T) {
-		require.Zero(t, dirF.Chown(-1, -1))
-		checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
+		require.EqualErrno(t, 0, dirF.Chown(-1, -1))
+		checkUidGid(t, dir, dirSt.Uid, dirSt.Gid)
 	})
 
 	t.Run("change gid, but not uid", func(t *testing.T) {
-		require.Zero(t, dirF.Chown(-1, gid))
-		checkUidGid(t, dir, dirSys.Uid, uint32(gid))
+		require.EqualErrno(t, 0, dirF.Chown(-1, gid))
+		checkUidGid(t, dir, dirSt.Uid, uint32(gid))
 	})
 
 	// Now, try any other groups of the current user.
@@ -95,17 +92,17 @@ func TestDefaultFileChown(t *testing.T) {
 		g := g
 		t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
 			// Test using our Chown
-			require.Zero(t, dirF.Chown(-1, g))
-			checkUidGid(t, dir, dirSys.Uid, uint32(g))
+			require.EqualErrno(t, 0, dirF.Chown(-1, g))
+			checkUidGid(t, dir, dirSt.Uid, uint32(g))
 
-			// Revert back with os.File.Chown
-			require.NoError(t, dirF.File().(*os.File).Chown(-1, gid))
-			checkUidGid(t, dir, dirSys.Uid, uint32(gid))
+			// Revert back
+			require.EqualErrno(t, 0, dirF.Chown(-1, gid))
+			checkUidGid(t, dir, dirSt.Uid, uint32(gid))
 		})
 	}
 
 	t.Run("closed", func(t *testing.T) {
-		require.Zero(t, dirF.Close())
+		require.EqualErrno(t, 0, dirF.Close())
 		require.EqualErrno(t, syscall.EBADF, dirF.Chown(-1, gid))
 	})
 }
@@ -119,10 +116,8 @@ func TestLchown(t *testing.T) {
 	dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
 	defer dirF.Close()
 
-	dirStat, err := dirF.File().Stat()
-	require.NoError(t, err)
-
-	dirSys := dirStat.Sys().(*syscall.Stat_t)
+	dirSt, errno := dirF.Stat()
+	require.EqualErrno(t, 0, errno)
 
 	link := path.Join(tmpDir, "link")
 	require.NoError(t, os.Symlink(dir, link))
@@ -130,10 +125,8 @@ func TestLchown(t *testing.T) {
 	linkF := openFsFile(t, link, syscall.O_RDONLY, 0)
 	defer linkF.Close()
 
-	linkStat, err := linkF.File().Stat()
-	require.NoError(t, err)
-
-	linkSys := linkStat.Sys().(*syscall.Stat_t)
+	linkSt, errno := linkF.Stat()
+	require.EqualErrno(t, 0, errno)
 
 	// Similar to TestLchown in os_unix_test.go, we can't expect to change
 	// owner unless root, and with another user. Instead, test gid.
@@ -142,15 +135,15 @@ func TestLchown(t *testing.T) {
 	require.NoError(t, err)
 
 	t.Run("-1 parameters means leave alone", func(t *testing.T) {
-		require.Zero(t, Lchown(link, -1, -1))
-		checkUidGid(t, link, linkSys.Uid, linkSys.Gid)
+		require.EqualErrno(t, 0, Lchown(link, -1, -1))
+		checkUidGid(t, link, linkSt.Uid, linkSt.Gid)
 	})
 
 	t.Run("change gid, but not uid", func(t *testing.T) {
-		require.Zero(t, Chown(dir, -1, gid))
-		checkUidGid(t, link, linkSys.Uid, uint32(gid))
+		require.EqualErrno(t, 0, Chown(dir, -1, gid))
+		checkUidGid(t, link, linkSt.Uid, uint32(gid))
 		// Make sure the target didn't change.
-		checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
+		checkUidGid(t, dir, dirSt.Uid, dirSt.Gid)
 	})
 
 	// Now, try any other groups of the current user.
@@ -158,14 +151,14 @@ func TestLchown(t *testing.T) {
 		g := g
 		t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
 			// Test using our Lchown
-			require.Zero(t, Lchown(link, -1, g))
-			checkUidGid(t, link, linkSys.Uid, uint32(g))
+			require.EqualErrno(t, 0, Lchown(link, -1, g))
+			checkUidGid(t, link, linkSt.Uid, uint32(g))
 			// Make sure the target didn't change.
-			checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
+			checkUidGid(t, dir, dirSt.Uid, dirSt.Gid)
 
-			// Revert back with syscall.Lchown
-			require.NoError(t, syscall.Lchown(link, -1, gid))
-			checkUidGid(t, link, linkSys.Uid, uint32(gid))
+			// Revert back
+			require.EqualErrno(t, 0, Lchown(link, -1, gid))
+			checkUidGid(t, link, linkSt.Uid, uint32(gid))
 		})
 	}
 
diff --git a/internal/platform/file_test.go b/internal/platform/file_test.go
index 32655d5f..bad8fed1 100644
--- a/internal/platform/file_test.go
+++ b/internal/platform/file_test.go
@@ -45,28 +45,57 @@ var embedFS embed.FS
 var (
 	//go:embed testdata
 	testdata  embed.FS
-	preadFile = "wazero.txt"
+	readFile  = "wazero.txt"
 	emptyFile = "empty.txt"
 )
 
-func TestFsFileReadAndPread(t *testing.T) {
-	embedFS, err := fs.Sub(testdata, "testdata")
-	require.NoError(t, err)
+func TestFsFileIsDir(t *testing.T) {
+	dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
 
-	f, err := embedFS.Open(preadFile)
-	require.NoError(t, err)
-	defer f.Close()
+	tests := []struct {
+		name string
+		fs   fs.FS
+	}{
+		{name: "os.DirFS", fs: dirFS},
+		{name: "embed.FS", fs: embedFS},
+		{name: "fstest.MapFS", fs: mapFS},
+	}
 
-	bytes, err := io.ReadAll(f)
-	require.NoError(t, err)
+	for _, tc := range tests {
+		tc := tc
 
-	mapFS := gofstest.MapFS{preadFile: &gofstest.MapFile{Data: bytes}}
+		t.Run(tc.name, func(t *testing.T) {
+			t.Run("file", func(t *testing.T) {
+				f, err := tc.fs.Open(readFile)
+				require.NoError(t, err)
+				defer f.Close()
 
-	// Write a file as can't open "testdata" in scratch tests because they
-	// can't read the original filesystem.
-	tmpDir := t.TempDir()
-	require.NoError(t, os.WriteFile(path.Join(tmpDir, preadFile), bytes, 0o600))
-	dirFS := os.DirFS(tmpDir)
+				fsF := NewFsFile(readFile, syscall.O_RDONLY, f)
+
+				isDir, errno := fsF.IsDir()
+				require.EqualErrno(t, 0, errno)
+				require.False(t, isDir)
+				require.Equal(t, &cachedStat{fileType: 0}, fsF.(*fsFile).cachedSt)
+			})
+
+			t.Run("dir", func(t *testing.T) {
+				f, err := tc.fs.Open(".")
+				require.NoError(t, err)
+				defer f.Close()
+
+				fsF := NewFsFile(readFile, syscall.O_RDONLY, f)
+
+				isDir, errno := fsF.IsDir()
+				require.EqualErrno(t, 0, errno)
+				require.True(t, isDir)
+				require.Equal(t, &cachedStat{fileType: fs.ModeDir}, fsF.(*fsFile).cachedSt)
+			})
+		})
+	}
+}
+
+func TestFsFileReadAndPread(t *testing.T) {
+	dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
 
 	tests := []struct {
 		name string
@@ -83,11 +112,11 @@ func TestFsFileReadAndPread(t *testing.T) {
 		tc := tc
 
 		t.Run(tc.name, func(t *testing.T) {
-			f, err := tc.fs.Open(preadFile)
+			f, err := tc.fs.Open(readFile)
 			require.NoError(t, err)
 			defer f.Close()
 
-			fs := NewFsFile(preadFile, syscall.O_RDONLY, f)
+			fs := NewFsFile(readFile, syscall.O_RDONLY, f)
 
 			// The file should be readable (base case)
 			requireRead(t, fs, buf)
@@ -124,20 +153,7 @@ func requirePread(t *testing.T, f File, buf []byte, off int64) {
 }
 
 func TestFsFileRead_empty(t *testing.T) {
-	embedFS, err := fs.Sub(testdata, "testdata")
-	require.NoError(t, err)
-
-	f, err := embedFS.Open(preadFile)
-	require.NoError(t, err)
-	defer f.Close()
-
-	mapFS := gofstest.MapFS{emptyFile: &gofstest.MapFile{}}
-
-	// Write a file as can't open "testdata" in scratch tests because they
-	// can't read the original filesystem.
-	tmpDir := t.TempDir()
-	require.NoError(t, os.WriteFile(path.Join(tmpDir, emptyFile), []byte{}, 0o600))
-	dirFS := os.DirFS(tmpDir)
+	dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
 
 	tests := []struct {
 		name string
@@ -158,7 +174,7 @@ func TestFsFileRead_empty(t *testing.T) {
 			require.NoError(t, err)
 			defer f.Close()
 
-			fs := NewFsFile(preadFile, syscall.O_RDONLY, f)
+			fs := NewFsFile(readFile, syscall.O_RDONLY, f)
 
 			t.Run("Read", func(t *testing.T) {
 				// We should be able to read an empty file
@@ -187,17 +203,190 @@ func TestFsFilePread_Unsupported(t *testing.T) {
 	// mask both io.ReaderAt and io.Seeker
 	f = struct{ fs.File }{f}
 
-	fs := NewFsFile(preadFile, syscall.O_RDONLY, f)
+	fs := NewFsFile(readFile, syscall.O_RDONLY, f)
 
 	buf := make([]byte, 3)
 	_, errno := fs.Pread(buf, 0)
 	require.EqualErrno(t, syscall.ENOSYS, errno)
 }
 
+func TestFsFileRead_Errors(t *testing.T) {
+	// Create the file
+	path := path.Join(t.TempDir(), emptyFile)
+	of, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, of.Close())
+
+	// Open the file write-only
+	flag := syscall.O_WRONLY
+	f := openFsFile(t, path, flag, 0o600)
+	defer f.Close()
+	buf := make([]byte, 5)
+
+	tests := []struct {
+		name string
+		fn   func(File) syscall.Errno
+	}{
+		{name: "Read", fn: func(f File) syscall.Errno {
+			_, errno := f.Read(buf)
+			return errno
+		}},
+		{name: "Pread", fn: func(f File) syscall.Errno {
+			_, errno := f.Pread(buf, 0)
+			return errno
+		}},
+	}
+
+	for _, tc := range tests {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			t.Run("EBADF when not open for reading", func(t *testing.T) {
+				// The descriptor exists, but not open for reading
+				errno := tc.fn(f)
+				require.EqualErrno(t, syscall.EBADF, errno)
+			})
+			testEISDIR(t, tc.fn)
+		})
+	}
+}
+
+func TestFsFileSeek(t *testing.T) {
+	dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
+
+	tests := []struct {
+		name string
+		fs   fs.FS
+	}{
+		{name: "os.DirFS", fs: dirFS},
+		{name: "embed.FS", fs: embedFS},
+		{name: "fstest.MapFS", fs: mapFS},
+	}
+
+	buf := make([]byte, 3)
+
+	for _, tc := range tests {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			f, err := tc.fs.Open(readFile)
+			require.NoError(t, err)
+			defer f.Close()
+
+			fs := NewFsFile(readFile, syscall.O_RDONLY, f)
+
+			// Shouldn't be able to use an invalid whence
+			_, errno := fs.Seek(0, io.SeekEnd+1)
+			require.EqualErrno(t, syscall.EINVAL, errno)
+			_, errno = fs.Seek(0, -1)
+			require.EqualErrno(t, syscall.EINVAL, errno)
+
+			// Shouldn't be able to seek before the file starts.
+			_, errno = fs.Seek(-1, io.SeekStart)
+			require.EqualErrno(t, syscall.EINVAL, errno)
+
+			requireRead(t, fs, buf) // read 3 bytes
+
+			// Seek to the start
+			newOffset, errno := fs.Seek(0, io.SeekStart)
+			require.EqualErrno(t, 0, errno)
+
+			// verify we can re-read from the beginning now.
+			require.Zero(t, newOffset)
+			requireRead(t, fs, buf) // read 3 bytes again
+			require.Equal(t, "waz", string(buf))
+			buf = buf[:]
+
+			// Seek to the start with zero allows you to read it back.
+			newOffset, errno = fs.Seek(0, io.SeekCurrent)
+			require.EqualErrno(t, 0, errno)
+			require.Equal(t, int64(3), newOffset)
+
+			// Seek to the last two bytes
+			newOffset, errno = fs.Seek(-2, io.SeekEnd)
+			require.EqualErrno(t, 0, errno)
+
+			// verify we can read the last two bytes
+			require.Equal(t, int64(5), newOffset)
+			n, errno := fs.Read(buf)
+			require.EqualErrno(t, 0, errno)
+			require.Equal(t, 2, n)
+			require.Equal(t, "o\n", string(buf[:2]))
+		})
+	}
+
+	seekToZero := func(f File) syscall.Errno {
+		_, errno := f.Seek(0, io.SeekStart)
+		return errno
+	}
+	testEBADFIfFileClosed(t, seekToZero)
+	testEISDIR(t, seekToZero)
+}
+
+func requireSeek(t *testing.T, f File, off int64, whence int) int64 {
+	n, errno := f.Seek(off, whence)
+	require.EqualErrno(t, 0, errno)
+	return n
+}
+
+func TestFsFileSeek_empty(t *testing.T) {
+	dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
+
+	tests := []struct {
+		name string
+		fs   fs.FS
+	}{
+		{name: "os.DirFS", fs: dirFS},
+		{name: "embed.FS", fs: embedFS},
+		{name: "fstest.MapFS", fs: mapFS},
+	}
+
+	for _, tc := range tests {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			f, err := tc.fs.Open(emptyFile)
+			require.NoError(t, err)
+			defer f.Close()
+
+			fs := NewFsFile(readFile, syscall.O_RDONLY, f)
+
+			t.Run("Start", func(t *testing.T) {
+				require.Zero(t, requireSeek(t, fs, 0, io.SeekStart))
+			})
+
+			t.Run("Current", func(t *testing.T) {
+				require.Zero(t, requireSeek(t, fs, 0, io.SeekCurrent))
+			})
+
+			t.Run("End", func(t *testing.T) {
+				require.Zero(t, requireSeek(t, fs, 0, io.SeekEnd))
+			})
+		})
+	}
+}
+
+func TestFsFileSeek_Unsupported(t *testing.T) {
+	embedFS, err := fs.Sub(testdata, "testdata")
+	require.NoError(t, err)
+
+	f, err := embedFS.Open(emptyFile)
+	require.NoError(t, err)
+	defer f.Close()
+
+	// mask io.Seeker
+	f = struct{ fs.File }{f}
+
+	fs := NewFsFile(readFile, syscall.O_RDONLY, f)
+
+	_, errno := fs.Seek(0, io.SeekCurrent)
+	require.EqualErrno(t, syscall.ENOSYS, errno)
+}
+
 func TestFsFileWriteAndPwrite(t *testing.T) {
 	// fs.FS doesn't support writes, and there is no other built-in
 	// implementation except os.File.
-	path := path.Join(t.TempDir(), preadFile)
+	path := path.Join(t.TempDir(), readFile)
 	f := openFsFile(t, path, syscall.O_RDWR|os.O_CREATE, 0o600)
 	defer f.Close()
 
@@ -291,7 +480,7 @@ func TestFsFileWrite_Unsupported(t *testing.T) {
 	embedFS, err := fs.Sub(testdata, "testdata")
 	require.NoError(t, err)
 
-	f, err := embedFS.Open(preadFile)
+	f, err := embedFS.Open(readFile)
 	require.NoError(t, err)
 	defer f.Close()
 
@@ -314,14 +503,14 @@ func TestFsFileWrite_Unsupported(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			// Use syscall.O_RDWR so that it fails due to type not flags
-			f := NewFsFile(preadFile, syscall.O_RDWR, f)
+			f := NewFsFile(readFile, syscall.O_RDWR, f)
 			_, errno := tc.fn(f, buf)
 			require.EqualErrno(t, syscall.ENOSYS, errno)
 		})
 	}
 }
 
-func TestFsFileWrite_BadFile(t *testing.T) {
+func TestFsFileWrite_Errors(t *testing.T) {
 	// Create the file
 	path := path.Join(t.TempDir(), emptyFile)
 	of, err := os.Create(path)
@@ -332,29 +521,32 @@ func TestFsFileWrite_BadFile(t *testing.T) {
 	flag := syscall.O_RDONLY
 	f := openFsFile(t, path, flag, 0o600)
 	defer f.Close()
+	buf := []byte("wazero")
 
 	tests := []struct {
 		name string
-		fn   func(File, []byte) (int, syscall.Errno)
+		fn   func(File) syscall.Errno
 	}{
-		{name: "Write", fn: func(f File, buf []byte) (int, syscall.Errno) {
-			return f.Write(buf)
+		{name: "Write", fn: func(f File) syscall.Errno {
+			_, errno := f.Write(buf)
+			return errno
 		}},
-		{name: "Pwrite", fn: func(f File, buf []byte) (int, syscall.Errno) {
-			return f.Pwrite(buf, 0)
+		{name: "Pwrite", fn: func(f File) syscall.Errno {
+			_, errno := f.Pwrite(buf, 0)
+			return errno
 		}},
 	}
 
-	buf := []byte("wazero")
-
 	for _, tc := range tests {
 		tc := tc
 
 		t.Run(tc.name, func(t *testing.T) {
-			_, errno := tc.fn(f, buf)
-
-			// The descriptor exists, but not open for writing
-			require.EqualErrno(t, syscall.EBADF, errno)
+			t.Run("EBADF when not open for writing", func(t *testing.T) {
+				// The descriptor exists, but not open for writing
+				errno := tc.fn(f)
+				require.EqualErrno(t, syscall.EBADF, errno)
+			})
+			testEISDIR(t, tc.fn)
 		})
 	}
 }
@@ -400,7 +592,7 @@ func testSync_NoError(t *testing.T, sync func(File) syscall.Errno) {
 		tc := tt
 
 		t.Run(tc.name, func(b *testing.T) {
-			require.Zero(t, sync(tc.f))
+			require.EqualErrno(t, 0, sync(tc.f))
 		})
 	}
 }
@@ -442,8 +634,8 @@ func testSync(t *testing.T, sync func(File) syscall.Errno) {
 	require.EqualErrno(t, 0, errno)
 
 	// Rewind while the file is still open.
-	_, err = f.File().(io.Seeker).Seek(0, io.SeekStart)
-	require.NoError(t, err)
+	_, errno = f.Seek(0, io.SeekStart)
+	require.EqualErrno(t, 0, errno)
 
 	// Read data from the file
 	buf := make([]byte, 50)
@@ -513,8 +705,7 @@ func TestFsFileTruncate(t *testing.T) {
 	}
 
 	if runtime.GOOS != "windows" {
-		// TODO: os.Truncate on windows can create the file even when it
-		// doesn't exist.
+		// TODO: os.Truncate on windows passes even when closed
 		testEBADFIfFileClosed(t, truncateToZero)
 	}
 
@@ -558,7 +749,7 @@ func testEBADFIfDirClosed(t *testing.T, fn func(File) syscall.Errno) bool {
 		d := openFsFile(t, t.TempDir(), syscall.O_RDONLY, 0o755)
 
 		// close the directory underneath
-		require.Zero(t, d.Close())
+		require.EqualErrno(t, 0, d.Close())
 
 		require.EqualErrno(t, syscall.EBADF, fn(d))
 	})
@@ -571,7 +762,7 @@ func testEBADFIfFileClosed(t *testing.T, fn func(File) syscall.Errno) bool {
 		f := openForWrite(t, path.Join(tmpDir, "EBADF"), []byte{1, 2, 3, 4})
 
 		// close the file underneath
-		require.Zero(t, f.Close())
+		require.EqualErrno(t, 0, f.Close())
 
 		require.EqualErrno(t, syscall.EBADF, fn(f))
 	})
@@ -596,3 +787,27 @@ func openFsFile(t *testing.T, path string, flag int, perm fs.FileMode) File {
 	require.EqualErrno(t, 0, errno)
 	return NewFsFile(path, flag, f)
 }
+
+func dirEmbedMapFS(t *testing.T, tmpDir string) (fs.FS, fs.FS, fs.FS) {
+	embedFS, err := fs.Sub(testdata, "testdata")
+	require.NoError(t, err)
+
+	f, err := embedFS.Open(readFile)
+	require.NoError(t, err)
+	defer f.Close()
+
+	bytes, err := io.ReadAll(f)
+	require.NoError(t, err)
+
+	mapFS := gofstest.MapFS{
+		emptyFile: &gofstest.MapFile{},
+		readFile:  &gofstest.MapFile{Data: bytes},
+	}
+
+	// Write a file as can't open "testdata" in scratch tests because they
+	// can't read the original filesystem.
+	require.NoError(t, os.WriteFile(path.Join(tmpDir, emptyFile), nil, 0o600))
+	require.NoError(t, os.WriteFile(path.Join(tmpDir, readFile), bytes, 0o600))
+	dirFS := os.DirFS(tmpDir)
+	return dirFS, embedFS, mapFS
+}
diff --git a/internal/platform/futimens_test.go b/internal/platform/futimens_test.go
index 29c3b40d..538649ef 100644
--- a/internal/platform/futimens_test.go
+++ b/internal/platform/futimens_test.go
@@ -170,7 +170,7 @@ func testUtimens(t *testing.T, futimes bool) {
 					f := openFsFile(t, path, flag, 0)
 
 					errno = f.Utimens(tc.times)
-					require.Zero(t, f.Close())
+					require.EqualErrno(t, 0, f.Close())
 					require.EqualErrno(t, 0, errno)
 				}
 
diff --git a/internal/platform/open_file_test.go b/internal/platform/open_file_test.go
index b8f52ae7..c93fe072 100644
--- a/internal/platform/open_file_test.go
+++ b/internal/platform/open_file_test.go
@@ -76,7 +76,7 @@ func TestOpenFile_Errors(t *testing.T) {
 		require.EqualErrno(t, syscall.EBADF, errno)
 	})
 
-	t.Run("writing to a directory is EBADF", func(t *testing.T) {
+	t.Run("writing to a directory is EISDIR", func(t *testing.T) {
 		path := path.Join(tmpDir, "diragain")
 		require.NoError(t, os.Mkdir(path, 0o755))
 
@@ -84,7 +84,7 @@ func TestOpenFile_Errors(t *testing.T) {
 		defer f.Close()
 
 		_, errno := f.Write([]byte{1, 2, 3, 4})
-		require.EqualErrno(t, syscall.EBADF, errno)
+		require.EqualErrno(t, syscall.EISDIR, errno)
 	})
 
 	// This is similar to https://github.com/WebAssembly/wasi-testsuite/blob/dc7f8d27be1030cd4788ebdf07d9b57e5d23441e/tests/rust/src/bin/dangling_symlink.rs
diff --git a/internal/platform/stat_test.go b/internal/platform/stat_test.go
index 6d22a264..4c5b6975 100644
--- a/internal/platform/stat_test.go
+++ b/internal/platform/stat_test.go
@@ -173,7 +173,7 @@ func TestStatFile(t *testing.T) {
 	// not by file descriptor.
 	if runtime.GOOS != "windows" {
 		t.Run("closed dir", func(t *testing.T) {
-			require.Zero(t, tmpDirF.Close())
+			require.EqualErrno(t, 0, tmpDirF.Close())
 			_, errno := tmpDirF.Stat()
 			require.EqualErrno(t, syscall.EBADF, errno)
 		})
@@ -193,7 +193,7 @@ func TestStatFile(t *testing.T) {
 	})
 
 	t.Run("closed fsFile", func(t *testing.T) {
-		require.Zero(t, fileF.Close())
+		require.EqualErrno(t, 0, fileF.Close())
 		_, errno := fileF.Stat()
 		require.EqualErrno(t, syscall.EBADF, errno)
 	})
@@ -213,7 +213,7 @@ func TestStatFile(t *testing.T) {
 
 	if runtime.GOOS != "windows" { // windows allows you to stat a closed dir
 		t.Run("closed subdir", func(t *testing.T) {
-			require.Zero(t, subdirF.Close())
+			require.EqualErrno(t, 0, subdirF.Close())
 			_, errno := subdirF.Stat()
 			require.EqualErrno(t, syscall.EBADF, errno)
 		})
@@ -318,12 +318,12 @@ func TestStatFile_dev_inode(t *testing.T) {
 
 	// On Windows, we cannot rename while opening.
 	// So we manually close here before renaming.
-	require.Zero(t, f1.Close())
-	require.Zero(t, f2.Close())
-	require.Zero(t, l2.Close())
+	require.EqualErrno(t, 0, f1.Close())
+	require.EqualErrno(t, 0, f2.Close())
+	require.EqualErrno(t, 0, l2.Close())
 
 	// Renaming a file shouldn't change its inodes.
-	require.Zero(t, Rename(path1, path2))
+	require.EqualErrno(t, 0, Rename(path1, path2))
 	f1 = openFsFile(t, path2, os.O_RDONLY, 0)
 	defer f1.Close()
 
@@ -361,7 +361,7 @@ func TestStat_uid_gid(t *testing.T) {
 		tmpDir := t.TempDir()
 		dir := path.Join(tmpDir, "dir")
 		require.NoError(t, os.Mkdir(dir, 0o0700))
-		require.Zero(t, chgid(dir, gid))
+		require.EqualErrno(t, 0, chgid(dir, gid))
 
 		st, errno := Stat(dir)
 		require.EqualErrno(t, 0, errno)
@@ -374,7 +374,7 @@ func TestStat_uid_gid(t *testing.T) {
 		tmpDir := t.TempDir()
 		link := path.Join(tmpDir, "link")
 		require.NoError(t, os.Symlink(tmpDir, link))
-		require.Zero(t, chgid(link, gid))
+		require.EqualErrno(t, 0, chgid(link, gid))
 
 		st, errno := Lstat(link)
 		require.EqualErrno(t, 0, errno)
@@ -387,7 +387,7 @@ func TestStat_uid_gid(t *testing.T) {
 		tmpDir := t.TempDir()
 		file := path.Join(tmpDir, "file")
 		require.NoError(t, os.WriteFile(file, nil, 0o0600))
-		require.Zero(t, chgid(file, gid))
+		require.EqualErrno(t, 0, chgid(file, gid))
 
 		st, errno := Lstat(file)
 		require.EqualErrno(t, 0, errno)
diff --git a/internal/platform/unlink_test.go b/internal/platform/unlink_test.go
index beaf631d..cdd579b1 100644
--- a/internal/platform/unlink_test.go
+++ b/internal/platform/unlink_test.go
@@ -51,7 +51,7 @@ func TestUnlink(t *testing.T) {
 
 		require.NoError(t, os.WriteFile(name, []byte{}, 0o600))
 
-		require.Zero(t, Unlink(name))
+		require.EqualErrno(t, 0, Unlink(name))
 		_, err := os.Stat(name)
 		require.Error(t, err)
 	})
diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go
index fd944013..d4d34beb 100644
--- a/internal/sys/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -20,8 +20,8 @@ import (
 
 var (
 	noopStdin  = &FileEntry{Name: "stdin", File: platform.NewFsFile("", syscall.O_RDONLY, NewStdioFileReader(eofReader{}, noopStdinStat, PollerDefaultStdin))}
-	noopStdout = &FileEntry{Name: "stdout", File: platform.NewFsFile("", syscall.O_WRONLY, &stdioFileWriter{w: io.Discard, s: noopStdoutStat})}
-	noopStderr = &FileEntry{Name: "stderr", File: platform.NewFsFile("", syscall.O_WRONLY, &stdioFileWriter{w: io.Discard, s: noopStderrStat})}
+	noopStdout = &FileEntry{Name: "stdout", File: platform.NewFsFile("", syscall.O_WRONLY, &StdioFileWriter{w: io.Discard, s: noopStdoutStat})}
+	noopStderr = &FileEntry{Name: "stderr", File: platform.NewFsFile("", syscall.O_WRONLY, &StdioFileWriter{w: io.Discard, s: noopStderrStat})}
 )
 
 //go:embed testdata
@@ -87,7 +87,7 @@ func TestNewFSContext(t *testing.T) {
 			// test to ensure that our implementation properly reuses descriptor
 			// numbers but if we were to change the reuse strategy, this test
 			// would likely break and need to be updated.
-			require.Zero(t, fsc.CloseFile(f1))
+			require.EqualErrno(t, 0, fsc.CloseFile(f1))
 			f2, errno := fsc.OpenFile(preopenedDir.FS, preopenedDir.Name, 0, 0)
 			require.EqualErrno(t, 0, errno)
 			require.Equal(t, f1, f2)
@@ -113,7 +113,7 @@ func TestFSContext_CloseFile(t *testing.T) {
 	require.EqualErrno(t, 0, errno)
 
 	// Close
-	require.Zero(t, fsc.CloseFile(fdToClose))
+	require.EqualErrno(t, 0, fsc.CloseFile(fdToClose))
 
 	// Verify fdToClose is closed and removed from the opened FDs.
 	_, ok := fsc.LookupFile(fdToClose)
@@ -127,7 +127,7 @@ func TestFSContext_CloseFile(t *testing.T) {
 		require.EqualErrno(t, syscall.EBADF, fsc.CloseFile(42)) // 42 is an arbitrary invalid FD
 	})
 	t.Run("Can close a pre-open", func(t *testing.T) {
-		require.Zero(t, fsc.CloseFile(FdPreopen))
+		require.EqualErrno(t, 0, fsc.CloseFile(FdPreopen))
 	})
 }
 
@@ -275,7 +275,7 @@ func TestFSContext_ReOpenDir(t *testing.T) {
 		require.EqualErrno(t, 0, errno)
 
 		_, errno = fsc.ReOpenDir(fd)
-		require.EqualErrno(t, syscall.EISDIR, errno)
+		require.EqualErrno(t, syscall.ENOTDIR, errno)
 	})
 }
 
@@ -301,7 +301,7 @@ func TestFSContext_Renumber(t *testing.T) {
 		prevDirFile, ok := fsc.LookupFile(fromFd)
 		require.True(t, ok)
 
-		require.Zero(t, fsc.Renumber(fromFd, toFd))
+		require.EqualErrno(t, 0, fsc.Renumber(fromFd, toFd))
 
 		renumberedDirFile, ok := fsc.LookupFile(toFd)
 		require.True(t, ok)
@@ -355,13 +355,13 @@ func TestFSContext_ChangeOpenFlag(t *testing.T) {
 	require.Equal(t, f0.openFlag&syscall.O_APPEND, 0)
 
 	// Set the APPEND flag.
-	require.Zero(t, fsc.ChangeOpenFlag(fd, syscall.O_APPEND))
+	require.EqualErrno(t, 0, fsc.ChangeOpenFlag(fd, syscall.O_APPEND))
 	f1, ok := fsc.openedFiles.Lookup(fd)
 	require.True(t, ok)
 	require.Equal(t, f1.openFlag&syscall.O_APPEND, syscall.O_APPEND)
 
 	// Remove the APPEND flag.
-	require.Zero(t, fsc.ChangeOpenFlag(fd, 0))
+	require.EqualErrno(t, 0, fsc.ChangeOpenFlag(fd, 0))
 	f2, ok := fsc.openedFiles.Lookup(fd)
 	require.True(t, ok)
 	require.Equal(t, f2.openFlag&syscall.O_APPEND, 0)
diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go
index e0b5c58e..27e30e6b 100644
--- a/internal/sys/sys_test.go
+++ b/internal/sys/sys_test.go
@@ -2,7 +2,6 @@ package sys
 
 import (
 	"bytes"
-	"io/fs"
 	"runtime"
 	"strings"
 	"testing"
@@ -67,7 +66,7 @@ func TestDefaultSysContext(t *testing.T) {
 	require.Equal(t, expectedOpenedFiles, sysCtx.FS().openedFiles)
 }
 
-func TestFileEntry_cachedStat(t *testing.T) {
+func TestFileEntryInode(t *testing.T) {
 	t.Parallel()
 
 	tmpDir := t.TempDir()
@@ -98,14 +97,13 @@ func TestFileEntry_cachedStat(t *testing.T) {
 
 			f, ok := fsc.LookupFile(FdPreopen)
 			require.True(t, ok)
-			ino, ft, errno := f.CachedStat()
+			ino, errno := f.Inode()
 			require.EqualErrno(t, 0, errno)
-			require.Equal(t, fs.ModeDir, ft)
 			if !canReadDirInode() {
 				tc.expectedIno = 0
 			}
 			require.Equal(t, tc.expectedIno, ino)
-			require.Equal(t, &cachedStat{Ino: tc.expectedIno, Type: fs.ModeDir}, f.cachedStat)
+			require.Equal(t, &cachedStat{Ino: tc.expectedIno}, f.cachedStat)
 		})
 	}
 }
diff --git a/internal/sysfs/dirfs_test.go b/internal/sysfs/dirfs_test.go
index 63a08455..dbbb0986 100644
--- a/internal/sysfs/dirfs_test.go
+++ b/internal/sysfs/dirfs_test.go
@@ -21,7 +21,7 @@ func TestNewDirFS(t *testing.T) {
 	// Guest can look up /
 	f, errno := testFS.OpenFile("/", os.O_RDONLY, 0)
 	require.EqualErrno(t, 0, errno)
-	require.Zero(t, f.Close())
+	require.EqualErrno(t, 0, f.Close())
 
 	t.Run("host path not found", func(t *testing.T) {
 		testFS := NewDirFS("a")
@@ -66,7 +66,7 @@ func TestDirFS_Lstat(t *testing.T) {
 
 	testFS := NewDirFS(tmpDir)
 	for _, path := range []string{"animals.txt", "sub", "sub-link"} {
-		require.Zero(t, testFS.Symlink(path, path+"-link"))
+		require.EqualErrno(t, 0, testFS.Symlink(path, path+"-link"))
 	}
 
 	testLstat(t, testFS)
@@ -80,7 +80,7 @@ func TestDirFS_MkDir(t *testing.T) {
 	realPath := path.Join(tmpDir, name)
 
 	t.Run("doesn't exist", func(t *testing.T) {
-		require.Zero(t, testFS.Mkdir(name, fs.ModeDir))
+		require.EqualErrno(t, 0, testFS.Mkdir(name, fs.ModeDir))
 
 		stat, err := os.Stat(realPath)
 		require.NoError(t, err)
@@ -131,12 +131,12 @@ func testChmod(t *testing.T, testFS FS, path string) {
 	requireMode(t, testFS, path, 0o444)
 
 	// Test adding write, using 0o666 not 0o600 for read-back on windows.
-	require.Zero(t, testFS.Chmod(path, 0o666))
+	require.EqualErrno(t, 0, testFS.Chmod(path, 0o666))
 	requireMode(t, testFS, path, 0o666)
 
 	if runtime.GOOS != "windows" {
 		// Test clearing group and world, setting owner read+execute.
-		require.Zero(t, testFS.Chmod(path, 0o500))
+		require.EqualErrno(t, 0, testFS.Chmod(path, 0o500))
 		requireMode(t, testFS, path, 0o500)
 	}
 }
@@ -195,7 +195,7 @@ func TestDirFS_Rename(t *testing.T) {
 		dir2 := "dir2"
 		dir2Path := path.Join(tmpDir, dir2)
 		errrno := testFS.Rename(dir1, dir2)
-		require.Zero(t, errrno)
+		require.EqualErrno(t, 0, errrno)
 
 		// Show the prior path no longer exists
 		_, err := os.Stat(dir1Path)
@@ -418,7 +418,7 @@ func TestDirFS_Rmdir(t *testing.T) {
 		name := "rmdir"
 		realPath := path.Join(tmpDir, name)
 		require.NoError(t, os.Mkdir(realPath, 0o700))
-		require.Zero(t, testFS.Rmdir(name))
+		require.EqualErrno(t, 0, testFS.Rmdir(name))
 		_, err := os.Stat(realPath)
 		require.Error(t, err)
 	})
@@ -433,11 +433,9 @@ func TestDirFS_Rmdir(t *testing.T) {
 
 		f, errno := testFS.OpenFile(name, platform.O_DIRECTORY, 0o700)
 		require.EqualErrno(t, 0, errno)
-		defer func() {
-			require.Zero(t, f.Close())
-		}()
+		defer f.Close()
 
-		require.Zero(t, testFS.Rmdir(name))
+		require.EqualErrno(t, 0, testFS.Rmdir(name))
 		_, err := os.Stat(realPath)
 		require.Error(t, err)
 	})
@@ -494,7 +492,7 @@ func TestDirFS_Unlink(t *testing.T) {
 
 		// Create a symlink to the subdirectory.
 		const symlinkName = "symlink-to-dir"
-		require.Zero(t, testFS.Symlink("subdir", symlinkName))
+		require.EqualErrno(t, 0, testFS.Symlink("subdir", symlinkName))
 
 		// Unlinking the symlink should suceed.
 		errno := testFS.Unlink(symlinkName)
@@ -510,7 +508,7 @@ func TestDirFS_Unlink(t *testing.T) {
 
 		require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
 
-		require.Zero(t, testFS.Unlink(name))
+		require.EqualErrno(t, 0, testFS.Unlink(name))
 
 		_, err := os.Stat(realPath)
 		require.Error(t, err)
diff --git a/internal/sysfs/dirfs_unix_test.go b/internal/sysfs/dirfs_unix_test.go
index 96e91bc5..3857a8ef 100644
--- a/internal/sysfs/dirfs_unix_test.go
+++ b/internal/sysfs/dirfs_unix_test.go
@@ -16,7 +16,7 @@ func TestDirFS_Chown(t *testing.T) {
 	tmpDir := t.TempDir()
 	testFS := NewDirFS(tmpDir)
 
-	require.Zero(t, testFS.Mkdir("dir", 0o0777))
+	require.EqualErrno(t, 0, testFS.Mkdir("dir", 0o0777))
 	dirF, errno := testFS.OpenFile("dir", syscall.O_RDONLY, 0)
 	require.EqualErrno(t, 0, errno)
 
@@ -32,12 +32,12 @@ func TestDirFS_Chown(t *testing.T) {
 	require.NoError(t, err)
 
 	t.Run("-1 parameters means leave alone", func(t *testing.T) {
-		require.Zero(t, testFS.Chown("dir", -1, -1))
+		require.EqualErrno(t, 0, testFS.Chown("dir", -1, -1))
 		checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, dirSys.Gid)
 	})
 
 	t.Run("change gid, but not uid", func(t *testing.T) {
-		require.Zero(t, testFS.Chown("dir", -1, gid))
+		require.EqualErrno(t, 0, testFS.Chown("dir", -1, gid))
 		checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, uint32(gid))
 	})
 
@@ -46,11 +46,11 @@ func TestDirFS_Chown(t *testing.T) {
 		g := g
 		t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
 			// Test using our Chown
-			require.Zero(t, testFS.Chown("dir", -1, g))
+			require.EqualErrno(t, 0, testFS.Chown("dir", -1, g))
 			checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, uint32(g))
 
-			// Revert back with File.Chown
-			require.Zero(t, dirF.Chown(-1, gid))
+			// Revert back
+			require.EqualErrno(t, 0, dirF.Chown(-1, gid))
 			checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, uint32(gid))
 		})
 	}
@@ -64,7 +64,7 @@ func TestDirFS_Lchown(t *testing.T) {
 	tmpDir := t.TempDir()
 	testFS := NewDirFS(tmpDir)
 
-	require.Zero(t, testFS.Mkdir("dir", 0o0777))
+	require.EqualErrno(t, 0, testFS.Mkdir("dir", 0o0777))
 	dirF, errno := testFS.OpenFile("dir", syscall.O_RDONLY, 0)
 	require.EqualErrno(t, 0, errno)
 
@@ -73,7 +73,7 @@ func TestDirFS_Lchown(t *testing.T) {
 
 	dirSys := dirStat.Sys().(*syscall.Stat_t)
 
-	require.Zero(t, testFS.Symlink("dir", "link"))
+	require.EqualErrno(t, 0, testFS.Symlink("dir", "link"))
 	linkF, errno := testFS.OpenFile("link", syscall.O_RDONLY, 0)
 	require.EqualErrno(t, 0, errno)
 
@@ -89,12 +89,12 @@ func TestDirFS_Lchown(t *testing.T) {
 	require.NoError(t, err)
 
 	t.Run("-1 parameters means leave alone", func(t *testing.T) {
-		require.Zero(t, testFS.Lchown("link", -1, -1))
+		require.EqualErrno(t, 0, testFS.Lchown("link", -1, -1))
 		checkUidGid(t, path.Join(tmpDir, "link"), linkSys.Uid, linkSys.Gid)
 	})
 
 	t.Run("change gid, but not uid", func(t *testing.T) {
-		require.Zero(t, testFS.Chown("dir", -1, gid))
+		require.EqualErrno(t, 0, testFS.Chown("dir", -1, gid))
 		checkUidGid(t, path.Join(tmpDir, "link"), linkSys.Uid, uint32(gid))
 		// Make sure the target didn't change.
 		checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, dirSys.Gid)
@@ -105,13 +105,13 @@ func TestDirFS_Lchown(t *testing.T) {
 		g := g
 		t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
 			// Test using our Lchown
-			require.Zero(t, testFS.Lchown("link", -1, g))
+			require.EqualErrno(t, 0, testFS.Lchown("link", -1, g))
 			checkUidGid(t, path.Join(tmpDir, "link"), linkSys.Uid, uint32(g))
 			// Make sure the target didn't change.
 			checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, dirSys.Gid)
 
-			// Revert back with syscall.Lchown
-			require.NoError(t, syscall.Lchown(path.Join(tmpDir, "link"), -1, gid))
+			// Revert back
+			require.EqualErrno(t, 0, testFS.Lchown("link", -1, gid))
 			checkUidGid(t, path.Join(tmpDir, "link"), linkSys.Uid, uint32(gid))
 		})
 	}
diff --git a/internal/sysfs/readfs_test.go b/internal/sysfs/readfs_test.go
index e053f335..9ffa016b 100644
--- a/internal/sysfs/readfs_test.go
+++ b/internal/sysfs/readfs_test.go
@@ -41,7 +41,7 @@ func TestReadFS_Lstat(t *testing.T) {
 
 	writeable := NewDirFS(tmpDir)
 	for _, path := range []string{"animals.txt", "sub", "sub-link"} {
-		require.Zero(t, writeable.Symlink(path, path+"-link"))
+		require.EqualErrno(t, 0, writeable.Symlink(path, path+"-link"))
 	}
 
 	testFS := NewReadFS(writeable)
diff --git a/internal/sysfs/rootfs_test.go b/internal/sysfs/rootfs_test.go
index ade6eab1..2bb7408e 100644
--- a/internal/sysfs/rootfs_test.go
+++ b/internal/sysfs/rootfs_test.go
@@ -50,7 +50,7 @@ func TestNewRootFS(t *testing.T) {
 		// Guest can look up /tmp
 		f, errno := rootFS.OpenFile("/tmp", os.O_RDONLY, 0)
 		require.EqualErrno(t, 0, errno)
-		require.Zero(t, f.Close())
+		require.EqualErrno(t, 0, f.Close())
 
 		// Guest can look up / and see "/tmp" in it
 		f, errno = rootFS.OpenFile("/", os.O_RDONLY, 0)
@@ -283,7 +283,7 @@ func TestRootFS_examples(t *testing.T) {
 			for _, p := range tc.expected {
 				f, errno := root.OpenFile(p, os.O_RDONLY, 0)
 				require.Zero(t, errno, p)
-				require.Zero(t, f.Close(), p)
+				require.EqualErrno(t, 0, f.Close(), p)
 			}
 
 			for _, p := range tc.unexpected {
diff --git a/internal/sysfs/sysfs_test.go b/internal/sysfs/sysfs_test.go
index da889feb..6d4f8a9b 100644
--- a/internal/sysfs/sysfs_test.go
+++ b/internal/sysfs/sysfs_test.go
@@ -36,7 +36,7 @@ func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS FS) {
 	require.NoError(t, err)
 	require.Equal(t, fileContents, b)
 
-	require.Zero(t, f.Close())
+	require.EqualErrno(t, 0, f.Close())
 
 	// re-create as read-only, using 0444 to allow read-back on windows.
 	require.NoError(t, os.Remove(realPath))
@@ -194,25 +194,24 @@ shark
 dinosaur
 human
 `)
-		// Ensure it implements io.ReaderAt
-		r, ok := f.File().(io.ReaderAt)
-		require.True(t, ok)
+		// Ensure it implements Pread
 		lenToRead := len(fileContents) - 1
 		buf := make([]byte, lenToRead)
-		n, err := r.ReadAt(buf, 1)
-		require.NoError(t, err)
+		n, errno := f.Pread(buf, 1)
+		require.EqualErrno(t, 0, errno)
 		require.Equal(t, lenToRead, n)
 		require.Equal(t, fileContents[1:], buf)
 
-		// Ensure it implements io.Seeker
-		s, ok := f.File().(io.Seeker)
-		require.True(t, ok)
-		offset, err := s.Seek(1, io.SeekStart)
-		require.NoError(t, err)
+		// Ensure it implements Seek
+		offset, errno := f.Seek(1, io.SeekStart)
+		require.EqualErrno(t, 0, errno)
 		require.Equal(t, int64(1), offset)
-		b, err := io.ReadAll(f.File())
-		require.NoError(t, err)
-		require.Equal(t, fileContents[1:], b)
+
+		// Read should pick up from position 1
+		n, errno = f.Read(buf)
+		require.EqualErrno(t, 0, errno)
+		require.Equal(t, lenToRead, n)
+		require.Equal(t, fileContents[1:], buf)
 	})
 
 	// Make sure O_RDONLY isn't treated bitwise as it is usually zero.
@@ -228,20 +227,20 @@ human
 
 	t.Run("writing to a read-only file is EBADF", func(t *testing.T) {
 		f, errno := testFS.OpenFile("animals.txt", os.O_RDONLY, 0)
-		defer require.Zero(t, f.Close())
+		defer f.Close()
 		require.EqualErrno(t, 0, errno)
 
-		_, err := f.Write([]byte{1, 2, 3, 4})
-		require.EqualErrno(t, syscall.EBADF, platform.UnwrapOSError(err))
+		_, errno = f.Write([]byte{1, 2, 3, 4})
+		require.EqualErrno(t, syscall.EBADF, errno)
 	})
 
-	t.Run("writing to a directory is EBADF", func(t *testing.T) {
+	t.Run("writing to a directory is EISDIR", func(t *testing.T) {
 		f, errno := testFS.OpenFile("sub", os.O_RDONLY, 0)
-		defer require.Zero(t, f.Close())
+		defer f.Close()
 		require.EqualErrno(t, 0, errno)
 
-		_, err := f.Write([]byte{1, 2, 3, 4})
-		require.EqualErrno(t, syscall.EBADF, platform.UnwrapOSError(err))
+		_, errno = f.Write([]byte{1, 2, 3, 4})
+		require.EqualErrno(t, syscall.EISDIR, errno)
 	})
 }
 
diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go
index deed42cc..6729d208 100644
--- a/internal/testing/enginetest/enginetest.go
+++ b/internal/testing/enginetest/enginetest.go
@@ -20,6 +20,7 @@ package enginetest
 
 import (
 	"context"
+	"debug/dwarf"
 	"errors"
 	"math"
 	"strings"
@@ -823,6 +824,195 @@ func (f *fnListener) After(ctx context.Context, mod api.Module, def api.Function
 	}
 }
 
+func RunTestModuleEngineStackIteratorOffset(t *testing.T, et EngineTester) {
+	e := et.NewEngine(api.CoreFeaturesV2)
+
+	type frame struct {
+		function api.FunctionDefinition
+		offset   uint64
+	}
+
+	var tape [][]frame
+
+	fnListener := &fnListener{
+		beforeFn: func(ctx context.Context, mod api.Module, def api.FunctionDefinition, paramValues []uint64, si experimental.StackIterator) context.Context {
+			var stack []frame
+			for si.Next() {
+				stack = append(stack, frame{si.FunctionDefinition(), si.SourceOffset()})
+			}
+			tape = append(tape, stack)
+			return ctx
+		},
+	}
+
+	functionTypes := []wasm.FunctionType{
+		// f1 type
+		{
+			Params:            []api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32},
+			ParamNumInUint64:  3,
+			Results:           []api.ValueType{},
+			ResultNumInUint64: 0,
+		},
+		// f2 type
+		{
+			Params:            []api.ValueType{},
+			ParamNumInUint64:  0,
+			Results:           []api.ValueType{api.ValueTypeI32},
+			ResultNumInUint64: 1,
+		},
+		// f3 type
+		{
+			Params:            []api.ValueType{api.ValueTypeI32},
+			ParamNumInUint64:  1,
+			Results:           []api.ValueType{api.ValueTypeI32},
+			ResultNumInUint64: 1,
+		},
+	}
+
+	// Minimal DWARF info section to make debug/dwarf.New() happy.
+	// Necessary to make the compiler emit source offset maps.
+	info := []byte{
+		0x7, 0x0, 0x0, 0x0, // length (len(info) - 4)
+		0x3, 0x0, // version (between 3 and 5 makes it easier)
+		0x0, 0x0, 0x0, 0x0, // abbrev offset
+		0x0, // asize
+	}
+
+	d, err := dwarf.New(nil, nil, nil, info, nil, nil, nil, nil)
+	if err != nil {
+		panic(err)
+	}
+
+	hostgofn := wasm.MustParseGoReflectFuncCode(func(x int32) int32 {
+		return x + 100
+	})
+
+	m := &wasm.Module{
+		DWARFLines:      wasmdebug.NewDWARFLines(d),
+		TypeSection:     functionTypes,
+		FunctionSection: []wasm.Index{0, 1, 2},
+		NameSection: &wasm.NameSection{
+			ModuleName: "whatever",
+			FunctionNames: wasm.NameMap{
+				{Index: wasm.Index(0), Name: "f1"},
+				{Index: wasm.Index(1), Name: "f2"},
+				{Index: wasm.Index(2), Name: "f3"},
+			},
+		},
+		GlobalSection: []wasm.Global{
+			{
+				Type: wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true},
+				Init: wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(100)},
+			},
+			{
+				Type: wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true},
+				Init: wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(200)},
+			},
+		},
+		CodeSection: []wasm.Code{
+			{ // f1
+				Body: []byte{
+					wasm.OpcodeI32Const, 42,
+					wasm.OpcodeGlobalSet, 0, // store 42 in global 0
+					wasm.OpcodeI32Const, 11,
+					wasm.OpcodeGlobalSet, 1, // store 11 in global 1
+					wasm.OpcodeI32Const, 0, // reserve return for f2
+					wasm.OpcodeCall, 1, // call f2
+					wasm.OpcodeEnd,
+				},
+			},
+			{ // f2
+				LocalTypes: []wasm.ValueType{wasm.ValueTypeI32},
+				Body: []byte{
+					wasm.OpcodeI32Const, 42, // local for f2
+					wasm.OpcodeLocalSet, 0,
+					wasm.OpcodeI32Const, 6,
+					wasm.OpcodeCall, 2, // call host function
+					wasm.OpcodeEnd,
+				},
+			},
+			// f3
+			hostgofn,
+		},
+		ExportSection: []wasm.Export{
+			{Name: "f1", Type: wasm.ExternTypeFunc, Index: 0},
+			{Name: "f2", Type: wasm.ExternTypeFunc, Index: 1},
+			{Name: "f3", Type: wasm.ExternTypeFunc, Index: 2},
+		},
+		ID: wasm.ModuleID{0},
+	}
+
+	f1offset := uint64(0)
+	f2offset := f1offset + uint64(len(m.CodeSection[0].Body))
+	f3offset := f2offset + uint64(len(m.CodeSection[1].Body))
+	m.CodeSection[0].BodyOffsetInCodeSection = f1offset
+	m.CodeSection[1].BodyOffsetInCodeSection = f2offset
+
+	m.BuildFunctionDefinitions()
+
+	listeners := buildListeners(fnListener, m)
+	err = e.CompileModule(testCtx, m, listeners, false)
+	require.NoError(t, err)
+
+	module := &wasm.ModuleInstance{
+		ModuleName:  t.Name(),
+		TypeIDs:     []wasm.FunctionTypeID{0, 1, 2},
+		Definitions: m.FunctionDefinitionSection,
+		Exports:     exportMap(m),
+		Globals: []*wasm.GlobalInstance{
+			{Val: 100, Type: wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true}},
+			{Val: 200, Type: wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true}},
+		},
+	}
+
+	me, err := e.NewModuleEngine(m, module)
+	require.NoError(t, err)
+	linkModuleToEngine(module, me)
+
+	initCallEngine := me.NewFunction(0) // f1
+	_, err = initCallEngine.Call(testCtx, 2, 3, 4)
+	require.NoError(t, err)
+
+	defs := module.ExportedFunctionDefinitions()
+	f1 := defs["f1"]
+	f2 := defs["f2"]
+	f3 := defs["f3"]
+	t.Logf("f1 offset: %#x", f1offset)
+	t.Logf("f2 offset: %#x", f2offset)
+	t.Logf("f3 offset: %#x", f3offset)
+
+	expectedStacks := [][]frame{
+		{
+			{f1, f1offset + 0},
+		},
+		{
+			{f2, f2offset + 0},
+			{f1, f1offset + 10}, // index of call opcode in f1's code
+		},
+		{
+			{f3, 0},             // host functions don't have a wasm code offset
+			{f2, f2offset + 6},  // index of call opcode in f2's code
+			{f1, f1offset + 10}, // index of call opcode in f1's code
+		},
+	}
+
+	for si, stack := range tape {
+		t.Log("Recorded stack", si, ":")
+		require.True(t, len(expectedStacks) > 0, "more recorded stacks than expected stacks")
+		expectedStack := expectedStacks[0]
+		expectedStacks = expectedStacks[1:]
+		for fi, frame := range stack {
+			t.Logf("\t%d -> %s :: %#x", fi, frame.function.Name(), frame.offset)
+			require.True(t, len(expectedStack) > 0, "more frames in stack than expected")
+			expectedFrame := expectedStack[0]
+			expectedStack = expectedStack[1:]
+			require.Equal(t, expectedFrame, frame)
+		}
+		require.Zero(t, len(expectedStack), "expected more frames in stack")
+	}
+	require.Zero(t, len(expectedStacks), "expected more stacks")
+}
+
 // RunTestModuleEngineMemory shows that the byte slice returned from api.Memory Read is not a copy, rather a re-slice
 // of the underlying memory. This allows both host and Wasm to see each other's writes, unless one side changes the
 // capacity of the slice.