Repository: tetratelabs/wazero
Pull request #1391 (2023-04-21 12:26:26)

fs: returns EBADF on negative file descriptor
This changes file descriptors from uint32 to int32 and the corresponding file table to reject negative values. This ensures invalid values aren't mistaken for very large descriptor entries, which can use a lot of memory as the table implementation isn't designed to be sparse.


Thanks to @jerbob92 for noticing this!

See https://pubs.opengroup.org/onlinepubs/9699919799/functions/dirfd.html#tag_16_90

codefromthecrypt: > Regarding the attack vector of using fd_renumber with a very large value, we could add a "max open file" limit which then defines the maximum value that a file descriptor can take?

sounds like a nice follow-up when we are ready to do a minor.. maybe `FSConfig.WithMaxFiles(int32)`? Then change the table to guard on that (EBADF when over or something else..)

Patch:
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index 7ca64331..70b60c9f 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -39,7 +39,7 @@ var fdAdvise = newHostFunc(
 )
 
 func fdAdviseFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	_ = params[1]
 	_ = params[2]
 	advice := byte(params[3])
@@ -82,7 +82,7 @@ var fdAllocate = newHostFunc(
 )
 
 func fdAllocateFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	offset := params[1]
 	length := params[2]
 
@@ -135,7 +135,7 @@ var fdClose = newHostFunc(wasip1.FdCloseName, fdCloseFn, []api.ValueType{i32}, "
 
 func fdCloseFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
-	fd := uint32(params[0])
+	fd := int32(params[0])
 
 	return fsc.CloseFile(fd)
 }
