Repository: tetratelabs/wazero
Pull request #1432 (2023-05-03 13:47:26)

compiler: update RATIONALE.md for native check
Relates to #1409, #1413. Explains why the native check won't work.

Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>



Patch:
diff --git a/internal/engine/compiler/RATIONALE.md b/internal/engine/compiler/RATIONALE.md
index 0fcea3a7..2db59e72 100644
--- a/internal/engine/compiler/RATIONALE.md
+++ b/internal/engine/compiler/RATIONALE.md
@@ -75,3 +75,19 @@ and as of Go 1.20, these assembler functions are considered as _unsafe_ for asyn
 
 From the Go runtime point of view, the execution of runtime-generated machine codes is considered as a part of
 that trampoline function. Therefore, runtime-generated machine code is also correctly considered unsafe for async preemption.
+
+## Why context cancellation is handled in Go code rather than native code
+
+Since [wazero v1.0.0-pre.9](https://github.com/tetratelabs/wazero/releases/tag/v1.0.0-pre.9), the runtime
+supports integration with Go contexts to interrupt execution after a timeout, or in response to explicit cancellation.
+This support is internally implemented as a special opcode `builtinFunctionCheckExitCode` that triggers the execution of
+a Go function (`ModuleInstance.FailIfClosed`) that atomically checks a sentinel value at strategic points in the code
+(e.g. [within loops][checkexitcode_loop]).
+
+[It _is indeed_ possible to check the sentinel value directly, without leaving the native world][native_check], thus sparing some cycles;
+however, because native code never preempts (see section above), this may lead to a state where the other goroutines
+never get the chance to run, and thus never get the chance to set the sentinel value; effectively preventing
+cancellation from taking place.
+
+[checkexitcode_loop]: https://github.com/tetratelabs/wazero/blob/86444c67a37dbf9e693ae5b365901f64968d9025/internal/wazeroir/compiler.go#L467-L476
+[native_check]: https://github.com/tetratelabs/wazero/issues/1409
diff --git a/internal/platform/sync_linux.go b/internal/platform/datasync_linux.go
similarity index 65%
rename from internal/platform/sync_linux.go
rename to internal/platform/datasync_linux.go
index 41ae44c8..36384063 100644
--- a/internal/platform/sync_linux.go
+++ b/internal/platform/datasync_linux.go
@@ -7,14 +7,11 @@ import (
 	"syscall"
 )
 
-func fdatasync(f fs.File) syscall.Errno {
+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.
-	if s, ok := f.(syncFile); ok {
-		return UnwrapOSError(s.Sync())
-	}
-	return 0
+	return sync(f)
 }
diff --git a/internal/platform/sync_unsupported.go b/internal/platform/datasync_unsupported.go
similarity index 54%
rename from internal/platform/sync_unsupported.go
rename to internal/platform/datasync_unsupported.go
index 38284aa5..ce752db3 100644
--- a/internal/platform/sync_unsupported.go
+++ b/internal/platform/datasync_unsupported.go
@@ -7,10 +7,7 @@ import (
 	"syscall"
 )
 
-func fdatasync(f fs.File) syscall.Errno {
+func datasync(f fs.File) syscall.Errno {
 	// Attempt to sync everything, even if we only need to sync the data.
-	if s, ok := f.(syncFile); ok {
-		return UnwrapOSError(s.Sync())
-	}
-	return 0
+	return sync(f)
 }
diff --git a/internal/platform/file.go b/internal/platform/file.go
index 0676c600..a60a5bd6 100644
--- a/internal/platform/file.go
+++ b/internal/platform/file.go
@@ -27,13 +27,19 @@ 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.
+	//
+	// Note: This can drift on rename.
+	Path() string
+
 	// Stat is similar to syscall.Fstat.
 	//
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
-	//   - syscall.EBADF if the file or directory was closed.
+	//   - syscall.ENOSYS: the implementation does not support this function.
+	//   - syscall.EBADF: the file or directory was closed.
 	//
 	// # Notes
 	//
@@ -49,8 +55,8 @@ type File interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
-	//   - syscall.EBADF if the file or directory was closed.
+	//   - syscall.ENOSYS: the implementation does not support this function.
+	//   - syscall.EBADF: the file or directory was closed.
 	//
 	// # Notes
 	//
@@ -66,8 +72,8 @@ type File interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
-	//   - syscall.EBADF if the file or directory was closed.
+	//   - syscall.ENOSYS: the implementation does not support this function.
+	//   - syscall.EBADF: the file or directory was closed.
 	//
 	// # Notes
 	//
@@ -81,7 +87,7 @@ type File interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.EBADF if the file or directory was closed.
+	//   - syscall.EBADF: the file or directory was closed.
 	//
 	// # Notes
 	//
@@ -96,7 +102,7 @@ type File interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.EBADF if the file or directory was closed.
+	//   - syscall.EBADF: the file or directory was closed.
 	//
 	// # Notes
 	//
@@ -109,7 +115,7 @@ type File interface {
 	// Close closes the underlying file.
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//
 	// # Notes
 	//
@@ -150,13 +156,23 @@ func (UnimplementedFile) Datasync() syscall.Errno {
 	return 0 // not syscall.ENOSYS
 }
 
-type DefaultFile struct {
-	F fs.File
+func NewFsFile(path string, f fs.File) File {
+	return &fsFile{path, f}
+}
+
+type fsFile struct {
+	path string
+	file fs.File
+}
+
+// Path implements File.Path
+func (f *fsFile) Path() string {
+	return f.path
 }
 
 // Stat implements File.Stat
-func (f *DefaultFile) Stat() (Stat_t, syscall.Errno) {
-	st, errno := statFile(f.F)
+func (f *fsFile) Stat() (Stat_t, syscall.Errno) {
+	st, errno := statFile(f.file)
 	if errno == syscall.EIO {
 		errno = syscall.EBADF
 	}
@@ -164,42 +180,42 @@ func (f *DefaultFile) Stat() (Stat_t, syscall.Errno) {
 }
 
 // Chmod implements File.Chmod
-func (f *DefaultFile) Chmod(mode fs.FileMode) syscall.Errno {
-	if f, ok := f.F.(chmodFile); ok {
+func (f *fsFile) Chmod(mode fs.FileMode) syscall.Errno {
+	if f, ok := f.file.(chmodFile); ok {
 		return UnwrapOSError(f.Chmod(mode))
 	}
 	return syscall.ENOSYS
 }
 
 // Chown implements File.Chown
-func (f *DefaultFile) Chown(uid, gid int) syscall.Errno {
-	if f, ok := f.F.(fdFile); ok {
+func (f *fsFile) Chown(uid, gid int) syscall.Errno {
+	if f, ok := f.file.(fdFile); ok {
 		return fchown(f.Fd(), uid, gid)
 	}
 	return syscall.ENOSYS
 }
 
 // Sync implements File.Sync
-func (f *DefaultFile) Sync() syscall.Errno {
-	if f, ok := f.F.(syncFile); ok {
+func (f *fsFile) Sync() syscall.Errno {
+	if f, ok := f.file.(syncFile); ok {
 		return UnwrapOSError(f.Sync())
 	}
 	return 0 // don't error
 }
 
 // Datasync implements File.Datasync
-func (f *DefaultFile) Datasync() syscall.Errno {
-	return fdatasync(f.F)
+func (f *fsFile) Datasync() syscall.Errno {
+	return datasync(f.file)
 }
 
 // Close implements File.Close
-func (f *DefaultFile) Close() syscall.Errno {
-	return UnwrapOSError(f.F.Close())
+func (f *fsFile) Close() syscall.Errno {
+	return UnwrapOSError(f.file.Close())
 }
 
 // File implements File.File
-func (f *DefaultFile) File() fs.File {
-	return f.F
+func (f *fsFile) File() fs.File {
+	return f.file
 }
 
 // ReadFile declares all read interfaces defined on os.File used by wazero.
diff --git a/internal/platform/open_file.go b/internal/platform/open_file.go
index 62950538..6fb3f5b7 100644
--- a/internal/platform/open_file.go
+++ b/internal/platform/open_file.go
@@ -17,7 +17,10 @@ const (
 
 // 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) (File, syscall.Errno) {
+func OpenFile(path string, flag int, perm fs.FileMode) (fs.File, syscall.Errno) {
 	f, err := os.OpenFile(path, flag, perm)
-	return &DefaultFile{F: f}, UnwrapOSError(err)
+	// 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
+	// pre-opens.
+	return f, UnwrapOSError(err)
 }
diff --git a/internal/platform/open_file_js.go b/internal/platform/open_file_js.go
index f59663a8..5a45038f 100644
--- a/internal/platform/open_file_js.go
+++ b/internal/platform/open_file_js.go
@@ -12,8 +12,8 @@ const (
 	O_NOFOLLOW  = 1 << 30
 )
 
-func OpenFile(path string, flag int, perm fs.FileMode) (File, syscall.Errno) {
+func OpenFile(path string, flag int, perm fs.FileMode) (fs.File, syscall.Errno) {
 	flag &= ^(O_DIRECTORY | O_NOFOLLOW) // erase placeholders
 	f, err := os.OpenFile(path, flag, perm)
-	return &DefaultFile{F: f}, UnwrapOSError(err)
+	return f, UnwrapOSError(err)
 }
diff --git a/internal/platform/open_file_sun.go b/internal/platform/open_file_sun.go
index 5553f68b..791072ed 100644
--- a/internal/platform/open_file_sun.go
+++ b/internal/platform/open_file_sun.go
@@ -14,7 +14,7 @@ const (
 	O_NOFOLLOW  = syscall.O_NOFOLLOW
 )
 
-func OpenFile(path string, flag int, perm fs.FileMode) (File, syscall.Errno) {
+func OpenFile(path string, flag int, perm fs.FileMode) (fs.File, syscall.Errno) {
 	f, err := os.OpenFile(path, flag, perm)
-	return &DefaultFile{F: f}, UnwrapOSError(err)
+	return f, UnwrapOSError(err)
 }
diff --git a/internal/platform/open_file_windows.go b/internal/platform/open_file_windows.go
index 6d9c9d4a..2589647a 100644
--- a/internal/platform/open_file_windows.go
+++ b/internal/platform/open_file_windows.go
@@ -28,12 +28,12 @@ const (
 	O_NOFOLLOW  = 1 << 30
 )
 
-func OpenFile(path string, flag int, perm fs.FileMode) (File, syscall.Errno) {
+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 {
+	} else { // TODO: revisit windowsWrappedFile once fsFile is complete
 		f := &windowsWrappedFile{WriteFile: f, path: path, flag: flag, perm: perm}
-		return &DefaultFile{F: f}, 0
+		return f, 0
 	}
 }
 
diff --git a/internal/platform/sync.go b/internal/platform/sync.go
new file mode 100644
index 00000000..a847c3c1
--- /dev/null
+++ b/internal/platform/sync.go
@@ -0,0 +1,13 @@
+package platform
+
+import (
+	"io/fs"
+	"syscall"
+)
+
+func sync(f fs.File) syscall.Errno {
+	if s, ok := f.(syncFile); ok {
+		return UnwrapOSError(s.Sync())
+	}
+	return 0
+}
diff --git a/internal/platform/wrap_windows.go b/internal/platform/wrap_windows.go
index a10ed198..82b3a054 100644
--- a/internal/platform/wrap_windows.go
+++ b/internal/platform/wrap_windows.go
@@ -1,6 +1,7 @@
 package platform
 
 import (
+	"errors"
 	"io/fs"
 	"syscall"
 )
@@ -61,7 +62,13 @@ func (w *windowsWrappedFile) Write(p []byte) (n int, err error) {
 		return
 	}
 
-	return w.WriteFile.Write(p)
+	n, err = w.WriteFile.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
diff --git a/internal/sys/fs.go b/internal/sys/fs.go
index 3e8dda9c..2bbcc8a8 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -160,6 +160,11 @@ type lazyDir struct {
 	f  platform.File
 }
 
+// Path implements the same method as documented on platform.File
+func (r *lazyDir) Path() string {
+	return "."
+}
+
 // Stat implements the same method as documented on platform.File
 func (r *lazyDir) Stat() (platform.Stat_t, syscall.Errno) {
 	if f, ok := r.file(); !ok {
@@ -273,7 +278,6 @@ type FileEntry struct {
 	// was called.
 	ReadDir *ReadDir
 
-	openPath string
 	openFlag int
 	openPerm fs.FileMode
 }
@@ -408,9 +412,7 @@ func stdinReader(r io.Reader) (*FileEntry, error) {
 		freader = NewStdioFileReader(r, s, PollerDefaultStdin)
 	}
 	return &FileEntry{
-		Name: noopStdinStat.Name(), File: &platform.DefaultFile{
-			F: freader,
-		},
+		Name: noopStdinStat.Name(), File: platform.NewFsFile("", freader),
 	}, nil
 }
 
@@ -423,9 +425,7 @@ func stdioWriter(w io.Writer, defaultStat stdioFileInfo) (*FileEntry, error) {
 		return nil, err
 	}
 	return &FileEntry{
-		Name: s.Name(), File: &platform.DefaultFile{
-			F: &stdioFileWriter{w: w, s: s},
-		},
+		Name: s.Name(), File: platform.NewFsFile("", &stdioFileWriter{w: w, s: s}),
 	}, nil
 }
 
@@ -453,7 +453,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{openPath: path, FS: fs, File: f, openFlag: flag, openPerm: perm}
+		fe := &FileEntry{FS: fs, File: f, openFlag: flag, openPerm: perm}
 		if path == "/" || path == "." {
 			fe.Name = ""
 		} else {
@@ -493,7 +493,7 @@ func (c *FSContext) reopen(f *FileEntry) syscall.Errno {
 	}
 
 	// Re-opens with  the same parameters as before.
-	opened, errno := f.FS.OpenFile(f.openPath, f.openFlag, f.openPerm)
+	opened, errno := f.FS.OpenFile(f.File.Path(), f.openFlag, f.openPerm)
 	if errno != 0 {
 		return errno
 	}
diff --git a/internal/sysfs/adapter.go b/internal/sysfs/adapter.go
index 1393f097..762daa66 100644
--- a/internal/sysfs/adapter.go
+++ b/internal/sysfs/adapter.go
@@ -48,7 +48,7 @@ func (a *adapter) Open(name string) (fs.File, error) {
 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.DefaultFile{F: f}, platform.UnwrapOSError(err)
+	return platform.NewFsFile(path, f), platform.UnwrapOSError(err)
 }
 
 // Stat implements FS.Stat
@@ -59,7 +59,7 @@ func (a *adapter) Stat(path string) (platform.Stat_t, syscall.Errno) {
 		return platform.Stat_t{}, platform.UnwrapOSError(err)
 	}
 	defer f.Close()
-	return (&platform.DefaultFile{F: f}).Stat()
+	return platform.NewFsFile(path, f).Stat()
 }
 
 // Lstat implements FS.Lstat
diff --git a/internal/sysfs/dirfs.go b/internal/sysfs/dirfs.go
index 0a698310..bdad24bd 100644
--- a/internal/sysfs/dirfs.go
+++ b/internal/sysfs/dirfs.go
@@ -42,7 +42,11 @@ func (d *dirFS) Open(name string) (fs.File, error) {
 
 // OpenFile implements FS.OpenFile
 func (d *dirFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) {
-	return platform.OpenFile(d.join(path), flag, perm)
+	f, errno := platform.OpenFile(d.join(path), flag, perm)
+	if errno != 0 {
+		return nil, errno
+	}
+	return platform.NewFsFile(path, f), 0
 }
 
 // Lstat implements FS.Lstat
diff --git a/internal/sysfs/readfs.go b/internal/sysfs/readfs.go
index 50d71339..47ed66fd 100644
--- a/internal/sysfs/readfs.go
+++ b/internal/sysfs/readfs.go
@@ -67,7 +67,7 @@ func (r *readFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.Fil
 	if errno != 0 {
 		return nil, errno
 	}
-	return &platform.DefaultFile{F: maskForReads(f.File())}, 0
+	return platform.NewFsFile(path, maskForReads(f.File())), 0
 }
 
 // maskForReads masks the file with read-only interfaces used by wazero.
diff --git a/internal/sysfs/rootfs.go b/internal/sysfs/rootfs.go
index 8fe48018..bd973923 100644
--- a/internal/sysfs/rootfs.go
+++ b/internal/sysfs/rootfs.go
@@ -137,7 +137,7 @@ func (c *CompositeFS) OpenFile(path string, flag int, perm fs.FileMode) (f platf
 		case ".", "/", "":
 			if len(c.rootGuestPaths) > 0 {
 				dir := &openRootDir{c: c, f: f.File().(fs.ReadDirFile)}
-				f = &platform.DefaultFile{F: dir}
+				f = platform.NewFsFile(path, dir)
 			}
 		}
 	}
@@ -483,7 +483,7 @@ type fakeRootFS struct{ UnimplementedFS }
 func (*fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) {
 	switch path {
 	case ".", "/", "":
-		return &platform.DefaultFile{F: fakeRootDir{}}, 0
+		return platform.NewFsFile(path, fakeRootDir{}), 0
 	}
 	return nil, syscall.ENOENT
 }
diff --git a/internal/sysfs/sysfs.go b/internal/sysfs/sysfs.go
index 78cfdee6..87294a45 100644
--- a/internal/sysfs/sysfs.go
+++ b/internal/sysfs/sysfs.go
@@ -48,7 +48,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `path` or `flag` is invalid.
 	//   - syscall.ENOENT: `path` doesn't exist and `flag` doesn't contain
 	//     os.O_CREATE.
@@ -83,7 +83,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.ENOENT: `path` doesn't exist.
 	//
 	// # Notes
@@ -103,7 +103,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.ENOENT: `path` doesn't exist.
 	//
 	// # Notes
@@ -123,7 +123,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `path` is invalid.
 	//   - syscall.EEXIST: `path` exists and is a directory.
 	//   - syscall.ENOTDIR: `path` exists and is a file.
@@ -144,7 +144,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `path` is invalid.
 	//   - syscall.ENOENT: `path` does not exist.
 	//
@@ -164,7 +164,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `path` is invalid.
 	//   - syscall.ENOENT: `path` does not exist.
 	//
@@ -182,7 +182,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `path` is invalid.
 	//   - syscall.ENOENT: `path` does not exist.
 	//
@@ -200,7 +200,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `from` or `to` is invalid.
 	//   - syscall.ENOENT: `from` or `to` don't exist.
 	//   - syscall.ENOTDIR: `from` is a directory and `to` exists as a file.
@@ -222,7 +222,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `path` is invalid.
 	//   - syscall.ENOENT: `path` doesn't exist.
 	//   - syscall.ENOTDIR: `path` exists, but isn't a directory.
@@ -242,7 +242,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `path` is invalid.
 	//   - syscall.ENOENT: `path` doesn't exist.
 	//   - syscall.EISDIR: `path` exists, but is a directory.
@@ -264,7 +264,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EPERM: `oldPath` is invalid.
 	//   - syscall.ENOENT: `oldPath` doesn't exist.
 	//   - syscall.EISDIR: `newPath` exists, but is a directory.
@@ -283,7 +283,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EPERM: `oldPath` or `newPath` is invalid.
 	//   - syscall.EEXIST: `newPath` exists.
 	//
@@ -308,7 +308,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `path` is invalid.
 	//
 	// # Notes
@@ -327,9 +327,10 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `path` is invalid or size is negative.
-	//   - syscall.ENOENT: `path` doesn't exist
+	//   - syscall.ENOENT: `path` doesn't exist.
+	//   - syscall.EISDIR: `path` is a directory.
 	//   - syscall.EACCES: `path` doesn't have write access.
 	//
 	// # Notes
@@ -356,7 +357,7 @@ type FS interface {
 	// # Errors
 	//
 	// A zero syscall.Errno is success. The below are expected otherwise:
-	//   - syscall.ENOSYS the implementation does not support this function.
+	//   - syscall.ENOSYS: the implementation does not support this function.
 	//   - syscall.EINVAL: `path` is invalid.
 	//   - syscall.EEXIST: `path` exists and is a directory.
 	//   - syscall.ENOTDIR: `path` exists and is a file.


Test patch:
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index d0c92dbf..bb825745 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -3667,11 +3667,8 @@ func Test_pathLink(t *testing.T) {
 			uint64(newFd), uint64(destination), uint64(len(destinationName)))
 		require.Contains(t, log.String(), wasip1.ErrnoName(wasip1.ErrnoSuccess))
 
-		f, errno := platform.OpenFile(destinationRealPath, os.O_RDONLY, 0)
-		require.EqualErrno(t, 0, errno)
-		defer func() {
-			require.Zero(t, f.Close())
-		}()
+		f := openFsFile(t, destinationRealPath, os.O_RDONLY, 0)
+		defer f.Close()
 
 		st, errno := f.Stat()
 		require.EqualErrno(t, 0, errno)
@@ -5028,8 +5025,7 @@ func Test_fdReaddir_opened_file_written(t *testing.T) {
 	require.EqualErrno(t, 0, errno)
 
 	// Then write a file to the directory.
-	f, errno := platform.OpenFile(joinPath(dirPath, "file"), os.O_CREATE, 0)
-	require.EqualErrno(t, 0, errno)
+	f := openFsFile(t, joinPath(dirPath, "file"), os.O_CREATE, 0)
 	defer f.Close()
 
 	// get the real inode of the current directory
@@ -5076,3 +5072,9 @@ func Test_fdReaddir_opened_file_written(t *testing.T) {
 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)
+	require.EqualErrno(t, 0, errno)
+	return platform.NewFsFile(path, f)
+}
diff --git a/internal/platform/chown_unix_test.go b/internal/platform/chown_unix_test.go
index b9567aa6..32bd0728 100644
--- a/internal/platform/chown_unix_test.go
+++ b/internal/platform/chown_unix_test.go
@@ -18,8 +18,8 @@ func TestChown(t *testing.T) {
 	dir := path.Join(tmpDir, "dir")
 	require.NoError(t, os.Mkdir(dir, 0o0777))
 
-	dirF, errno := OpenFile(dir, syscall.O_RDONLY, 0)
-	require.EqualErrno(t, 0, errno)
+	dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
+	defer dirF.Close()
 
 	dirStat, err := dirF.File().Stat()
 	require.NoError(t, err)
@@ -66,8 +66,8 @@ func TestDefaultFileChown(t *testing.T) {
 	dir := path.Join(tmpDir, "dir")
 	require.NoError(t, os.Mkdir(dir, 0o0777))
 
-	dirF, errno := OpenFile(dir, syscall.O_RDONLY, 0)
-	require.EqualErrno(t, 0, errno)
+	dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
+	defer dirF.Close()
 
 	dirStat, err := dirF.File().Stat()
 	require.NoError(t, err)
@@ -116,8 +116,8 @@ func TestLchown(t *testing.T) {
 	dir := path.Join(tmpDir, "dir")
 	require.NoError(t, os.Mkdir(dir, 0o0777))
 
-	dirF, errno := OpenFile(dir, syscall.O_RDONLY, 0)
-	require.EqualErrno(t, 0, errno)
+	dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
+	defer dirF.Close()
 
 	dirStat, err := dirF.File().Stat()
 	require.NoError(t, err)
@@ -127,8 +127,8 @@ func TestLchown(t *testing.T) {
 	link := path.Join(tmpDir, "link")
 	require.NoError(t, os.Symlink(dir, link))
 
-	linkF, errno := OpenFile(link, syscall.O_RDONLY, 0)
-	require.EqualErrno(t, 0, errno)
+	linkF := openFsFile(t, link, syscall.O_RDONLY, 0)
+	defer linkF.Close()
 
 	linkStat, err := linkF.File().Stat()
 	require.NoError(t, err)
diff --git a/internal/platform/file_test.go b/internal/platform/file_test.go
index 19b7a4f3..2fa5f6eb 100644
--- a/internal/platform/file_test.go
+++ b/internal/platform/file_test.go
@@ -7,6 +7,7 @@ import (
 	"io/fs"
 	"os"
 	"path"
+	"runtime"
 	"syscall"
 	"testing"
 
@@ -21,6 +22,11 @@ type NoopFile struct {
 	UnimplementedFile
 }
 
+// The current design requires the user to implement Path.
+func (NoopFile) Path() string {
+	return "noop"
+}
+
 // The current design requires the user to consciously implement Close.
 // However, we could change UnimplementedFile to return zero.
 func (NoopFile) Close() (errno syscall.Errno) { return }
@@ -40,11 +46,13 @@ func TestFileDatasync_NoError(t *testing.T) {
 }
 
 func testSync_NoError(t *testing.T, sync func(File) syscall.Errno) {
-	ro, err := embedFS.Open("file_test.go")
+	roPath := "file_test.go"
+	ro, err := embedFS.Open(roPath)
 	require.NoError(t, err)
 	defer ro.Close()
 
-	rw, err := os.Create(path.Join(t.TempDir(), "datasync"))
+	rwPath := path.Join(t.TempDir(), "datasync")
+	rw, err := os.Create(rwPath)
 	require.NoError(t, err)
 	defer rw.Close()
 
@@ -58,11 +66,11 @@ func testSync_NoError(t *testing.T, sync func(File) syscall.Errno) {
 		},
 		{
 			name: "File of read-only fs.File",
-			f:    &DefaultFile{F: ro},
+			f:    NewFsFile(roPath, ro),
 		},
 		{
 			name: "File of os.File",
-			f:    &DefaultFile{F: rw},
+			f:    NewFsFile(rwPath, rw),
 		},
 	}
 
@@ -75,11 +83,11 @@ func testSync_NoError(t *testing.T, sync func(File) syscall.Errno) {
 	}
 }
 
-func TestFileSync(t *testing.T) {
+func TestFsFileSync(t *testing.T) {
 	testSync(t, File.Sync)
 }
 
-func TestFileDatasync(t *testing.T) {
+func TestFsFileDatasync(t *testing.T) {
 	testSync(t, File.Datasync)
 }
 
@@ -87,31 +95,60 @@ func TestFileDatasync(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) {
-	f, errno := os.CreateTemp("", t.Name())
-	require.NoError(t, errno)
+	dPath := t.TempDir()
+	fPath := path.Join(dPath, t.Name())
+
+	f := openFsFile(t, fPath, os.O_RDWR|os.O_CREATE, 0o600)
 	defer f.Close()
 
 	expected := "hello world!"
 
 	// Write the expected data
-	_, errno = f.Write([]byte(expected))
-	require.NoError(t, errno)
+	_, err := f.File().(io.Writer).Write([]byte(expected))
+	require.NoError(t, err)
 
 	// Sync the data.
-	if errno = sync(&DefaultFile{F: f}); errno == syscall.ENOSYS {
-		return // don't continue if it isn't supported.
-	}
+	errno := sync(f)
 	require.EqualErrno(t, 0, errno)
 
 	// Rewind while the file is still open.
-	_, err := f.Seek(0, io.SeekStart)
+	_, err = f.File().(io.Seeker).Seek(0, io.SeekStart)
 	require.NoError(t, err)
 
 	// Read data from the file
 	var buf bytes.Buffer
-	_, errno = io.Copy(&buf, f)
-	require.NoError(t, errno)
+	_, err = io.Copy(&buf, f.File().(io.Reader))
+	require.NoError(t, err)
 
 	// It may be the case that sync worked.
 	require.Equal(t, expected, buf.String())
+
+	// Windows allows you to sync a closed file
+	if runtime.GOOS != "windows" {
+		testEBADFIfFileClosed(t, sync)
+	}
+}
+
+func testEBADFIfFileClosed(t *testing.T, fn func(File) syscall.Errno) bool {
+	return t.Run("EBADF if file closed", func(t *testing.T) {
+		tmpDir := t.TempDir()
+
+		f := openForWrite(t, path.Join(tmpDir, "EBADF"), []byte{1, 2, 3, 4})
+
+		// close the file underneath
+		require.Zero(t, f.Close())
+
+		require.EqualErrno(t, syscall.EBADF, fn(f))
+	})
+}
+
+func openForWrite(t *testing.T, path string, content []byte) File {
+	require.NoError(t, os.WriteFile(path, content, 0o0600))
+	return openFsFile(t, path, os.O_RDWR, 0o666)
+}
+
+func openFsFile(t *testing.T, path string, flag int, perm fs.FileMode) File {
+	f, errno := OpenFile(path, flag, perm)
+	require.EqualErrno(t, 0, errno)
+	return NewFsFile(path, f)
 }
diff --git a/internal/platform/futimens_test.go b/internal/platform/futimens_test.go
index 37c8540a..4aca0bc6 100644
--- a/internal/platform/futimens_test.go
+++ b/internal/platform/futimens_test.go
@@ -167,8 +167,7 @@ func testFutimens(t *testing.T, usePath bool) {
 						}
 					}
 
-					f, errno := OpenFile(path, flag, 0)
-					require.EqualErrno(t, 0, errno)
+					f := openFsFile(t, path, flag, 0)
 
 					errno = UtimensFile(f.File(), tc.times)
 					require.Zero(t, f.Close())
@@ -222,11 +221,10 @@ func TestUtimensFile(t *testing.T) {
 		err := os.WriteFile(file, []byte{}, 0o700)
 		require.NoError(t, err)
 
-		fileF, errno := OpenFile(file, syscall.O_RDWR, 0)
-		require.EqualErrno(t, 0, errno)
+		fileF := openFsFile(t, file, syscall.O_RDWR, 0)
 		require.Zero(t, fileF.Close())
 
-		errno = UtimensFile(fileF.File(), nil)
+		errno := UtimensFile(fileF.File(), nil)
 		require.EqualErrno(t, syscall.EBADF, errno)
 	})
 
@@ -235,8 +233,7 @@ func TestUtimensFile(t *testing.T) {
 		err := os.Mkdir(dir, 0o700)
 		require.NoError(t, err)
 
-		dirF, errno := OpenFile(dir, syscall.O_RDONLY, 0)
-		require.EqualErrno(t, 0, errno)
+		dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
 		require.Zero(t, dirF.Close())
 
 		err = UtimensFile(dirF.File(), nil)
diff --git a/internal/platform/open_file_test.go b/internal/platform/open_file_test.go
index f4204e23..37bef1ae 100644
--- a/internal/platform/open_file_test.go
+++ b/internal/platform/open_file_test.go
@@ -21,7 +21,7 @@ func TestOpenFile(t *testing.T) {
 
 		f, errno := OpenFile(path+"/", os.O_RDONLY, 0)
 		require.EqualErrno(t, 0, errno)
-		require.Zero(t, f.Close())
+		require.NoError(t, f.Close())
 	})
 
 	// from os.TestDirFSPathsValid
@@ -29,7 +29,7 @@ func TestOpenFile(t *testing.T) {
 		t.Run("strange name", func(t *testing.T) {
 			f, errno := OpenFile(path.Join(tmpDir, `e:xperi\ment.txt`), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
 			require.EqualErrno(t, 0, errno)
-			require.Zero(t, f.Close())
+			require.NoError(t, f.Close())
 		})
 	}
 }
@@ -60,19 +60,18 @@ func TestOpenFile_Errors(t *testing.T) {
 		filepath := path.Join(tmpDir, "file.txt")
 		f, errno := OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o666)
 		require.EqualErrno(t, 0, errno)
-		defer require.Zero(t, f.Close())
+		defer f.Close()
 
-		_, errno = OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o666)
-		require.EqualErrno(t, syscall.EEXIST, errno)
+		_, err := OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o666)
+		require.EqualErrno(t, syscall.EEXIST, err)
 	})
 
 	t.Run("writing to a read-only file is EBADF", func(t *testing.T) {
 		path := path.Join(tmpDir, "file")
 		require.NoError(t, os.WriteFile(path, nil, 0o600))
 
-		f, errno := OpenFile(path, os.O_RDONLY, 0)
-		defer require.Zero(t, f.Close())
-		require.EqualErrno(t, 0, errno)
+		f := openFsFile(t, path, os.O_RDONLY, 0)
+		defer f.Close()
 
 		_, err := f.File().(io.Writer).Write([]byte{1, 2, 3, 4})
 		require.EqualErrno(t, syscall.EBADF, UnwrapOSError(err))
@@ -82,9 +81,8 @@ func TestOpenFile_Errors(t *testing.T) {
 		path := path.Join(tmpDir, "diragain")
 		require.NoError(t, os.Mkdir(path, 0o755))
 
-		f, errno := OpenFile(path, os.O_RDONLY, 0)
-		defer require.Zero(t, f.Close())
-		require.EqualErrno(t, 0, errno)
+		f := openFsFile(t, path, os.O_RDONLY, 0)
+		defer f.Close()
 
 		_, err := f.File().(io.Writer).Write([]byte{1, 2, 3, 4})
 		require.EqualErrno(t, syscall.EBADF, UnwrapOSError(err))
diff --git a/internal/platform/stat_test.go b/internal/platform/stat_test.go
index 4942569f..3d9cd217 100644
--- a/internal/platform/stat_test.go
+++ b/internal/platform/stat_test.go
@@ -158,14 +158,11 @@ func TestStat(t *testing.T) {
 func TestStatFile(t *testing.T) {
 	tmpDir := t.TempDir()
 
-	var st Stat_t
-
-	tmpDirF, errno := OpenFile(tmpDir, syscall.O_RDONLY, 0)
-	require.EqualErrno(t, 0, errno)
+	tmpDirF := openFsFile(t, tmpDir, syscall.O_RDONLY, 0)
 	defer tmpDirF.Close()
 
 	t.Run("dir", func(t *testing.T) {
-		st, errno = tmpDirF.Stat()
+		st, errno := tmpDirF.Stat()
 		require.EqualErrno(t, 0, errno)
 
 		require.True(t, st.Mode.IsDir())
@@ -177,19 +174,18 @@ func TestStatFile(t *testing.T) {
 	if runtime.GOOS != "windows" {
 		t.Run("closed dir", func(t *testing.T) {
 			require.Zero(t, tmpDirF.Close())
-			st, errno = tmpDirF.Stat()
+			_, errno := tmpDirF.Stat()
 			require.EqualErrno(t, syscall.EBADF, errno)
 		})
 	}
 
 	file := path.Join(tmpDir, "file")
 	require.NoError(t, os.WriteFile(file, nil, 0o400))
-	fileF, errno := OpenFile(file, syscall.O_RDONLY, 0)
-	require.EqualErrno(t, 0, errno)
+	fileF := openFsFile(t, file, syscall.O_RDONLY, 0)
 	defer fileF.Close()
 
 	t.Run("file", func(t *testing.T) {
-		st, errno = fileF.Stat()
+		st, errno := fileF.Stat()
 		require.EqualErrno(t, 0, errno)
 
 		require.False(t, st.Mode.IsDir())
@@ -198,18 +194,17 @@ func TestStatFile(t *testing.T) {
 
 	t.Run("closed file", func(t *testing.T) {
 		require.Zero(t, fileF.Close())
-		_, errno = fileF.Stat()
+		_, errno := fileF.Stat()
 		require.EqualErrno(t, syscall.EBADF, errno)
 	})
 
 	subdir := path.Join(tmpDir, "sub")
 	require.NoError(t, os.Mkdir(subdir, 0o500))
-	subdirF, errno := OpenFile(subdir, syscall.O_RDONLY, 0)
-	require.EqualErrno(t, 0, errno)
+	subdirF := openFsFile(t, subdir, syscall.O_RDONLY, 0)
 	defer subdirF.Close()
 
 	t.Run("subdir", func(t *testing.T) {
-		st, errno = subdirF.Stat()
+		st, errno := subdirF.Stat()
 		require.EqualErrno(t, 0, errno)
 
 		require.True(t, st.Mode.IsDir())
@@ -219,7 +214,7 @@ func TestStatFile(t *testing.T) {
 	if runtime.GOOS != "windows" { // windows allows you to stat a closed dir
 		t.Run("closed subdir", func(t *testing.T) {
 			require.Zero(t, subdirF.Close())
-			st, errno = subdirF.Stat()
+			_, errno := subdirF.Stat()
 			require.EqualErrno(t, syscall.EBADF, errno)
 		})
 	}
@@ -264,8 +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, errno := OpenFile(file, syscall.O_RDONLY, 0)
-			require.EqualErrno(t, 0, errno)
+			f := openFsFile(t, file, syscall.O_RDONLY, 0)
 			defer f.Close()
 
 			st, errno := f.Stat()
@@ -279,25 +273,21 @@ func Test_StatFile_times(t *testing.T) {
 
 func TestStatFile_dev_inode(t *testing.T) {
 	tmpDir := t.TempDir()
-	d, errno := OpenFile(tmpDir, os.O_RDONLY, 0)
-	require.EqualErrno(t, 0, errno)
+	d := openFsFile(t, tmpDir, os.O_RDONLY, 0)
 	defer d.Close()
 
 	path1 := path.Join(tmpDir, "1")
-	f1, errno := OpenFile(path1, os.O_CREATE, 0o666)
-	require.EqualErrno(t, 0, errno)
+	f1 := openFsFile(t, path1, os.O_CREATE, 0o666)
 	defer f1.Close()
 
 	path2 := path.Join(tmpDir, "2")
-	f2, errno := OpenFile(path2, os.O_CREATE, 0o666)
-	require.EqualErrno(t, 0, errno)
+	f2 := openFsFile(t, path2, os.O_CREATE, 0o666)
 	defer f2.Close()
 
 	pathLink2 := path.Join(tmpDir, "link2")
 	err := os.Symlink(path2, pathLink2)
 	require.NoError(t, err)
-	l2, errno := OpenFile(pathLink2, os.O_RDONLY, 0)
-	require.EqualErrno(t, 0, errno)
+	l2 := openFsFile(t, pathLink2, os.O_RDONLY, 0)
 	defer l2.Close()
 
 	// First, stat the directory
@@ -334,8 +324,7 @@ func TestStatFile_dev_inode(t *testing.T) {
 
 	// Renaming a file shouldn't change its inodes.
 	require.Zero(t, Rename(path1, path2))
-	f1, errno = OpenFile(path2, os.O_RDONLY, 0)
-	require.EqualErrno(t, 0, errno)
+	f1 = openFsFile(t, path2, os.O_RDONLY, 0)
 	defer f1.Close()
 
 	st1Again, errno = f1.Stat()
diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go
index b961a06d..147cbb1e 100644
--- a/internal/sys/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -19,9 +19,9 @@ import (
 )
 
 var (
-	noopStdin  = &FileEntry{Name: "stdin", File: &platform.DefaultFile{F: NewStdioFileReader(eofReader{}, noopStdinStat, PollerDefaultStdin)}}
-	noopStdout = &FileEntry{Name: "stdout", File: &platform.DefaultFile{F: &stdioFileWriter{w: io.Discard, s: noopStdoutStat}}}
-	noopStderr = &FileEntry{Name: "stderr", File: &platform.DefaultFile{F: &stdioFileWriter{w: io.Discard, s: noopStderrStat}}}
+	noopStdin  = &FileEntry{Name: "stdin", File: platform.NewFsFile("", NewStdioFileReader(eofReader{}, noopStdinStat, PollerDefaultStdin))}
+	noopStdout = &FileEntry{Name: "stdout", File: platform.NewFsFile("", &stdioFileWriter{w: io.Discard, s: noopStdoutStat})}
+	noopStderr = &FileEntry{Name: "stderr", File: platform.NewFsFile("", &stdioFileWriter{w: io.Discard, s: noopStderrStat})}
 )
 
 //go:embed testdata