Repository: tetratelabs/wazero
Pull request #1466 (2023-05-14 22:52:37)

compiler: remove intermediary copy of compiled functions code
This PR modifies the compilation of modules to remove the intermediary buffers where the code of each function was copied before being written to the executable memory mapping.

The optimization reduces memory usage since we now only keep a single copy of the module code in memory.

Tested on the compilation of Python:
```
Type: alloc_space
Time: May 14, 2023 at 3:51pm (PDT)
Showing nodes accounting for -51.12MB, 15.15% of 337.43MB total
Dropped 11 nodes (cum <= 1.69MB)
      flat  flat%   sum%        cum   cum%
  -55.28MB 16.38% 16.38%   -51.15MB 15.16%  github.com/tetratelabs/wazero.(*runtime).CompileModule
    3.10MB  0.92% 15.46%     3.10MB  0.92%  github.com/tetratelabs/wazero/internal/wazeroir.(*Compiler).emit
    2.60MB  0.77% 14.69%     2.48MB  0.73%  github.com/tetratelabs/wazero/internal/engine/compiler.compileWasmFunction
    2.01MB   0.6% 14.10%     2.01MB   0.6%  github.com/tetratelabs/wazero/internal/engine/compiler.(*runtimeValueLocationStack).push (inline)
   -1.51MB  0.45% 14.55%    -1.51MB  0.45%  debug/dwarf.(*Data).parseAbbrev
   -1.49MB  0.44% 14.99%    -1.49MB  0.44%  github.com/tetratelabs/wazero/internal/engine/compiler.(*runtimeValueLocationStack).cloneFrom (inline)
   -0.64MB  0.19% 15.18%    -0.64MB  0.19%  github.com/tetratelabs/wazero/internal/wasm/binary.decodeCustomSection (inline)
    0.64MB  0.19% 14.99%     6.71MB  1.99%  github.com/tetratelabs/wazero/internal/engine/compiler.(*engine).CompileModule
   -0.54MB  0.16% 15.15%    -0.54MB  0.16%  github.com/tetratelabs/wazero/internal/asm/arm64.(*AssemblerImpl).encodeRelativeBranch
   -0.50MB  0.15% 15.30%    -0.50MB  0.15%  github.com/tetratelabs/wazero/internal/wasm.(*Module).validateFunctionWithMaxStackValues
   -0.50MB  0.15% 15.45%    -2.01MB  0.59%  debug/dwarf.New
    0.50MB  0.15% 15.30%     0.50MB  0.15%  github.com/tetratelabs/wazero/internal/wasm.(*Store).GetFunctionTypeID
    0.50MB  0.15% 15.15%     3.60MB  1.07%  github.com/tetratelabs/wazero/internal/wazeroir.(*Compiler).handleInstruction
```

mathetake: hah this is looking fantastic 

Patch:
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index 44543942..a7cd46d0 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -87,7 +87,7 @@ func fdAllocateFn(_ context.Context, mod api.Module, params []uint64) syscall.Er
 		return syscall.EINVAL
 	}
 
-	st, errno := f.Stat()
+	st, errno := f.File.Stat()
 	if errno != 0 {
 		return errno
 	}
@@ -199,13 +199,13 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) syscall.E
 	var errno syscall.Errno
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return syscall.EBADF
-	} else if st, errno = f.Stat(); errno != 0 {
+	} else if st, errno = f.File.Stat(); errno != 0 {
 		return errno
 	} else if isPreopenedStdio(fd, f) {
 		if f.File.IsNonblock() {
 			fdflags |= wasip1.FD_NONBLOCK
 		}
-	} else if f.IsAppend() {
+	} else if f.File.IsAppend() {
 		fdflags |= wasip1.FD_APPEND
 	}
 
@@ -311,15 +311,11 @@ func fdFdstatSetFlagsFn(_ context.Context, mod api.Module, params []uint64) sysc
 	} else if isPreopenedStdio(fd, f) {
 		nonblock := wasip1.FD_NONBLOCK&wasiFlag != 0
 		return f.File.SetNonblock(nonblock)
+	} else {
+		// For normal files, proceed to apply an append flag.
+		append := wasip1.FD_APPEND&wasiFlag != 0
+		return f.File.SetAppend(append)
 	}
-
-	// For normal files, proceed to apply an append flag.
-	var flag int
-	if wasip1.FD_APPEND&wasiFlag != 0 {
-		flag = syscall.O_APPEND
-	}
-
-	return fsc.ChangeOpenFlag(fd, flag)
 }
 
 // fdFdstatSetRights will not be implemented as rights were removed from WASI.
@@ -400,7 +396,7 @@ func fdFilestatGetFunc(mod api.Module, fd int32, resultBuf uint32) syscall.Errno
 		return syscall.EBADF
 	}
 
-	st, errno := f.Stat()
+	st, errno := f.File.Stat()
 	if errno != 0 {
 		return errno
 	}
@@ -731,12 +727,12 @@ type preader struct {
 }
 
 // Read implements the same function as documented on platform.File.
-func (w *preader) Read(p []byte) (n int, errno syscall.Errno) {
-	if len(p) == 0 {
+func (w *preader) Read(buf []byte) (n int, errno syscall.Errno) {
+	if len(buf) == 0 {
 		return 0, 0 // less overhead on zero-length reads.
 	}
 
-	n, err := w.f.Pread(p, w.offset)
+	n, err := w.f.Pread(buf, w.offset)
 	w.offset += int64(n)
 	return n, err
 }
@@ -754,7 +750,7 @@ func fdReadOrPread(mod api.Module, params []uint64, isPread bool) syscall.Errno
 	iovsCount := uint32(params[2])
 
 	var resultNread uint32
-	var reader func(p []byte) (n int, errno syscall.Errno)
+	var reader func(buf []byte) (n int, errno syscall.Errno)
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return syscall.EBADF
 	} else if f.File.AccessMode() == syscall.O_WRONLY {
@@ -834,22 +830,21 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) syscall.Err
 	}
 
 	// Validate the FD is a directory
-	rd, dir, errno := openedDir(fsc, fd)
+	f, errno := openedDir(fsc, fd)
 	if errno != 0 {
 		return errno
 	}
