Repository: tetratelabs/wazero
Pull request #1154 (2023-02-23 08:19:59)

wasi: adds platform.Dirent in preparation of inode fetching
wasi_snapshot_preview1 recently requires fd_readdir to return actual inode values. On zero, wasi-libc will call fdstat to retrieve them.

This introduces our own `platform.Dirent` type and `Readdir` function which a later change will allow fetching of inodes.

See https://github.com/WebAssembly/wasi-libc/pull/345


Patch:
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index 867ccc0e..023a5f18 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -813,7 +813,7 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) Errno {
 		if err != nil {
 			return ToErrno(err)
 		}
-		rd, dir = f.File.(fs.ReadDirFile), f.ReadDir
+		rd, dir = f.File, f.ReadDir
 	}
 
 	// First, determine the maximum directory entries that can be encoded as
@@ -831,26 +831,26 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) Errno {
 
 	// The host keeps state for any unread entries from the prior call because
 	// we cannot seek to a previous directory position. Collect these entries.
-	entries, errno := lastDirEntries(dir, cookie)
+	dirents, errno := lastDirents(dir, cookie)
 	if errno != ErrnoSuccess {
 		return errno
 	}
 
 	// Add entries for dot and dot-dot as wasi-testsuite requires them.
-	if cookie == 0 && entries == nil {
+	if cookie == 0 && dirents == nil {
 		var err error
 		if f, ok := fsc.LookupFile(fd); !ok {
 			return ErrnoBadf
-		} else if entries, err = sys.DotEntries(f.File); err != nil {
+		} else if dirents, err = dotDirents(f.File); err != nil {
 			return ToErrno(err)
 		}
-		dir.Entries = entries
+		dir.Dirents = dirents
 		dir.CountRead = 2 // . and ..
 	}
 
 	// Check if we have maxDirEntries, and read more from the FS as needed.
-	if entryCount := len(entries); entryCount < maxDirEntries {
-		l, err := rd.ReadDir(maxDirEntries - entryCount)
+	if entryCount := len(dirents); entryCount < maxDirEntries {
+		l, err := platform.Readdir(rd, maxDirEntries-entryCount)
 		if err == io.EOF { // EOF is not an error
 		} else if err != nil {
 			if errno = ToErrno(err); errno == ErrnoNoent {
@@ -864,15 +864,15 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) Errno {
 			}
 		} else {
 			dir.CountRead += uint64(len(l))
-			entries = append(entries, l...)
+			dirents = append(dirents, l...)
 			// Replace the cache with up to maxDirEntries, starting at cookie.
-			dir.Entries = entries
+			dir.Dirents = dirents
 		}
 	}
 
 	// Determine how many dirents we can write, excluding a potentially
 	// truncated entry.
-	bufused, direntCount, writeTruncatedEntry := maxDirents(entries, bufLen)
+	bufused, direntCount, writeTruncatedEntry := maxDirents(dirents, bufLen)
 
 	// Now, write entries to the underlying buffer.
 	if bufused > 0 {
@@ -883,12 +883,12 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) Errno {
 		// ^^ yes this can overflow to negative, which means our implementation
 		// doesn't support writing greater than max int64 entries.
 
-		dirents, ok := mem.Read(buf, bufused)
+		buf, ok := mem.Read(buf, bufused)
 		if !ok {
 			return ErrnoFault
 		}
 
-		writeDirents(entries, direntCount, writeTruncatedEntry, dirents, d_next)
+		writeDirents(dirents, direntCount, writeTruncatedEntry, buf, d_next)
 	}
 
 	if !mem.WriteUint32Le(resultBufused, bufused) {
@@ -897,16 +897,29 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	return ErrnoSuccess
 }
 
+// dotDirents returns "." and "..", where "." has a real stat because
+// wasi-testsuite does inode validation.
+func dotDirents(f fs.File) ([]*platform.Dirent, error) {
+	var st platform.Stat_t
+	if err := platform.StatFile(f, &st); err != nil {
+		return nil, err
+	}
+	return []*platform.Dirent{
+		{Name: ".", Ino: st.Ino, Type: fs.ModeDir},
+		{Name: "..", Type: fs.ModeDir},
+	}, nil
+}
+
 const largestDirent = int64(math.MaxUint32 - DirentSize)
 
-// lastDirEntries is broken out from fdReaddirFn for testability.
-func lastDirEntries(dir *sys.ReadDir, cookie int64) (entries []fs.DirEntry, errno Errno) {
+// lastDirents is broken out from fdReaddirFn for testability.
+func lastDirents(dir *sys.ReadDir, cookie int64) (dirents []*platform.Dirent, errno Errno) {
 	if cookie < 0 {
 		errno = ErrnoInval // invalid as we will never send a negative cookie.
 		return
 	}
 
-	entryCount := int64(len(dir.Entries))
+	entryCount := int64(len(dir.Dirents))
 	if entryCount == 0 { // there was no prior call
 		if cookie != 0 {
 			errno = ErrnoInval // invalid as we haven't sent that cookie
@@ -924,12 +937,12 @@ func lastDirEntries(dir *sys.ReadDir, cookie int64) (entries []fs.DirEntry, errn
 	case cookiePos > entryCount:
 		errno = ErrnoInval // invalid as we read that far, yet.
 	case cookiePos > 0: // truncate so to avoid large lists.
-		entries = dir.Entries[cookiePos:]
+		dirents = dir.Dirents[cookiePos:]
 	default:
-		entries = dir.Entries
+		dirents = dir.Dirents
 	}
-	if len(entries) == 0 {
-		entries = nil
+	if len(dirents) == 0 {
+		dirents = nil
 	}
 	return
 }
@@ -943,7 +956,7 @@ func lastDirEntries(dir *sys.ReadDir, cookie int64) (entries []fs.DirEntry, errn
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_readdir
 // See https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/cloudlibc/src/libc/dirent/readdir.c#L44
-func maxDirents(entries []fs.DirEntry, bufLen uint32) (bufused, direntCount uint32, writeTruncatedEntry bool) {
+func maxDirents(entries []*platform.Dirent, bufLen uint32) (bufused, direntCount uint32, writeTruncatedEntry bool) {
 	lenRemaining := bufLen
 	for _, e := range entries {
 		if lenRemaining < DirentSize {
@@ -957,7 +970,7 @@ func maxDirents(entries []fs.DirEntry, bufLen uint32) (bufused, direntCount uint
 		}
 
 		// use int64 to guard against huge filenames
-		nameLen := int64(len(e.Name()))
+		nameLen := int64(len(e.Name))
 		var entryLen uint32
 
 		// Check to see if DirentSize + nameLen overflows, or if it would be
@@ -1000,21 +1013,21 @@ func maxDirents(entries []fs.DirEntry, bufLen uint32) (bufused, direntCount uint
 // based on maxDirents.	truncatedEntryLen means write one past entryCount,
 // without its name. See maxDirents for why
 func writeDirents(
-	entries []fs.DirEntry,
-	entryCount uint32,
+	dirents []*platform.Dirent,
+	direntCount uint32,
 	writeTruncatedEntry bool,
-	dirents []byte,
+	buf []byte,
 	d_next uint64,
 ) {
 	pos, i := uint32(0), uint32(0)
-	for ; i < entryCount; i++ {
-		e := entries[i]
-		nameLen := uint32(len(e.Name()))
+	for ; i < direntCount; i++ {
+		e := dirents[i]
+		nameLen := uint32(len(e.Name))
 
-		writeDirent(dirents[pos:], d_next, nameLen, e.IsDir())
+		writeDirent(buf[pos:], d_next, nameLen, e.IsDir())
 		pos += DirentSize
 
-		copy(dirents[pos:], e.Name())
+		copy(buf[pos:], e.Name)
 		pos += nameLen
 		d_next++
 	}
@@ -1025,11 +1038,11 @@ func writeDirents(
 
 	// Write a dirent without its name
 	dirent := make([]byte, DirentSize)
-	e := entries[i]
-	writeDirent(dirent, d_next, uint32(len(e.Name())), e.IsDir())
+	e := dirents[i]
+	writeDirent(dirent, d_next, uint32(len(e.Name)), e.IsDir())
 
 	// Potentially truncate it
-	copy(dirents[pos:], dirent)
+	copy(buf[pos:], dirent)
 }
 
 // writeDirent writes DirentSize bytes
@@ -1046,10 +1059,10 @@ func writeDirent(buf []byte, dNext uint64, dNamlen uint32, dType bool) {
 }
 
 // openedDir returns the directory and ErrnoSuccess if the fd points to a readable directory.
-func openedDir(fsc *sys.FSContext, fd uint32) (fs.ReadDirFile, *sys.ReadDir, Errno) {
+func openedDir(fsc *sys.FSContext, fd uint32) (fs.File, *sys.ReadDir, Errno) {
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return nil, nil, ErrnoBadf
-	} else if d, ok := f.File.(fs.ReadDirFile); !ok {
+	} else if !f.IsDir() {
 		// fd_readdir docs don't indicate whether to return ErrnoNotdir or
 		// ErrnoBadf. It has been noticed that rust will crash on ErrnoNotdir,
 		// and POSIX C ref seems to not return this, so we don't either.
@@ -1061,7 +1074,7 @@ func openedDir(fsc *sys.FSContext, fd uint32) (fs.ReadDirFile, *sys.ReadDir, Err
 		if f.ReadDir == nil {
 			f.ReadDir = &sys.ReadDir{}
 		}
-		return d, f.ReadDir, ErrnoSuccess
+		return f.File, f.ReadDir, ErrnoSuccess
 	}
 }
 
diff --git a/internal/platform/dir.go b/internal/platform/dir.go
index 98a58ede..f9515e54 100644
--- a/internal/platform/dir.go
+++ b/internal/platform/dir.go
@@ -1,6 +1,7 @@
 package platform
 
 import (
+	"io"
 	"io/fs"
 	"syscall"
 )
@@ -21,11 +22,69 @@ func Readdirnames(f fs.File, n int) (names []string, err error) {
 	case fs.ReadDirFile:
 		var entries []fs.DirEntry
 		entries, err = f.ReadDir(n)
-		if err == nil {
-			names = make([]string, 0, len(entries))
-			for _, e := range entries {
-				names = append(names, e.Name())
-			}
+		if err != nil {
+			break
+		}
+		names = make([]string, 0, len(entries))
+		for _, e := range entries {
+			names = append(names, e.Name())
+		}
+	default:
+		err = syscall.ENOTDIR
+	}
+	err = UnwrapOSError(err)
+	return
+}
+
+// Dirent is an entry read from a directory.
+//
+// This is a portable variant of syscall.Dirent containing fields needed for
+// WebAssembly ABI including WASI snapshot-01 and wasi-filesystem. Unlike
+// fs.DirEntry, this may include the Ino.
+type Dirent struct {
+	// ^^ Dirent name matches syscall.Dirent
+
+	// Name is the base name of the directory entry.
+	Name string
+
+	// Ino is the file serial number, or zero if not available.
+	Ino uint64
+
+	// Type is fs.FileMode masked on fs.ModeType. For example, zero is a
+	// regular file, fs.ModeDir is a directory and fs.ModeIrregular is unknown.
+	Type fs.FileMode
+}
+
+// IsDir returns true if the Type is fs.ModeDir.
+func (d *Dirent) IsDir() bool {
+	return d.Type == fs.ModeDir
+}
+
+// Readdir reads the contents of the directory associated with file and returns
+// a slice of up to n Dirent values in an arbitrary order. This is a stateful
+// function, so subsequent calls return any next values.
+//
+// If n > 0, Readdir returns at most n entries or an error.
+// If n <= 0, Readdir returns all remaining entries or an error.
+//
+// Note: The error will be nil or a syscall.Errno. No error is returned on EOF.
+func Readdir(f fs.File, n int) (dirents []*Dirent, err error) {
+	// ^^ case format is to match POSIX and similar to os.File.Readdir
+
+	switch f := f.(type) {
+	case fs.ReadDirFile:
+		var entries []fs.DirEntry
+		entries, err = f.ReadDir(n)
+		if err == io.EOF {
+			err = nil
+		}
+		if err != nil {
+			break
+		}
+		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:
 		err = syscall.ENOTDIR
diff --git a/internal/sys/fs.go b/internal/sys/fs.go
index a5a7095a..71360e39 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -187,20 +187,20 @@ func (f *FileEntry) IsDir() bool {
 }
 
 // Stat returns the underlying stat of this file.
-func (f *FileEntry) Stat(stat *platform.Stat_t) (err error) {
-	err = platform.StatFile(f.File, stat)
-	if err == nil && stat.Mode.IsDir() {
-		f.isDirectory = true
+func (f *FileEntry) Stat(st *platform.Stat_t) (err error) {
+	err = platform.StatFile(f.File, st)
+	if err == nil {
+		f.isDirectory = st.Mode.IsDir()
 	}
 	return
 }
 
 // ReadDir is the status of a prior fs.ReadDirFile call.
 type ReadDir struct {
-	// CountRead is the total count of files read including Entries.
+	// CountRead is the total count of files read including Dirents.
 	CountRead uint64
 
-	// Entries is the contents of the last fs.ReadDirFile call. Notably,
+	// Dirents is the contents of the last platform.Readdir call. Notably,
 	// directory listing are not rewindable, so we keep entries around in case
 	// the caller mis-estimated their buffer and needs a few still cached.
 	//
@@ -208,7 +208,7 @@ type ReadDir struct {
 	// In wasi preview1, dot and dot-dot entries are required to exist, but the
 	// reverse is true for preview2. More importantly, preview2 holds separate
 	// stateful dir-entry-streams per file.
-	Entries []fs.DirEntry
+	Dirents []*platform.Dirent
 }
 
 type FSContext struct {
@@ -333,7 +333,7 @@ func (c *FSContext) ReOpenDir(fd uint32) (*FileEntry, error) {
 		return f, err
 	}
 
-	f.ReadDir.CountRead, f.ReadDir.Entries = 0, nil
+	f.ReadDir.CountRead, f.ReadDir.Dirents = 0, nil
 	return f, nil
 }
 
@@ -454,46 +454,3 @@ func WriterForFile(fsc *FSContext, fd uint32) (writer io.Writer) {
 	}
 	return
 }
-
-// DotEntries returns "." and "..", where "." has a real stat because
-// wasi-testsuite does inode validation.
-func DotEntries(f fs.File) ([]fs.DirEntry, error) {
-	var st platform.Stat_t
-	if err := platform.StatFile(f, &st); err != nil {
-		return nil, err
-	}
-	return []fs.DirEntry{&dotEntry{stat: &st}, dotDotEntry{}}, nil
-}
-
-// dotEntry is a fs.DirEntry representing the directory being listed.
-type dotEntry struct {
-	// stat is the stat of the opened directory
-	stat *platform.Stat_t
-}
-
-func (i *dotEntry) Name() string               { return "." }
-func (i *dotEntry) Type() fs.FileMode          { return i.stat.Mode.Type() }
-func (i *dotEntry) Info() (fs.FileInfo, error) { return i, nil }
-func (i *dotEntry) Size() int64                { return i.stat.Size }
-func (i *dotEntry) Mode() fs.FileMode          { return i.stat.Mode }
-func (i *dotEntry) ModTime() time.Time {
-	return time.Unix(i.stat.Mtim/1e9, i.stat.Mtim%1e9)
-}
-func (i *dotEntry) IsDir() bool      { return true }
-func (i *dotEntry) Sys() interface{} { return nil }
-
-// dotDotEntry is a fake entry for dot-dot (".."), added to satisfy WASI tests.
-//
-// Note: This is intentionally invalid as WASI decided that it must be present
-// on any list, including the root directory: No values are tested apart from
-// the name.
-type dotDotEntry struct{}
-
-func (dotDotEntry) Name() string               { return ".." }
-func (dotDotEntry) Type() fs.FileMode          { return fs.ModeDir }
-func (dotDotEntry) Info() (fs.FileInfo, error) { return dotDotEntry{}, nil }
-func (dotDotEntry) Size() int64                { return 0 }
-func (dotDotEntry) Mode() fs.FileMode          { return fs.ModeDir }
-func (dotDotEntry) ModTime() time.Time         { return time.Unix(0, 0) }
-func (dotDotEntry) IsDir() bool                { return true }
-func (dotDotEntry) Sys() interface{}           { return nil }


Test patch:
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index 60e8756f..6e4feff0 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -1801,21 +1801,21 @@ func Test_fdRead_Errors(t *testing.T) {
 }
 
 var (
-	testDirEntries = func() []fs.DirEntry {
-		entries, err := fstest.FS.ReadDir("dir")
-		if err != nil {
-			panic(err)
-		}
+	testDirents = func() []*platform.Dirent {
 		d, err := fstest.FS.Open("dir")
 		if err != nil {
 			panic(err)
 		}
 		defer d.Close()
-		dots, err := sys.DotEntries(d)
+		dirents, err := platform.Readdir(d, -1)
 		if err != nil {
 			panic(err)
 		}
-		return append(dots, entries...)
+		dots := []*platform.Dirent{
+			{Name: ".", Type: fs.ModeDir},
+			{Name: "..", Type: fs.ModeDir},
+		}
+		return append(dots, dirents...)
 	}()
 
 	direntDot = []byte{
@@ -1891,7 +1891,7 @@ func Test_fdReaddir(t *testing.T) {
 			expectedMem:     direntDot,
 			expectedReadDir: &sys.ReadDir{
 				CountRead: 2,
-				Entries:   testDirEntries[0:2], // dot and dot-dot
+				Dirents:   testDirents[0:2], // dot and dot-dot
 			},
 		},
 		{
@@ -1908,7 +1908,7 @@ func Test_fdReaddir(t *testing.T) {
 			expectedMem:     dirents,
 			expectedReadDir: &sys.ReadDir{
 				CountRead: 5,
-				Entries:   testDirEntries,
+				Dirents:   testDirents,
 			},
 		},
 		{
@@ -1925,7 +1925,7 @@ func Test_fdReaddir(t *testing.T) {
 			expectedMem:     direntDot[:DirentSize], // header without name
 			expectedReadDir: &sys.ReadDir{
 				CountRead: 3,
-				Entries:   testDirEntries[0:3],
+				Dirents:   testDirents[0:3],
 			},
 		},
 		{
@@ -1942,7 +1942,7 @@ func Test_fdReaddir(t *testing.T) {
 			expectedMem:     direntDot,
 			expectedReadDir: &sys.ReadDir{
 				CountRead: 3,
-				Entries:   testDirEntries[0:3],
+				Dirents:   testDirents[0:3],
 			},
 		},
 		{
@@ -1950,14 +1950,14 @@ func Test_fdReaddir(t *testing.T) {
 			dir: func() *sys.FileEntry {
 				dir, err := fstest.FS.Open("dir")
 				require.NoError(t, err)
-				entry, err := dir.(fs.ReadDirFile).ReadDir(1)
+				dirent, err := platform.Readdir(dir, 1)
 				require.NoError(t, err)
 
 				return &sys.FileEntry{
 					File: dir,
 					ReadDir: &sys.ReadDir{
 						CountRead: 3,
-						Entries:   append(testDirEntries[0:2], entry...),
+						Dirents:   append(testDirents[0:2], dirent...),
 					},
 				}
 			},
@@ -1967,7 +1967,7 @@ func Test_fdReaddir(t *testing.T) {
 			expectedMem:     direntDotDot,
 			expectedReadDir: &sys.ReadDir{
 				CountRead: 4,
-				Entries:   testDirEntries[1:4],
+				Dirents:   testDirents[1:4],
 			},
 		},
 		{
@@ -1975,14 +1975,14 @@ func Test_fdReaddir(t *testing.T) {
 			dir: func() *sys.FileEntry {
 				dir, err := fstest.FS.Open("dir")
 				require.NoError(t, err)
-				entry, err := dir.(fs.ReadDirFile).ReadDir(1)
+				dirent, err := platform.Readdir(dir, 1)
 				require.NoError(t, err)
 
 				return &sys.FileEntry{
 					File: dir,
 					ReadDir: &sys.ReadDir{
 						CountRead: 3,
-						Entries:   append(testDirEntries[0:2], entry...),
+						Dirents:   append(testDirents[0:2], dirent...),
 					},
 				}
 			},
@@ -1993,7 +1993,7 @@ func Test_fdReaddir(t *testing.T) {
 			expectedMemSize: len(direntDotDot), // we do not want to compare the full buffer since we don't know what the leftover 4 bytes will contain.
 			expectedReadDir: &sys.ReadDir{
 				CountRead: 4,
-				Entries:   testDirEntries[1:4],
+				Dirents:   testDirents[1:4],
 			},
 		},
 		{
@@ -2001,14 +2001,14 @@ func Test_fdReaddir(t *testing.T) {
 			dir: func() *sys.FileEntry {
 				dir, err := fstest.FS.Open("dir")
 				require.NoError(t, err)
-				entry, err := dir.(fs.ReadDirFile).ReadDir(1)
+				dirent, err := platform.Readdir(dir, 1)
 				require.NoError(t, err)
 
 				return &sys.FileEntry{
 					File: dir,
 					ReadDir: &sys.ReadDir{
 						CountRead: 3,
-						Entries:   append(testDirEntries[0:2], entry...),
+						Dirents:   append(testDirents[0:2], dirent...),
 					},
 				}
 			},
@@ -2018,7 +2018,7 @@ func Test_fdReaddir(t *testing.T) {
 			expectedMem:     append(direntDotDot, dirent1[0:24]...),
 			expectedReadDir: &sys.ReadDir{
 				CountRead: 5,
-				Entries:   testDirEntries[1:5],
+				Dirents:   testDirents[1:5],
 			},
 		},
 		{
@@ -2026,14 +2026,14 @@ func Test_fdReaddir(t *testing.T) {
 			dir: func() *sys.FileEntry {
 				dir, err := fstest.FS.Open("dir")
 				require.NoError(t, err)
-				entry, err := dir.(fs.ReadDirFile).ReadDir(1)
+				dirent, err := platform.Readdir(dir, 1)
 				require.NoError(t, err)
 
 				return &sys.FileEntry{
 					File: dir,
 					ReadDir: &sys.ReadDir{
 						CountRead: 3,
-						Entries:   append(testDirEntries[0:2], entry...),
+						Dirents:   append(testDirents[0:2], dirent...),
 					},
 				}
 			},
@@ -2043,7 +2043,7 @@ func Test_fdReaddir(t *testing.T) {
 			expectedMem:     append(direntDotDot, dirent1...),
 			expectedReadDir: &sys.ReadDir{
 				CountRead: 5,
-				Entries:   testDirEntries[1:5],
+				Dirents:   testDirents[1:5],
 			},
 		},
 		{
@@ -2051,14 +2051,14 @@ func Test_fdReaddir(t *testing.T) {
 			dir: func() *sys.FileEntry {
 				dir, err := fstest.FS.Open("dir")
 				require.NoError(t, err)
-				two, err := dir.(fs.ReadDirFile).ReadDir(2)
+				two, err := platform.Readdir(dir, 2)
 				require.NoError(t, err)
 
 				return &sys.FileEntry{
 					File: dir,
 					ReadDir: &sys.ReadDir{
 						CountRead: 4,
-						Entries:   append(testDirEntries[0:2], two[0:]...),
+						Dirents:   append(testDirents[0:2], two[0:]...),
 					},
 				}
 			},
@@ -2068,7 +2068,7 @@ func Test_fdReaddir(t *testing.T) {
 			expectedMem:     dirent1,
 			expectedReadDir: &sys.ReadDir{
 				CountRead: 5,
-				Entries:   testDirEntries[2:],
+				Dirents:   testDirents[2:],
 			},
 		},
 		{
@@ -2076,14 +2076,14 @@ func Test_fdReaddir(t *testing.T) {
 			dir: func() *sys.FileEntry {
 				dir, err := fstest.FS.Open("dir")
 				require.NoError(t, err)
-				two, err := dir.(fs.ReadDirFile).ReadDir(2)
+				two, err := platform.Readdir(dir, 2)
 				require.NoError(t, err)
 
 				return &sys.FileEntry{
 					File: dir,
 					ReadDir: &sys.ReadDir{
 						CountRead: 4,
-						Entries:   append(testDirEntries[0:2], two[0:]...),
+						Dirents:   append(testDirents[0:2], two[0:]...),
 					},
 				}
 			},
@@ -2093,7 +2093,7 @@ func Test_fdReaddir(t *testing.T) {
 			expectedMem:     append(dirent1, dirent2...),
 			expectedReadDir: &sys.ReadDir{
 				CountRead: 5,
-				Entries:   testDirEntries[2:],
+				Dirents:   testDirents[2:],
 			},
 		},
 	}
diff --git a/imports/wasi_snapshot_preview1/fs_unit_test.go b/imports/wasi_snapshot_preview1/fs_unit_test.go
index 49e01f82..096ded41 100644
--- a/imports/wasi_snapshot_preview1/fs_unit_test.go
+++ b/imports/wasi_snapshot_preview1/fs_unit_test.go
@@ -2,7 +2,6 @@ package wasi_snapshot_preview1
 
 import (
 	"io"
-	"io/fs"
 	"syscall"
 	"testing"
 
@@ -98,12 +97,12 @@ func Test_fdRead_shouldContinueRead(t *testing.T) {
 	}
 }
 
-func Test_lastDirEntries(t *testing.T) {
+func Test_lastDirents(t *testing.T) {
 	tests := []struct {
 		name            string
 		f               *sys.ReadDir
 		cookie          int64
-		expectedEntries []fs.DirEntry
+		expectedDirents []*platform.Dirent
 		expectedErrno   Errno
 	}{
 		{
@@ -118,7 +117,7 @@ func Test_lastDirEntries(t *testing.T) {
 			name: "cookie is negative",
 			f: &sys.ReadDir{
 				CountRead: 3,
-				Entries:   testDirEntries,
+				Dirents:   testDirents,
 			},
 			cookie:        -1,
 			expectedErrno: ErrnoInval,
@@ -127,7 +126,7 @@ func Test_lastDirEntries(t *testing.T) {
 			name: "cookie is greater than last d_next",
 			f: &sys.ReadDir{
 				CountRead: 3,
-				Entries:   testDirEntries,
+				Dirents:   testDirents,
 			},
 			cookie:        5,
 			expectedErrno: ErrnoInval,
@@ -136,25 +135,25 @@ func Test_lastDirEntries(t *testing.T) {
 			name: "cookie is last pos",
 			f: &sys.ReadDir{
 				CountRead: 3,
-				Entries:   testDirEntries,
+				Dirents:   testDirents,
 			},
 			cookie:          3,
-			expectedEntries: nil,
+			expectedDirents: nil,
 		},
 		{
 			name: "cookie is one before last pos",
 			f: &sys.ReadDir{
 				CountRead: 3,
-				Entries:   testDirEntries,
+				Dirents:   testDirents,
 			},
 			cookie:          2,
-			expectedEntries: testDirEntries[2:],
+			expectedDirents: testDirents[2:],
 		},
 		{
 			name: "cookie is before current entries",
 			f: &sys.ReadDir{
 				CountRead: 5,
-				Entries:   testDirEntries,
+				Dirents:   testDirents,
 			},
 			cookie:        1,
 			expectedErrno: ErrnoNosys, // not implemented
@@ -163,10 +162,10 @@ func Test_lastDirEntries(t *testing.T) {
 			name: "read from the beginning (cookie=0)",
 			f: &sys.ReadDir{
 				CountRead: 3,
-				Entries:   testDirEntries,
+				Dirents:   testDirents,
 			},
 			cookie:          0,
-			expectedEntries: testDirEntries,
+			expectedDirents: testDirents,
 		},
 	}
 
@@ -178,9 +177,9 @@ func Test_lastDirEntries(t *testing.T) {
 			if f == nil {
 				f = &sys.ReadDir{}
 			}
-			entries, errno := lastDirEntries(f, tc.cookie)
+			entries, errno := lastDirents(f, tc.cookie)
 			require.Equal(t, tc.expectedErrno, errno)
-			require.Equal(t, tc.expectedEntries, entries)
+			require.Equal(t, tc.expectedDirents, entries)
 		})
 	}
 }
@@ -188,7 +187,7 @@ func Test_lastDirEntries(t *testing.T) {
 func Test_maxDirents(t *testing.T) {
 	tests := []struct {
 		name                        string
-		entries                     []fs.DirEntry
+		dirents                     []*platform.Dirent
 		maxLen                      uint32
 		expectedCount               uint32
 		expectedwriteTruncatedEntry bool
@@ -199,28 +198,28 @@ func Test_maxDirents(t *testing.T) {
 		},
 		{
 			name:                        "can't fit one",
-			entries:                     testDirEntries,
+			dirents:                     testDirents,
 			maxLen:                      23,
 			expectedBufused:             23,
 			expectedwriteTruncatedEntry: false,
 		},
 		{
 			name:                        "only fits header",
-			entries:                     testDirEntries,
+			dirents:                     testDirents,
 			maxLen:                      24,
 			expectedBufused:             24,
 			expectedwriteTruncatedEntry: true,
 		},
 		{
 			name:            "one",
-			entries:         testDirEntries,
+			dirents:         testDirents,
 			maxLen:          25,
 			expectedCount:   1,
 			expectedBufused: 25,
 		},
 		{
 			name:                        "one but not room for two's name",
-			entries:                     testDirEntries,
+			dirents:                     testDirents,
 			maxLen:                      25 + 25,
 			expectedCount:               1,
 			expectedwriteTruncatedEntry: true, // can write DirentSize
@@ -228,14 +227,14 @@ func Test_maxDirents(t *testing.T) {
 		},
 		{
 			name:            "two",
-			entries:         testDirEntries,
+			dirents:         testDirents,
 			maxLen:          25 + 26,
 			expectedCount:   2,
 			expectedBufused: 25 + 26,
 		},
 		{
 			name:                        "two but not three's dirent",
-			entries:                     testDirEntries,
+			dirents:                     testDirents,
 			maxLen:                      25 + 26 + 20,
 			expectedCount:               2,
 			expectedwriteTruncatedEntry: false, // 20 + 4 == DirentSize
@@ -243,7 +242,7 @@ func Test_maxDirents(t *testing.T) {
 		},
 		{
 			name:                        "two but not three's name",
-			entries:                     testDirEntries,
+			dirents:                     testDirents,
 			maxLen:                      25 + 26 + 26,
 			expectedCount:               2,
 			expectedwriteTruncatedEntry: true, // can write DirentSize
@@ -251,7 +250,7 @@ func Test_maxDirents(t *testing.T) {
 		},
 		{
 			name:                        "three",
-			entries:                     testDirEntries,
+			dirents:                     testDirents,
 			maxLen:                      25 + 26 + 27,
 			expectedCount:               3,
 			expectedwriteTruncatedEntry: false, // end of dir
@@ -259,7 +258,7 @@ func Test_maxDirents(t *testing.T) {
 		},
 		{
 			name:                        "max",
-			entries:                     testDirEntries,
+			dirents:                     testDirents,
 			maxLen:                      100,
 			expectedCount:               3,
 			expectedwriteTruncatedEntry: false, // end of dir
@@ -271,7 +270,7 @@ func Test_maxDirents(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			bufused, direntCount, writeTruncatedEntry := maxDirents(tc.entries, tc.maxLen)
+			bufused, direntCount, writeTruncatedEntry := maxDirents(tc.dirents, tc.maxLen)
 			require.Equal(t, tc.expectedCount, direntCount)
 			require.Equal(t, tc.expectedwriteTruncatedEntry, writeTruncatedEntry)
 			require.Equal(t, tc.expectedBufused, bufused)
@@ -280,12 +279,17 @@ func Test_maxDirents(t *testing.T) {
 }
 
 var (
-	testDirEntries = func() []fs.DirEntry {
-		entries, err := fstest.FS.ReadDir("dir")
+	testDirents = func() []*platform.Dirent {
+		dir, err := fstest.FS.Open("dir")
 		if err != nil {
 			panic(err)
 		}
-		return entries
+		defer dir.Close()
+		dirents, err := platform.Readdir(dir, -1)
+		if err != nil {
+			panic(err)
+		}
+		return dirents
 	}()
 
 	dirent1 = []byte{
@@ -314,37 +318,37 @@ var (
 func Test_writeDirents(t *testing.T) {
 	tests := []struct {
 		name                string
-		entries             []fs.DirEntry
+		entries             []*platform.Dirent
 		entryCount          uint32
 		writeTruncatedEntry bool
 		expectedEntriesBuf  []byte
 	}{
 		{
 			name:    "none",
-			entries: testDirEntries,
+			entries: testDirents,
 		},
 		{
 			name:               "one",
-			entries:            testDirEntries,
+			entries:            testDirents,
 			entryCount:         1,
 			expectedEntriesBuf: dirent1,
 		},
 		{
 			name:               "two",
-			entries:            testDirEntries,
+			entries:            testDirents,
 			entryCount:         2,
 			expectedEntriesBuf: append(dirent1, dirent2...),
 		},
 		{
 			name:                "two with truncated",
-			entries:             testDirEntries,
+			entries:             testDirents,
 			entryCount:          2,
 			writeTruncatedEntry: true,
 			expectedEntriesBuf:  append(append(dirent1, dirent2...), dirent3[0:10]...),
 		},
 		{
 			name:               "three",
-			entries:            testDirEntries,
+			entries:            testDirents,
 			entryCount:         3,
 			expectedEntriesBuf: append(append(dirent1, dirent2...), dirent3...),
 		},
diff --git a/internal/platform/dir_test.go b/internal/platform/dir_test.go
index 25cb614e..d04458eb 100644
--- a/internal/platform/dir_test.go
+++ b/internal/platform/dir_test.go
@@ -77,3 +77,84 @@ func TestReaddirnames(t *testing.T) {
 		})
 	}
 }
+
+func TestReaddir(t *testing.T) {
+	t.Parallel()
+
+	tmpDir := t.TempDir()
+	require.NoError(t, fstest.WriteTestFiles(tmpDir))
+	dirFS := os.DirFS(tmpDir)
+
+	tests := []struct {
+		name string
+		fs   fs.FS
+	}{
+		{name: "os.DirFS", fs: dirFS},         // To test readdirFile
+		{name: "fstest.MapFS", fs: fstest.FS}, // To test adaptation of ReadDirFile
+	}
+
+	for _, tc := range tests {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			dirF, err := tc.fs.Open(".")
+			require.NoError(t, err)
+			defer dirF.Close()
+
+			t.Run("dir", func(t *testing.T) {
+				dirents, err := platform.Readdir(dirF, -1)
+				require.NoError(t, err)
+				sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name })
+
+				require.Equal(t, 5, len(dirents))
+				require.Equal(t, "animals.txt", dirents[0].Name)
+				require.Zero(t, dirents[0].Type)
+				require.Equal(t, "dir", dirents[1].Name)
+				require.Equal(t, fs.ModeDir, dirents[1].Type)
+				require.Equal(t, "empty.txt", dirents[2].Name)
+				require.Zero(t, dirents[2].Type)
+				require.Equal(t, "emptydir", dirents[3].Name)
+				require.Equal(t, fs.ModeDir, dirents[3].Type)
+				require.Equal(t, "sub", dirents[4].Name)
+				require.Equal(t, fs.ModeDir, dirents[4].Type)
+
+				// read again even though it is exhausted
+				dirents, err = platform.Readdir(dirF, 100)
+				require.NoError(t, err)
+				require.Zero(t, len(dirents))
+			})
+
+			// windows and fstest.MapFS allow you to read a closed dir
+			if runtime.GOOS != "windows" && tc.name != "fstest.MapFS" {
+				t.Run("closed dir", func(t *testing.T) {
+					require.NoError(t, dirF.Close())
+					_, err := platform.Readdir(dirF, -1)
+					require.EqualErrno(t, syscall.EIO, err)
+				})
+			}
+
+			fileF, err := tc.fs.Open("empty.txt")
+			require.NoError(t, err)
+			defer fileF.Close()
+
+			t.Run("file", func(t *testing.T) {
+				_, err := platform.Readdir(fileF, -1)
+				require.EqualErrno(t, syscall.ENOTDIR, err)
+			})
+
+			subdirF, err := tc.fs.Open("sub")
+			require.NoError(t, err)
+			defer subdirF.Close()
+
+			t.Run("subdir", func(t *testing.T) {
+				dirents, err := platform.Readdir(subdirF, -1)
+				require.NoError(t, err)
+				sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name })
+
+				require.Equal(t, 1, len(dirents))
+				require.Equal(t, "test.txt", dirents[0].Name)
+				require.Zero(t, dirents[0].Type)
+			})
+		})
+	}
+}
diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go
index 4db9d290..97a516d2 100644
--- a/internal/sys/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -12,6 +12,7 @@ import (
 	"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"
@@ -240,7 +241,7 @@ func TestFSContext_ReOpenDir(t *testing.T) {
 		require.True(t, ok)
 
 		// Set arbitrary state.
-		ent.ReadDir = &ReadDir{Entries: make([]fs.DirEntry, 10), CountRead: 12345}
+		ent.ReadDir = &ReadDir{Dirents: make([]*platform.Dirent, 10), CountRead: 12345}
 
 		// Then reopen the same file descriptor.
 		ent, err = fsc.ReOpenDir(dirFd)