Repository: tetratelabs/wazero
Pull request #1441 (2023-05-08 05:17:47)

Adds IsDir and Seek to platform.File
This adds `IsDir` and `Seek` to `platform.File`. `IsDir` is used in several places to error early if the file is the wrong type in file or directory. `Seek` replaces where we originally cast to `io.Seeker`, notably for `fd_seek`.

See #1417

codefromthecrypt: this also cleans up tests to remove most casting from `platform.File` to `fs.File` as it makes it easier to hunt down what's remaining.

Patch:
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index a96e9dfc..fe7ea8b5 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -925,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 {
@@ -1093,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.
@@ -1179,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
@@ -1677,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
 		}
@@ -1737,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
@@ -1980,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/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/sys/fs.go b/internal/sys/fs.go
index 5a61d7ea..69c69e3d 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -214,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 {
@@ -233,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
@@ -359,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.
@@ -387,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
 }
@@ -547,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 {
@@ -583,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/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index 91b681f5..9c2d713c 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -2690,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))
@@ -2704,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.
 		})
 	}
@@ -2751,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)
 `,
 		},
 		{
@@ -2848,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))
@@ -2862,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.
 }
 
@@ -3945,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=)
@@ -3962,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/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 98df0fe6..d4d34beb 100644
--- a/internal/sys/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -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)
 	})
 }