+	rd, dir := f.File, f.ReadDir
 
 	if cookie == 0 && dir.CountRead > 0 {
 		// This means that there was a previous call to the dir, but cookie is reset.
 		// This happens when the program calls rewinddir, for example:
 		// https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/cloudlibc/src/libc/dirent/rewinddir.c#L10-L12
-		//
-		// Since we cannot unwind fs.ReadDirFile results, we re-open while keeping the same file descriptor.
-		f, errno := fsc.ReOpenDir(fd)
-		if errno != 0 {
+		if _, errno = rd.Seek(0, io.SeekStart); errno != 0 {
 			return errno
 		}
-		rd, dir = f.File, f.ReadDir
+		f.ReadDir = &sys.ReadDir{}
+		dir = f.ReadDir
 	}
 
 	// First, determine the maximum directory entries that can be encoded as
@@ -936,7 +931,7 @@ func dotDirents(f *sys.FileEntry) ([]platform.Dirent, syscall.Errno) {
 	} else if !isDir {
 		return nil, syscall.ENOTDIR
 	}
-	dotIno, errno := f.Inode()
+	dotIno, errno := f.File.Ino()
 	if errno != 0 {
 		return nil, errno
 	}
@@ -1100,11 +1095,11 @@ func writeDirent(buf []byte, dNext uint64, ino uint64, dNamlen uint32, dType fs.
 }
 
 // openedDir returns the directory and 0 if the fd points to a readable directory.
-func openedDir(fsc *sys.FSContext, fd int32) (platform.File, *sys.ReadDir, syscall.Errno) {
+func openedDir(fsc *sys.FSContext, fd int32) (*sys.FileEntry, syscall.Errno) {
 	if f, ok := fsc.LookupFile(fd); !ok {
-		return nil, nil, syscall.EBADF
+		return nil, syscall.EBADF
 	} else if isDir, errno := f.File.IsDir(); errno != 0 {
-		return nil, nil, errno
+		return nil, errno
 	} 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,
@@ -1112,12 +1107,12 @@ func openedDir(fsc *sys.FSContext, fd int32) (platform.File, *sys.ReadDir, sysca
 		//
 		// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_readdir
 		// and https://en.wikibooks.org/wiki/C_Programming/POSIX_Reference/dirent.h
-		return nil, nil, syscall.EBADF
+		return nil, syscall.EBADF
 	} else {
 		if f.ReadDir == nil {
 			f.ReadDir = &sys.ReadDir{}
 		}
-		return f.File, f.ReadDir, 0
+		return f, 0
 	}
 }
 
@@ -1161,6 +1156,7 @@ func fdRenumberFn(_ context.Context, mod api.Module, params []uint64) syscall.Er
 //   - syscall.EFAULT: `resultNewoffset` points to an offset out of memory
 //   - syscall.EINVAL: `whence` is an invalid value
 //   - syscall.EIO: a file system error
+//   - syscall.EISDIR: the file was a directory.
 //
 // For example, if fd 3 is a file with offset 0, and parameters fd=3, offset=4,
 // whence=0 (=io.SeekStart), resultNewOffset=1, this function writes the below
@@ -1191,6 +1187,8 @@ func fdSeekFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno
 
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return syscall.EBADF
+	} else if isDir, _ := f.File.IsDir(); isDir {
+		return syscall.EISDIR // POSIX doesn't forbid seeking a directory, but wasi-testsuite does.
 	} else if newOffset, errno := f.File.Seek(int64(offset), int(whence)); errno != 0 {
 		return errno
 	} else if !mod.Memory().WriteUint64Le(resultNewoffset, uint64(newOffset)) {
@@ -1308,12 +1306,12 @@ type pwriter struct {
 }
 
 // Write implements the same function as documented on platform.File.
-func (w *pwriter) Write(p []byte) (n int, errno syscall.Errno) {
-	if len(p) == 0 {
+func (w *pwriter) Write(buf []byte) (n int, errno syscall.Errno) {
+	if len(buf) == 0 {
 		return 0, 0 // less overhead on zero-length writes.
 	}
 
-	n, err := w.f.Pwrite(p, w.offset)
+	n, err := w.f.Pwrite(buf, w.offset)
 	w.offset += int64(n)
 	return n, err
 }
@@ -1327,7 +1325,7 @@ func fdWriteOrPwrite(mod api.Module, params []uint64, isPwrite bool) syscall.Err
 	iovsCount := uint32(params[2])
 
 	var resultNwritten uint32
-	var writer func(p []byte) (n int, errno syscall.Errno)
+	var writer func(buf []byte) (n int, errno syscall.Errno)
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return syscall.EBADF
 	} else if f.File.AccessMode() == syscall.O_RDONLY {
@@ -1463,13 +1461,6 @@ func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) sysca
 	}
 
 	// Stat the file without allocating a file descriptor.
-	//
-	// Note: `preopen` is a `sysfs.FS` interface, so passing the address of `st`
-	// causes the value to escape to the heap because the compiler doesn't know
-	// whether the pointer will be retained by the method.
-	//
-	// This could be optimized by modifying Stat/Lstat to return the `Stat_t`
-	// value instead of passing a pointer as output parameter.
 	var st platform.Stat_t
 
 	if (flags & wasip1.LOOKUP_SYMLINK_FOLLOW) == 0 {
diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go
index 7ad734ec..05dbae62 100644
--- a/internal/engine/compiler/engine.go
+++ b/internal/engine/compiler/engine.go
@@ -520,12 +520,34 @@ func (e *engine) CompileModule(_ context.Context, module *wasm.Module, listeners
 		return e.addCompiledModule(module, cm, withGoFunc)
 	}
 
-	bodies := make([][]byte, localFuncs)
 	// As this uses mmap, we need to munmap on the compiled machine code when it's GCed.
 	e.setFinalizer(cm, releaseCompiledModule)
 	ln := len(listeners)
 	cmp := newCompiler()
 	asmNodes := new(asmNodes)
+
+	// The executable code is allocated in memory mappings of executableLength,
+	// and grown on demand when we exhaust the memory mapping capacity.
+	//
+	// The executableOffset variable tracks the position where the next function
+	// code will be written, and is always aligned on 16 bytes boundaries.
+	var executableOffset int
+	var executableLength int
+	var executable []byte
+
+	defer func() {
+		// At the end of the function, the executable is set on the compiled
+		// module and the local variable cleared; until then, the function owns
+		// the memory mapping and is reponsible for clearing it if it returns
+		// due to an error. Note that an error at this stage is not recoverable
+		// so we panic if we fail to unmap the memory segment.
+		if executable != nil {
+			if err := platform.MunmapCodeSegment(executable); err != nil {
+				panic(fmt.Errorf("compiler: failed to munmap code segment: %w", err))
+			}
+		}
+	}()
+
 	for i := range module.CodeSection {
 		typ := &module.TypeSection[module.FunctionSection[i]]
 		var lsn experimental.FunctionListener
@@ -534,6 +556,7 @@ func (e *engine) CompileModule(_ context.Context, module *wasm.Module, listeners
 		}
 		funcIndex := wasm.Index(i)
 		compiledFn := &cm.functions[i]
+
 		var body []byte
 		if codeSeg := &module.CodeSection[i]; codeSeg.GoFunc != nil {
 			cmp.Init(typ, nil, lsn != nil)
@@ -557,39 +580,38 @@ func (e *engine) CompileModule(_ context.Context, module *wasm.Module, listeners
 			}
 		}
 
-		// The `body` here is the view owned by assembler and will be overridden by the next iteration, so copy the body here.
-		bodyCopied := make([]byte, len(body))
-		copy(bodyCopied, body)
-		bodies[i] = bodyCopied
+		functionEndOffset := executableOffset + len(body)
+		if executableLength < functionEndOffset {
+			if executableLength == 0 {
+				executableLength = 65536
+			}
+			for executableLength < functionEndOffset {
+				executableLength *= 2
+			}
+			b, err := platform.RemapCodeSegment(executable, executableLength)
+			if err != nil {
+				return err
+			}
+			executable = b
+		}
+
+		compiledFn.executableOffset = executableOffset
 		compiledFn.listener = lsn
 		compiledFn.parent = cm
 		compiledFn.index = importedFuncs + funcIndex
-	}
 
-	var executableOffset int
-	for i, b := range bodies {
-		cm.functions[i].executableOffset = executableOffset
+		copy(executable[executableOffset:], body)
 		// Align 16-bytes boundary.
-		executableOffset = (executableOffset + len(b) + 15) &^ 15
-	}
-
-	executable, err := platform.MmapCodeSegment(executableOffset)
-	if err != nil {
-		return err
-	}
-
-	for i, b := range bodies {
-		offset := cm.functions[i].executableOffset
-		copy(executable[offset:], b)
+		executableOffset = (executableOffset + len(body) + 15) &^ 15
 	}
 
 	if runtime.GOARCH == "arm64" {
 		// On arm64, we cannot give all of rwx at the same time, so we change it to exec.
-		if err = platform.MprotectRX(executable); err != nil {
+		if err := platform.MprotectRX(executable); err != nil {
 			return err
 		}
 	}
-	cm.executable = executable
+	cm.executable, executable = executable, nil
 	return e.addCompiledModule(module, cm, withGoFunc)
 }
 
diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go
index eee324d7..ef429f70 100644
--- a/internal/gojs/fs.go
+++ b/internal/gojs/fs.go
@@ -177,7 +177,7 @@ func syscallFstat(fsc *internalsys.FSContext, fd int32) (*jsSt, error) {
 		return nil, syscall.EBADF
 	}
 
-	if st, errno := f.Stat(); errno != 0 {
+	if st, errno := f.File.Stat(); errno != 0 {
 		return nil, errno
 	} else {
 		return newJsSt(st), nil
@@ -241,15 +241,15 @@ func (jsfsRead) invoke(ctx context.Context, mod api.Module, args ...interface{})
 }
 
 // syscallRead is like syscall.Read
-func syscallRead(mod api.Module, fd int32, offset interface{}, p []byte) (n int, errno syscall.Errno) {
+func syscallRead(mod api.Module, fd int32, offset interface{}, buf []byte) (n int, errno syscall.Errno) {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return 0, syscall.EBADF
 	} else if offset != nil {
-		return f.File.Pread(p, toInt64(offset))
+		return f.File.Pread(buf, toInt64(offset))
 	} else {
-		return f.File.Read(p)
+		return f.File.Read(buf)
 	}
 }
 
@@ -284,16 +284,16 @@ func (jsfsWrite) invoke(ctx context.Context, mod api.Module, args ...interface{}
 }
 
 // syscallWrite is like syscall.Write
-func syscallWrite(mod api.Module, fd int32, offset interface{}, p []byte) (n int, errno syscall.Errno) {
+func syscallWrite(mod api.Module, fd int32, offset interface{}, buf []byte) (n int, errno syscall.Errno) {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 	if f, ok := fsc.LookupFile(fd); !ok {
 		errno = syscall.EBADF
 	} else if f.File.AccessMode() == syscall.O_RDONLY {
 		errno = syscall.EBADF
 	} else if offset != nil {
-		n, errno = f.File.Pwrite(p, toInt64(offset))
+		n, errno = f.File.Pwrite(buf, toInt64(offset))
 	} else {
-		n, errno = f.File.Write(p)
+		n, errno = f.File.Write(buf)
 	}
 	return
 }
diff --git a/internal/platform/datasync_linux.go b/internal/platform/datasync_linux.go
index 36384063..515664e3 100644
--- a/internal/platform/datasync_linux.go
+++ b/internal/platform/datasync_linux.go
@@ -3,15 +3,10 @@
 package platform
 
 import (
-	"io/fs"
+	"os"
 	"syscall"
 )
 
-func datasync(f fs.File) syscall.Errno {
-	if fd, ok := f.(fdFile); ok {
-		return UnwrapOSError(syscall.Fdatasync(int(fd.Fd())))
-	}
-
-	// Attempt to sync everything, even if we only need to sync the data.
-	return sync(f)
+func datasync(f *os.File) syscall.Errno {
+	return UnwrapOSError(syscall.Fdatasync(int(f.Fd())))
 }
diff --git a/internal/platform/datasync_unsupported.go b/internal/platform/datasync_unsupported.go
index ce752db3..9d3127fc 100644
--- a/internal/platform/datasync_unsupported.go
+++ b/internal/platform/datasync_unsupported.go
@@ -3,11 +3,11 @@
 package platform
 
 import (
-	"io/fs"
+	"os"
 	"syscall"
 )
 
-func datasync(f fs.File) syscall.Errno {
+func datasync(f *os.File) syscall.Errno {
 	// Attempt to sync everything, even if we only need to sync the data.
 	return sync(f)
 }
diff --git a/internal/platform/dir.go b/internal/platform/dir.go
index 9f2e183b..30e0b2a4 100644
--- a/internal/platform/dir.go
+++ b/internal/platform/dir.go
@@ -36,45 +36,11 @@ func (d *Dirent) IsDir() bool {
 	return d.Type == fs.ModeDir
 }
 
-func readdir(f fs.File, n int) (dirents []Dirent, errno syscall.Errno) {
-	// ^^ case format is to match POSIX and similar to os.File.Readdir
-
-	switch f := f.(type) {
-	case readdirFile:
-		fis, e := f.Readdir(n)
-		if errno = adjustReaddirErr(e); errno != 0 {
-			return
-		}
-		dirents = make([]Dirent, 0, len(fis))
-
-		// linux/darwin won't have to fan out to lstat, but windows will.
-		var ino uint64
-		for _, t := range fis {
-			if ino, errno = inoFromFileInfo(f, t); errno != 0 {
-				return
-			}
-			dirents = append(dirents, Dirent{Name: t.Name(), Ino: ino, Type: t.Mode().Type()})
-		}
-	case fs.ReadDirFile:
-		entries, e := f.ReadDir(n)
-		if errno = adjustReaddirErr(e); errno != 0 {
-			return
-		}
-		dirents = make([]Dirent, 0, len(entries))
-		for _, e := range entries {
-			// By default, we don't attempt to read inode data
-			dirents = append(dirents, Dirent{Name: e.Name(), Type: e.Type()})
-		}
-	default:
-		errno = syscall.ENOTDIR
-	}
-	return
-}
-
-func adjustReaddirErr(err error) syscall.Errno {
+func adjustReaddirErr(f File, isClosed bool, err error) syscall.Errno {
 	if err == io.EOF {
 		return 0 // e.g. Readdir on darwin returns io.EOF, but linux doesn't.
 	} else if errno := UnwrapOSError(err); errno != 0 {
+		errno = dirError(f, isClosed, errno)
 		// Ignore errors when the file was closed or removed.
 		switch errno {
 		case syscall.EIO, syscall.EBADF: // closed while open
@@ -95,6 +61,16 @@ func (DirFile) AccessMode() int {
 	return syscall.O_RDONLY
 }
 
+// IsAppend implements File.IsAppend
+func (DirFile) IsAppend() bool {
+	return false
+}
+
+// SetAppend implements File.SetAppend
+func (DirFile) SetAppend(bool) syscall.Errno {
+	return syscall.EISDIR
+}
+
 // IsNonblock implements File.IsNonblock
 func (DirFile) IsNonblock() bool {
 	return false
diff --git a/internal/platform/errno_windows.go b/internal/platform/errno_windows.go
index f335f0e3..d07e10cb 100644
--- a/internal/platform/errno_windows.go
+++ b/internal/platform/errno_windows.go
@@ -60,7 +60,9 @@ func adjustErrno(err syscall.Errno) syscall.Errno {
 	case ERROR_INVALID_HANDLE:
 		return syscall.EBADF
 	case ERROR_ACCESS_DENIED:
-		return syscall.EACCES
+		// POSIX read and write functions expect EBADF, not EACCES when not
+		// open for reading or writing.
+		return syscall.EBADF
 	case ERROR_PRIVILEGE_NOT_HELD:
 		return syscall.EPERM
 	case ERROR_NEGATIVE_SEEK, ERROR_INVALID_NAME:
diff --git a/internal/platform/file.go b/internal/platform/file.go
index 6488f58c..2ca644c2 100644
--- a/internal/platform/file.go
+++ b/internal/platform/file.go
@@ -3,6 +3,7 @@ package platform
 import (
 	"io"
 	"io/fs"
+	"os"
 	"syscall"
 	"time"
 )
@@ -28,11 +29,18 @@ import (
 // A writable filesystem abstraction is not yet implemented as of Go 1.20. See
 // https://github.com/golang/go/issues/45757
 type File interface {
-	// Path returns path used to open the file or empty if not applicable. For
-	// example, a file representing stdout will return empty.
+	// Ino returns the inode (Stat_t.Ino) of this file, zero if unknown or an
+	// error there was an error retrieving it.
 	//
-	// Note: This can drift on rename.
-	Path() string
+	// # Errors
+	//
+	// Possible errors are those from Stat, except syscall.ENOSYS should not
+	// be returned. Zero should be returned if there is no implementation.
+	//
+	// # Notes
+	//
+	//   - Some implementations implement this with a cached call to Stat.
+	Ino() (uint64, syscall.Errno)
 
 	// AccessMode returns the access mode the file was opened with.
 	//
@@ -41,6 +49,7 @@ type File interface {
 	//   - syscall.O_WRONLY: write-only, e.g. os.Stdout
 	//   - syscall.O_RDWR: read-write, e.g. os.CreateTemp
 	AccessMode() int
+	// ^-- TODO: see if we can remove this
 
 	// IsNonblock returns true if SetNonblock was successfully enabled on this
 	// file.
@@ -66,6 +75,30 @@ type File interface {
 	//     POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html
 	SetNonblock(enable bool) syscall.Errno
 
+	// IsAppend returns true if SetAppend was successfully enabled on this file.
+	//
+	// # Notes
+	//
+	//   - This might not match the underlying state of the file descriptor if
+	//     it was opened (OpenFile) in append mode.
+	IsAppend() bool
+	// ^-- TODO: We should be able to cache the open flag and remove this note.
+
+	// SetAppend toggles the append mode of this 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.
+	//
+	// # Notes
+	//
+	//   - There is no `O_APPEND` for `fcntl` in POSIX, so implementations may
+	//     have to re-open the underlying file to apply this. See
+	//     https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html
+	SetAppend(enable bool) syscall.Errno
+
 	// Stat is similar to syscall.Fstat.
 	//
 	// # Errors
@@ -88,8 +121,7 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS: the implementation does not support this function.
+	// Possible errors are those from Stat.
 	//
 	// # Notes
 	//
@@ -131,7 +163,7 @@ type File interface {
 	//     of io.ReaderAt. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/pread.html
 	//   - Unlike io.ReaderAt, there is no io.EOF returned on end-of-file. To
 	//     read the file completely, the caller must repeat until `n` is zero.
-	Pread(p []byte, off int64) (n int, errno syscall.Errno)
+	Pread(buf []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.
@@ -146,13 +178,18 @@ type File interface {
 	//   - 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.
 	//
+	// # Behavior when a directory
+	//
+	// The only supported use case for a directory is seeking to `offset` zero
+	// (`whence` = io.SeekStart). This should have the same behavior as
+	// os.File, which resets any internal state used by Readdir.
+	//
 	// # 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
 	//
@@ -214,14 +251,13 @@ 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.
+	//   - syscall.EBADF: the file was closed, not writeable, or a directory.
 	//
 	// # Notes
 	//
 	//   - This is like io.Writer and `write` in POSIX, preferring semantics of
 	//     io.Writer. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/write.html
-	Write(p []byte) (n int, errno syscall.Errno)
+	Write(buf []byte) (n int, errno syscall.Errno)
 
 	// Pwrite attempts to write all bytes in `p` to the file at the given
 	// offset `off`, and returns the count written even on error.
@@ -238,7 +274,7 @@ type File interface {
 	//
 	//   - This is like io.WriterAt and `pwrite` in POSIX, preferring semantics
 	//     of io.WriterAt. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/pwrite.html
-	Pwrite(p []byte, off int64) (n int, errno syscall.Errno)
+	Pwrite(buf []byte, off int64) (n int, errno syscall.Errno)
 
 	// Truncate truncates a file to a specified length.
 	//
@@ -361,6 +397,21 @@ type File interface {
 // This should be embedded to have forward compatible implementations.
 type UnimplementedFile struct{}
 
+// Ino implements File.Ino
+func (UnimplementedFile) Ino() (uint64, syscall.Errno) {
+	return 0, 0
+}
+
+// IsAppend implements File.IsAppend
+func (UnimplementedFile) IsAppend() bool {
+	return false
+}
+
+// SetAppend implements File.SetAppend
+func (UnimplementedFile) SetAppend(bool) syscall.Errno {
+	return syscall.ENOSYS
+}
+
 // IsNonblock implements File.IsNonblock
 func (UnimplementedFile) IsNonblock() bool {
 	return false
@@ -462,22 +513,51 @@ func NewStdioFile(stdin bool, f fs.File) (File, error) {
 	} else {
 		accessMode = syscall.O_WRONLY
 	}
-	return &stdioFile{
-		fsFile: fsFile{accessMode: accessMode, file: f},
-		st:     Stat_t{Mode: mode, Nlink: 1},
-	}, nil
+	var file File
+	if of, ok := f.(*os.File); ok {
+		// This is ok because functions that need path aren't used by stdioFile
+		file = newOsFile("", accessMode, 0, of)
+	} else {
+		file = &fsFile{accessMode: accessMode, file: f}
+	}
+	return &stdioFile{File: file, st: Stat_t{Mode: mode, Nlink: 1}}, nil
+}
+
+func OpenFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) {
+	if flag&O_DIRECTORY != 0 && flag&(syscall.O_WRONLY|syscall.O_RDWR) != 0 {
+		return nil, syscall.EISDIR // invalid to open a directory writeable
+	}
+	return openFile(path, flag, perm)
+}
+
+func OpenOSFile(path string, flag int, perm fs.FileMode) (File, syscall.Errno) {
+	f, errno := OpenFile(path, flag, perm)
+	if errno != 0 {
+		return nil, errno
+	}
+	return newOsFile(path, flag, perm, f), 0
 }
 
-func NewFsFile(openPath string, openFlag int, f fs.File) File {
+func OpenFSFile(fs fs.FS, path string, flag int, perm fs.FileMode) (File, syscall.Errno) {
+	if flag&O_DIRECTORY != 0 && flag&(syscall.O_WRONLY|syscall.O_RDWR) != 0 {
+		return nil, syscall.EISDIR // invalid to open a directory writeable
+	}
+	f, err := fs.Open(path)
+	if errno := UnwrapOSError(err); errno != 0 {
+		return nil, errno
+	}
+	// Don't return an os.File because the path is not absolute. osFile needs
+	// the path to be real and certain fs.File impls are subrooted.
 	return &fsFile{
-		path:       openPath,
-		accessMode: openFlag & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR),
+		fs:         fs,
+		name:       path,
+		accessMode: flag & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR),
 		file:       f,
-	}
+	}, 0
 }
 
 type stdioFile struct {
-	fsFile
+	File
 	st Stat_t
 }
 
@@ -496,12 +576,27 @@ func (f *stdioFile) Close() syscall.Errno {
 	return 0
 }
 
+// fsFile is used for wrapped os.File, like os.Stdin or any fs.File
+// implementation. Notably, this does not have access to the full file path.
+// so certain operations can't be supported, such as inode lookups on Windows.
 type fsFile struct {
-	path       string
+	UnimplementedFile
+
+	// fs is the file-system that opened the file, or nil when wrapped for
+	// pre-opens like stdio.
+	fs fs.FS
+
+	// name is what was used in fs for Open, so it may not be the actual path.
+	name string
+
+	// accessMode is only set when OpenFSFile opened this file.
 	accessMode int
-	file       fs.File
 
-	nonblock bool
+	// file is always set, possibly an os.File like os.Stdin.
+	file fs.File
+
+	// closed is true when closed was called. This ensures proper syscall.EBADF
+	closed bool
 
 	// cachedStat includes fields that won't change while a file is open.
 	cachedSt *cachedStat
@@ -510,22 +605,29 @@ type fsFile struct {
 type cachedStat struct {
 	// fileType is the same as what's documented on Dirent.
 	fileType fs.FileMode
+
+	// ino is the same as what's documented on Dirent.
+	ino uint64
 }
 
 // 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) {
+func (f *fsFile) cachedStat() (fileType fs.FileMode, ino uint64, errno syscall.Errno) {
 	if f.cachedSt == nil {
 		if _, errno = f.Stat(); errno != 0 {
 			return
 		}
 	}
-	return f.cachedSt.fileType, 0
+	return f.cachedSt.fileType, f.cachedSt.ino, 0
 }
 
-// Path implements File.Path
-func (f *fsFile) Path() string {
-	return f.path
+// Ino implements File.Ino
+func (f *fsFile) Ino() (uint64, syscall.Errno) {
+	if _, ino, errno := f.cachedStat(); errno != 0 {
+		return 0, errno
+	} else {
+		return ino, 0
+	}
 }
 
 // AccessMode implements File.AccessMode
@@ -533,26 +635,19 @@ func (f *fsFile) AccessMode() int {
 	return f.accessMode
 }
 
-// IsNonblock implements File.IsNonblock
-func (f *fsFile) IsNonblock() bool {
-	return f.nonblock
+// IsAppend implements File.IsAppend
+func (f *fsFile) IsAppend() bool {
+	return false
 }
 
-// SetNonblock implements File.SetNonblock
-func (f *fsFile) SetNonblock(enable bool) syscall.Errno {
-	if fd, ok := f.file.(fdFile); ok {
-		if err := setNonblock(fd.Fd(), enable); err != nil {
-			return UnwrapOSError(err)
-		}
-		f.nonblock = enable
-		return 0
-	}
-	return syscall.ENOSYS
+// SetAppend implements File.SetAppend
+func (f *fsFile) SetAppend(bool) (errno syscall.Errno) {
+	return fileError(f, f.closed, syscall.ENOSYS)
 }
 
 // IsDir implements File.IsDir
 func (f *fsFile) IsDir() (bool, syscall.Errno) {
-	if ft, errno := f.cachedStat(); errno != 0 {
+	if ft, _, errno := f.cachedStat(); errno != 0 {
 		return false, errno
 	} else if ft.Type() == fs.ModeDir {
 		return true, 0
@@ -561,52 +656,51 @@ func (f *fsFile) IsDir() (bool, syscall.Errno) {
 }
 
 // Stat implements File.Stat
-func (f *fsFile) Stat() (Stat_t, syscall.Errno) {
-	st, errno := statFile(f.file)
-	switch errno {
-	case 0:
-		f.cachedSt = &cachedStat{fileType: st.Mode & fs.ModeType}
-	case syscall.EIO:
+func (f *fsFile) Stat() (st Stat_t, errno syscall.Errno) {
+	if f.closed {
 		errno = syscall.EBADF
-	}
-	return st, errno
-}
-
-// Read implements File.Read
-func (f *fsFile) Read(p []byte) (n int, errno syscall.Errno) {
-	if len(p) == 0 {
-		return 0, 0 // less overhead on zero-length reads.
+		return
 	}
 
-	if errno = f.isDirErrno(); errno != 0 {
+	// While some functions in platform.File need the full path, especially in
+	// Windows, stat does not. Casting here allows os.DirFS to return inode
+	// information.
+	if of, ok := f.file.(*os.File); ok {
+		if st, errno = statFile(of); errno != 0 {
+			return
+		}
+		return f.cacheStat(st)
+	} else if t, err := f.file.Stat(); err != nil {
+		errno = UnwrapOSError(err)
 		return
-	} else if f.accessMode == syscall.O_WRONLY {
-		return 0, syscall.EBADF
+	} else {
+		st = statFromDefaultFileInfo(t)
+		return f.cacheStat(st)
 	}
+}
 
-	if w, ok := f.file.(io.Reader); ok {
-		n, err := w.Read(p)
-		return n, UnwrapOSError(err)
-	}
-	return 0, syscall.EBADF
+func (f *fsFile) cacheStat(st Stat_t) (Stat_t, syscall.Errno) {
+	f.cachedSt = &cachedStat{fileType: st.Mode & fs.ModeType, ino: st.Ino}
+	return st, 0
 }
 
-// Pread implements File.Pread
-func (f *fsFile) Pread(p []byte, off int64) (n int, errno syscall.Errno) {
-	if len(p) == 0 {
-		return 0, 0 // less overhead on zero-length reads.
+// Read implements File.Read
+func (f *fsFile) Read(buf []byte) (n int, errno syscall.Errno) {
+	if n, errno = read(f.file, buf); errno != 0 {
+		// Defer validation overhead until we've already had an error.
+		errno = fileError(f, f.closed, errno)
 	}
+	return
+}
 
-	if errno = f.isDirErrno(); errno != 0 {
+// Pread implements File.Pread
+func (f *fsFile) Pread(buf []byte, off int64) (n int, errno syscall.Errno) {
+	if ra, ok := f.file.(io.ReaderAt); ok {
+		if n, errno = pread(ra, buf, off); errno != 0 {
+			// Defer validation overhead until we've already had an error.
+			errno = fileError(f, f.closed, errno)
+		}
 		return
-	} else if f.accessMode == syscall.O_WRONLY {
-		return 0, syscall.EBADF
-	}
-
-	// Simple case, handle with io.ReaderAt.
-	if w, ok := f.file.(io.ReaderAt); ok {
-		n, err := w.ReadAt(p, off)
-		return n, UnwrapOSError(err)
 	}
 
 	// See /RATIONALE.md "fd_pread: io.Seeker fallback when io.ReaderAt is not supported"
@@ -614,7 +708,7 @@ func (f *fsFile) Pread(p []byte, off int64) (n int, errno syscall.Errno) {
 		// Determine the current position in the file, as we need to revert it.
 		currentOffset, err := rs.Seek(0, io.SeekCurrent)
 		if err != nil {
-			return 0, UnwrapOSError(err)
+			return 0, fileError(f, f.closed, UnwrapOSError(err))
 		}
 
 		// Put the read position back when complete.
@@ -623,173 +717,218 @@ func (f *fsFile) Pread(p []byte, off int64) (n int, errno syscall.Errno) {
 		// If the current offset isn't in sync with this reader, move it.
 		if off != currentOffset {
 			if _, err = rs.Seek(off, io.SeekStart); err != nil {
-				return 0, UnwrapOSError(err)
+				return 0, fileError(f, f.closed, UnwrapOSError(err))
 			}
 		}
 
-		n, err := rs.Read(p)
-		return n, UnwrapOSError(err)
+		n, err = rs.Read(buf)
+		if errno = UnwrapOSError(err); errno != 0 {
+			// Defer validation overhead until we've already had an error.
+			errno = fileError(f, f.closed, errno)
+		}
+	} else {
+		errno = syscall.ENOSYS // unsupported
 	}
-
-	return 0, syscall.ENOSYS // unsupported
+	return
 }
 
-// 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)
+// Seek implements File.Seek.
+func (f *fsFile) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) {
+	// If this is a directory, and we're attempting to seek to position zero,
+	// we have to re-open the file to ensure the directory state is reset.
+	var isDir bool
+	if offset == 0 && whence == io.SeekStart {
+		if isDir, errno = f.IsDir(); errno != 0 {
+			return
+		} else if isDir {
+			return 0, f.reopen()
+		}
 	}
-	return 0, syscall.ENOSYS
-}
 
-// PollRead implements File.PollRead
-func (f *fsFile) PollRead(timeout *time.Duration) (ready bool, errno syscall.Errno) {
-	if f, ok := f.file.(fdFile); ok {
-		fdSet := FdSet{}
-		fd := int(f.Fd())
-		fdSet.Set(fd)
-		nfds := fd + 1 // See https://man7.org/linux/man-pages/man2/select.2.html#:~:text=condition%20has%20occurred.-,nfds,-This%20argument%20should
-		count, err := _select(nfds, &fdSet, nil, nil, timeout)
-		return count > 0, UnwrapOSError(err)
+	if s, ok := f.file.(io.Seeker); ok {
+		if newOffset, errno = seek(s, offset, whence); errno != 0 {
+			// Defer validation overhead until we've already had an error.
+			errno = fileError(f, f.closed, errno)
+		}
+	} else {
+		errno = syscall.ENOSYS // unsupported
 	}
-	return false, syscall.ENOSYS
+	return
 }
 
-// Readdir implements File.Readdir
-func (f *fsFile) Readdir(n int) ([]Dirent, syscall.Errno) {
-	if isDir, errno := f.IsDir(); errno != 0 {
-		return nil, errno
-	} else if !isDir {
-		return nil, syscall.ENOTDIR
-	}
-	return readdir(f.file, n)
+func (f *fsFile) reopen() syscall.Errno {
+	_ = f.close()
+	var err error
+	f.file, err = f.fs.Open(f.name)
+	return UnwrapOSError(err)
 }
 
-// Write implements File.Write
-func (f *fsFile) Write(p []byte) (n int, errno syscall.Errno) {
-	if errno = f.isDirErrno(); errno != 0 {
+// Readdir implements File.Readdir. Notably, this uses fs.ReadDirFile if
+// available.
+func (f *fsFile) Readdir(n int) (dirents []Dirent, errno syscall.Errno) {
+	if of, ok := f.file.(*os.File); ok {
+		// We can't use f.name here because it is the path up to the fs.FS, not
+		// necessarily the real path. For this reason, Windows may not be able
+		// to populate inodes. However, Darwin and Linux will.
+		if dirents, errno = readdir(of, "", n); errno != 0 {
+			errno = adjustReaddirErr(f, f.closed, errno)
+		}
 		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.
+	// Try with fs.ReadDirFile which is available on fs.FS implementations
+	// like embed:fs.
+	if rdf, ok := f.file.(fs.ReadDirFile); ok {
+		entries, e := rdf.ReadDir(n)
+		if errno = adjustReaddirErr(f, f.closed, e); errno != 0 {
+			return
+		}
+		dirents = make([]Dirent, 0, len(entries))
+		for _, e := range entries {
+			// By default, we don't attempt to read inode data
+			dirents = append(dirents, Dirent{Name: e.Name(), Type: e.Type()})
+		}
+	} else {
+		errno = syscall.ENOTDIR
 	}
+	return
+}
+
+// Write implements File.Write
+func (f *fsFile) Write(buf []byte) (n int, errno syscall.Errno) {
 	if w, ok := f.file.(io.Writer); ok {
-		n, err := w.Write(p)
-		return n, UnwrapOSError(err)
+		if n, errno = write(w, buf); errno != 0 {
+			// Defer validation overhead until we've already had an error.
+			errno = fileError(f, f.closed, errno)
+		}
+	} else {
+		errno = syscall.ENOSYS // unsupported
 	}
-	return 0, syscall.ENOSYS // unsupported
+	return
 }
 
 // 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
+func (f *fsFile) Pwrite(buf []byte, off int64) (n int, errno syscall.Errno) {
+	if wa, ok := f.file.(io.WriterAt); ok {
+		if n, errno = pwrite(wa, buf, off); errno != 0 {
+			// Defer validation overhead until we've already had an error.
+			errno = fileError(f, f.closed, errno)
+		}
+	} else {
+		errno = syscall.ENOSYS // unsupported
 	}
+	return
+}
 
-	if len(p) == 0 {
-		return 0, 0 // less overhead on zero-length writes.
+// Close implements File.Close
+func (f *fsFile) Close() syscall.Errno {
+	if f.closed {
+		return 0
 	}
+	f.closed = true
+	return f.close()
+}
 
-	if w, ok := f.file.(io.WriterAt); ok {
-		n, err := w.WriteAt(p, off)
-		return n, UnwrapOSError(err)
-	}
-	return 0, syscall.ENOSYS // unsupported
+func (f *fsFile) close() syscall.Errno {
+	return UnwrapOSError(f.file.Close())
 }
 
-// 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
+// dirError is used for commands that work against a directory, but not a file.
+func dirError(f File, isClosed bool, errno syscall.Errno) syscall.Errno {
+	if vErrno := validate(f, isClosed, false, true); vErrno != 0 {
+		return vErrno
 	}
+	return errno
+}
 
-	if tf, ok := f.file.(truncateFile); ok {
-		return UnwrapOSError(tf.Truncate(size))
+// fileError is used for commands that work against a file, but not a directory.
+func fileError(f File, isClosed bool, errno syscall.Errno) syscall.Errno {
+	if vErrno := validate(f, isClosed, true, false); vErrno != 0 {
+		return vErrno
 	}
-	return syscall.ENOSYS
+	return errno
 }
 
-// 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 {
+// validate is used to making syscalls which will fail.
+func validate(f File, isClosed, wantFile, wantDir bool) syscall.Errno {
+	if isClosed {
+		return syscall.EBADF
+	}
+
+	isDir, errno := f.IsDir()
+	if errno != 0 {
 		return errno
-	} else if isDir {
+	}
+
+	if wantFile && isDir {
 		return syscall.EISDIR
+	} else if wantDir && !isDir {
+		return syscall.ENOTDIR
 	}
 	return 0
 }
 
-// Sync implements File.Sync
-func (f *fsFile) Sync() syscall.Errno {
-	return sync(f.file)
-}
+func read(r io.Reader, buf []byte) (n int, errno syscall.Errno) {
+	if len(buf) == 0 {
+		return 0, 0 // less overhead on zero-length reads.
+	}
 
-// Datasync implements File.Datasync
-func (f *fsFile) Datasync() syscall.Errno {
-	return datasync(f.file)
+	n, err := r.Read(buf)
+	return n, UnwrapOSError(err)
 }
 
-// Chmod implements File.Chmod
-func (f *fsFile) Chmod(mode fs.FileMode) syscall.Errno {
-	if f, ok := f.file.(chmodFile); ok {
-		return UnwrapOSError(f.Chmod(mode))
+func pread(ra io.ReaderAt, buf []byte, off int64) (n int, errno syscall.Errno) {
+	if len(buf) == 0 {
+		return 0, 0 // less overhead on zero-length reads.
 	}
-	return syscall.ENOSYS
+
+	n, err := ra.ReadAt(buf, off)
+	return n, UnwrapOSError(err)
 }
 
-// Chown implements File.Chown
-func (f *fsFile) Chown(uid, gid int) syscall.Errno {
-	if f, ok := f.file.(fdFile); ok {
-		return fchown(f.Fd(), uid, gid)
+func seek(s io.Seeker, offset int64, whence int) (int64, syscall.Errno) {
+	if uint(whence) > io.SeekEnd {
+		return 0, syscall.EINVAL // negative or exceeds the largest valid whence
 	}
-	return syscall.ENOSYS
+
+	newOffset, err := s.Seek(offset, whence)
+	return newOffset, UnwrapOSError(err)
 }
 
-// Utimens implements File.Utimens
-func (f *fsFile) Utimens(times *[2]syscall.Timespec) syscall.Errno {
-	if f, ok := f.file.(fdFile); ok {
-		err := futimens(f.Fd(), times)
-		return UnwrapOSError(err)
+func readdir(f *os.File, path string, n int) (dirents []Dirent, errno syscall.Errno) {
+	fis, e := f.Readdir(n)
+	if errno = UnwrapOSError(e); errno != 0 {
+		return
 	}
-	return syscall.ENOSYS
+
+	dirents = make([]Dirent, 0, len(fis))
+
+	// linux/darwin won't have to fan out to lstat, but windows will.
+	var ino uint64
+	for fi := range fis {
+		t := fis[fi]
+		if ino, errno = inoFromFileInfo(path, t); errno != 0 {
+			return
+		}
+		dirents = append(dirents, Dirent{Name: t.Name(), Ino: ino, Type: t.Mode().Type()})
+	}
+	return
 }
 
-// Close implements File.Close
-func (f *fsFile) Close() syscall.Errno {
-	return UnwrapOSError(f.file.Close())
+func write(w io.Writer, buf []byte) (n int, errno syscall.Errno) {
+	if len(buf) == 0 {
+		return 0, 0 // less overhead on zero-length writes.
+	}
+
+	n, err := w.Write(buf)
+	return n, UnwrapOSError(err)
 }
 
-// The following interfaces are used until we finalize our own FD-scoped file.
-type (
-	// PathFile is implemented on files that retain the path to their pre-open.
-	PathFile interface {
-		Path() string
-	}
-	// fdFile is implemented by os.File in file_unix.go and file_windows.go
-	fdFile interface{ Fd() (fd uintptr) }
-	// readdirFile is implemented by os.File in dir.go
-	readdirFile interface {
-		Readdir(n int) ([]fs.FileInfo, error)
-	}
-	// chmodFile is implemented by os.File in file_posix.go
-	chmodFile interface{ Chmod(fs.FileMode) error }
-	// syncFile is implemented by os.File in file_posix.go
-	syncFile interface{ Sync() error }
-	// truncateFile is implemented by os.File in file_posix.go
-	truncateFile interface{ Truncate(size int64) error }
-)
+func pwrite(w io.WriterAt, buf []byte, off int64) (n int, errno syscall.Errno) {
+	if len(buf) == 0 {
+		return 0, 0 // less overhead on zero-length writes.
+	}
+
+	n, err := w.WriteAt(buf, off)
+	return n, UnwrapOSError(err)
+}
diff --git a/internal/platform/mremap_linux.go b/internal/platform/mremap_linux.go
new file mode 100644
index 00000000..77bfec41
--- /dev/null
+++ b/internal/platform/mremap_linux.go
@@ -0,0 +1,43 @@
+package platform
+
+import (
+	"syscall"
+	"unsafe"
+)
+
+const (
+	__MREMAP_MAYMOVE = 1
+)
+
+func remapCodeSegmentAMD64(code []byte, size int) ([]byte, error) {
+	return remapCodeSegment(code, size)
+}
+
+func remapCodeSegmentARM64(code []byte, size int) ([]byte, error) {
+	return remapCodeSegment(code, size)
+}
+
+func remapCodeSegment(code []byte, size int) ([]byte, error) {
+	p, err := mremap(*(*unsafe.Pointer)(unsafe.Pointer(&code)), len(code), size, __MREMAP_MAYMOVE)
+	if err != nil {
+		return nil, err
+	}
+	return unsafe.Slice((*byte)(p), size), nil
+}
+
+//go:nosplit
+func mremap(oldAddr unsafe.Pointer, oldSize, newSize, flags int) (unsafe.Pointer, error) {
+	p, _, err := syscall.Syscall6(
+		syscall.SYS_MREMAP,
+		uintptr(oldAddr),
+		uintptr(oldSize),
+		uintptr(newSize),
+		uintptr(flags),
+		uintptr(0),
+		uintptr(0),
+	)
+	if err != 0 {
+		return nil, err
+	}
+	return unsafe.Pointer(p), nil
+}
diff --git a/internal/platform/mremap_other.go b/internal/platform/mremap_other.go
new file mode 100644
index 00000000..02c8fcb7
--- /dev/null
+++ b/internal/platform/mremap_other.go
@@ -0,0 +1,23 @@
+//go:build !linux
+
+package platform
+
+func remapCodeSegmentAMD64(code []byte, size int) ([]byte, error) {
+	b, err := mmapCodeSegmentAMD64(size)
+	if err != nil {
+		return nil, err
+	}
+	copy(b, code)
+	munmapCodeSegment(code)
+	return b, nil
+}
+
+func remapCodeSegmentARM64(code []byte, size int) ([]byte, error) {
+	b, err := mmapCodeSegmentARM64(size)
+	if err != nil {
+		return nil, err
+	}
+	copy(b, code)
+	munmapCodeSegment(code)
+	return b, nil
+}
diff --git a/internal/platform/open_file.go b/internal/platform/open_file.go
index 6fb3f5b7..fc68986f 100644
--- a/internal/platform/open_file.go
+++ b/internal/platform/open_file.go
@@ -13,11 +13,16 @@ import (
 const (
 	O_DIRECTORY = syscall.O_DIRECTORY
 	O_NOFOLLOW  = syscall.O_NOFOLLOW
+	O_NONBLOCK  = syscall.O_NONBLOCK
 )
 
+func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) File {
+	return newDefaultOsFile(openPath, openFlag, openPerm, f)
+}
+
 // OpenFile is like os.OpenFile except it returns syscall.Errno. A zero
 // syscall.Errno is success.
-func OpenFile(path string, flag int, perm fs.FileMode) (fs.File, syscall.Errno) {
+func openFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) {
 	f, err := os.OpenFile(path, flag, perm)
 	// Note: This does not return a platform.File because sysfs.FS that returns
 	// one may want to hide the real OS path. For example, this is needed for
diff --git a/internal/platform/open_file_js.go b/internal/platform/open_file_js.go
index 5a45038f..e4389a13 100644
--- a/internal/platform/open_file_js.go
+++ b/internal/platform/open_file_js.go
@@ -10,9 +10,14 @@ import (
 const (
 	O_DIRECTORY = 1 << 29
 	O_NOFOLLOW  = 1 << 30
+	O_NONBLOCK  = 1 << 31
 )
 
-func OpenFile(path string, flag int, perm fs.FileMode) (fs.File, syscall.Errno) {
+func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) File {
+	return newDefaultOsFile(openPath, openFlag, openPerm, f)
+}
+
+func openFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) {
 	flag &= ^(O_DIRECTORY | O_NOFOLLOW) // erase placeholders
 	f, err := os.OpenFile(path, flag, perm)
 	return f, UnwrapOSError(err)
diff --git a/internal/platform/open_file_sun.go b/internal/platform/open_file_sun.go
index 791072ed..9714cc1b 100644
--- a/internal/platform/open_file_sun.go
+++ b/internal/platform/open_file_sun.go
@@ -12,9 +12,14 @@ const (
 	// See https://github.com/illumos/illumos-gate/blob/edd580643f2cf1434e252cd7779e83182ea84945/usr/src/uts/common/sys/fcntl.h#L90
 	O_DIRECTORY = 0x1000000
 	O_NOFOLLOW  = syscall.O_NOFOLLOW
+	O_NONBLOCK  = syscall.O_NONBLOCK
 )
 
-func OpenFile(path string, flag int, perm fs.FileMode) (fs.File, syscall.Errno) {
+func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) File {
+	return newDefaultOsFile(openPath, openFlag, openPerm, f)
+}
+
+func openFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) {
 	f, err := os.OpenFile(path, flag, perm)
 	return f, UnwrapOSError(err)
 }
diff --git a/internal/platform/open_file_windows.go b/internal/platform/open_file_windows.go
index 16d60979..96ab2711 100644
--- a/internal/platform/open_file_windows.go
+++ b/internal/platform/open_file_windows.go
@@ -26,14 +26,12 @@ import (
 const (
 	O_DIRECTORY = 1 << 29
 	O_NOFOLLOW  = 1 << 30
+	O_NONBLOCK  = syscall.O_NONBLOCK
 )
 
-func OpenFile(path string, flag int, perm fs.FileMode) (fs.File, syscall.Errno) {
-	if f, errno := openFile(path, flag, perm); errno != 0 {
-		return nil, errno
-	} else { // TODO: revisit windowsWrappedFile once fsFile is complete
-		f := &windowsWrappedFile{osFile: f, path: path, flag: flag, perm: perm}
-		return f, 0
+func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) File {
+	return &windowsOsFile{
+		osFile: osFile{path: openPath, flag: openFlag, perm: openPerm, file: f},
 	}
 }
 
@@ -174,3 +172,46 @@ func open(path string, mode int, perm uint32) (fd syscall.Handle, err error) {
 	h, e := syscall.CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
 	return h, e
 }
+
+// windowsOsFile overrides osFile to special case directory handling in Windows.
+type windowsOsFile struct {
+	osFile
+
+	dirInitialized bool
+}
+
+// Readdir implements File.Readdir
+func (f *windowsOsFile) Readdir(n int) (dirents []Dirent, errno syscall.Errno) {
+	if errno = f.maybeInitDir(); errno != 0 {
+		return
+	}
+
+	return f.osFile.Readdir(n)
+}
+
+func (f *windowsOsFile) maybeInitDir() syscall.Errno {
+	if f.dirInitialized {
+		return 0
+	}
+
+	if isDir, errno := f.IsDir(); errno != 0 {
+		return errno
+	} else if !isDir {
+		return syscall.ENOTDIR
+	}
+
+	// On Windows, once the directory is opened, changes to the directory are
+	// not visible on ReadDir on that already-opened file handle.
+	//
+	// To provide consistent behavior with other platforms, we re-open it.
+	if errno := f.osFile.Close(); errno != 0 {
+		return errno
+	}
+	newW, errno := openFile(f.path, f.flag, f.perm)
+	if errno != 0 {
+		return errno
+	}
+	f.osFile.file = newW
+	f.dirInitialized = true
+	return 0
+}
diff --git a/internal/platform/osfile.go b/internal/platform/osfile.go
new file mode 100644
index 00000000..674272a4
--- /dev/null
+++ b/internal/platform/osfile.go
@@ -0,0 +1,261 @@
+package platform
+
+import (
+	"io"
+	"io/fs"
+	"os"
+	"syscall"
+	"time"
+)
+
+func newDefaultOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) File {
+	return &osFile{path: openPath, flag: openFlag, perm: openPerm, file: f}
+}
+
+// osFile is a file opened with this package, and uses os.File or syscalls to
+// implement platform.File.
+type osFile struct {
+	path string
+	flag int
+	perm fs.FileMode
+	file *os.File
+
+	// closed is true when closed was called. This ensures proper syscall.EBADF
+	closed bool
+
+	// cachedStat includes fields that won't change while a file is open.
+	cachedSt *cachedStat
+}
+
+// cachedStat returns the cacheable parts of platform.Stat_t or an error if
+// they couldn't be retrieved.
+func (f *osFile) cachedStat() (fileType fs.FileMode, ino uint64, errno syscall.Errno) {
+	if f.cachedSt == nil {
+		if _, errno = f.Stat(); errno != 0 {
+			return
+		}
+	}
+	return f.cachedSt.fileType, f.cachedSt.ino, 0
+}
+
+// Ino implements File.Ino
+func (f *osFile) Ino() (uint64, syscall.Errno) {
+	if _, ino, errno := f.cachedStat(); errno != 0 {
+		return 0, errno
+	} else {
+		return ino, 0
+	}
+}
+
+// AccessMode implements File.AccessMode
+func (f *osFile) AccessMode() int {
+	return f.flag & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR)
+}
+
+// IsAppend implements File.IsAppend
+func (f *osFile) IsAppend() bool {
+	return f.flag&syscall.O_APPEND == syscall.O_APPEND
+}
+
+// SetAppend implements File.SetAppend
+func (f *osFile) SetAppend(enable bool) (errno syscall.Errno) {
+	if enable {
+		f.flag |= syscall.O_APPEND
+	} else {
+		f.flag &= ^syscall.O_APPEND
+	}
+
+	// Clear any create flag, as we are re-opening, not re-creating.
+	f.flag &= ^syscall.O_CREAT
+
+	// appendMode (bool) cannot be changed later, so we have to re-open the
+	// file. https://github.com/golang/go/blob/go1.20/src/os/file_unix.go#L60
+	return fileError(f, f.closed, f.reopen())
+}
+
+func (f *osFile) reopen() (errno syscall.Errno) {
+	// Clear any create flag, as we are re-opening, not re-creating.
+	f.flag &= ^syscall.O_CREAT
+
+	_ = f.close()
+	f.file, errno = OpenFile(f.path, f.flag, f.perm)
+	return
+}
+
+// IsNonblock implements File.IsNonblock
+func (f *osFile) IsNonblock() bool {
+	return f.flag&O_NONBLOCK == O_NONBLOCK
+}
+
+// SetNonblock implements File.SetNonblock
+func (f *osFile) SetNonblock(enable bool) (errno syscall.Errno) {
+	if enable {
+		f.flag |= O_NONBLOCK
+	} else {
+		f.flag &= ^O_NONBLOCK
+	}
+	if err := setNonblock(f.file.Fd(), enable); err != nil {
+		return fileError(f, f.closed, UnwrapOSError(err))
+	}
+	return 0
+}
+
+// IsDir implements File.IsDir
+func (f *osFile) 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 *osFile) Stat() (Stat_t, syscall.Errno) {
+	if f.closed {
+		return Stat_t{}, syscall.EBADF
+	}
+
+	st, errno := statFile(f.file)
+	switch errno {
+	case 0:
+		f.cachedSt = &cachedStat{fileType: st.Mode & fs.ModeType, ino: st.Ino}
+	case syscall.EIO:
+		errno = syscall.EBADF
+	}
+	return st, errno
+}
+
+// Read implements File.Read
+func (f *osFile) Read(buf []byte) (n int, errno syscall.Errno) {
+	if n, errno = read(f.file, buf); errno != 0 {
+		// Defer validation overhead until we've already had an error.
+		errno = fileError(f, f.closed, errno)
+	}
+	return
+}
+
+// Pread implements File.Pread
+func (f *osFile) Pread(buf []byte, off int64) (n int, errno syscall.Errno) {
+	if n, errno = pread(f.file, buf, off); errno != 0 {
+		// Defer validation overhead until we've already had an error.
+		errno = fileError(f, f.closed, errno)
+	}
+	return
+}
+
+// Seek implements File.Seek
+func (f *osFile) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) {
+	if newOffset, errno = seek(f.file, offset, whence); errno != 0 {
+		// Defer validation overhead until we've already had an error.
+		errno = fileError(f, f.closed, errno)
+
+		// If the error was trying to rewind a directory, re-open it. Notably,
+		// seeking to zero on a directory doesn't work on Windows with Go 1.18.
+		if errno == syscall.EISDIR && offset == 0 && whence == io.SeekStart {
+			return 0, f.reopen()
+		}
+	}
+	return
+}
+
+// PollRead implements File.PollRead
+func (f *osFile) PollRead(timeout *time.Duration) (ready bool, errno syscall.Errno) {
+	fdSet := FdSet{}
+	fd := int(f.file.Fd())
+	fdSet.Set(fd)
+	nfds := fd + 1 // See https://man7.org/linux/man-pages/man2/select.2.html#:~:text=condition%20has%20occurred.-,nfds,-This%20argument%20should
+	count, err := _select(nfds, &fdSet, nil, nil, timeout)
+	if errno = UnwrapOSError(err); errno != 0 {
+		// Defer validation overhead until we've already had an error.
+		errno = fileError(f, f.closed, errno)
+	}
+	return count > 0, errno
+}
+
+// Readdir implements File.Readdir. Notably, this uses "Readdir", not
+// "ReadDir", from os.File.
+func (f *osFile) Readdir(n int) (dirents []Dirent, errno syscall.Errno) {
+	if dirents, errno = readdir(f.file, f.path, n); errno != 0 {
+		errno = adjustReaddirErr(f, f.closed, errno)
+	}
+	return
+}
+
+// Write implements File.Write
+func (f *osFile) Write(buf []byte) (n int, errno syscall.Errno) {
+	if n, errno = write(f.file, buf); errno != 0 {
+		// Defer validation overhead until we've already had an error.
+		errno = fileError(f, f.closed, errno)
+	}
+	return
+}
+
+// Pwrite implements File.Pwrite
+func (f *osFile) Pwrite(buf []byte, off int64) (n int, errno syscall.Errno) {
+	if n, errno = pwrite(f.file, buf, off); errno != 0 {
+		// Defer validation overhead until we've already had an error.
+		errno = fileError(f, f.closed, errno)
+	}
+	return
+}
+
+// Truncate implements File.Truncate
+func (f *osFile) Truncate(size int64) (errno syscall.Errno) {
+	if errno = UnwrapOSError(f.file.Truncate(size)); errno != 0 {
+		// Defer validation overhead until we've already had an error.
+		errno = fileError(f, f.closed, errno)
+	}
+	return
+}
+
+// Sync implements File.Sync
+func (f *osFile) Sync() syscall.Errno {
+	return sync(f.file)
+}
+
+// Datasync implements File.Datasync
+func (f *osFile) Datasync() syscall.Errno {
+	return datasync(f.file)
+}
+
+// Chmod implements File.Chmod
+func (f *osFile) Chmod(mode fs.FileMode) syscall.Errno {
+	if f.closed {
+		return syscall.EBADF
+	}
+
+	return UnwrapOSError(f.file.Chmod(mode))
+}
+
+// Chown implements File.Chown
+func (f *osFile) Chown(uid, gid int) syscall.Errno {
+	if f.closed {
+		return syscall.EBADF
+	}
+
+	return fchown(f.file.Fd(), uid, gid)
+}
+
+// Utimens implements File.Utimens
+func (f *osFile) Utimens(times *[2]syscall.Timespec) syscall.Errno {
+	if f.closed {
+		return syscall.EBADF
+	}
+
+	err := futimens(f.file.Fd(), times)
+	return UnwrapOSError(err)
+}
+
+// Close implements File.Close
+func (f *osFile) Close() syscall.Errno {
+	if f.closed {
+		return 0
+	}
+	f.closed = true
+	return f.close()
+}
+
+func (f *osFile) close() syscall.Errno {
+	return UnwrapOSError(f.file.Close())
+}
diff --git a/internal/platform/platform.go b/internal/platform/platform.go
index 518e11e4..46f69991 100644
--- a/internal/platform/platform.go
+++ b/internal/platform/platform.go
@@ -5,7 +5,6 @@
 package platform
 
 import (
-	"errors"
 	"runtime"
 	"strings"
 )
@@ -34,7 +33,7 @@ func CompilerSupported() bool {
 // See https://man7.org/linux/man-pages/man2/mmap.2.html for mmap API and flags.
 func MmapCodeSegment(size int) ([]byte, error) {
 	if size == 0 {
-		panic(errors.New("BUG: MmapCodeSegment with zero length"))
+		panic("BUG: MmapCodeSegment with zero length")
 	}
 	if runtime.GOARCH == "amd64" {
 		return mmapCodeSegmentAMD64(size)
@@ -43,10 +42,32 @@ func MmapCodeSegment(size int) ([]byte, error) {
 	}
 }
 
+// RemapCodeSegment reallocates the memory mapping of an existing code segment
+// to increase its size. The previous code mapping is unmapped and must not be
+// reused after the function returns.
+//
+// This is similar to mremap(2) on linux, and emulated on platforms which do not
+// have this syscall.
+//
+// See https://man7.org/linux/man-pages/man2/mremap.2.html
+func RemapCodeSegment(code []byte, size int) ([]byte, error) {
+	if size < len(code) {
+		panic("BUG: RemapCodeSegment with size less than code")
+	}
+	if code == nil {
+		return MmapCodeSegment(size)
+	}
+	if runtime.GOARCH == "amd64" {
+		return remapCodeSegmentAMD64(code, size)
+	} else {
+		return remapCodeSegmentARM64(code, size)
+	}
+}
+
 // MunmapCodeSegment unmaps the given memory region.
 func MunmapCodeSegment(code []byte) error {
 	if len(code) == 0 {
-		panic(errors.New("BUG: MunmapCodeSegment with zero length"))
+		panic("BUG: MunmapCodeSegment with zero length")
 	}
 	return munmapCodeSegment(code)
 }
diff --git a/internal/platform/stat.go b/internal/platform/stat.go
index 3e66ccf8..038f77a8 100644
--- a/internal/platform/stat.go
+++ b/internal/platform/stat.go
@@ -2,6 +2,7 @@ package platform
 
 import (
 	"io/fs"
+	"os"
 	"syscall"
 )
 
@@ -68,7 +69,7 @@ func Stat(path string) (Stat_t, syscall.Errno) {
 	return stat(path) // extracted to override more expensively in windows
 }
 
-func defaultStatFile(f fs.File) (Stat_t, syscall.Errno) {
+func defaultStatFile(f *os.File) (Stat_t, syscall.Errno) {
 	if t, err := f.Stat(); err != nil {
 		return Stat_t{}, UnwrapOSError(err)
 	} else {
diff --git a/internal/platform/stat_bsd.go b/internal/platform/stat_bsd.go
index 90959f8f..e6f404d7 100644
--- a/internal/platform/stat_bsd.go
+++ b/internal/platform/stat_bsd.go
@@ -24,11 +24,11 @@ func stat(path string) (Stat_t, syscall.Errno) {
 	}
 }
 
-func statFile(f fs.File) (Stat_t, syscall.Errno) {
+func statFile(f *os.File) (Stat_t, syscall.Errno) {
 	return defaultStatFile(f)
 }
 
-func inoFromFileInfo(_ readdirFile, t fs.FileInfo) (ino uint64, err syscall.Errno) {
+func inoFromFileInfo(_ string, t fs.FileInfo) (ino uint64, err syscall.Errno) {
 	if d, ok := t.Sys().(*syscall.Stat_t); ok {
 		ino = d.Ino
 	}
diff --git a/internal/platform/stat_linux.go b/internal/platform/stat_linux.go
index 136081ef..2fe256c2 100644
--- a/internal/platform/stat_linux.go
+++ b/internal/platform/stat_linux.go
@@ -27,11 +27,11 @@ func stat(path string) (Stat_t, syscall.Errno) {
 	}
 }
 
-func statFile(f fs.File) (Stat_t, syscall.Errno) {
+func statFile(f *os.File) (Stat_t, syscall.Errno) {
 	return defaultStatFile(f)
 }
 
-func inoFromFileInfo(_ readdirFile, t fs.FileInfo) (ino uint64, err syscall.Errno) {
+func inoFromFileInfo(_ string, t fs.FileInfo) (ino uint64, err syscall.Errno) {
 	if d, ok := t.Sys().(*syscall.Stat_t); ok {
 		ino = d.Ino
 	}
diff --git a/internal/platform/stat_unsupported.go b/internal/platform/stat_unsupported.go
index 32e94438..f3bb313d 100644
--- a/internal/platform/stat_unsupported.go
+++ b/internal/platform/stat_unsupported.go
@@ -26,11 +26,11 @@ func stat(path string) (Stat_t, syscall.Errno) {
 	}
 }
 
-func statFile(f fs.File) (Stat_t, syscall.Errno) {
+func statFile(f *os.File) (Stat_t, syscall.Errno) {
 	return defaultStatFile(f)
 }
 
-func inoFromFileInfo(_ readdirFile, t fs.FileInfo) (ino uint64, err syscall.Errno) {
+func inoFromFileInfo(_ string, t fs.FileInfo) (ino uint64, err syscall.Errno) {
 	return
 }
 
diff --git a/internal/platform/stat_windows.go b/internal/platform/stat_windows.go
index 289b95fa..9a938f5f 100644
--- a/internal/platform/stat_windows.go
+++ b/internal/platform/stat_windows.go
@@ -4,6 +4,7 @@ package platform
 
 import (
 	"io/fs"
+	"os"
 	"path"
 	"syscall"
 )
@@ -46,33 +47,35 @@ func statPath(createFileAttrs uint32, path string) (Stat_t, syscall.Errno) {
 	return statHandle(h)
 }
 
-func statFile(f fs.File) (Stat_t, syscall.Errno) {
-	if of, ok := f.(fdFile); ok {
-		// Attempt to get the stat by handle, which works for normal files
-		st, err := statHandle(syscall.Handle(of.Fd()))
-
-		// ERROR_INVALID_HANDLE happens before Go 1.20. Don't fail as we only
-		// use that approach to fill in inode data, which is not critical.
-		//
-		// Note: statHandle uses UnwrapOSError which coerces
-		// ERROR_INVALID_HANDLE to EBADF.
-		if err != syscall.EBADF {
-			return st, err
-		}
+func statFile(f *os.File) (Stat_t, syscall.Errno) {
+	// Attempt to get the stat by handle, which works for normal files
+	st, err := statHandle(syscall.Handle(f.Fd()))
+
+	// ERROR_INVALID_HANDLE happens before Go 1.20. Don't fail as we only
+	// use that approach to fill in inode data, which is not critical.
+	//
+	// Note: statHandle uses UnwrapOSError which coerces
+	// ERROR_INVALID_HANDLE to EBADF.
+	if err != syscall.EBADF {
+		return st, err
 	}
 	return defaultStatFile(f)
 }
 
 // inoFromFileInfo uses stat to get the inode information of the file.
-func inoFromFileInfo(f readdirFile, t fs.FileInfo) (ino uint64, errno syscall.Errno) {
-	if pf, ok := f.(PathFile); ok {
-		inoPath := path.Clean(path.Join(pf.Path(), t.Name()))
-		var st Stat_t
-		if st, errno = Lstat(inoPath); errno == 0 {
-			ino = st.Ino
-		}
+func inoFromFileInfo(filePath string, t fs.FileInfo) (ino uint64, errno syscall.Errno) {
+	if filePath == "" {
+		// This is a fs.File backed implementation which doesn't have access to
+		// the original file path.
+		return
+	}
+	// ino is no not in Win32FileAttributeData
+	inoPath := path.Clean(path.Join(filePath, t.Name()))
+	var st Stat_t
+	if st, errno = Lstat(inoPath); errno == 0 {
+		ino = st.Ino
 	}
-	return // not in Win32FileAttributeData
+	return
 }
 
 func statFromFileInfo(t fs.FileInfo) Stat_t {
diff --git a/internal/platform/sync.go b/internal/platform/sync.go
index 9ca14874..b9c080b9 100644
--- a/internal/platform/sync.go
+++ b/internal/platform/sync.go
@@ -3,13 +3,10 @@
 package platform
 
 import (
-	"io/fs"
+	"os"
 	"syscall"
 )
 
-func sync(f fs.File) syscall.Errno {
-	if s, ok := f.(syncFile); ok {
-		return UnwrapOSError(s.Sync())
-	}
-	return 0
+func sync(f *os.File) syscall.Errno {
+	return UnwrapOSError(f.Sync())
 }
diff --git a/internal/platform/sync_windows.go b/internal/platform/sync_windows.go
index d0551867..534f7936 100644
--- a/internal/platform/sync_windows.go
+++ b/internal/platform/sync_windows.go
@@ -1,22 +1,19 @@
 package platform
 
 import (
-	"io/fs"
+	"os"
 	"syscall"
 )
 
-func sync(f fs.File) syscall.Errno {
-	if s, ok := f.(syncFile); ok {
-		errno := UnwrapOSError(s.Sync())
-		// Coerce error performing stat on a directory to 0, as it won't work
-		// on Windows.
-		switch errno {
-		case syscall.EACCES /* Go 1.20 */, syscall.EBADF /* Go 1.18 */ :
-			if st, err := f.Stat(); err == nil && st.IsDir() {
-				errno = 0
-			}
+func sync(f *os.File) syscall.Errno {
+	errno := UnwrapOSError(f.Sync())
+	// Coerce error performing stat on a directory to 0, as it won't work
+	// on Windows.
+	switch errno {
+	case syscall.EACCES /* Go 1.20 */, syscall.EBADF /* Go 1.18 */ :
+		if st, err := f.Stat(); err == nil && st.IsDir() {
+			errno = 0
 		}
-		return errno
 	}
-	return 0
+	return errno
 }
diff --git a/internal/platform/unlink_windows.go b/internal/platform/unlink_windows.go
index 7488be6d..5ff607ac 100644
--- a/internal/platform/unlink_windows.go
+++ b/internal/platform/unlink_windows.go
@@ -13,7 +13,7 @@ func Unlink(name string) syscall.Errno {
 		return 0
 	}
 	errno := UnwrapOSError(err)
-	if errno == syscall.EACCES {
+	if errno == syscall.EBADF {
 		lstat, errLstat := os.Lstat(name)
 		if errLstat == nil && lstat.Mode()&os.ModeSymlink != 0 {
 			errno = UnwrapOSError(os.Remove(name))
diff --git a/internal/platform/wrap_windows.go b/internal/platform/wrap_windows.go
deleted file mode 100644
index 8a49d46d..00000000
--- a/internal/platform/wrap_windows.go
+++ /dev/null
@@ -1,156 +0,0 @@
-package platform
-
-import (
-	"errors"
-	"io"
-	"io/fs"
-	"syscall"
-)
-
-// osFile contains the functions on os.File needed to support fsFile. Since
-// we are embedding, we need to declare everything used.
-type osFile interface {
-	fdFile // for the number of links.
-	readdirFile
-	fs.ReadDirFile
-	io.ReaderAt // for pread
-	io.Seeker   // fallback for ReaderAt for embed:fs
-
-	io.Writer
-	io.WriterAt // for pwrite
-	chmodFile
-	syncFile
-	truncateFile
-}
-
-// windowsWrappedFile deals with errno portability issues in Windows. This code
-// is likely to change as we complete WASI and GOOS=js.
-//
-// If we don't map to syscall.Errno, wasm will crash in odd way attempting the
-// same. This approach is an alternative to making our own fs.File public type.
-// We aren't doing that yet, as mapping problems are generally contained to
-// Windows. Hence, file is intentionally not exported.
-//
-// Note: Don't test for this type as it is wrapped when using sysfs.NewReadFS.
-type windowsWrappedFile struct {
-	osFile
-	path           string
-	flag           int
-	perm           fs.FileMode
-	dirInitialized bool
-
-	fileType *fs.FileMode
-
-	// closed is true when closed was called. This ensures proper syscall.EBADF
-	// TODO: extract a base wrapper type to cover all cases.
-	closed bool
-}
-
-// Path implements PathFile
-func (w *windowsWrappedFile) Path() string {
-	return w.path
-}
-
-// Readdir implements the same method as documented on os.File.
-func (w *windowsWrappedFile) Readdir(n int) (fis []fs.FileInfo, err error) {
-	if err = w.requireFile("Readdir", false, true); err != nil {
-		return
-	} else if err = w.maybeInitDir(); err != nil {
-		return
-	}
-
-	return w.osFile.Readdir(n)
-}
-
-// ReadDir implements fs.ReadDirFile.
-func (w *windowsWrappedFile) ReadDir(n int) (dirents []fs.DirEntry, err error) {
-	if err = w.requireFile("ReadDir", false, true); err != nil {
-		return
-	} else if err = w.maybeInitDir(); err != nil {
-		return
-	}
-
-	return w.osFile.ReadDir(n)
-}
-
-// Write implements io.Writer
-func (w *windowsWrappedFile) Write(p []byte) (n int, err error) {
-	if err = w.requireFile("Write", false, false); err != nil {
-		return
-	}
-
-	n, err = w.osFile.Write(p)
-	// ERROR_ACCESS_DENIED is often returned instead of EBADF
-	// when a file is used after close.
-	if errors.Is(err, ERROR_ACCESS_DENIED) {
-		err = syscall.EBADF
-	}
-	return
-}
-
-// Close implements io.Closer
-func (w *windowsWrappedFile) Close() (err error) {
-	if w.closed {
-		return
-	}
-
-	if err = w.osFile.Close(); err != nil {
-		w.closed = true
-	}
-	return
-}
-
-func (w *windowsWrappedFile) maybeInitDir() error {
-	if w.dirInitialized {
-		return nil
-	}
-
-	// On Windows, once the directory is opened, changes to the directory are
-	// not visible on ReadDir on that already-opened file handle.
-	//
-	// To provide consistent behavior with other platforms, we re-open it.
-	if err := w.osFile.Close(); err != nil {
-		return err
-	}
-	newW, errno := openFile(w.path, w.flag, w.perm)
-	if errno != 0 {
-		return &fs.PathError{Op: "OpenFile", Path: w.path, Err: errno}
-	}
-	w.osFile = newW
-	w.dirInitialized = true
-	return nil
-}
-
-// requireFile is used to making syscalls which will fail.
-func (w *windowsWrappedFile) requireFile(op string, readOnly, isDir bool) error {
-	var ft fs.FileMode
-	var err error
-	if w.closed {
-		err = syscall.EBADF
-	} else if readOnly && w.flag&syscall.O_RDONLY == 0 {
-		err = syscall.EBADF
-	} else if ft, err = w.getFileType(); err == nil && isDir != ft.IsDir() {
-		if isDir {
-			err = syscall.ENOTDIR
-		} else {
-			err = syscall.EBADF
-		}
-	} else if err == nil {
-		return nil
-	}
-	return &fs.PathError{Op: op, Path: w.path, Err: err}
-}
-
-// getFileType caches the file type as this cannot change on an open file.
-func (w *windowsWrappedFile) getFileType() (fs.FileMode, error) {
-	if w.fileType == nil {
-		st, errno := statFile(w.osFile)
-		if errno != 0 {
-			return 0, nil
-		}
-		ft := st.Mode & fs.ModeType
-		w.fileType = &ft
-		return ft, nil
-	}
-	return *w.fileType, nil
-}
diff --git a/internal/sys/fs.go b/internal/sys/fs.go
index d9b0d66e..a09687c2 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -92,19 +92,14 @@ func (noopStdoutFile) AccessMode() int {
 }
 
 // Write implements the same method as documented on platform.File
-func (noopStdoutFile) Write(p []byte) (int, syscall.Errno) {
-	return len(p), 0 // same as io.Discard
+func (noopStdoutFile) Write(buf []byte) (int, syscall.Errno) {
+	return len(buf), 0 // same as io.Discard
 }
 
 type noopStdioFile struct {
 	platform.UnimplementedFile
 }
 
-// Path implements the same method as documented on platform.File
-func (noopStdioFile) Path() string {
-	return ""
-}
-
 // Stat implements the same method as documented on platform.File
 func (noopStdioFile) Stat() (platform.Stat_t, syscall.Errno) {
 	return platform.Stat_t{Mode: modeDevice, Nlink: 1}, 0
@@ -128,9 +123,32 @@ type lazyDir struct {
 	f  platform.File
 }
 
-// Path implements the same method as documented on platform.File
-func (r *lazyDir) Path() string {
-	return "."
+// Ino implements the same method as documented on platform.File
+func (r *lazyDir) Ino() (uint64, syscall.Errno) {
+	if f, ok := r.file(); !ok {
+		return 0, syscall.EBADF
+	} else {
+		return f.Ino()
+	}
+}
+
+// IsAppend implements the same method as documented on platform.File
+func (r *lazyDir) IsAppend() bool {
+	return false
+}
+
+// SetAppend implements the same method as documented on platform.File
+func (r *lazyDir) SetAppend(bool) syscall.Errno {
+	return syscall.EISDIR
+}
+
+// Seek implements the same method as documented on platform.File
+func (r *lazyDir) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) {
+	if f, ok := r.file(); !ok {
+		return 0, syscall.EBADF
+	} else {
+		return f.Seek(offset, whence)
+	}
 }
 
 // Stat implements the same method as documented on platform.File
@@ -227,7 +245,11 @@ type FileEntry struct {
 	// Name is the name of the directory up to its pre-open, or the pre-open
 	// name itself when IsPreopen.
 	//
-	// Note: This can drift on rename.
+	// # Notes
+	//
+	//   - This can drift on rename.
+	//   - This relates to the guest path, which is not the real file path
+	//     except if the entire host filesystem was made available.
 	Name string
 
 	// IsPreopen is a directory that is lazily opened.
@@ -236,56 +258,12 @@ type FileEntry struct {
 	// FS is the filesystem associated with the pre-open.
 	FS sysfs.FS
 
-	// cachedStat includes fields that won't change while a file is open.
-	cachedStat *cachedStat
-
 	// File is always non-nil.
 	File platform.File
 
 	// ReadDir is present when this File is a fs.ReadDirFile and `ReadDir`
 	// was called.
 	ReadDir *ReadDir
-
-	openFlag int
-	openPerm fs.FileMode
-}
-
-type cachedStat struct {
-	// Ino is the file serial number, or zero if not available.
-	Ino uint64
-}
-
-// IsAppend returns true if the file is open with syscall.O_APPEND.
-func (f *FileEntry) IsAppend() bool {
-	return f.openFlag&syscall.O_APPEND != 0
-}
-
-// 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, 0
-}
-
-// Stat returns the underlying stat of this file.
-func (f *FileEntry) Stat() (st platform.Stat_t, errno syscall.Errno) {
-	if ld, ok := f.File.(*lazyDir); ok {
-		var sf platform.File
-		if sf, ok = ld.file(); ok {
-			st, errno = sf.Stat()
-		}
-	} else {
-		st, errno = f.File.Stat()
-	}
-
-	if errno == 0 {
-		f.cachedStat = &cachedStat{Ino: st.Ino}
-	}
-	return
 }
 
 // ReadDir is the status of a prior fs.ReadDirFile call.
@@ -407,7 +385,7 @@ func (c *FSContext) OpenFile(fs sysfs.FS, path string, flag int, perm fs.FileMod
 	if f, errno := fs.OpenFile(path, flag, perm); errno != 0 {
 		return 0, errno
 	} else {
-		fe := &FileEntry{FS: fs, File: f, openFlag: flag, openPerm: perm}
+		fe := &FileEntry{FS: fs, File: f}
 		if path == "/" || path == "." {
 			fe.Name = ""
 		} else {
@@ -421,78 +399,6 @@ func (c *FSContext) OpenFile(fs sysfs.FS, path string, flag int, perm fs.FileMod
 	}
 }
 
-// ReOpenDir re-opens the directory while keeping the same file descriptor.
-// TODO: this might not be necessary once we have our own File type.
-func (c *FSContext) ReOpenDir(fd int32) (*FileEntry, syscall.Errno) {
-	f, ok := c.openedFiles.Lookup(fd)
-	if !ok {
-		return nil, syscall.EBADF
-	} else if isDir, errno := f.File.IsDir(); errno != 0 {
-		return nil, errno
-	} else if !isDir {
-		return nil, syscall.ENOTDIR
-	}
-
-	if errno := c.reopen(f); errno != 0 {
-		return nil, errno
-	}
-
-	f.ReadDir.CountRead, f.ReadDir.Dirents = 0, nil
-	return f, 0
-}
-
-func (c *FSContext) reopen(f *FileEntry) syscall.Errno {
-	if errno := f.File.Close(); errno != 0 {
-		return errno
-	}
-
-	// Re-opens with  the same parameters as before.
-	opened, errno := f.FS.OpenFile(f.File.Path(), f.openFlag, f.openPerm)
-	if errno != 0 {
-		return errno
-	}
-
-	// Reset the state.
-	f.File = opened
-	return 0
-}
-
-// ChangeOpenFlag changes the open flag of the given opened file pointed by `fd`.
-// Currently, this only supports the change of syscall.O_APPEND flag.
-func (c *FSContext) ChangeOpenFlag(fd int32, flag int) syscall.Errno {
-	f, ok := c.LookupFile(fd)
-	if !ok {
-		return syscall.EBADF
-	} else if isDir, errno := f.File.IsDir(); errno != 0 {
-		return errno
-	} else if isDir {
-		return syscall.EISDIR
-	} else if flag&syscall.O_APPEND == f.openFlag&syscall.O_APPEND {
-		return 0 // don't re-open
-	}
-
-	if flag&syscall.O_APPEND != 0 {
-		f.openFlag |= syscall.O_APPEND
-	} else {
-		f.openFlag &= ^syscall.O_APPEND
-	}
-
-	// Clear any create flag, as we are re-opening, not re-creating.
-	f.openFlag &= ^syscall.O_CREAT
-
-	// Changing the flag while opening is not really supported well in Go. Even when using
-	// syscall package, the feasibility of doing so really depends on the platform. For examples:
-	//
-	// 	* This appendMode (bool) cannot be changed later.
-	// 	https://github.com/golang/go/blob/go1.20/src/os/file_unix.go#L60
-	// 	* On Windows, re-opening it is the only way to emulate the behavior.
-	// 	https://github.com/bytecodealliance/system-interface/blob/62b97f9776b86235f318c3a6e308395a1187439b/src/fs/fd_flags.rs#L196
-	//
-	// Therefore, here we re-open the file while keeping the file descriptor.
-	// TODO: this might be improved once we have our own File type.
-	return c.reopen(f)
-}
-
 // LookupFile returns a file if it is in the table.
 func (c *FSContext) LookupFile(fd int32) (*FileEntry, bool) {
 	return c.openedFiles.Lookup(fd)
diff --git a/internal/sysfs/adapter.go b/internal/sysfs/adapter.go
index 18bad053..87189aba 100644
--- a/internal/sysfs/adapter.go
+++ b/internal/sysfs/adapter.go
@@ -38,20 +38,17 @@ func (a *adapter) String() string {
 
 // OpenFile implements FS.OpenFile
 func (a *adapter) OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) {
-	path = cleanPath(path)
-	f, err := a.fs.Open(path)
-	return platform.NewFsFile(path, flag, f), platform.UnwrapOSError(err)
+	return platform.OpenFSFile(a.fs, cleanPath(path), flag, perm)
 }
 
 // Stat implements FS.Stat
 func (a *adapter) Stat(path string) (platform.Stat_t, syscall.Errno) {
-	name := cleanPath(path)
-	f, err := a.fs.Open(name)
-	if err != nil {
-		return platform.Stat_t{}, platform.UnwrapOSError(err)
+	f, errno := a.OpenFile(path, syscall.O_RDONLY, 0)
+	if errno != 0 {
+		return platform.Stat_t{}, errno
 	}
 	defer f.Close()
-	return platform.NewFsFile(path, syscall.O_RDONLY, f).Stat()
+	return f.Stat()
 }
 
 // Lstat implements FS.Lstat
diff --git a/internal/sysfs/dirfs.go b/internal/sysfs/dirfs.go
index 315115b0..ab2203ef 100644
--- a/internal/sysfs/dirfs.go
+++ b/internal/sysfs/dirfs.go
@@ -37,11 +37,7 @@ func (d *dirFS) String() string {
 
 // OpenFile implements FS.OpenFile
 func (d *dirFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) {
-	f, errno := platform.OpenFile(d.join(path), flag, perm)
-	if errno != 0 {
-		return nil, errno
-	}
-	return platform.NewFsFile(path, flag, f), 0
+	return platform.OpenOSFile(d.join(path), flag, perm)
 }
 
 // Lstat implements FS.Lstat
diff --git a/internal/sysfs/readfs.go b/internal/sysfs/readfs.go
index 5a9faa7a..2c84ae52 100644
--- a/internal/sysfs/readfs.go
+++ b/internal/sysfs/readfs.go
@@ -54,8 +54,11 @@ func (r *readFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.Fil
 	// check if they are the opposite of read or not.
 	switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) {
 	case os.O_WRONLY, os.O_RDWR:
+		if flag&platform.O_DIRECTORY != 0 {
+			return nil, syscall.EISDIR
+		}
 		return nil, syscall.ENOSYS
-	default: // os.O_RDONLY so we are ok!
+	default: // os.O_RDONLY (or no flag) so we are ok!
 	}
 
 	f, errno := r.fs.OpenFile(path, flag, perm)
@@ -72,9 +75,9 @@ type readFile struct {
 	f platform.File
 }
 
-// Path implements the same method as documented on platform.File.
-func (r *readFile) Path() string {
-	return r.f.Path()
+// Ino implements the same method as documented on platform.File.
+func (r *readFile) Ino() (uint64, syscall.Errno) {
+	return r.f.Ino()
 }
 
 // AccessMode implements the same method as documented on platform.File.
@@ -92,6 +95,16 @@ func (r *readFile) SetNonblock(enabled bool) syscall.Errno {
 	return r.f.SetNonblock(enabled)
 }
 
+// IsAppend implements the same method as documented on platform.File.
+func (r *readFile) IsAppend() bool {
+	return r.f.IsAppend()
+}
+
+// SetAppend implements the same method as documented on platform.File.
+func (r *readFile) SetAppend(enabled bool) syscall.Errno {
+	return r.f.SetAppend(enabled)
+}
+
 // Stat implements the same method as documented on platform.File.
 func (r *readFile) Stat() (platform.Stat_t, syscall.Errno) {
 	return r.f.Stat()
diff --git a/internal/sysfs/rootfs.go b/internal/sysfs/rootfs.go
index e39eff8f..fa0e621d 100644
--- a/internal/sysfs/rootfs.go
+++ b/internal/sysfs/rootfs.go
@@ -2,10 +2,10 @@ package sysfs
 
 import (
 	"fmt"
+	"io"
 	"io/fs"
 	"strings"
 	"syscall"
-	"time"
 
 	"github.com/tetratelabs/wazero/internal/platform"
 )
@@ -149,9 +149,9 @@ type openRootDir struct {
 	direntsI int               // the read offset, an index into the files slice
 }
 
-// Path implements the same method as documented on platform.File
-func (d *openRootDir) Path() string {
-	return d.path
+// Ino implements the same method as documented on platform.File
+func (d *openRootDir) Ino() (uint64, syscall.Errno) {
+	return d.f.Ino()
 }
 
 // Stat implements the same method as documented on platform.File
@@ -159,6 +159,17 @@ func (d *openRootDir) Stat() (platform.Stat_t, syscall.Errno) {
 	return d.f.Stat()
 }
 
+// Seek implements the same method as documented on platform.File
+func (d *openRootDir) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) {
+	if offset != 0 || whence != io.SeekStart {
+		errno = syscall.ENOSYS
+		return
+	}
+	d.dirents = nil
+	d.direntsI = 0
+	return d.f.Seek(offset, whence)
+}
+
 // Readdir implements the same method as documented on platform.File
 func (d *openRootDir) Readdir(count int) (dirents []platform.Dirent, errno syscall.Errno) {
 	if d.dirents == nil {
@@ -484,33 +495,64 @@ loop:
 	return
 }
 
-type fakeRootFS struct{ UnimplementedFS }
+type fakeRootFS struct {
+	UnimplementedFS
+}
 
 // OpenFile implements FS.OpenFile
-func (*fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) {
+func (fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) {
 	switch path {
 	case ".", "/", "":
-		return platform.NewFsFile(path, flag, fakeRootDir{}), 0
+		return fakeRootDir{}, 0
 	}
 	return nil, syscall.ENOENT
 }
 
-type fakeRootDir struct{}
+type fakeRootDir struct {
+	platform.DirFile
+}
+
+// Ino implements the same method as documented on platform.File
+func (fakeRootDir) Ino() (uint64, syscall.Errno) {
+	return 0, 0
+}
+
+// Stat implements the same method as documented on platform.File
+func (fakeRootDir) Stat() (platform.Stat_t, syscall.Errno) {
+	return platform.Stat_t{Mode: fs.ModeDir, Nlink: 1}, 0
+}
+
+// Readdir implements the same method as documented on platform.File
+func (fakeRootDir) Readdir(int) (dirents []platform.Dirent, errno syscall.Errno) {
+	return // empty
+}
+
+// Sync implements the same method as documented on platform.File
+func (fakeRootDir) Sync() syscall.Errno {
+	return 0
+}
 
-func (fakeRootDir) Close() (err error) { return }
+// Datasync implements the same method as documented on platform.File
+func (fakeRootDir) Datasync() syscall.Errno {
+	return 0
+}
 
-func (fakeRootDir) Stat() (fs.FileInfo, error) { return fakeRootDirInfo{}, nil }
+// Chmod implements the same method as documented on platform.File
+func (fakeRootDir) Chmod(fs.FileMode) syscall.Errno {
+	return syscall.ENOSYS
+}
 
-func (fakeRootDir) Read([]byte) (int, error) {
-	return 0, &fs.PathError{Op: "read", Path: "/", Err: syscall.EISDIR}
+// Chown implements the same method as documented on platform.File
+func (fakeRootDir) Chown(int, int) syscall.Errno {
+	return syscall.ENOSYS
 }
 
-type fakeRootDirInfo struct{}
+// Utimens implements the same method as documented on platform.File
+func (fakeRootDir) Utimens(*[2]syscall.Timespec) syscall.Errno {
+	return syscall.ENOSYS
+}
 
-func (fakeRootDirInfo) Name() string                               { return "/" }
-func (fakeRootDirInfo) Size() int64                                { return 0 }
-func (fakeRootDirInfo) Mode() fs.FileMode                          { return fs.ModeDir | 0o500 }
-func (fakeRootDirInfo) ModTime() time.Time                         { return time.Unix(0, 0) }
-func (fakeRootDirInfo) IsDir() bool                                { return true }
-func (fakeRootDirInfo) Sys() interface{}                           { return nil }
-func (fakeRootDir) ReadDir(int) (dirents []fs.DirEntry, err error) { return }
+// Close implements the same method as documented on platform.File
+func (fakeRootDir) Close() syscall.Errno {
+	return 0
+}
diff --git a/internal/sysfs/sysfs.go b/internal/sysfs/sysfs.go
index 3a5caef8..2a697ba5 100644
--- a/internal/sysfs/sysfs.go
+++ b/internal/sysfs/sysfs.go
@@ -49,6 +49,8 @@ type FS interface {
 	// A zero syscall.Errno is success. The below are expected otherwise:
 	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `path` or `flag` is invalid.
+	//   - syscall.EISDIR: the path was a directory, but flag included
+	//     syscall.O_RDWR or syscall.O_WRONLY
 	//   - syscall.ENOENT: `path` doesn't exist and `flag` doesn't contain
 	//     os.O_CREATE.
 	//


Test patch:
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index fd4d2b83..c09dbe8b 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -65,7 +65,7 @@ func Test_fdAllocate(t *testing.T) {
 	require.True(t, ok)
 
 	requireSizeEqual := func(exp int64) {
-		st, errno := f.Stat()
+		st, errno := f.File.Stat()
 		require.EqualErrno(t, 0, errno)
 		require.Equal(t, exp, st.Size)
 	}
@@ -251,7 +251,7 @@ func Test_fdFdstatGet(t *testing.T) {
 	stdin.File = stdinFile
 
 	// Make this file writeable, to ensure flags read-back correctly.
-	fileFD, errno := fsc.OpenFile(preopen, file, os.O_RDWR|os.O_APPEND, 0)
+	fileFD, errno := fsc.OpenFile(preopen, file, os.O_RDWR, 0)
 	require.EqualErrno(t, 0, errno)
 
 	dirFD, errno := fsc.OpenFile(preopen, dir, os.O_RDONLY, 0)
@@ -326,13 +326,13 @@ func Test_fdFdstatGet(t *testing.T) {
 			fd:   fileFD,
 			expectedMemory: []byte{
 				4, 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=4)
-<== (stat={filetype=REGULAR_FILE,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=REGULAR_FILE,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)
 `,
 		},
 		{
@@ -991,7 +991,7 @@ func Test_fdFilestatSetTimes(t *testing.T) {
 			f, ok := fsc.LookupFile(fd)
 			require.True(t, ok)
 
-			st, errno := f.Stat()
+			st, errno := f.File.Stat()
 			require.EqualErrno(t, 0, errno)
 			prevAtime, prevMtime := st.Atim, st.Mtim
 
@@ -1004,7 +1004,7 @@ func Test_fdFilestatSetTimes(t *testing.T) {
 				f, ok := fsc.LookupFile(fd)
 				require.True(t, ok)
 
-				st, errno = f.Stat()
+				st, errno = f.File.Stat()
 				require.EqualErrno(t, 0, errno)
 				if tc.flags&wasip1.FstflagsAtim != 0 {
 					require.Equal(t, tc.atime, st.Atim)
@@ -1952,14 +1952,12 @@ func Test_fdRead_Errors(t *testing.T) {
 
 var (
 	testDirents = func() []platform.Dirent {
-		dPath := "dir"
-		d, err := fstest.FS.Open(dPath)
-		if err != nil {
-			panic(err)
+		d, errno := platform.OpenFSFile(fstest.FS, "dir", 0, 0)
+		if errno != 0 {
+			panic(errno)
 		}
 		defer d.Close()
-		pf := platform.NewFsFile(dPath, 0, d)
-		dirents, errno := pf.Readdir(-1)
+		dirents, errno := d.Readdir(-1)
 		if errno != 0 {
 			panic(errno)
 		}
@@ -2734,7 +2732,7 @@ func Test_fdSeek_Errors(t *testing.T) {
 	defer r.Close(testCtx)
 
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
-	require.Zero(t, fsc.RootFS().Mkdir("dir", 0o0700))
+	require.Zero(t, fsc.RootFS().Mkdir("dir", 0o0777))
 	dirFD := requireOpenFD(t, mod, "dir")
 
 	memorySize := mod.Memory().Size()
@@ -3763,7 +3761,7 @@ func Test_pathLink(t *testing.T) {
 			uint64(newFd), uint64(destination), uint64(len(destinationName)))
 		require.Contains(t, log.String(), wasip1.ErrnoName(wasip1.ErrnoSuccess))
 
-		f := openFsFile(t, destinationRealPath, os.O_RDONLY, 0)
+		f := openFile(t, destinationRealPath, os.O_RDONLY, 0)
 		defer f.Close()
 
 		st, errno := f.Stat()
@@ -5131,7 +5129,7 @@ func Test_fdReaddir_opened_file_written(t *testing.T) {
 	require.EqualErrno(t, 0, errno)
 
 	// Then write a file to the directory.
-	f := openFsFile(t, joinPath(dirPath, "file"), os.O_CREATE, 0)
+	f := openFile(t, joinPath(dirPath, "file"), os.O_CREATE, 0)
 	defer f.Close()
 
 	// get the real inode of the current directory
@@ -5179,8 +5177,8 @@ func joinPath(dirName, baseName string) string {
 	return path.Join(dirName, baseName)
 }
 
-func openFsFile(t *testing.T, path string, flag int, perm fs.FileMode) platform.File {
-	f, errno := platform.OpenFile(path, flag, perm)
+func openFile(t *testing.T, path string, flag int, perm fs.FileMode) platform.File {
+	f, errno := platform.OpenOSFile(path, flag, perm)
 	require.EqualErrno(t, 0, errno)
-	return platform.NewFsFile(path, flag, f)
+	return f
 }
diff --git a/imports/wasi_snapshot_preview1/fs_unit_test.go b/imports/wasi_snapshot_preview1/fs_unit_test.go
index 8e6270ff..d0c07932 100644
--- a/imports/wasi_snapshot_preview1/fs_unit_test.go
+++ b/imports/wasi_snapshot_preview1/fs_unit_test.go
@@ -196,13 +196,12 @@ func Test_maxDirents(t *testing.T) {
 var (
 	testDirents = func() []platform.Dirent {
 		dPath := "dir"
-		d, err := fstest.FS.Open(dPath)
-		if err != nil {
-			panic(err)
+		d, errno := platform.OpenFSFile(fstest.FS, dPath, syscall.O_RDONLY, 0)
+		if errno != 0 {
+			panic(errno)
 		}
 		defer d.Close()
-		pf := platform.NewFsFile(dPath, 0, d)
-		dirents, errno := pf.Readdir(-1)
+		dirents, errno := d.Readdir(-1)
 		if errno != 0 {
 			panic(errno)
 		}
diff --git a/imports/wasi_snapshot_preview1/wasi_bench_test.go b/imports/wasi_snapshot_preview1/wasi_bench_test.go
index f4edb58f..c3a48a68 100644
--- a/imports/wasi_snapshot_preview1/wasi_bench_test.go
+++ b/imports/wasi_snapshot_preview1/wasi_bench_test.go
@@ -361,11 +361,11 @@ func requireESuccess(b *testing.B, results []uint64) {
 	}
 }
 
-type writerFunc func(p []byte) (n int, err error)
+type writerFunc func(buf []byte) (n int, err error)
 
 // Write implements io.Writer by calling writerFunc.
-func (f writerFunc) Write(p []byte) (n int, err error) {
-	return f(p)
+func (f writerFunc) Write(buf []byte) (n int, err error) {
+	return f(buf)
 }
 
 func Benchmark_fdWrite(b *testing.B) {
@@ -373,7 +373,7 @@ func Benchmark_fdWrite(b *testing.B) {
 	defer r.Close(testCtx)
 
 	mod, err := instantiateProxyModule(r, wazero.NewModuleConfig().
-		WithStdout(writerFunc(func(p []byte) (n int, err error) { return len(p), nil })),
+		WithStdout(writerFunc(func(buf []byte) (n int, err error) { return len(buf), nil })),
 	)
 	if err != nil {
 		b.Fatal(err)
diff --git a/imports/wasi_snapshot_preview1/wasi_test.go b/imports/wasi_snapshot_preview1/wasi_test.go
index 4b90867a..16051e97 100644
--- a/imports/wasi_snapshot_preview1/wasi_test.go
+++ b/imports/wasi_snapshot_preview1/wasi_test.go
@@ -162,7 +162,7 @@ type blockingReader struct {
 }
 
 // Read implements io.Reader
-func (b blockingReader) Read(p []byte) (n int, err error) {
+func (b blockingReader) Read(buf []byte) (n int, err error) {
 	<-b.ctx.Done()
 	return 0, nil
 }
diff --git a/internal/platform/bench_test.go b/internal/platform/bench_test.go
index eb80e0b3..a3512474 100644
--- a/internal/platform/bench_test.go
+++ b/internal/platform/bench_test.go
@@ -10,14 +10,11 @@ import (
 )
 
 func BenchmarkFsFileUtimesNs(b *testing.B) {
-	tmpDir := b.TempDir()
-	path := path.Join(tmpDir, "file")
-	f, err := os.Create(path)
-	if err != nil {
-		b.Fatal(err)
+	f, errno := OpenOSFile(path.Join(b.TempDir(), "file"), syscall.O_CREAT, 0)
+	if errno != 0 {
+		b.Fatal(errno)
 	}
 	defer f.Close()
-	fs := NewFsFile(path, syscall.O_RDONLY, f)
 
 	times := &[2]syscall.Timespec{
 		{Sec: 123, Nsec: 4 * 1e3},
@@ -26,7 +23,7 @@ func BenchmarkFsFileUtimesNs(b *testing.B) {
 
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
-		if errno := fs.Utimens(times); errno != 0 {
+		if errno := f.Utimens(times); errno != 0 {
 			b.Fatal(errno)
 		}
 	}
@@ -57,9 +54,9 @@ func BenchmarkFsFileRead(b *testing.B) {
 
 		b.Run(bc.name, func(b *testing.B) {
 			name := "wazero.txt"
-			f, err := bc.fs.Open(name)
-			if err != nil {
-				b.Fatal(err)
+			f, errno := OpenFSFile(bc.fs, name, syscall.O_RDONLY, 0)
+			if errno != 0 {
+				b.Fatal(errno)
 			}
 			defer f.Close()
 
@@ -67,21 +64,19 @@ func BenchmarkFsFileRead(b *testing.B) {
 			for i := 0; i < b.N; i++ {
 				b.StopTimer()
 
-				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 {
+				if _, errno = f.Seek(0, io.SeekStart); errno != 0 {
 					b.Fatal(errno)
 				}
 
 				b.StartTimer()
 				if bc.pread {
-					n, errno = fs.Pread(buf, 3)
+					n, errno = f.Pread(buf, 3)
 				} else {
-					n, errno = fs.Read(buf)
+					n, errno = f.Read(buf)
 				}
 				b.StopTimer()
 
diff --git a/internal/platform/chown_unix_test.go b/internal/platform/chown_unix_test.go
index c34d40cf..5eecdf79 100644
--- a/internal/platform/chown_unix_test.go
+++ b/internal/platform/chown_unix_test.go
@@ -18,7 +18,7 @@ func TestChown(t *testing.T) {
 	dir := path.Join(tmpDir, "dir")
 	require.NoError(t, os.Mkdir(dir, 0o0777))
 
-	dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
+	dirF := requireOpenFile(t, dir, syscall.O_RDONLY, 0)
 	defer dirF.Close()
 
 	dirSt, errno := dirF.Stat()
@@ -65,7 +65,7 @@ func TestDefaultFileChown(t *testing.T) {
 	dir := path.Join(tmpDir, "dir")
 	require.NoError(t, os.Mkdir(dir, 0o0777))
 
-	dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
+	dirF := requireOpenFile(t, dir, syscall.O_RDONLY, 0)
 	defer dirF.Close()
 
 	dirSt, errno := dirF.Stat()
@@ -113,7 +113,7 @@ func TestLchown(t *testing.T) {
 	dir := path.Join(tmpDir, "dir")
 	require.NoError(t, os.Mkdir(dir, 0o0777))
 
-	dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
+	dirF := requireOpenFile(t, dir, syscall.O_RDONLY, 0)
 	defer dirF.Close()
 
 	dirSt, errno := dirF.Stat()
@@ -122,7 +122,7 @@ func TestLchown(t *testing.T) {
 	link := path.Join(tmpDir, "link")
 	require.NoError(t, os.Symlink(dir, link))
 
-	linkF := openFsFile(t, link, syscall.O_RDONLY, 0)
+	linkF := requireOpenFile(t, link, syscall.O_RDONLY, 0)
 	defer linkF.Close()
 
 	linkSt, errno := linkF.Stat()
diff --git a/internal/platform/dir_test.go b/internal/platform/dir_test.go
index fb450916..561c74ca 100644
--- a/internal/platform/dir_test.go
+++ b/internal/platform/dir_test.go
@@ -1,6 +1,7 @@
 package platform_test
 
 import (
+	"io"
 	"io/fs"
 	"os"
 	"path"
@@ -34,35 +35,25 @@ func TestReaddir(t *testing.T) {
 		tc := tc
 
 		t.Run(tc.name, func(t *testing.T) {
-			dF, err := tc.fs.Open(".")
-			require.NoError(t, err)
-			defer dF.Close()
-			dotF := platform.NewFsFile(".", 0, dF)
+			dotF, errno := platform.OpenFSFile(tc.fs, ".", syscall.O_RDONLY, 0)
+			require.EqualErrno(t, 0, errno)
+			defer dotF.Close()
 
 			t.Run("dir", func(t *testing.T) {
-				dirents, errno := dotF.Readdir(-1)
-				require.EqualErrno(t, 0, errno) // no io.EOF when -1 is used
-				sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name })
-
-				requireIno(t, dirents, tc.expectIno)
-
-				// Scrub inodes so we can compare expectations without them.
-				for i := range dirents {
-					dirents[i].Ino = 0
-				}
-
-				require.Equal(t, []platform.Dirent{
-					{Name: "animals.txt", Type: 0},
-					{Name: "dir", Type: fs.ModeDir},
-					{Name: "empty.txt", Type: 0},
-					{Name: "emptydir", Type: fs.ModeDir},
-					{Name: "sub", Type: fs.ModeDir},
-				}, dirents)
+				testReaddirAll(t, dotF, tc.expectIno)
 
 				// read again even though it is exhausted
-				dirents, errno = dotF.Readdir(100)
+				dirents, errno := dotF.Readdir(100)
 				require.EqualErrno(t, 0, errno)
 				require.Zero(t, len(dirents))
+
+				// rewind via seek to zero
+				newOffset, errno := dotF.Seek(0, io.SeekStart)
+				require.EqualErrno(t, 0, errno)
+				require.Zero(t, newOffset)
+
+				// We should be able to read again
+				testReaddirAll(t, dotF, tc.expectIno)
 			})
 
 			// Don't err if something else closed the directory while reading.
@@ -72,20 +63,18 @@ func TestReaddir(t *testing.T) {
 				require.EqualErrno(t, 0, errno)
 			})
 
-			fF, err := tc.fs.Open("empty.txt")
-			require.NoError(t, err)
-			defer fF.Close()
-			fileF := platform.NewFsFile("empty.txt", 0, fF)
+			fileF, errno := platform.OpenFSFile(tc.fs, "empty.txt", syscall.O_RDONLY, 0)
+			require.EqualErrno(t, 0, errno)
+			defer fileF.Close()
 
 			t.Run("file", func(t *testing.T) {
 				_, errno := fileF.Readdir(-1)
 				require.EqualErrno(t, syscall.ENOTDIR, errno)
 			})
 
-			dF, err = tc.fs.Open("dir")
-			require.NoError(t, err)
-			defer dF.Close()
-			dirF := platform.NewFsFile("dir", 0, dF)
+			dirF, errno := platform.OpenFSFile(tc.fs, "dir", syscall.O_RDONLY, 0)
+			require.EqualErrno(t, 0, errno)
+			defer dirF.Close()
 
 			t.Run("partial", func(t *testing.T) {
 				dirents1, errno := dirF.Readdir(1)
@@ -122,10 +111,9 @@ func TestReaddir(t *testing.T) {
 				require.EqualErrno(t, 0, errno)
 			})
 
-			sF, err := tc.fs.Open("sub")
-			require.NoError(t, err)
-			defer sF.Close()
-			subdirF := platform.NewFsFile("sub", 0, sF)
+			subdirF, errno := platform.OpenFSFile(tc.fs, "sub", syscall.O_RDONLY, 0)
+			require.EqualErrno(t, 0, errno)
+			defer subdirF.Close()
 
 			t.Run("subdir", func(t *testing.T) {
 				dirents, errno := subdirF.Readdir(-1)
@@ -141,10 +129,9 @@ func TestReaddir(t *testing.T) {
 
 	// Don't err if something else removed the directory while reading.
 	t.Run("removed while open", func(t *testing.T) {
-		dF, err := dirFS.Open("dir")
-		require.NoError(t, err)
-		defer dF.Close()
-		dirF := platform.NewFsFile("dir", 0, dF)
+		dirF, errno := platform.OpenFSFile(dirFS, "dir", syscall.O_RDONLY, 0)
+		require.EqualErrno(t, 0, errno)
+		defer dirF.Close()
 
 		dirents, errno := dirF.Readdir(1)
 		require.EqualErrno(t, 0, errno)
@@ -152,7 +139,7 @@ func TestReaddir(t *testing.T) {
 
 		// Speculatively try to remove even if it won't likely work
 		// on windows.
-		err = os.RemoveAll(path.Join(tmpDir, "dir"))
+		err := os.RemoveAll(path.Join(tmpDir, "dir"))
 		if err != nil && runtime.GOOS == "windows" {
 			t.Skip()
 		} else {
@@ -165,6 +152,27 @@ func TestReaddir(t *testing.T) {
 	})
 }
 
+func testReaddirAll(t *testing.T, dotF platform.File, expectIno bool) {
+	dirents, errno := dotF.Readdir(-1)
+	require.EqualErrno(t, 0, errno) // no io.EOF when -1 is used
+	sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name })
+
+	requireIno(t, dirents, expectIno)
+
+	// Scrub inodes so we can compare expectations without them.
+	for i := range dirents {
+		dirents[i].Ino = 0
+	}
+
+	require.Equal(t, []platform.Dirent{
+		{Name: "animals.txt", Type: 0},
+		{Name: "dir", Type: fs.ModeDir},
+		{Name: "empty.txt", Type: 0},
+		{Name: "emptydir", Type: fs.ModeDir},
+		{Name: "sub", Type: fs.ModeDir},
+	}, dirents)
+}
+
 func requireIno(t *testing.T, dirents []platform.Dirent, expectIno bool) {
 	for _, e := range dirents {
 		if expectIno {
diff --git a/internal/platform/file_test.go b/internal/platform/file_test.go
index 9e125182..fc99ff74 100644
--- a/internal/platform/file_test.go
+++ b/internal/platform/file_test.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"path"
 	"runtime"
+	"strings"
 	"syscall"
 	"testing"
 	gofstest "testing/fstest"
@@ -23,11 +24,6 @@ type NoopFile struct {
 	UnimplementedFile
 }
 
-// The current design requires the user to implement Path.
-func (NoopFile) Path() string {
-	return ""
-}
-
 // The current design requires the user to implement AccessMode.
 func (NoopFile) AccessMode() int {
 	return syscall.O_RDONLY
@@ -47,14 +43,15 @@ var (
 	emptyFile  = "empty.txt"
 )
 
-func TestFsFileSetNonblock(t *testing.T) {
+func TestFileSetNonblock(t *testing.T) {
 	// Test using os.Pipe as it is known to support non-blocking reads.
 	r, w, err := os.Pipe()
 	require.NoError(t, err)
 	defer r.Close()
 	defer w.Close()
 
-	rF := NewFsFile(wazeroFile, syscall.O_RDONLY, r)
+	rF, err := NewStdioFile(true, r)
+	require.NoError(t, err)
 
 	errno := rF.SetNonblock(true)
 	require.EqualErrno(t, 0, errno)
@@ -65,7 +62,101 @@ func TestFsFileSetNonblock(t *testing.T) {
 	require.False(t, rF.IsNonblock())
 }
 
-func TestFsFileIsDir(t *testing.T) {
+func TestFileSetAppend(t *testing.T) {
+	tmpDir := t.TempDir()
+
+	fPath := path.Join(tmpDir, "file")
+	require.NoError(t, os.WriteFile(fPath, []byte("0123456789"), 0o600))
+
+	// Open without APPEND.
+	f, errno := OpenOSFile(fPath, os.O_RDWR, 0o600)
+	require.EqualErrno(t, 0, errno)
+	require.False(t, f.IsAppend())
+
+	// Set the APPEND flag.
+	require.EqualErrno(t, 0, f.SetAppend(true))
+	require.True(t, f.IsAppend())
+
+	requireFileContent := func(exp string) {
+		buf, err := os.ReadFile(fPath)
+		require.NoError(t, err)
+		require.Equal(t, exp, string(buf))
+	}
+
+	// with O_APPEND flag, the data is appended to buffer.
+	_, errno = f.Write([]byte("wazero"))
+	require.EqualErrno(t, 0, errno)
+	requireFileContent("0123456789wazero")
+
+	// Remove the APPEND flag.
+	require.EqualErrno(t, 0, f.SetAppend(false))
+	require.False(t, f.IsAppend())
+
+	// without O_APPEND flag, the data writes at offset zero
+	_, errno = f.Write([]byte("wazero"))
+	require.EqualErrno(t, 0, errno)
+	requireFileContent("wazero6789wazero")
+}
+
+func TestFileIno(t *testing.T) {
+	tmpDir := t.TempDir()
+	dirFS, embedFS, mapFS := dirEmbedMapFS(t, tmpDir)
+
+	// get the expected inode
+	st, errno := Stat(tmpDir)
+	require.EqualErrno(t, 0, errno)
+
+	tests := []struct {
+		name        string
+		fs          fs.FS
+		expectedIno uint64
+	}{
+		{name: "os.DirFS", fs: dirFS, expectedIno: st.Ino},
+		{name: "embed.FS", fs: embedFS},
+		{name: "fstest.MapFS", fs: mapFS},
+	}
+
+	for _, tc := range tests {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			d, errno := OpenFSFile(tc.fs, ".", syscall.O_RDONLY, 0)
+			require.EqualErrno(t, 0, errno)
+			defer d.Close()
+
+			ino, errno := d.Ino()
+			require.EqualErrno(t, 0, errno)
+			if !canReadDirInode() {
+				tc.expectedIno = 0
+			}
+			require.Equal(t, tc.expectedIno, ino)
+		})
+	}
+
+	t.Run("OS", func(t *testing.T) {
+		d, errno := OpenOSFile(tmpDir, syscall.O_RDONLY, 0)
+		require.EqualErrno(t, 0, errno)
+		defer d.Close()
+
+		ino, errno := d.Ino()
+		require.EqualErrno(t, 0, errno)
+		if canReadDirInode() {
+			require.Equal(t, st.Ino, ino)
+		} else {
+			require.Zero(t, ino)
+		}
+	})
+}
+
+func canReadDirInode() bool {
+	if runtime.GOOS != "windows" {
+		return true
+	} else {
+		return strings.HasPrefix(runtime.Version(), "go1.20")
+	}
+}
+
+func TestFileIsDir(t *testing.T) {
 	dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
 
 	tests := []struct {
@@ -82,35 +173,39 @@ func TestFsFileIsDir(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			t.Run("file", func(t *testing.T) {
-				f, err := tc.fs.Open(wazeroFile)
-				require.NoError(t, err)
+				f, errno := OpenFSFile(tc.fs, wazeroFile, syscall.O_RDONLY, 0)
+				require.EqualErrno(t, 0, errno)
 				defer f.Close()
 
-				fsF := NewFsFile(wazeroFile, syscall.O_RDONLY, f)
-
-				isDir, errno := fsF.IsDir()
+				isDir, errno := f.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(wazeroFile, syscall.O_RDONLY, f)
+				d, errno := OpenFSFile(tc.fs, ".", syscall.O_RDONLY, 0)
+				require.EqualErrno(t, 0, errno)
+				defer d.Close()
 
-				isDir, errno := fsF.IsDir()
+				isDir, errno := d.IsDir()
 				require.EqualErrno(t, 0, errno)
 				require.True(t, isDir)
-				require.Equal(t, &cachedStat{fileType: fs.ModeDir}, fsF.(*fsFile).cachedSt)
 			})
 		})
 	}
+
+	t.Run("OS dir", func(t *testing.T) {
+		d, errno := OpenOSFile(t.TempDir(), syscall.O_RDONLY, 0)
+		require.EqualErrno(t, 0, errno)
+		defer d.Close()
+
+		isDir, errno := d.IsDir()
+		require.EqualErrno(t, 0, errno)
+		require.True(t, isDir)
+	})
 }
 
-func TestFsFileReadAndPread(t *testing.T) {
+func TestFileReadAndPread(t *testing.T) {
 	dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
 
 	tests := []struct {
@@ -128,42 +223,41 @@ func TestFsFileReadAndPread(t *testing.T) {
 		tc := tc
 
 		t.Run(tc.name, func(t *testing.T) {
-			f, err := tc.fs.Open(wazeroFile)
-			require.NoError(t, err)
+			f, errno := OpenFSFile(tc.fs, wazeroFile, syscall.O_RDONLY, 0)
+			require.EqualErrno(t, 0, errno)
 			defer f.Close()
 
-			fs := NewFsFile(wazeroFile, syscall.O_RDONLY, f)
-
 			// The file should be readable (base case)
-			requireRead(t, fs, buf)
+			requireRead(t, f, buf)
 			require.Equal(t, "waz", string(buf))
 			buf = buf[:]
 
 			// We should be able to pread from zero also
-			requirePread(t, fs, buf, 0)
+			requirePread(t, f, buf, 0)
 			require.Equal(t, "waz", string(buf))
 			buf = buf[:]
 
 			// If the offset didn't change, read should expect the next three chars.
-			requireRead(t, fs, buf)
+			requireRead(t, f, buf)
 			require.Equal(t, "ero", string(buf))
 			buf = buf[:]
 
 			// We should also be able pread from any offset
-			requirePread(t, fs, buf, 2)
+			requirePread(t, f, buf, 2)
 			require.Equal(t, "zer", string(buf))
 		})
 	}
 }
 
-func TestFsFilePollRead(t *testing.T) {
+func TestFilePollRead(t *testing.T) {
 	// Test using os.Pipe as it is known to support poll.
 	r, w, err := os.Pipe()
 	require.NoError(t, err)
 	defer r.Close()
 	defer w.Close()
 
-	rF := NewFsFile(wazeroFile, syscall.O_RDONLY, r)
+	rF, err := NewStdioFile(true, r)
+	require.NoError(t, err)
 	buf := make([]byte, 10)
 	timeout := time.Duration(0) // return immediately
 
@@ -205,7 +299,7 @@ func requirePread(t *testing.T, f File, buf []byte, off int64) {
 	require.Equal(t, len(buf), n)
 }
 
-func TestFsFileRead_empty(t *testing.T) {
+func TestFileRead_empty(t *testing.T) {
 	dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
 
 	tests := []struct {
@@ -223,21 +317,19 @@ func TestFsFileRead_empty(t *testing.T) {
 		tc := tc
 
 		t.Run(tc.name, func(t *testing.T) {
-			f, err := tc.fs.Open(emptyFile)
-			require.NoError(t, err)
+			f, errno := OpenFSFile(tc.fs, emptyFile, syscall.O_RDONLY, 0)
+			require.EqualErrno(t, 0, errno)
 			defer f.Close()
 
-			fs := NewFsFile(wazeroFile, syscall.O_RDONLY, f)
-
 			t.Run("Read", func(t *testing.T) {
 				// We should be able to read an empty file
-				n, errno := fs.Read(buf)
+				n, errno := f.Read(buf)
 				require.EqualErrno(t, 0, errno)
 				require.Zero(t, n)
 			})
 
 			t.Run("Pread", func(t *testing.T) {
-				n, errno := fs.Pread(buf, 0)
+				n, errno := f.Pread(buf, 0)
 				require.EqualErrno(t, 0, errno)
 				require.Zero(t, n)
 			})
@@ -245,34 +337,35 @@ func TestFsFileRead_empty(t *testing.T) {
 	}
 }
 
-func TestFsFilePread_Unsupported(t *testing.T) {
+type maskFS struct {
+	fs.FS
+}
+
+func (m *maskFS) Open(name string) (fs.File, error) {
+	f, err := m.FS.Open(name)
+	return struct{ fs.File }{f}, err
+}
+
+func TestFilePread_Unsupported(t *testing.T) {
 	embedFS, err := fs.Sub(testdata, "testdata")
 	require.NoError(t, err)
 
-	f, err := embedFS.Open(emptyFile)
-	require.NoError(t, err)
+	f, errno := OpenFSFile(&maskFS{embedFS}, emptyFile, syscall.O_RDONLY, 0)
+	require.EqualErrno(t, 0, errno)
 	defer f.Close()
 
-	// mask both io.ReaderAt and io.Seeker
-	f = struct{ fs.File }{f}
-
-	fs := NewFsFile(wazeroFile, syscall.O_RDONLY, f)
-
 	buf := make([]byte, 3)
-	_, errno := fs.Pread(buf, 0)
+	_, errno = f.Pread(buf, 0)
 	require.EqualErrno(t, syscall.ENOSYS, errno)
 }
 
-func TestFsFileRead_Errors(t *testing.T) {
+func TestFileRead_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)
+	flag := syscall.O_WRONLY | syscall.O_CREAT
+	f := requireOpenFile(t, path, flag, 0o600)
 	defer f.Close()
 	buf := make([]byte, 5)
 
@@ -304,7 +397,7 @@ func TestFsFileRead_Errors(t *testing.T) {
 	}
 }
 
-func TestFsFileSeek(t *testing.T) {
+func TestFileSeek(t *testing.T) {
 	dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
 
 	tests := []struct {
@@ -322,58 +415,72 @@ func TestFsFileSeek(t *testing.T) {
 		tc := tc
 
 		t.Run(tc.name, func(t *testing.T) {
-			f, err := tc.fs.Open(wazeroFile)
-			require.NoError(t, err)
+			f, errno := OpenFSFile(tc.fs, wazeroFile, syscall.O_RDONLY, 0)
+			require.EqualErrno(t, 0, errno)
 			defer f.Close()
 
-			fs := NewFsFile(wazeroFile, syscall.O_RDONLY, f)
-
 			// Shouldn't be able to use an invalid whence
-			_, errno := fs.Seek(0, io.SeekEnd+1)
+			_, errno = f.Seek(0, io.SeekEnd+1)
 			require.EqualErrno(t, syscall.EINVAL, errno)
-			_, errno = fs.Seek(0, -1)
+			_, errno = f.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)
+			_, errno = f.Seek(-1, io.SeekStart)
 			require.EqualErrno(t, syscall.EINVAL, errno)
 
-			requireRead(t, fs, buf) // read 3 bytes
+			requireRead(t, f, buf) // read 3 bytes
 
 			// Seek to the start
-			newOffset, errno := fs.Seek(0, io.SeekStart)
+			newOffset, errno := f.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
+			requireRead(t, f, 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)
+			newOffset, errno = f.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)
+			newOffset, errno = f.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)
+			n, errno := f.Read(buf)
 			require.EqualErrno(t, 0, errno)
 			require.Equal(t, 2, n)
 			require.Equal(t, "o\n", string(buf[:2]))
+
+			t.Run("directory seek to zero", func(t *testing.T) {
+				d, errno := OpenFSFile(tc.fs, ".", syscall.O_RDONLY, 0)
+				require.EqualErrno(t, 0, errno)
+				defer d.Close()
+
+				_, errno = d.Seek(0, io.SeekStart)
+				require.EqualErrno(t, 0, errno)
+			})
 		})
 	}
 
+	t.Run("os.File directory seek to zero", func(t *testing.T) {
+		d := requireOpenFile(t, os.TempDir(), syscall.O_RDONLY|O_DIRECTORY, 0o666)
+		defer d.Close()
+
+		_, errno := d.Seek(0, io.SeekStart)
+		require.EqualErrno(t, 0, errno)
+	})
+
 	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 {
@@ -382,7 +489,7 @@ func requireSeek(t *testing.T, f File, off int64, whence int) int64 {
 	return n
 }
 
-func TestFsFileSeek_empty(t *testing.T) {
+func TestFileSeek_empty(t *testing.T) {
 	dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
 
 	tests := []struct {
@@ -398,49 +505,42 @@ func TestFsFileSeek_empty(t *testing.T) {
 		tc := tc
 
 		t.Run(tc.name, func(t *testing.T) {
-			f, err := tc.fs.Open(emptyFile)
-			require.NoError(t, err)
+			f, errno := OpenFSFile(tc.fs, emptyFile, syscall.O_RDONLY, 0)
+			require.EqualErrno(t, 0, errno)
 			defer f.Close()
 
-			fs := NewFsFile(wazeroFile, syscall.O_RDONLY, f)
-
 			t.Run("Start", func(t *testing.T) {
-				require.Zero(t, requireSeek(t, fs, 0, io.SeekStart))
+				require.Zero(t, requireSeek(t, f, 0, io.SeekStart))
 			})
 
 			t.Run("Current", func(t *testing.T) {
-				require.Zero(t, requireSeek(t, fs, 0, io.SeekCurrent))
+				require.Zero(t, requireSeek(t, f, 0, io.SeekCurrent))
 			})
 
 			t.Run("End", func(t *testing.T) {
-				require.Zero(t, requireSeek(t, fs, 0, io.SeekEnd))
+				require.Zero(t, requireSeek(t, f, 0, io.SeekEnd))
 			})
 		})
 	}
 }
 
-func TestFsFileSeek_Unsupported(t *testing.T) {
+func TestFileSeek_Unsupported(t *testing.T) {
 	embedFS, err := fs.Sub(testdata, "testdata")
 	require.NoError(t, err)
 
-	f, err := embedFS.Open(emptyFile)
-	require.NoError(t, err)
+	f, errno := OpenFSFile(&maskFS{embedFS}, emptyFile, syscall.O_RDONLY, 0)
+	require.EqualErrno(t, 0, errno)
 	defer f.Close()
 
-	// mask io.Seeker
-	f = struct{ fs.File }{f}
-
-	fs := NewFsFile(wazeroFile, syscall.O_RDONLY, f)
-
-	_, errno := fs.Seek(0, io.SeekCurrent)
+	_, errno = f.Seek(0, io.SeekCurrent)
 	require.EqualErrno(t, syscall.ENOSYS, errno)
 }
 
-func TestFsFileWriteAndPwrite(t *testing.T) {
+func TestFileWriteAndPwrite(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(), wazeroFile)
-	f := openFsFile(t, path, syscall.O_RDWR|os.O_CREATE, 0o600)
+	f := requireOpenFile(t, path, syscall.O_RDWR|os.O_CREATE, 0o600)
 	defer f.Close()
 
 	text := "wazero"
@@ -489,11 +589,11 @@ func requirePwrite(t *testing.T, f File, buf []byte, off int64) {
 	require.Equal(t, len(buf), n)
 }
 
-func TestFsFileWrite_empty(t *testing.T) {
+func TestFileWrite_empty(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(), emptyFile)
-	f := openFsFile(t, path, syscall.O_RDWR|os.O_CREATE, 0o600)
+	f := requireOpenFile(t, path, syscall.O_RDWR|os.O_CREATE, 0o600)
 	defer f.Close()
 
 	tests := []struct {
@@ -529,12 +629,13 @@ func TestFsFileWrite_empty(t *testing.T) {
 	}
 }
 
-func TestFsFileWrite_Unsupported(t *testing.T) {
+func TestFileWrite_Unsupported(t *testing.T) {
 	embedFS, err := fs.Sub(testdata, "testdata")
 	require.NoError(t, err)
 
-	f, err := embedFS.Open(wazeroFile)
-	require.NoError(t, err)
+	// Use syscall.O_RDWR so that it fails due to type not flags
+	f, errno := OpenFSFile(&maskFS{embedFS}, wazeroFile, syscall.O_RDWR, 0)
+	require.EqualErrno(t, 0, errno)
 	defer f.Close()
 
 	tests := []struct {
@@ -555,15 +656,13 @@ func TestFsFileWrite_Unsupported(t *testing.T) {
 		tc := tc
 
 		t.Run(tc.name, func(t *testing.T) {
-			// Use syscall.O_RDWR so that it fails due to type not flags
-			f := NewFsFile(wazeroFile, syscall.O_RDWR, f)
 			_, errno := tc.fn(f, buf)
 			require.EqualErrno(t, syscall.ENOSYS, errno)
 		})
 	}
 }
 
-func TestFsFileWrite_Errors(t *testing.T) {
+func TestFileWrite_Errors(t *testing.T) {
 	// Create the file
 	path := path.Join(t.TempDir(), emptyFile)
 	of, err := os.Create(path)
@@ -572,7 +671,7 @@ func TestFsFileWrite_Errors(t *testing.T) {
 
 	// Open the file read-only
 	flag := syscall.O_RDONLY
-	f := openFsFile(t, path, flag, 0o600)
+	f := requireOpenFile(t, path, flag, 0o600)
 	defer f.Close()
 	buf := []byte("wazero")
 
@@ -604,41 +703,32 @@ func TestFsFileWrite_Errors(t *testing.T) {
 	}
 }
 
-func TestFsFileSync_NoError(t *testing.T) {
+func TestFileSync_NoError(t *testing.T) {
 	testSync_NoError(t, File.Sync)
 }
 
-func TestFsFileDatasync_NoError(t *testing.T) {
+func TestFileDatasync_NoError(t *testing.T) {
 	testSync_NoError(t, File.Datasync)
 }
 
 func testSync_NoError(t *testing.T, sync func(File) syscall.Errno) {
 	roPath := "file_test.go"
-	ro, err := embedFS.Open(roPath)
-	require.NoError(t, err)
+	ro, errno := OpenFSFile(embedFS, roPath, syscall.O_RDONLY, 0)
+	require.EqualErrno(t, 0, errno)
 	defer ro.Close()
 
 	rwPath := path.Join(t.TempDir(), "datasync")
-	rw, err := os.Create(rwPath)
-	require.NoError(t, err)
+	rw, errno := OpenOSFile(rwPath, syscall.O_CREAT|syscall.O_RDWR, 0o600)
+	require.EqualErrno(t, 0, errno)
 	defer rw.Close()
 
 	tests := []struct {
 		name string
 		f    File
 	}{
-		{
-			name: "UnimplementedFile",
-			f:    NoopFile{},
-		},
-		{
-			name: "File of read-only fs.File",
-			f:    NewFsFile(roPath, syscall.O_RDONLY, ro),
-		},
-		{
-			name: "File of os.File",
-			f:    NewFsFile(rwPath, syscall.O_RDWR, rw),
-		},
+		{name: "UnimplementedFile", f: NoopFile{}},
+		{name: "File of read-only fs.File", f: ro},
+		{name: "File of os.File", f: rw},
 	}
 
 	for _, tt := range tests {
@@ -650,11 +740,11 @@ func testSync_NoError(t *testing.T, sync func(File) syscall.Errno) {
 	}
 }
 
-func TestFsFileSync(t *testing.T) {
+func TestFileSync(t *testing.T) {
 	testSync(t, File.Sync)
 }
 
-func TestFsFileDatasync(t *testing.T) {
+func TestFileDatasync(t *testing.T) {
 	testSync(t, File.Datasync)
 }
 
@@ -662,18 +752,17 @@ func TestFsFileDatasync(t *testing.T) {
 // sync anyway. There is no test in Go for syscall.Fdatasync, but closest is
 // similar to below. Effectively, this only tests that things don't error.
 func testSync(t *testing.T, sync func(File) syscall.Errno) {
+	// Even though it is invalid, try to sync a directory
 	dPath := t.TempDir()
-	d, err := os.Open(dPath)
-	require.NoError(t, err)
+	d := requireOpenFile(t, dPath, syscall.O_RDONLY, 0)
 	defer d.Close()
 
-	// Even though it is invalid, try to sync a directory
-	errno := sync(NewFsFile(dPath, syscall.O_RDONLY, d))
+	errno := sync(d)
 	require.EqualErrno(t, 0, errno)
 
 	fPath := path.Join(dPath, t.Name())
 
-	f := openFsFile(t, fPath, syscall.O_RDWR|os.O_CREATE, 0o600)
+	f := requireOpenFile(t, fPath, syscall.O_RDWR|os.O_CREATE, 0o600)
 	defer f.Close()
 
 	expected := "hello world!"
@@ -705,7 +794,7 @@ func testSync(t *testing.T, sync func(File) syscall.Errno) {
 	}
 }
 
-func TestFsFileTruncate(t *testing.T) {
+func TestFileTruncate(t *testing.T) {
 	content := []byte("123456")
 
 	tests := []struct {
@@ -741,13 +830,14 @@ func TestFsFileTruncate(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			tmpDir := t.TempDir()
 
-			f := openForWrite(t, path.Join(tmpDir, tc.name), content)
+			fPath := path.Join(tmpDir, tc.name)
+			f := openForWrite(t, fPath, content)
 			defer f.Close()
 
 			errno := f.Truncate(tc.size)
 			require.EqualErrno(t, 0, errno)
 
-			actual, err := os.ReadFile(f.Path())
+			actual, err := os.ReadFile(fPath)
 			require.NoError(t, err)
 			require.Equal(t, tc.expectedContent, actual)
 		})
@@ -775,7 +865,7 @@ func TestFsFileTruncate(t *testing.T) {
 	})
 }
 
-func TestFsFileUtimens(t *testing.T) {
+func TestFileUtimens(t *testing.T) {
 	switch runtime.GOOS {
 	case "linux", "darwin": // supported
 	case "freebsd": // TODO: support freebsd w/o CGO
@@ -878,7 +968,7 @@ func TestNewStdioFile(t *testing.T) {
 
 func testEBADFIfDirClosed(t *testing.T, fn func(File) syscall.Errno) bool {
 	return t.Run("EBADF if dir closed", func(t *testing.T) {
-		d := openFsFile(t, t.TempDir(), syscall.O_RDONLY, 0o755)
+		d := requireOpenFile(t, t.TempDir(), syscall.O_RDONLY, 0o755)
 
 		// close the directory underneath
 		require.EqualErrno(t, 0, d.Close())
@@ -902,7 +992,7 @@ func testEBADFIfFileClosed(t *testing.T, fn func(File) syscall.Errno) bool {
 
 func testEISDIR(t *testing.T, fn func(File) syscall.Errno) bool {
 	return t.Run("EISDIR if directory", func(t *testing.T) {
-		f := openFsFile(t, os.TempDir(), syscall.O_RDONLY|O_DIRECTORY, 0o666)
+		f := requireOpenFile(t, os.TempDir(), syscall.O_RDONLY|O_DIRECTORY, 0o666)
 		defer f.Close()
 
 		require.EqualErrno(t, syscall.EISDIR, fn(f))
@@ -910,14 +1000,14 @@ func testEISDIR(t *testing.T, fn func(File) syscall.Errno) bool {
 }
 
 func openForWrite(t *testing.T, path string, content []byte) File {
-	require.NoError(t, os.WriteFile(path, content, 0o0600))
-	return openFsFile(t, path, syscall.O_RDWR, 0o666)
+	require.NoError(t, os.WriteFile(path, content, 0o0666))
+	return requireOpenFile(t, path, syscall.O_RDWR, 0o666)
 }
 
-func openFsFile(t *testing.T, path string, flag int, perm fs.FileMode) File {
-	f, errno := OpenFile(path, flag, perm)
+func requireOpenFile(t *testing.T, path string, flag int, perm fs.FileMode) File {
+	f, errno := OpenOSFile(path, flag, perm)
 	require.EqualErrno(t, 0, errno)
-	return NewFsFile(path, flag, f)
+	return f
 }
 
 func dirEmbedMapFS(t *testing.T, tmpDir string) (fs.FS, fs.FS, fs.FS) {
diff --git a/internal/platform/futimens_test.go b/internal/platform/futimens_test.go
index 538649ef..a33636c1 100644
--- a/internal/platform/futimens_test.go
+++ b/internal/platform/futimens_test.go
@@ -167,7 +167,7 @@ func testUtimens(t *testing.T, futimes bool) {
 						}
 					}
 
-					f := openFsFile(t, path, flag, 0)
+					f := requireOpenFile(t, path, flag, 0)
 
 					errno = f.Utimens(tc.times)
 					require.EqualErrno(t, 0, f.Close())
diff --git a/internal/platform/open_file_test.go b/internal/platform/open_file_test.go
index c93fe072..03954dde 100644
--- a/internal/platform/open_file_test.go
+++ b/internal/platform/open_file_test.go
@@ -69,7 +69,7 @@ func TestOpenFile_Errors(t *testing.T) {
 		path := path.Join(tmpDir, "file")
 		require.NoError(t, os.WriteFile(path, nil, 0o600))
 
-		f := openFsFile(t, path, os.O_RDONLY, 0)
+		f := requireOpenFile(t, path, os.O_RDONLY, 0)
 		defer f.Close()
 
 		_, errno := f.Write([]byte{1, 2, 3, 4})
@@ -80,7 +80,7 @@ func TestOpenFile_Errors(t *testing.T) {
 		path := path.Join(tmpDir, "diragain")
 		require.NoError(t, os.Mkdir(path, 0o755))
 
-		f := openFsFile(t, path, os.O_RDONLY, 0)
+		f := requireOpenFile(t, path, os.O_RDONLY, 0)
 		defer f.Close()
 
 		_, errno := f.Write([]byte{1, 2, 3, 4})
@@ -101,4 +101,12 @@ func TestOpenFile_Errors(t *testing.T) {
 		_, errno = OpenFile(symlink, O_NOFOLLOW, 0o0666)
 		require.EqualErrno(t, syscall.ELOOP, errno)
 	})
+
+	t.Run("opening a directory writeable is EISDIR", func(t *testing.T) {
+		_, errno := OpenFile(tmpDir, O_DIRECTORY|syscall.O_WRONLY, 0o0666)
+		require.EqualErrno(t, syscall.EISDIR, errno)
+
+		_, errno = OpenFile(tmpDir, O_DIRECTORY|syscall.O_WRONLY, 0o0666)
+		require.EqualErrno(t, syscall.EISDIR, errno)
+	})
 }
diff --git a/internal/platform/stat_test.go b/internal/platform/stat_test.go
index 4c5b6975..2b0bb837 100644
--- a/internal/platform/stat_test.go
+++ b/internal/platform/stat_test.go
@@ -158,7 +158,7 @@ func TestStat(t *testing.T) {
 func TestStatFile(t *testing.T) {
 	tmpDir := t.TempDir()
 
-	tmpDirF := openFsFile(t, tmpDir, syscall.O_RDONLY, 0)
+	tmpDirF := requireOpenFile(t, tmpDir, syscall.O_RDONLY, 0)
 	defer tmpDirF.Close()
 
 	t.Run("dir", func(t *testing.T) {
@@ -181,7 +181,7 @@ func TestStatFile(t *testing.T) {
 
 	file := path.Join(tmpDir, "file")
 	require.NoError(t, os.WriteFile(file, nil, 0o400))
-	fileF := openFsFile(t, file, syscall.O_RDONLY, 0)
+	fileF := requireOpenFile(t, file, syscall.O_RDONLY, 0)
 	defer fileF.Close()
 
 	t.Run("file", func(t *testing.T) {
@@ -200,7 +200,7 @@ func TestStatFile(t *testing.T) {
 
 	subdir := path.Join(tmpDir, "sub")
 	require.NoError(t, os.Mkdir(subdir, 0o500))
-	subdirF := openFsFile(t, subdir, syscall.O_RDONLY, 0)
+	subdirF := requireOpenFile(t, subdir, syscall.O_RDONLY, 0)
 	defer subdirF.Close()
 
 	t.Run("subdir", func(t *testing.T) {
@@ -259,7 +259,7 @@ func Test_StatFile_times(t *testing.T) {
 			err := os.Chtimes(file, time.UnixMicro(tc.atimeNsec/1e3), time.UnixMicro(tc.mtimeNsec/1e3))
 			require.NoError(t, err)
 
-			f := openFsFile(t, file, syscall.O_RDONLY, 0)
+			f := requireOpenFile(t, file, syscall.O_RDONLY, 0)
 			defer f.Close()
 
 			st, errno := f.Stat()
@@ -273,21 +273,21 @@ func Test_StatFile_times(t *testing.T) {
 
 func TestStatFile_dev_inode(t *testing.T) {
 	tmpDir := t.TempDir()
-	d := openFsFile(t, tmpDir, os.O_RDONLY, 0)
+	d := requireOpenFile(t, tmpDir, os.O_RDONLY, 0)
 	defer d.Close()
 
 	path1 := path.Join(tmpDir, "1")
-	f1 := openFsFile(t, path1, os.O_CREATE, 0o666)
+	f1 := requireOpenFile(t, path1, os.O_CREATE, 0o666)
 	defer f1.Close()
 
 	path2 := path.Join(tmpDir, "2")
-	f2 := openFsFile(t, path2, os.O_CREATE, 0o666)
+	f2 := requireOpenFile(t, path2, os.O_CREATE, 0o666)
 	defer f2.Close()
 
 	pathLink2 := path.Join(tmpDir, "link2")
 	err := os.Symlink(path2, pathLink2)
 	require.NoError(t, err)
-	l2 := openFsFile(t, pathLink2, os.O_RDONLY, 0)
+	l2 := requireOpenFile(t, pathLink2, os.O_RDONLY, 0)
 	defer l2.Close()
 
 	// First, stat the directory
@@ -324,7 +324,7 @@ func TestStatFile_dev_inode(t *testing.T) {
 
 	// Renaming a file shouldn't change its inodes.
 	require.EqualErrno(t, 0, Rename(path1, path2))
-	f1 = openFsFile(t, path2, os.O_RDONLY, 0)
+	f1 = requireOpenFile(t, path2, os.O_RDONLY, 0)
 	defer f1.Close()
 
 	st1Again, errno = f1.Stat()
@@ -360,7 +360,7 @@ func TestStat_uid_gid(t *testing.T) {
 	t.Run("Stat", func(t *testing.T) {
 		tmpDir := t.TempDir()
 		dir := path.Join(tmpDir, "dir")
-		require.NoError(t, os.Mkdir(dir, 0o0700))
+		require.NoError(t, os.Mkdir(dir, 0o0777))
 		require.EqualErrno(t, 0, chgid(dir, gid))
 
 		st, errno := Stat(dir)
@@ -386,7 +386,7 @@ func TestStat_uid_gid(t *testing.T) {
 	t.Run("StatFile", func(t *testing.T) {
 		tmpDir := t.TempDir()
 		file := path.Join(tmpDir, "file")
-		require.NoError(t, os.WriteFile(file, nil, 0o0600))
+		require.NoError(t, os.WriteFile(file, nil, 0o0666))
 		require.EqualErrno(t, 0, chgid(file, gid))
 
 		st, errno := Lstat(file)
diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go
index 36dd7876..3f3accd6 100644
--- a/internal/sys/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -5,12 +5,10 @@ import (
 	"errors"
 	"io/fs"
 	"os"
-	"path"
 	"syscall"
 	"testing"
 	"testing/fstest"
 
-	"github.com/tetratelabs/wazero/internal/platform"
 	"github.com/tetratelabs/wazero/internal/sysfs"
 	testfs "github.com/tetratelabs/wazero/internal/testing/fs"
 	"github.com/tetratelabs/wazero/internal/testing/require"
@@ -225,55 +223,6 @@ func TestContext_Close_Error(t *testing.T) {
 	require.Zero(t, fsc.openedFiles.Len(), "expected no opened files")
 }
 
-func TestFSContext_ReOpenDir(t *testing.T) {
-	tmpDir := t.TempDir()
-	dirFs := sysfs.NewDirFS(tmpDir)
-
-	const dirName = "dir"
-	errno := dirFs.Mkdir(dirName, 0o700)
-	require.EqualErrno(t, 0, errno)
-
-	c := Context{}
-	err := c.NewFSContext(nil, nil, nil, dirFs)
-	require.NoError(t, err)
-	fsc := c.fsc
-
-	require.NoError(t, err)
-	defer fsc.Close()
-
-	t.Run("ok", func(t *testing.T) {
-		dirFd, errno := fsc.OpenFile(dirFs, dirName, os.O_RDONLY, 0o600)
-		require.EqualErrno(t, 0, errno)
-
-		ent, ok := fsc.LookupFile(dirFd)
-		require.True(t, ok)
-
-		// Set arbitrary state.
-		ent.ReadDir = &ReadDir{Dirents: make([]platform.Dirent, 10), CountRead: 12345}
-
-		// Then reopen the same file descriptor.
-		ent, errno = fsc.ReOpenDir(dirFd)
-		require.EqualErrno(t, 0, errno)
-
-		// Verify the read dir state has been reset.
-		require.Equal(t, &ReadDir{}, ent.ReadDir)
-	})
-
-	t.Run("non existing ", func(t *testing.T) {
-		_, errno = fsc.ReOpenDir(12345)
-		require.EqualErrno(t, syscall.EBADF, errno)
-	})
-
-	t.Run("not dir", func(t *testing.T) {
-		const fileName = "dog"
-		fd, errno := fsc.OpenFile(dirFs, fileName, os.O_CREATE, 0o600)
-		require.EqualErrno(t, 0, errno)
-
-		_, errno = fsc.ReOpenDir(fd)
-		require.EqualErrno(t, syscall.ENOTDIR, errno)
-	})
-}
-
 func TestFSContext_Renumber(t *testing.T) {
 	tmpDir := t.TempDir()
 	dirFs := sysfs.NewDirFS(tmpDir)
@@ -325,59 +274,6 @@ func TestFSContext_Renumber(t *testing.T) {
 	})
 }
 
-func TestFSContext_ChangeOpenFlag(t *testing.T) {
-	tmpDir := t.TempDir()
-	dirFs := sysfs.NewDirFS(tmpDir)
-
-	const fileName = "file"
-	require.NoError(t, os.WriteFile(path.Join(tmpDir, fileName), []byte("0123456789"), 0o600))
-
-	c := Context{}
-	err := c.NewFSContext(nil, nil, nil, dirFs)
-	require.NoError(t, err)
-	fsc := c.fsc
-
-	defer func() {
-		require.NoError(t, fsc.Close())
-	}()
-
-	// Without APPEND.
-	fd, errno := fsc.OpenFile(dirFs, fileName, os.O_RDWR, 0o600)
-	require.EqualErrno(t, 0, errno)
-
-	f0, ok := fsc.openedFiles.Lookup(fd)
-	require.True(t, ok)
-	require.Equal(t, f0.openFlag&syscall.O_APPEND, 0)
-
-	// Set the APPEND flag.
-	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.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)
-
-	t.Run("create exclusive", func(t *testing.T) {
-		// O_EXCL triggers an EEXIST error if called a second time with
-		// O_CREATE. This test proves the internal logic clears O_CREATE on
-		// re-open.
-		const fileName = "exclusive"
-		fd, errno := fsc.OpenFile(dirFs, fileName, os.O_RDWR|os.O_CREATE|syscall.O_EXCL, 0o600)
-		require.EqualErrno(t, 0, errno)
-
-		errno = fsc.ChangeOpenFlag(fd, syscall.O_APPEND)
-		require.EqualErrno(t, 0, errno)
-
-		f1, ok := fsc.openedFiles.Lookup(fd)
-		require.True(t, ok)
-		require.Equal(t, f1.openFlag&syscall.O_APPEND, syscall.O_APPEND)
-	})
-}
-
 func TestStdio(t *testing.T) {
 	// simulate regular file attached to stdin
 	f, err := os.CreateTemp(t.TempDir(), "somefile")
@@ -475,7 +371,7 @@ func TestStdio(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name+" Stat", func(t *testing.T) {
-			st, errno := tc.f.Stat()
+			st, errno := tc.f.File.Stat()
 			require.EqualErrno(t, 0, errno)
 			require.Equal(t, tc.expectedType, st.Mode&fs.ModeType)
 			require.Equal(t, uint64(1), st.Nlink)
diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go
index 24119687..5561c4ff 100644
--- a/internal/sys/sys_test.go
+++ b/internal/sys/sys_test.go
@@ -2,8 +2,6 @@ package sys
 
 import (
 	"bytes"
-	"runtime"
-	"strings"
 	"testing"
 	"time"
 
@@ -69,56 +67,6 @@ func TestDefaultSysContext(t *testing.T) {
 	require.Equal(t, expected, sysCtx.FS().openedFiles)
 }
 
-func TestFileEntryInode(t *testing.T) {
-	t.Parallel()
-
-	tmpDir := t.TempDir()
-	require.NoError(t, fstest.WriteTestFiles(tmpDir))
-	dirFS := sysfs.NewDirFS(tmpDir)
-
-	// get the expected inode
-	st, errno := platform.Stat(tmpDir)
-	require.EqualErrno(t, 0, errno)
-
-	tests := []struct {
-		name        string
-		fs          sysfs.FS
-		expectedIno uint64
-	}{
-		{name: "sysfs.FS", fs: dirFS, expectedIno: st.Ino},
-		{name: "fs.FS", fs: sysfs.Adapt(fstest.FS), expectedIno: 0},
-	}
-
-	for _, tc := range tests {
-		tc := tc
-
-		t.Run(tc.name, func(t *testing.T) {
-			c := Context{}
-			_ = c.NewFSContext(nil, nil, nil, tc.fs)
-			fsc := c.fsc
-			defer fsc.Close()
-
-			f, ok := fsc.LookupFile(FdPreopen)
-			require.True(t, ok)
-			ino, errno := f.Inode()
-			require.EqualErrno(t, 0, errno)
-			if !canReadDirInode() {
-				tc.expectedIno = 0
-			}
-			require.Equal(t, tc.expectedIno, ino)
-			require.Equal(t, &cachedStat{Ino: tc.expectedIno}, f.cachedStat)
-		})
-	}
-}
-
-func canReadDirInode() bool {
-	if runtime.GOOS != "windows" {
-		return true
-	} else {
-		return strings.HasPrefix(runtime.Version(), "go1.20")
-	}
-}
-
 func TestNewContext_Args(t *testing.T) {
 	tests := []struct {
 		name         string
diff --git a/internal/sysfs/dirfs_test.go b/internal/sysfs/dirfs_test.go
index 64af1e94..633e4393 100644
--- a/internal/sysfs/dirfs_test.go
+++ b/internal/sysfs/dirfs_test.go
@@ -758,7 +758,7 @@ func TestDirFS_Truncate(t *testing.T) {
 
 			name := "truncate"
 			realPath := path.Join(tmpDir, name)
-			require.NoError(t, os.WriteFile(realPath, content, 0o0600))
+			require.NoError(t, os.WriteFile(realPath, content, 0o0666))
 
 			errno := testFS.Truncate(name, tc.size)
 			require.EqualErrno(t, 0, errno)
diff --git a/internal/sysfs/sysfs_test.go b/internal/sysfs/sysfs_test.go
index 16ca0e2d..0be0eb84 100644
--- a/internal/sysfs/sysfs_test.go
+++ b/internal/sysfs/sysfs_test.go
@@ -189,19 +189,15 @@ 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 f.Close()
 		require.EqualErrno(t, 0, errno)
+		defer f.Close()
 
 		_, errno = f.Write([]byte{1, 2, 3, 4})
 		require.EqualErrno(t, syscall.EBADF, errno)
 	})
 
-	t.Run("writing to a directory is EISDIR", func(t *testing.T) {
-		f, errno := testFS.OpenFile("sub", os.O_RDONLY, 0)
-		defer f.Close()
-		require.EqualErrno(t, 0, errno)
-
-		_, errno = f.Write([]byte{1, 2, 3, 4})
+	t.Run("opening a directory with O_RDWR is EISDIR", func(t *testing.T) {
+		_, errno := testFS.OpenFile("sub", platform.O_DIRECTORY|os.O_RDWR, 0)
 		require.EqualErrno(t, syscall.EISDIR, errno)
 	})
 }