@@ -148,7 +148,7 @@ var fdDatasync = newHostFunc(wasip1.FdDatasyncName, fdDatasyncFn, []api.ValueTyp
 
 func fdDatasyncFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
-	fd := uint32(params[0])
+	fd := int32(params[0])
 
 	// Check to see if the file descriptor is available
 	if f, ok := fsc.LookupFile(fd); !ok {
@@ -202,7 +202,7 @@ var fdFdstatGet = newHostFunc(wasip1.FdFdstatGetName, fdFdstatGetFn, []api.Value
 func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd, resultFdstat := uint32(params[0]), uint32(params[1])
+	fd, resultFdstat := int32(params[0]), uint32(params[1])
 
 	// Ensure we can write the fdstat
 	buf, ok := mod.Memory().Read(resultFdstat, 24)
@@ -247,7 +247,7 @@ func writeFdstat(buf []byte, filetype uint8, fdflags uint16) {
 var fdFdstatSetFlags = newHostFunc(wasip1.FdFdstatSetFlagsName, fdFdstatSetFlagsFn, []wasm.ValueType{i32, i32}, "fd", "flags")
 
 func fdFdstatSetFlagsFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
-	fd, wasiFlag := uint32(params[0]), uint16(params[1])
+	fd, wasiFlag := int32(params[0]), uint16(params[1])
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
 	// We can only support APPEND flag.
@@ -324,10 +324,10 @@ var fdFilestatGet = newHostFunc(wasip1.FdFilestatGetName, fdFilestatGetFn, []api
 // fdFilestatGetFn cannot currently use proxyResultParams because filestat is
 // larger than api.ValueTypeI64 (i64 == 8 bytes, but filestat is 64).
 func fdFilestatGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
-	return fdFilestatGetFunc(mod, uint32(params[0]), uint32(params[1]))
+	return fdFilestatGetFunc(mod, int32(params[0]), uint32(params[1]))
 }
 
-func fdFilestatGetFunc(mod api.Module, fd, resultBuf uint32) syscall.Errno {
+func fdFilestatGetFunc(mod api.Module, fd int32, resultBuf uint32) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
 	// Ensure we can write the filestat
@@ -388,7 +388,7 @@ func writeFilestat(buf []byte, st *platform.Stat_t) (errno syscall.Errno) {
 var fdFilestatSetSize = newHostFunc(wasip1.FdFilestatSetSizeName, fdFilestatSetSizeFn, []wasm.ValueType{i32, i64}, "fd", "size")
 
 func fdFilestatSetSizeFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	size := uint32(params[1])
 
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -415,7 +415,7 @@ var fdFilestatSetTimes = newHostFunc(
 )
 
 func fdFilestatSetTimesFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	atim := int64(params[1])
 	mtim := int64(params[2])
 	fstFlags := uint16(params[3])
@@ -525,7 +525,7 @@ var fdPrestatGet = newHostFunc(wasip1.FdPrestatGetName, fdPrestatGetFn, []api.Va
 
 func fdPrestatGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
-	fd, resultPrestat := uint32(params[0]), uint32(params[1])
+	fd, resultPrestat := int32(params[0]), uint32(params[1])
 
 	name, errno := preopenPath(fsc, fd)
 	if errno != 0 {
@@ -579,7 +579,7 @@ var fdPrestatDirName = newHostFunc(
 
 func fdPrestatDirNameFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
-	fd, path, pathLen := uint32(params[0]), uint32(params[1]), uint32(params[2])
+	fd, path, pathLen := int32(params[0]), uint32(params[1]), uint32(params[2])
 
 	name, errno := preopenPath(fsc, fd)
 	if errno != 0 {
@@ -676,7 +676,7 @@ func fdReadOrPread(mod api.Module, params []uint64, isPread bool) syscall.Errno
 	mem := mod.Memory()
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd := uint32(params[0])
+	fd := int32(params[0])
 
 	r, ok := fsc.LookupFile(fd)
 	if !ok {
@@ -761,7 +761,7 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) syscall.Err
 	mem := mod.Memory()
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	buf := uint32(params[1])
 	bufLen := uint32(params[2])
 	// We control the value of the cookie, and it should never be negative.
@@ -1039,7 +1039,7 @@ 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 uint32) (fs.File, *sys.ReadDir, syscall.Errno) {
+func openedDir(fsc *sys.FSContext, fd int32) (fs.File, *sys.ReadDir, syscall.Errno) {
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return nil, nil, syscall.EBADF
 	} else if _, ft, err := f.CachedStat(); err != nil {
@@ -1069,8 +1069,8 @@ var fdRenumber = newHostFunc(wasip1.FdRenumberName, fdRenumberFn, []wasm.ValueTy
 func fdRenumberFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	from := uint32(params[0])
-	to := uint32(params[1])
+	from := int32(params[0])
+	to := int32(params[1])
 
 	if errno := fsc.Renumber(from, to); errno != 0 {
 		return errno
@@ -1123,7 +1123,7 @@ var fdSeek = newHostFunc(
 
 func fdSeekFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	offset := params[1]
 	whence := uint32(params[2])
 	resultNewoffset := uint32(params[3])
@@ -1164,7 +1164,7 @@ var fdSync = newHostFunc(wasip1.FdSyncName, fdSyncFn, []api.ValueType{i32}, "fd"
 
 func fdSyncFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
-	fd := uint32(params[0])
+	fd := int32(params[0])
 
 	// Check to see if the file descriptor is available
 	if f, ok := fsc.LookupFile(fd); !ok {
@@ -1265,7 +1265,7 @@ func fdWriteOrPwrite(mod api.Module, params []uint64, isPwrite bool) syscall.Err
 	mem := mod.Memory()
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	iovs := uint32(params[1])
 	iovsCount := uint32(params[2])
 
@@ -1347,7 +1347,7 @@ var pathCreateDirectory = newHostFunc(
 func pathCreateDirectoryFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	path := uint32(params[1])
 	pathLen := uint32(params[2])
 
@@ -1400,7 +1400,7 @@ var pathFilestatGet = newHostFunc(
 func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	flags := uint16(params[1])
 	path := uint32(params[2])
 	pathLen := uint32(params[3])
@@ -1450,7 +1450,7 @@ var pathFilestatSetTimes = newHostFunc(
 )
 
 func pathFilestatSetTimesFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	flags := uint16(params[1])
 	path := uint32(params[2])
 	pathLen := uint32(params[3])
@@ -1489,18 +1489,18 @@ func pathLinkFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
 	mem := mod.Memory()
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	oldFd := uint32(params[0])
+	oldFD := int32(params[0])
 	// TODO: use old_flags?
 	_ = uint32(params[1])
 	oldPath := uint32(params[2])
 	oldPathLen := uint32(params[3])
 
-	oldFS, oldName, errno := atPath(fsc, mem, oldFd, oldPath, oldPathLen)
+	oldFS, oldName, errno := atPath(fsc, mem, oldFD, oldPath, oldPathLen)
 	if errno != 0 {
 		return errno
 	}
 
-	newFD := uint32(params[4])
+	newFD := int32(params[4])
 	newPath := uint32(params[5])
 	newPathLen := uint32(params[6])
 
@@ -1530,7 +1530,7 @@ func pathLinkFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
 //   - fsRightsInheriting: ignored as rights were removed from WASI.
 //     created file descriptor for `path`
 //   - fdFlags: file descriptor flags
-//   - resultOpenedFd: offset in api.Memory to write the newly created file
+//   - resultOpenedFD: offset in api.Memory to write the newly created file
 //     descriptor to.
 //   - The result FD value is guaranteed to be less than 2**31
 //
@@ -1538,7 +1538,7 @@ func pathLinkFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
 //
 // The return value is 0 except the following error conditions:
 //   - syscall.EBADF: `fd` is invalid
-//   - syscall.EFAULT: `resultOpenedFd` points to an offset out of memory
+//   - syscall.EFAULT: `resultOpenedFD` points to an offset out of memory
 //   - syscall.ENOENT: `path` does not exist.
 //   - syscall.EEXIST: `path` exists, while `oFlags` requires that it must not.
 //   - syscall.ENOTDIR: `path` is not a directory, while `oFlags` requires it.
@@ -1554,7 +1554,7 @@ func pathLinkFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
 //	[]byte{ ?, 'w', 'a', 'z', 'e', 'r', 'o', ?... }
 //	     path --^
 //
-// Then, if parameters resultOpenedFd = 8, and this function opened a new file
+// Then, if parameters resultOpenedFD = 8, and this function opened a new file
 // descriptor 5 with the given flags, this function writes the below to
 // api.Memory:
 //
@@ -1562,7 +1562,7 @@ func pathLinkFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
 //	                 +--------+
 //	                 |        |
 //	[]byte{ 0..6, ?, 5, 0, 0, 0, ?}
-//	resultOpenedFd --^
+//	resultOpenedFD --^
 //
 // # Notes
 //   - This is similar to `openat` in POSIX. https://linux.die.net/man/3/openat
@@ -1578,7 +1578,7 @@ var pathOpen = newHostFunc(
 func pathOpenFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	preopenFD := uint32(params[0])
+	preopenFD := int32(params[0])
 
 	// TODO: dirflags is a lookupflags, and it only has one bit: symlink_follow
 	// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#lookupflags
@@ -1594,7 +1594,7 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
 	_ = params[6]
 
 	fdflags := uint16(params[7])
-	resultOpenedFd := uint32(params[8])
+	resultOpenedFD := uint32(params[8])
 
 	preopen, pathName, errno := atPath(fsc, mod.Memory(), preopenFD, path, pathLen)
 	if errno != 0 {
@@ -1626,7 +1626,7 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
 		}
 	}
 
-	if !mod.Memory().WriteUint32Le(resultOpenedFd, newFD) {
+	if !mod.Memory().WriteUint32Le(resultOpenedFD, uint32(newFD)) {
 		_ = fsc.CloseFile(newFD)
 		return syscall.EFAULT
 	}
@@ -1649,7 +1649,7 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
 //
 // See https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/sources/at_fdcwd.c
 // See https://linux.die.net/man/2/openat
-func atPath(fsc *sys.FSContext, mem api.Memory, fd, p, pathLen uint32) (sysfs.FS, string, syscall.Errno) {
+func atPath(fsc *sys.FSContext, mem api.Memory, fd int32, p, pathLen uint32) (sysfs.FS, string, syscall.Errno) {
 	b, ok := mem.Read(p, pathLen)
 	if !ok {
 		return nil, "", syscall.EFAULT
@@ -1676,7 +1676,7 @@ func atPath(fsc *sys.FSContext, mem api.Memory, fd, p, pathLen uint32) (sysfs.FS
 	}
 
 	if f, ok := fsc.LookupFile(fd); !ok {
-		return nil, "", syscall.EBADF // closed
+		return nil, "", syscall.EBADF // closed or invalid
 	} else if _, ft, err := f.CachedStat(); err != nil {
 		return nil, "", platform.UnwrapOSError(err)
 	} else if ft.Type() != fs.ModeDir {
@@ -1689,7 +1689,7 @@ func atPath(fsc *sys.FSContext, mem api.Memory, fd, p, pathLen uint32) (sysfs.FS
 	}
 }
 
-func preopenPath(fsc *sys.FSContext, fd uint32) (string, syscall.Errno) {
+func preopenPath(fsc *sys.FSContext, fd int32) (string, syscall.Errno) {
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return "", syscall.EBADF // closed
 	} else if !f.IsPreopen {
@@ -1744,7 +1744,7 @@ var pathReadlink = newHostFunc(
 func pathReadlinkFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	path := uint32(params[1])
 	pathLen := uint32(params[2])
 	buf := uint32(params[3])
@@ -1807,7 +1807,7 @@ var pathRemoveDirectory = newHostFunc(
 func pathRemoveDirectoryFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	path := uint32(params[1])
 	pathLen := uint32(params[2])
 
@@ -1853,11 +1853,11 @@ var pathRename = newHostFunc(
 func pathRenameFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	oldPath := uint32(params[1])
 	oldPathLen := uint32(params[2])
 
-	newFD := uint32(params[3])
+	newFD := int32(params[3])
 	newPath := uint32(params[4])
 	newPathLen := uint32(params[5])
 
@@ -1893,7 +1893,7 @@ func pathSymlinkFn(_ context.Context, mod api.Module, params []uint64) syscall.E
 
 	oldPath := uint32(params[0])
 	oldPathLen := uint32(params[1])
-	fd := uint32(params[2])
+	fd := int32(params[2])
 	newPath := uint32(params[3])
 	newPathLen := uint32(params[4])
 
@@ -1969,7 +1969,7 @@ var pathUnlinkFile = newHostFunc(
 func pathUnlinkFileFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	path := uint32(params[1])
 	pathLen := uint32(params[2])
 
diff --git a/imports/wasi_snapshot_preview1/poll.go b/imports/wasi_snapshot_preview1/poll.go
index cb60c679..057d76e3 100644
--- a/imports/wasi_snapshot_preview1/poll.go
+++ b/imports/wasi_snapshot_preview1/poll.go
@@ -128,7 +128,10 @@ func pollOneoffFn(ctx context.Context, mod api.Module, params []uint64) syscall.
 			// Ack the clock event to the outBuf.
 			writeEvent(outBuf, evt)
 		case wasip1.EventTypeFdRead:
-			fd := le.Uint32(argBuf)
+			fd := int32(le.Uint32(argBuf))
+			if fd < 0 {
+				return syscall.EBADF
+			}
 			if fd == internalsys.FdStdin {
 				// if the fd is Stdin, do not ack yet,
 				// append to a slice for delayed evaluation.
@@ -139,7 +142,10 @@ func pollOneoffFn(ctx context.Context, mod api.Module, params []uint64) syscall.
 				readySubs++
 			}
 		case wasip1.EventTypeFdWrite:
-			fd := le.Uint32(argBuf)
+			fd := int32(le.Uint32(argBuf))
+			if fd < 0 {
+				return syscall.EBADF
+			}
 			evt.errno = processFDEventWrite(fsc, fd)
 			readySubs++
 			writeEvent(outBuf, evt)
@@ -216,7 +222,7 @@ func processClockEvent(inBuf []byte) (time.Duration, syscall.Errno) {
 }
 
 // processFDEventRead returns ErrnoSuccess if the file exists and ErrnoBadf otherwise.
-func processFDEventRead(fsc *internalsys.FSContext, fd uint32) wasip1.Errno {
+func processFDEventRead(fsc *internalsys.FSContext, fd int32) wasip1.Errno {
 	if _, ok := fsc.LookupFile(fd); ok {
 		return wasip1.ErrnoSuccess
 	} else {
@@ -225,7 +231,7 @@ func processFDEventRead(fsc *internalsys.FSContext, fd uint32) wasip1.Errno {
 }
 
 // processFDEventWrite returns ErrnoNotsup if the file exists and ErrnoBadf otherwise.
-func processFDEventWrite(fsc *internalsys.FSContext, fd uint32) wasip1.Errno {
+func processFDEventWrite(fsc *internalsys.FSContext, fd int32) wasip1.Errno {
 	if internalsys.WriterForFile(fsc, fd) == nil {
 		return wasip1.ErrnoBadf
 	}
diff --git a/internal/descriptor/table.go b/internal/descriptor/table.go
index 0ed376f2..542958bc 100644
--- a/internal/descriptor/table.go
+++ b/internal/descriptor/table.go
@@ -4,16 +4,24 @@ import "math/bits"
 
 // Table is a data structure mapping 32 bit descriptor to items.
 //
+// # Negative keys are invalid.
+//
+// Negative keys (e.g. -1) are invalid inputs and will return a corresponding
+// not-found value. This matches POSIX behavior of file descriptors.
+// See https://pubs.opengroup.org/onlinepubs/9699919799/functions/dirfd.html#tag_16_90
+//
+// # Data structure design
+//
 // The data structure optimizes for memory density and lookup performance,
 // trading off compute at insertion time. This is a useful compromise for the
 // use cases we employ it with: items are usually accessed a lot more often
-// than they are inserted, each operation requires a table lookup so we are
+// than they are inserted, each operation requires a table lookup, so we are
 // better off spending extra compute to insert items in the table in order to
 // get cheaper lookups. Memory efficiency is also crucial to support scaling
 // with programs that maintain thousands of items: having a high or non-linear
-// memory-to-item ratio could otherwise be used as an attack vector by malicous
-// applications attempting to damage performance of the host.
-type Table[Key ~uint32, Item any] struct {
+// memory-to-item ratio could otherwise be used as an attack vector by
+// malicious applications attempting to damage performance of the host.
+type Table[Key ~int32, Item any] struct {
 	masks []uint64
 	items []Item
 }
@@ -29,9 +37,9 @@ func (t *Table[Key, Item]) Len() (n int) {
 	return n
 }
 
-// Grow ensures that t has enough room for n items, potentially reallocating the
+// grow ensures that t has enough room for n items, potentially reallocating the
 // internal buffers if their capacity was too small to hold this many items.
-func (t *Table[Key, Item]) Grow(n int) {
+func (t *Table[Key, Item]) grow(n int) {
 	// Round up to a multiple of 64 since this is the smallest increment due to
 	// using 64 bits masks.
 	n = (n*64 + 63) / 64
@@ -49,11 +57,11 @@ func (t *Table[Key, Item]) Grow(n int) {
 }
 
 // Insert inserts the given item to the table, returning the key that it is
-// mapped to.
+// mapped to or false if the table was full.
 //
 // The method does not perform deduplication, it is possible for the same item
 // to be inserted multiple times, each insertion will return a different key.
-func (t *Table[Key, Item]) Insert(item Item) (key Key) {
+func (t *Table[Key, Item]) Insert(item Item) (key Key, ok bool) {
 	offset := 0
 insert:
 	// Note: this loop could be made a lot more efficient using vectorized
@@ -66,7 +74,7 @@ insert:
 			key = Key(index)*64 + Key(shift)
 			t.items[key] = item
 			t.masks[index] = mask | uint64(1<<shift)
-			return key
+			return key, key >= 0
 		}
 	}
 
@@ -76,12 +84,15 @@ insert:
 		n = 1
 	}
 
-	t.Grow(n)
+	t.grow(n)
 	goto insert
 }
 
 // Lookup returns the item associated with the given key (may be nil).
 func (t *Table[Key, Item]) Lookup(key Key) (item Item, found bool) {
+	if key < 0 { // invalid key
+		return
+	}
 	if i := int(key); i >= 0 && i < len(t.items) {
 		index := uint(key) / 64
 		shift := uint(key) % 64
@@ -92,19 +103,27 @@ func (t *Table[Key, Item]) Lookup(key Key) (item Item, found bool) {
 	return
 }
 
-// InsertAt inserts the given `item` at the item descriptor `key`.
-func (t *Table[Key, Item]) InsertAt(item Item, key Key) {
+// InsertAt inserts the given `item` at the item descriptor `key`. This returns
+// false if the insert was impossible due to negative key.
+func (t *Table[Key, Item]) InsertAt(item Item, key Key) bool {
+	if key < 0 {
+		return false
+	}
 	if diff := int(key) - t.Len(); diff > 0 {
-		t.Grow(diff)
+		t.grow(diff)
 	}
 	index := uint(key) / 64
 	shift := uint(key) % 64
 	t.masks[index] |= 1 << shift
 	t.items[key] = item
+	return true
 }
 
 // Delete deletes the item stored at the given key from the table.
 func (t *Table[Key, Item]) Delete(key Key) {
+	if key < 0 { // invalid key
+		return
+	}
 	if index, shift := key/64, key%64; int(index) < len(t.masks) {
 		mask := t.masks[index]
 		if (mask & (1 << shift)) != 0 {
diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go
index 3a81a610..1fe298a4 100644
--- a/internal/gojs/fs.go
+++ b/internal/gojs/fs.go
@@ -176,7 +176,7 @@ type jsfsFstat struct{}
 func (jsfsFstat) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd := goos.ValueToUint32(args[0])
+	fd := goos.ValueToInt32(args[0])
 	callback := args[1].(funcWrapper)
 
 	fstat, err := syscallFstat(fsc, fd)
@@ -184,7 +184,7 @@ func (jsfsFstat) invoke(ctx context.Context, mod api.Module, args ...interface{}
 }
 
 // syscallFstat is like syscall.Fstat
-func syscallFstat(fsc *internalsys.FSContext, fd uint32) (*jsSt, error) {
+func syscallFstat(fsc *internalsys.FSContext, fd int32) (*jsSt, error) {
 	f, ok := fsc.LookupFile(fd)
 	if !ok {
 		return nil, syscall.EBADF
@@ -219,7 +219,7 @@ type jsfsClose struct{}
 func (jsfsClose) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
-	fd := goos.ValueToUint32(args[0])
+	fd := goos.ValueToInt32(args[0])
 	callback := args[1].(funcWrapper)
 
 	errno := fsc.CloseFile(fd)
@@ -234,7 +234,7 @@ func (jsfsClose) invoke(ctx context.Context, mod api.Module, args ...interface{}
 type jsfsRead struct{}
 
 func (jsfsRead) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := goos.ValueToUint32(args[0])
+	fd := goos.ValueToInt32(args[0])
 	buf, ok := args[1].(*goos.ByteArray)
 	if !ok {
 		return nil, fmt.Errorf("arg[1] is %v not a []byte", args[1])
@@ -249,7 +249,7 @@ func (jsfsRead) invoke(ctx context.Context, mod api.Module, args ...interface{})
 }
 
 // syscallRead is like syscall.Read
-func syscallRead(mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) {
+func syscallRead(mod api.Module, fd int32, offset interface{}, p []byte) (n uint32, err error) {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
 	f, ok := fsc.LookupFile(fd)
@@ -281,7 +281,7 @@ func syscallRead(mod api.Module, fd uint32, offset interface{}, p []byte) (n uin
 type jsfsWrite struct{}
 
 func (jsfsWrite) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := goos.ValueToUint32(args[0])
+	fd := goos.ValueToInt32(args[0])
 	buf, ok := args[1].(*goos.ByteArray)
 	if !ok {
 		return nil, fmt.Errorf("arg[1] is %v not a []byte", args[1])
@@ -299,7 +299,7 @@ func (jsfsWrite) invoke(ctx context.Context, mod api.Module, args ...interface{}
 }
 
 // syscallWrite is like syscall.Write
-func syscallWrite(mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) {
+func syscallWrite(mod api.Module, fd int32, offset interface{}, p []byte) (n uint32, err error) {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 
 	var writer io.Writer
@@ -377,7 +377,7 @@ func (m *jsfsMkdir) invoke(ctx context.Context, mod api.Module, args ...interfac
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 	root := fsc.RootFS()
 
-	var fd uint32
+	var fd int32
 	var errno syscall.Errno
 	// We need at least read access to open the file descriptor
 	if perm == 0 {
@@ -489,7 +489,7 @@ func (c *jsfsChmod) invoke(ctx context.Context, mod api.Module, args ...interfac
 type jsfsFchmod struct{}
 
 func (jsfsFchmod) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := goos.ValueToUint32(args[0])
+	fd := goos.ValueToInt32(args[0])
 	mode := custom.FromJsMode(goos.ValueToUint32(args[1]), 0)
 	callback := args[2].(funcWrapper)
 
@@ -532,7 +532,7 @@ func (c *jsfsChown) invoke(ctx context.Context, mod api.Module, args ...interfac
 type jsfsFchown struct{}
 
 func (jsfsFchown) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := goos.ValueToUint32(args[0])
+	fd := goos.ValueToInt32(args[0])
 	uid := goos.ValueToUint32(args[1])
 	gid := goos.ValueToUint32(args[2])
 	callback := args[3].(funcWrapper)
@@ -592,7 +592,7 @@ func (t *jsfsTruncate) invoke(ctx context.Context, mod api.Module, args ...inter
 type jsfsFtruncate struct{}
 
 func (jsfsFtruncate) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := goos.ValueToUint32(args[0])
+	fd := goos.ValueToInt32(args[0])
 	length := toInt64(args[1])
 	callback := args[2].(funcWrapper)
 
@@ -670,7 +670,7 @@ func (s *jsfsSymlink) invoke(ctx context.Context, mod api.Module, args ...interf
 type jsfsFsync struct{}
 
 func (jsfsFsync) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	fd := goos.ValueToUint32(args[0])
+	fd := goos.ValueToInt32(args[0])
 	callback := args[1].(funcWrapper)
 
 	// Check to see if the file descriptor is available
diff --git a/internal/gojs/goarch/wasm.go b/internal/gojs/goarch/wasm.go
index 54c5a90d..9f2f56c3 100644
--- a/internal/gojs/goarch/wasm.go
+++ b/internal/gojs/goarch/wasm.go
@@ -40,6 +40,8 @@ type Stack interface {
 	// positions i, i+1)
 	ParamString(mem api.Memory, i int) string
 
+	ParamInt32(i int) int32
+
 	ParamUint32(i int) uint32
 
 	// Refresh the stack from the current stack pointer (SP).
@@ -96,6 +98,11 @@ func (s *stack) ParamString(mem api.Memory, i int) string {
 	return string(s.ParamBytes(mem, i)) // safe copy of guest memory
 }
 
+// ParamInt32 implements Stack.ParamInt32
+func (s *stack) ParamInt32(i int) int32 {
+	return int32(s.Param(i))
+}
+
 // ParamUint32 implements Stack.ParamUint32
 func (s *stack) ParamUint32(i int) uint32 {
 	return uint32(s.Param(i))
diff --git a/internal/gojs/goos/goos.go b/internal/gojs/goos/goos.go
index 477b02a3..e46d93be 100644
--- a/internal/gojs/goos/goos.go
+++ b/internal/gojs/goos/goos.go
@@ -170,6 +170,11 @@ func (s *stack) ParamString(mem api.Memory, i int) string {
 	return s.s.ParamString(mem, i)
 }
 
+// ParamInt32 implements the same method as documented on goarch.Stack
+func (s *stack) ParamInt32(i int) int32 {
+	return s.s.ParamInt32(i)
+}
+
 // ParamUint32 implements the same method as documented on goarch.Stack
 func (s *stack) ParamUint32(i int) uint32 {
 	return s.s.ParamUint32(i)
diff --git a/internal/gojs/logging/logging.go b/internal/gojs/logging/logging.go
index 744558d2..7ca69dc3 100644
--- a/internal/gojs/logging/logging.go
+++ b/internal/gojs/logging/logging.go
@@ -173,7 +173,7 @@ func (s *syscallValueCallParamSampler) isSampled(ctx context.Context, mod api.Mo
 		// Don't amplify logs with stdio reads or writes
 		switch m {
 		case custom.NameFsWrite, custom.NameFsRead:
-			fd := goos.ValueToUint32(args[0])
+			fd := goos.ValueToInt32(args[0])
 			return fd > sys.FdStderr
 		}
 		return true
diff --git a/internal/gojs/runtime.go b/internal/gojs/runtime.go
index 68fc5f36..ad9ecf6d 100644
--- a/internal/gojs/runtime.go
+++ b/internal/gojs/runtime.go
@@ -38,7 +38,7 @@ func wasmExit(ctx context.Context, mod api.Module, stack goarch.Stack) {
 var WasmWrite = goarch.NewFunc(custom.NameRuntimeWasmWrite, wasmWrite)
 
 func wasmWrite(_ context.Context, mod api.Module, stack goarch.Stack) {
-	fd := stack.ParamUint32(0)
+	fd := stack.ParamInt32(0)
 	p := stack.ParamBytes(mod.Memory(), 1 /*, 2 */)
 
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
diff --git a/internal/sys/fs.go b/internal/sys/fs.go
index ce0ac1fe..7436e2d8 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -1,7 +1,6 @@
 package sys
 
 import (
-	"context"
 	"fmt"
 	"io"
 	"io/fs"
@@ -15,7 +14,7 @@ import (
 )
 
 const (
-	FdStdin uint32 = iota
+	FdStdin int32 = iota
 	FdStdout
 	FdStderr
 	// FdPreopen is the file descriptor of the first pre-opened directory.
@@ -32,10 +31,7 @@ const (
 	FdPreopen
 )
 
-const (
-	modeDevice     = uint32(fs.ModeDevice | 0o640)
-	modeCharDevice = uint32(fs.ModeDevice | fs.ModeCharDevice | 0o640)
-)
+const modeDevice = uint32(fs.ModeDevice | 0o640)
 
 type stdioFileWriter struct {
 	w io.Writer
@@ -132,9 +128,9 @@ func (r *StdioFileReader) Close() error {
 }
 
 var (
-	noopStdinStat  = stdioFileInfo{FdStdin, modeDevice}
-	noopStdoutStat = stdioFileInfo{FdStdout, modeDevice}
-	noopStderrStat = stdioFileInfo{FdStderr, modeDevice}
+	noopStdinStat  = stdioFileInfo{0, modeDevice}
+	noopStdoutStat = stdioFileInfo{1, modeDevice}
+	noopStderrStat = stdioFileInfo{2, modeDevice}
 )
 
 // stdioFileInfo implements fs.FileInfo where index zero is the FD and one is the mode.
@@ -142,11 +138,11 @@ type stdioFileInfo [2]uint32
 
 func (s stdioFileInfo) Name() string {
 	switch s[0] {
-	case FdStdin:
+	case 0:
 		return "stdin"
-	case FdStdout:
+	case 1:
 		return "stdout"
-	case FdStderr:
+	case 2:
 		return "stderr"
 	default:
 		panic(fmt.Errorf("BUG: incorrect FD %d", s[0]))
@@ -294,9 +290,9 @@ type FSContext struct {
 	openedFiles FileTable
 }
 
-// FileTable is an specialization of the descriptor.Table type used to map file
+// FileTable is a specialization of the descriptor.Table type used to map file
 // descriptors to file entries.
-type FileTable = descriptor.Table[uint32, *FileEntry]
+type FileTable = descriptor.Table[int32, *FileEntry]
 
 // NewFSContext creates a FSContext with stdio streams and an optional
 // pre-opened filesystem.
@@ -395,7 +391,7 @@ func (c *FSContext) RootFS() sysfs.FS {
 
 // OpenFile opens the file into the table and returns its file descriptor.
 // The result must be closed by CloseFile or Close.
-func (c *FSContext) OpenFile(fs sysfs.FS, path string, flag int, perm fs.FileMode) (uint32, syscall.Errno) {
+func (c *FSContext) OpenFile(fs sysfs.FS, path string, flag int, perm fs.FileMode) (int32, syscall.Errno) {
 	if f, errno := fs.OpenFile(path, flag, perm); errno != 0 {
 		return 0, errno
 	} else {
@@ -405,14 +401,17 @@ func (c *FSContext) OpenFile(fs sysfs.FS, path string, flag int, perm fs.FileMod
 		} else {
 			fe.Name = path
 		}
-		newFD := c.openedFiles.Insert(fe)
-		return newFD, 0
+		if newFD, ok := c.openedFiles.Insert(fe); !ok {
+			return 0, syscall.EBADF
+		} else {
+			return newFD, 0
+		}
 	}
 }
 
 // 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 uint32) (*FileEntry, syscall.Errno) {
+func (c *FSContext) ReOpenDir(fd int32) (*FileEntry, syscall.Errno) {
 	f, ok := c.openedFiles.Lookup(fd)
 	if !ok {
 		return nil, syscall.EBADF
@@ -448,7 +447,7 @@ func (c *FSContext) reopen(f *FileEntry) syscall.Errno {
 
 // 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 uint32, flag int) syscall.Errno {
+func (c *FSContext) ChangeOpenFlag(fd int32, flag int) syscall.Errno {
 	f, ok := c.LookupFile(fd)
 	if !ok {
 		return syscall.EBADF
@@ -478,15 +477,14 @@ func (c *FSContext) ChangeOpenFlag(fd uint32, flag int) syscall.Errno {
 }
 
 // LookupFile returns a file if it is in the table.
-func (c *FSContext) LookupFile(fd uint32) (*FileEntry, bool) {
-	f, ok := c.openedFiles.Lookup(fd)
-	return f, ok
+func (c *FSContext) LookupFile(fd int32) (*FileEntry, bool) {
+	return c.openedFiles.Lookup(fd)
 }
 
 // Renumber assigns the file pointed by the descriptor `from` to `to`.
-func (c *FSContext) Renumber(from, to uint32) syscall.Errno {
+func (c *FSContext) Renumber(from, to int32) syscall.Errno {
 	fromFile, ok := c.openedFiles.Lookup(from)
-	if !ok {
+	if !ok || to < 0 {
 		return syscall.EBADF
 	} else if fromFile.IsPreopen {
 		return syscall.ENOTSUP
@@ -505,12 +503,14 @@ func (c *FSContext) Renumber(from, to uint32) syscall.Errno {
 	}
 
 	c.openedFiles.Delete(from)
-	c.openedFiles.InsertAt(fromFile, to)
+	if !c.openedFiles.InsertAt(fromFile, to) {
+		return syscall.EBADF
+	}
 	return 0
 }
 
 // CloseFile returns any error closing the existing file.
-func (c *FSContext) CloseFile(fd uint32) syscall.Errno {
+func (c *FSContext) CloseFile(fd int32) syscall.Errno {
 	f, ok := c.openedFiles.Lookup(fd)
 	if !ok {
 		return syscall.EBADF
@@ -519,10 +519,10 @@ func (c *FSContext) CloseFile(fd uint32) syscall.Errno {
 	return platform.UnwrapOSError(f.File.Close())
 }
 
-// Close implements api.Closer
-func (c *FSContext) Close(context.Context) (err error) {
+// Close implements io.Closer
+func (c *FSContext) Close() (err error) {
 	// Close any files opened in this context
-	c.openedFiles.Range(func(fd uint32, entry *FileEntry) bool {
+	c.openedFiles.Range(func(fd int32, entry *FileEntry) bool {
 		if e := entry.File.Close(); e != nil {
 			err = e // This means err returned == the last non-nil error.
 		}
@@ -536,7 +536,7 @@ func (c *FSContext) Close(context.Context) (err error) {
 
 // WriterForFile returns a writer for the given file descriptor or nil if not
 // opened or not writeable (e.g. a directory or a file not opened for writes).
-func WriterForFile(fsc *FSContext, fd uint32) (writer io.Writer) {
+func WriterForFile(fsc *FSContext, fd int32) (writer io.Writer) {
 	if f, ok := fsc.LookupFile(fd); !ok {
 		return
 	} else if w, ok := f.File.(io.Writer); ok {
diff --git a/internal/wasip1/logging/logging.go b/internal/wasip1/logging/logging.go
index 008d15b9..01c62970 100644
--- a/internal/wasip1/logging/logging.go
+++ b/internal/wasip1/logging/logging.go
@@ -179,7 +179,7 @@ func Config(fnd api.FunctionDefinition) (pSampler logging.ParamSampler, pLoggers
 
 // Ensure we don't clutter log with reads and writes to stdio.
 func fdReadWriteSampler(_ context.Context, _ api.Module, params []uint64) bool {
-	fd := uint32(params[0])
+	fd := int32(params[0])
 	return fd > sys.FdStderr
 }
 
diff --git a/internal/wasm/module_instance.go b/internal/wasm/module_instance.go
index 61fb31e8..18c7cc64 100644
--- a/internal/wasm/module_instance.go
+++ b/internal/wasm/module_instance.go
@@ -142,7 +142,7 @@ func (m *ModuleInstance) setExitCode(exitCode uint32, flag exitCodeFlag) bool {
 // Multiple calls to this function is safe.
 func (m *ModuleInstance) ensureResourcesClosed(ctx context.Context) (err error) {
 	if sysCtx := m.Sys; sysCtx != nil { // nil if from HostModuleBuilder
-		if err = sysCtx.FS().Close(ctx); err != nil {
+		if err = sysCtx.FS().Close(); err != nil {
 			return err
 		}
 		m.Sys = nil


Test patch:
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index ad1e494b..1862cb39 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -183,7 +183,7 @@ func Test_fdDatasync(t *testing.T) {
 
 	tests := []struct {
 		name          string
-		fd            uint32
+		fd            int32
 		expectedErrno wasip1.Errno
 		expectedLog   string
 	}{
@@ -235,11 +235,12 @@ func Test_fdFdstatGet(t *testing.T) {
 	require.Zero(t, errno)
 
 	tests := []struct {
-		name             string
-		fd, resultFdstat uint32
-		expectedMemory   []byte
-		expectedErrno    wasip1.Errno
-		expectedLog      string
+		name           string
+		fd             int32
+		resultFdstat   uint32
+		expectedMemory []byte
+		expectedErrno  wasip1.Errno
+		expectedLog    string
 	}{
 		{
 			name: "stdin",
@@ -327,7 +328,7 @@ func Test_fdFdstatGet(t *testing.T) {
 		},
 		{
 			name:          "bad FD",
-			fd:            math.MaxUint32,
+			fd:            -1,
 			expectedErrno: wasip1.ErrnoBadf,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_fdstat_get(fd=-1)
@@ -480,11 +481,12 @@ func Test_fdFilestatGet(t *testing.T) {
 	require.Zero(t, errno)
 
 	tests := []struct {
-		name               string
-		fd, resultFilestat uint32
-		expectedMemory     []byte
-		expectedErrno      wasip1.Errno
-		expectedLog        string
+		name           string
+		fd             int32
+		resultFilestat uint32
+		expectedMemory []byte
+		expectedErrno  wasip1.Errno
+		expectedLog    string
 	}{
 		{
 			name: "stdin",
@@ -599,7 +601,7 @@ func Test_fdFilestatGet(t *testing.T) {
 		},
 		{
 			name:          "bad FD",
-			fd:            math.MaxUint32,
+			fd:            -1,
 			expectedErrno: wasip1.ErrnoBadf,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_filestat_get(fd=-1)
@@ -844,7 +846,7 @@ func Test_fdFilestatSetTimes(t *testing.T) {
 
 			paramFd := fd
 			if filepath == "badf" {
-				paramFd = math.MaxUint32
+				paramFd = -1
 			}
 
 			f, ok := fsc.LookupFile(fd)
@@ -1033,12 +1035,13 @@ func Test_fdPread_Errors(t *testing.T) {
 	defer r.Close(testCtx)
 
 	tests := []struct {
-		name                             string
-		fd, iovs, iovsCount, resultNread uint32
-		offset                           int64
-		memory                           []byte
-		expectedErrno                    wasip1.Errno
-		expectedLog                      string
+		name                         string
+		fd                           int32
+		iovs, iovsCount, resultNread uint32
+		offset                       int64
+		memory                       []byte
+		expectedErrno                wasip1.Errno
+		expectedLog                  string
 	}{
 		{
 			name:          "invalid FD",
@@ -1195,7 +1198,7 @@ func Test_fdPrestatGet_Errors(t *testing.T) {
 	memorySize := mod.Memory().Size()
 	tests := []struct {
 		name          string
-		fd            uint32
+		fd            int32
 		resultPrestat uint32
 		expectedErrno wasip1.Errno
 		expectedLog   string
@@ -1280,7 +1283,7 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 
 	tests := []struct {
 		name          string
-		fd            uint32
+		fd            int32
 		path          uint32
 		pathLen       uint32
 		expectedErrno wasip1.Errno
@@ -1524,12 +1527,13 @@ func Test_fdPwrite_Errors(t *testing.T) {
 	defer r.Close(testCtx)
 
 	tests := []struct {
-		name                                string
-		fd, iovs, iovsCount, resultNwritten uint32
-		offset                              int64
-		memory                              []byte
-		expectedErrno                       wasip1.Errno
-		expectedLog                         string
+		name                            string
+		fd                              int32
+		iovs, iovsCount, resultNwritten uint32
+		offset                          int64
+		memory                          []byte
+		expectedErrno                   wasip1.Errno
+		expectedLog                     string
 	}{
 		{
 			name:          "invalid FD",
@@ -1697,11 +1701,12 @@ func Test_fdRead_Errors(t *testing.T) {
 	defer r.Close(testCtx)
 
 	tests := []struct {
-		name                             string
-		fd, iovs, iovsCount, resultNread uint32
-		memory                           []byte
-		expectedErrno                    wasip1.Errno
-		expectedLog                      string
+		name                         string
+		fd                           int32
+		iovs, iovsCount, resultNread uint32
+		memory                       []byte
+		expectedErrno                wasip1.Errno
+		expectedLog                  string
 	}{
 		{
 			name:          "invalid FD",
@@ -2261,13 +2266,14 @@ func Test_fdReaddir_Errors(t *testing.T) {
 	require.Zero(t, errno)
 
 	tests := []struct {
-		name                           string
-		dir                            func() *sys.FileEntry
-		fd, buf, bufLen, resultBufused uint32
-		cookie                         int64
-		readDir                        *sys.ReadDir
-		expectedErrno                  wasip1.Errno
-		expectedLog                    string
+		name                       string
+		dir                        func() *sys.FileEntry
+		fd                         int32
+		buf, bufLen, resultBufused uint32
+		cookie                     int64
+		readDir                    *sys.ReadDir
+		expectedErrno              wasip1.Errno
+		expectedLog                string
 	}{
 		{
 			name:          "out-of-memory reading buf",
@@ -2378,7 +2384,7 @@ func Test_fdRenumber(t *testing.T) {
 
 	tests := []struct {
 		name          string
-		from, to      uint32
+		from, to      int32
 		expectedErrno wasip1.Errno
 		expectedLog   string
 	}{
@@ -2390,6 +2396,26 @@ func Test_fdRenumber(t *testing.T) {
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_renumber(fd=3,to=5)
 <== errno=ENOTSUP
+`,
+		},
+		{
+			name:          "from=badf",
+			from:          -1,
+			to:            sys.FdPreopen,
+			expectedErrno: wasip1.ErrnoBadf,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_renumber(fd=-1,to=3)
+<== errno=EBADF
+`,
+		},
+		{
+			name:          "to=badf",
+			from:          sys.FdPreopen,
+			to:            -1,
+			expectedErrno: wasip1.ErrnoBadf,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_renumber(fd=3,to=-1)
+<== errno=EBADF
 `,
 		},
 		{
@@ -2456,11 +2482,11 @@ func Test_fdRenumber(t *testing.T) {
 			// Sanity check of the file descriptor assignment.
 			fileFDAssigned, errno := fsc.OpenFile(preopen, "animals.txt", os.O_RDONLY, 0)
 			require.Zero(t, errno)
-			require.Equal(t, uint32(fileFD), fileFDAssigned)
+			require.Equal(t, int32(fileFD), fileFDAssigned)
 
 			dirFDAssigned, errno := fsc.OpenFile(preopen, "dir", os.O_RDONLY, 0)
 			require.Zero(t, errno)
-			require.Equal(t, uint32(dirFD), dirFDAssigned)
+			require.Equal(t, int32(dirFD), dirFDAssigned)
 
 			requireErrnoResult(t, tc.expectedErrno, mod, wasip1.FdRenumberName, uint64(tc.from), uint64(tc.to))
 			require.Equal(t, tc.expectedLog, "\n"+log.String())
@@ -2573,7 +2599,7 @@ func Test_fdSeek_Errors(t *testing.T) {
 
 	tests := []struct {
 		name                    string
-		fd                      uint32
+		fd                      int32
 		offset                  uint64
 		whence, resultNewoffset uint32
 		expectedErrno           wasip1.Errno
@@ -2639,7 +2665,7 @@ func Test_fdSync(t *testing.T) {
 
 	tests := []struct {
 		name          string
-		fd            uint32
+		fd            int32
 		expectedErrno wasip1.Errno
 		expectedLog   string
 	}{
@@ -2725,7 +2751,7 @@ func Test_fdTell_Errors(t *testing.T) {
 
 	tests := []struct {
 		name            string
-		fd              uint32
+		fd              int32
 		resultNewoffset uint32
 		expectedErrno   wasip1.Errno
 		expectedLog     string
@@ -2864,10 +2890,11 @@ func Test_fdWrite_Errors(t *testing.T) {
 	memSize := mod.Memory().Size()
 
 	tests := []struct {
-		name                     string
-		fd, iovs, resultNwritten uint32
-		expectedErrno            wasip1.Errno
-		expectedLog              string
+		name                 string
+		fd                   int32
+		iovs, resultNwritten uint32
+		expectedErrno        wasip1.Errno
+		expectedLog          string
 	}{
 		{
 			name:          "invalid FD",
@@ -3000,10 +3027,11 @@ func Test_pathCreateDirectory_Errors(t *testing.T) {
 	require.NoError(t, err)
 
 	tests := []struct {
-		name, pathName    string
-		fd, path, pathLen uint32
-		expectedErrno     wasip1.Errno
-		expectedLog       string
+		name, pathName string
+		fd             int32
+		path, pathLen  uint32
+		expectedErrno  wasip1.Errno
+		expectedLog    string
 	}{
 		{
 			name:          "unopened FD",
@@ -3102,12 +3130,13 @@ func Test_pathFilestatGet(t *testing.T) {
 	fileFD := requireOpenFD(t, mod, file)
 
 	tests := []struct {
-		name                        string
-		fd, pathLen, resultFilestat uint32
-		flags                       uint16
-		memory, expectedMemory      []byte
-		expectedErrno               wasip1.Errno
-		expectedLog                 string
+		name                    string
+		fd                      int32
+		pathLen, resultFilestat uint32
+		flags                   uint16
+		memory, expectedMemory  []byte
+		expectedErrno           wasip1.Errno
+		expectedLog             string
 	}{
 		{
 			name:           "file under root",
@@ -3177,7 +3206,7 @@ func Test_pathFilestatGet(t *testing.T) {
 		},
 		{
 			name:          "unopened FD",
-			fd:            math.MaxUint32,
+			fd:            -1,
 			expectedErrno: wasip1.ErrnoBadf,
 			expectedLog: `
 ==> wasi_snapshot_preview1.path_filestat_get(fd=-1,flags=,path=)
@@ -3302,7 +3331,7 @@ func Test_pathFilestatGet(t *testing.T) {
 		},
 		{
 			name:          "unopened FD (follow symlinks)",
-			fd:            math.MaxUint32,
+			fd:            -1,
 			flags:         wasip1.LOOKUP_SYMLINK_FOLLOW,
 			expectedErrno: wasip1.ErrnoBadf,
 			expectedLog: `
@@ -3650,9 +3679,11 @@ func Test_pathLink(t *testing.T) {
 
 	t.Run("errors", func(t *testing.T) {
 		for _, tc := range []struct {
-			errno                                      wasip1.Errno
-			oldFd /* oldFlags, */, oldPath, oldPathLen uint32
-			newFd, newPath, newPathLen                 uint32
+			errno wasip1.Errno
+			oldFd int32
+			/* oldFlags, */ oldPath, oldPathLen uint32
+			newFd                               int32
+			newPath, newPathLen                 uint32
 		}{
 			{errno: wasip1.ErrnoBadf, oldFd: 1000},
 			{errno: wasip1.ErrnoBadf, oldFd: oldFd, newFd: 1000},
@@ -3947,7 +3978,7 @@ func Test_pathOpen(t *testing.T) {
 			if tc.expectedErrno == wasip1.ErrnoSuccess {
 				openedFd, ok := mod.Memory().ReadUint32Le(pathLen)
 				require.True(t, ok)
-				require.Equal(t, expectedOpenedFd, openedFd)
+				require.Equal(t, expectedOpenedFd, int32(openedFd))
 
 				tc.expected(t, mod.(*wasm.ModuleInstance).Sys.FS())
 			}
@@ -3955,7 +3986,7 @@ func Test_pathOpen(t *testing.T) {
 	}
 }
 
-func requireOpenFD(t *testing.T, mod api.Module, path string) uint32 {
+func requireOpenFD(t *testing.T, mod api.Module, path string) int32 {
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
 	preopen := fsc.RootFS()
 
@@ -3964,7 +3995,7 @@ func requireOpenFD(t *testing.T, mod api.Module, path string) uint32 {
 	return fd
 }
 
-func requireContents(t *testing.T, fsc *sys.FSContext, expectedOpenedFd uint32, fileName string, fileContents []byte) {
+func requireContents(t *testing.T, fsc *sys.FSContext, expectedOpenedFd int32, fileName string, fileContents []byte) {
 	// verify the file was actually opened
 	f, ok := fsc.LookupFile(expectedOpenedFd)
 	require.True(t, ok)
@@ -4016,10 +4047,11 @@ func Test_pathOpen_Errors(t *testing.T) {
 	require.NoError(t, err)
 
 	tests := []struct {
-		name, pathName                            string
-		fd, path, pathLen, oflags, resultOpenedFd uint32
-		expectedErrno                             wasip1.Errno
-		expectedLog                               string
+		name, pathName                        string
+		fd                                    int32
+		path, pathLen, oflags, resultOpenedFd uint32
+		expectedErrno                         wasip1.Errno
+		expectedLog                           string
 	}{
 		{
 			name:          "unopened FD",
@@ -4253,9 +4285,10 @@ func Test_pathReadlink(t *testing.T) {
 
 	t.Run("errors", func(t *testing.T) {
 		for _, tc := range []struct {
-			name                                          string
-			fd, path, pathLen, buf, bufLen, resultBufused uint32
-			expectedErrno                                 wasip1.Errno
+			name                                      string
+			fd                                        int32
+			path, pathLen, buf, bufLen, resultBufused uint32
+			expectedErrno                             wasip1.Errno
 		}{
 			{expectedErrno: wasip1.ErrnoInval},
 			{expectedErrno: wasip1.ErrnoInval, pathLen: 100},
@@ -4352,10 +4385,11 @@ func Test_pathRemoveDirectory_Errors(t *testing.T) {
 	require.NoError(t, err)
 
 	tests := []struct {
-		name, pathName    string
-		fd, path, pathLen uint32
-		expectedErrno     wasip1.Errno
-		expectedLog       string
+		name, pathName string
+		fd             int32
+		path, pathLen  uint32
+		expectedErrno  wasip1.Errno
+		expectedLog    string
 	}{
 		{
 			name:          "unopened FD",
@@ -4490,8 +4524,10 @@ func Test_pathSymlink_errors(t *testing.T) {
 
 	t.Run("errors", func(t *testing.T) {
 		for _, tc := range []struct {
-			errno                                        wasip1.Errno
-			oldPath, oldPathLen, fd, newPath, newPathLen uint32
+			errno               wasip1.Errno
+			oldPath, oldPathLen uint32
+			fd                  int32
+			newPath, newPathLen uint32
 		}{
 			{errno: wasip1.ErrnoBadf, fd: 1000},
 			{errno: wasip1.ErrnoNotdir, fd: 2},
@@ -4594,8 +4630,10 @@ func Test_pathRename_Errors(t *testing.T) {
 
 	tests := []struct {
 		name, oldPathName, newPathName string
-		oldFd, oldPath, oldPathLen     uint32
-		newFd, newPath, newPathLen     uint32
+		oldFd                          int32
+		oldPath, oldPathLen            uint32
+		newFd                          int32
+		newPath, newPathLen            uint32
 		expectedErrno                  wasip1.Errno
 		expectedLog                    string
 	}{
@@ -4787,10 +4825,11 @@ func Test_pathUnlinkFile_Errors(t *testing.T) {
 	require.NoError(t, err)
 
 	tests := []struct {
-		name, pathName    string
-		fd, path, pathLen uint32
-		expectedErrno     wasip1.Errno
-		expectedLog       string
+		name, pathName string
+		fd             int32
+		path, pathLen  uint32
+		expectedErrno  wasip1.Errno
+		expectedLog    string
 	}{
 		{
 			name:          "unopened FD",
@@ -4871,7 +4910,7 @@ func Test_pathUnlinkFile_Errors(t *testing.T) {
 	}
 }
 
-func requireOpenFile(t *testing.T, tmpDir string, pathName string, data []byte, readOnly bool) (api.Module, uint32, *bytes.Buffer, api.Closer) {
+func requireOpenFile(t *testing.T, tmpDir string, pathName string, data []byte, readOnly bool) (api.Module, int32, *bytes.Buffer, api.Closer) {
 	oflags := os.O_RDWR
 
 	realPath := joinPath(tmpDir, pathName)
diff --git a/imports/wasi_snapshot_preview1/wasi_bench_test.go b/imports/wasi_snapshot_preview1/wasi_bench_test.go
index 64081e0d..b976ce16 100644
--- a/imports/wasi_snapshot_preview1/wasi_bench_test.go
+++ b/imports/wasi_snapshot_preview1/wasi_bench_test.go
@@ -257,7 +257,7 @@ func Benchmark_pathFilestat(b *testing.B) {
 		// dirMount ensures direct use of syscall.FS
 		dirMount string
 		path     string
-		fd       uint32
+		fd       int32
 	}{
 		{
 			name: "embed.FS fd=root",
@@ -399,7 +399,7 @@ func Benchmark_fdWrite(b *testing.B) {
 
 	benches := []struct {
 		name string
-		fd   uint32
+		fd   int32
 	}{
 		{
 			name: "io.Writer",
diff --git a/internal/descriptor/table_test.go b/internal/descriptor/table_test.go
index 753b583b..9713a27a 100644
--- a/internal/descriptor/table_test.go
+++ b/internal/descriptor/table_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/tetratelabs/wazero/internal/sys"
+	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
 func TestFileTable(t *testing.T) {
@@ -18,12 +19,19 @@ func TestFileTable(t *testing.T) {
 	v1 := &sys.FileEntry{Name: "2"}
 	v2 := &sys.FileEntry{Name: "3"}
 
-	k0 := table.Insert(v0)
-	k1 := table.Insert(v1)
-	k2 := table.Insert(v2)
+	k0, ok := table.Insert(v0)
+	require.True(t, ok)
+	k1, ok := table.Insert(v1)
+	require.True(t, ok)
+	k2, ok := table.Insert(v2)
+	require.True(t, ok)
+
+	// Try to re-order, but to an invalid value
+	ok = table.InsertAt(v2, -1)
+	require.False(t, ok)
 
 	for _, lookup := range []struct {
-		key uint32
+		key int32
 		val *sys.FileEntry
 	}{
 		{key: k0, val: v0},
@@ -44,7 +52,7 @@ func TestFileTable(t *testing.T) {
 	k0Found := false
 	k1Found := false
 	k2Found := false
-	table.Range(func(k uint32, v *sys.FileEntry) bool {
+	table.Range(func(k int32, v *sys.FileEntry) bool {
 		var want *sys.FileEntry
 		switch k {
 		case k0:
@@ -61,7 +69,7 @@ func TestFileTable(t *testing.T) {
 	})
 
 	for _, found := range []struct {
-		key uint32
+		key int32
 		ok  bool
 	}{
 		{key: k0, ok: k0Found},
@@ -74,7 +82,7 @@ func TestFileTable(t *testing.T) {
 	}
 
 	for i, deletion := range []struct {
-		key uint32
+		key int32
 	}{
 		{key: k1},
 		{key: k0},
@@ -107,11 +115,15 @@ func BenchmarkFileTableLookup(b *testing.B) {
 	const sentinel = "42"
 	const numFiles = 65536
 	table := new(sys.FileTable)
-	files := make([]uint32, numFiles)
+	files := make([]int32, numFiles)
 	entry := &sys.FileEntry{Name: sentinel}
 
+	var ok bool
 	for i := range files {
-		files[i] = table.Insert(entry)
+		files[i], ok = table.Insert(entry)
+		if !ok {
+			b.Fatal("unexpected failure to insert")
+		}
 	}
 
 	var f *sys.FileEntry
diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go
index 13780a6c..3b08eb90 100644
--- a/internal/sys/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -1,7 +1,6 @@
 package sys
 
 import (
-	"context"
 	"embed"
 	"errors"
 	"io"
@@ -19,9 +18,6 @@ import (
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
-// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
-var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
-
 var (
 	noopStdin  = &FileEntry{Name: "stdin", File: NewStdioFileReader(eofReader{}, noopStdinStat, PollerDefaultStdin)}
 	noopStdout = &FileEntry{Name: "stdout", File: &stdioFileWriter{w: io.Discard, s: noopStdoutStat}}
@@ -70,7 +66,7 @@ func TestNewFSContext(t *testing.T) {
 			err := c.NewFSContext(nil, nil, nil, tc.fs)
 			require.NoError(t, err)
 			fsc := c.fsc
-			defer fsc.Close(testCtx)
+			defer fsc.Close()
 
 			preopenedDir, _ := fsc.openedFiles.Lookup(FdPreopen)
 			require.Equal(t, tc.fs, fsc.rootFS)
@@ -108,7 +104,7 @@ func TestFSContext_CloseFile(t *testing.T) {
 	err = c.NewFSContext(nil, nil, nil, testFS)
 	require.NoError(t, err)
 	fsc := c.fsc
-	defer fsc.Close(testCtx)
+	defer fsc.Close()
 
 	fdToClose, errno := fsc.OpenFile(testFS, "empty.txt", os.O_RDONLY, 0)
 	require.Zero(t, errno)
@@ -148,7 +144,7 @@ func TestUnimplementedFSContext(t *testing.T) {
 	expected.openedFiles.Insert(noopStderr)
 
 	t.Run("Close closes", func(t *testing.T) {
-		err := testFS.Close(testCtx)
+		err := testFS.Close()
 		require.NoError(t, err)
 
 		// Closes opened files
@@ -180,7 +176,7 @@ func TestCompositeFSContext(t *testing.T) {
 	require.Equal(t, "/", preopen4.Name)
 
 	t.Run("Close closes", func(t *testing.T) {
-		err := testFS.Close(testCtx)
+		err := testFS.Close()
 		require.NoError(t, err)
 
 		// Closes opened files
@@ -197,20 +193,20 @@ func TestContext_Close(t *testing.T) {
 	fsc := c.fsc
 
 	// Verify base case
-	require.Equal(t, 1+FdPreopen, uint32(fsc.openedFiles.Len()))
+	require.Equal(t, 1+FdPreopen, int32(fsc.openedFiles.Len()))
 
 	_, errno := fsc.OpenFile(testFS, "foo", os.O_RDONLY, 0)
 	require.Zero(t, errno)
-	require.Equal(t, 2+FdPreopen, uint32(fsc.openedFiles.Len()))
+	require.Equal(t, 2+FdPreopen, int32(fsc.openedFiles.Len()))
 
 	// Closing should not err.
-	require.NoError(t, fsc.Close(testCtx))
+	require.NoError(t, fsc.Close())
 
 	// Verify our intended side-effect
 	require.Zero(t, fsc.openedFiles.Len())
 
 	// Verify no error closing again.
-	require.NoError(t, fsc.Close(testCtx))
+	require.NoError(t, fsc.Close())
 }
 
 func TestContext_Close_Error(t *testing.T) {
@@ -227,7 +223,7 @@ func TestContext_Close_Error(t *testing.T) {
 	_, errno := fsc.OpenFile(testFS, "foo", os.O_RDONLY, 0)
 	require.Zero(t, errno)
 
-	require.EqualError(t, fsc.Close(testCtx), "error closing")
+	require.EqualError(t, fsc.Close(), "error closing")
 
 	// Paths should clear even under error
 	require.Zero(t, fsc.openedFiles.Len(), "expected no opened files")
@@ -247,9 +243,7 @@ func TestFSContext_ReOpenDir(t *testing.T) {
 	fsc := c.fsc
 
 	require.NoError(t, err)
-	defer func() {
-		require.NoError(t, fsc.Close(context.Background()))
-	}()
+	defer fsc.Close()
 
 	t.Run("ok", func(t *testing.T) {
 		dirFd, errno := fsc.OpenFile(dirFs, dirName, os.O_RDONLY, 0o600)
@@ -297,11 +291,9 @@ func TestFSContext_Renumber(t *testing.T) {
 	require.NoError(t, err)
 	fsc := c.fsc
 
-	defer func() {
-		require.NoError(t, fsc.Close(context.Background()))
-	}()
+	defer fsc.Close()
 
-	for _, toFd := range []uint32{10, 100, 100} {
+	for _, toFd := range []int32{10, 100, 100} {
 		fromFd, errno := fsc.OpenFile(dirFs, dirName, os.O_RDONLY, 0)
 		require.Zero(t, errno)
 
@@ -350,7 +342,7 @@ func TestFSContext_ChangeOpenFlag(t *testing.T) {
 	fsc := c.fsc
 
 	defer func() {
-		require.NoError(t, fsc.Close(context.Background()))
+		require.NoError(t, fsc.Close())
 	}()
 
 	// Without APPEND.
diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go
index d4755943..2cf0e167 100644
--- a/internal/sys/sys_test.go
+++ b/internal/sys/sys_test.go
@@ -94,7 +94,7 @@ func TestFileEntry_cachedStat(t *testing.T) {
 			c := Context{}
 			_ = c.NewFSContext(nil, nil, nil, tc.fs)
 			fsc := c.fsc
-			defer fsc.Close(testCtx)
+			defer fsc.Close()
 
 			f, ok := fsc.LookupFile(FdPreopen)
 			require.True(t, ok)