Repository: tetratelabs/wazero
Pull request #1385 (2023-04-20 16:06:21)

packaging: switch to native WiX toolchain and Windows runner
Cross-platform msitools do not support the `Environment` directive,
which should be the proper way to set up the PATH environment variable, 
so we adapt the Makefile to assume a Windows environment when building.
We address #1374.

`make dist` now runs on all platforms, but on Windows, assuming
`candle.exe`, `light.exe` and GNU `tar` are installed, it builds
all tarballs, zip and msi packages.

On other platforms it will only build the tarballs.

We also address some other issues with msitools
and change the GUID for UpgradeCode which caused
the wazero package to be mistaken for func-e
when installing/uninstalling both on the same
machine.

Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>


codefromthecrypt: great notes. I think this is the path forward, but I can also see a reluctance to depend on a windows phase. That said, basically every maintainer needs to be able to run windows occasionally at this point, so I feel personally not too critical about it!
mathetake: yeah let's go with this rather than #1383 
evacchi: @codefromthecrypt speaking of func-e I noticed that while trying `func-e` _and_ `wazero` on my Windows machine, _and_ uninstalling I think it tried to uninstall wazero instead of func-e (or the other way round, don't remember right now). I suspect the reason is that they share the same GUID here:

    <!-- UpgradeCode needs to be constant until an incompatible change occurs
         See https://docs.microsoft.com/en-us/windows/win32/msi/upgrade-table -->
    <?ifndef var.UpgradeCode?>
    <?define UpgradeCode = "FC6C326B-1DB7-4D48-8F05-E13E79DF4D0E" ?>
    <?endif?>

https://github.com/tetratelabs/func-e/blob/master/packaging/msi/func-e.wxs#L36-L40

shall we change that on `wazero` maybe? I can do it in this PR.
codefromthecrypt: > shall we change that on wazero maybe? I can do it in this PR.

OMG please do
evacchi: I removed the warning because it was being printed unconditionally always for any `make` target.

Patch:
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 8e62ffb8..f83c57c2 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -25,8 +25,8 @@ concurrency:
 jobs:
   pre_release:
     name: Pre-release build
-    # This only runs on ubuntu so that we can simplify the installation of necessary toolchain to build artifacts.
-    runs-on: ubuntu-22.04
+    # This only runs on Windows so that we can simplify the installation of necessary toolchain to build artifacts.
+    runs-on: windows-2022
     # This allows us to test in the following job regardless of the event (tag or not).
     outputs:
       VERSION: ${{ steps.output-version.outputs.VERSION }}
@@ -44,10 +44,12 @@ jobs:
             ~/go/bin
           key: pre-release-check-${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum', 'Makefile') }}
 
-      # ubuntu is missing wixl https://github.com/actions/virtual-environments/issues/3857
-      # ubuntu is also missing osslsigncode (no issue, yet)
-      - name: "Install GNOME msitools (wixl) and osslsigncode"
-        run: sudo apt update -qq && sudo apt install -qq -y wixl osslsigncode
+      # windows-2022 is missing osslsigncode (no issue, yet)
+      - name: "Install osslsigncode, infozip; setup wix"
+        run: |
+          choco install osslsigncode -y
+          choco install zip -y
+          echo "C:\Program Files (x86)\WiX Toolset v3.11\bin" >> $GITHUB_PATH
 
       - name: Download Windows code signing certificate
         env:
@@ -57,7 +59,8 @@ jobs:
             echo $WINDOWS_CODESIGN_P12_BASE64 | base64 --decode > windows-certificate.p12
             echo "WINDOWS_CODESIGN_P12=windows-certificate.p12" >> $GITHUB_ENV
           fi
-
+        shell: bash
+      
       - name: Make artifacts (test)
         if: github.event_name != 'push' || !contains(github.ref, 'refs/tags/')
         run: | # On the fork PRs, our org secret is not visible. We unset the required env so that `make dist` uses default self-signed cert.
@@ -67,6 +70,7 @@ jobs:
           VERSION=${{ github.sha }}
           make dist VERSION=$VERSION
           echo "VERSION=${VERSION}" >> $GITHUB_ENV
+        shell: bash
 
       - name: Make artifacts
         # Triggers only on tag creation.
@@ -78,10 +82,12 @@ jobs:
           MSI_VERSION=${VERSION//-/.}
           make dist VERSION=$VERSION MSI_VERSION=$MSI_VERSION
           echo "VERSION=${VERSION}" >> $GITHUB_ENV
+        shell: bash
 
       # This allows us to test in the following job regardless of the event (tag or not).
       - id: output-version
         run: echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
+        shell: bash
 
       # In order to share the built artifacts in the subsequent tests, we use cache instead of actions/upload-artifacts.
       # The reason is that upload-artifacts are not globally consistent and sometimes pre_release_test won't be able to
@@ -145,14 +151,6 @@ jobs:
           call packaging\msi\verify_msi.cmd
         shell: cmd
 
-      # This tests the manifest via yamllint because validation via winget requires too much setup.
-      # See https://github.com/microsoft/winget-cli/issues/754#issuecomment-896475895
-      - name: Test winget manifest generation
-        if: runner.os == 'Windows'
-        run: |
-          ./packaging/msi/winget_manifest.sh ${{ needs.pre_release.outputs.VERSION }} "dist\wazero_${{ needs.pre_release.outputs.VERSION }}_windows_amd64.msi" > Tetrate.wazero.yaml
-          yamllint -sd '{extends: default, rules: {line-length: disable}}' Tetrate.wazero.yaml
-
   # Triggers only on the tag creation.
   release:
     if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
diff --git a/Makefile b/Makefile
index f2b6ef78..8e36cf8c 100644
--- a/Makefile
+++ b/Makefile
@@ -261,12 +261,11 @@ fuzz:
 #### CLI release related ####
 
 VERSION ?= dev
-# Default to a dummy version 0.0.1.rc1, which is always lower than a real release.
-# This must be in the form of [0-255].[0-255].[0-65535] plus optional fourth element which will be ignored.
-# We use the fourth field to represent the rc portion of release tag (e.g. rc1 of 1.0.0-rc2).
+# Default to a dummy version 0.0.1.1, which is always lower than a real release.
+# Legal version values should look like 'x.x.x.x' where x is an integer from 0 to 65534.
 # https://learn.microsoft.com/en-us/windows/win32/msi/productversion?redirectedfrom=MSDN
 # https://stackoverflow.com/questions/9312221/msi-version-numbers
-MSI_VERSION ?= 0.0.1.rc1
+MSI_VERSION ?= 0.0.1.1
 non_windows_platforms := darwin_amd64 darwin_arm64 linux_amd64 linux_arm64
 non_windows_archives  := $(non_windows_platforms:%=dist/wazero_$(VERSION)_%.tar.gz)
 windows_platforms     := windows_amd64 # TODO: add arm64 windows once we start testing on it.
@@ -288,7 +287,9 @@ build/wazero_%/wazero.exe:
 dist/wazero_$(VERSION)_%.tar.gz: build/wazero_%/wazero
 	@echo tar.gz "tarring $@"
 	@mkdir -p $(@D)
-	@tar -C $(<D) -cpzf $@ $(<F)
+# On Windows, we pass the special flag `--mode='+rx' to ensure that we set the executable flag.
+# This is only supported by GNU Tar, so we set it conditionally.
+	@tar -C $(<D) -cpzf $@ $(if $(findstring Windows_NT,$(OS)),--mode='+rx',) $(<F)
 	@echo tar.gz "ok"
 
 define go-build
@@ -325,12 +326,16 @@ define codesign
 	@printf "$(ansi_format_bright)" codesign "ok"
 endef
 
+# This task is only supported on Windows, where we use candle.exe (compile wxs to wixobj) and light.exe (link to msi)
 dist/wazero_$(VERSION)_%.msi: build/wazero_%/wazero.exe.signed
+ifeq ($(OS),Windows_NT)
 	@echo msi "building $@"
 	@mkdir -p $(@D)
-	@wixl -a $(call msi-arch,$@) -D Version=$(MSI_VERSION) -D Bin=$(<:.signed=) -o $@ packaging/msi/wazero.wxs
+	@candle -nologo -arch $(call msi-arch,$@) -dVersion=$(MSI_VERSION) -dBin=$(<:.signed=) -o build/wazero.wixobj packaging/msi/wazero.wxs
+	@light -nologo -o $@ build/wazero.wixobj -spdb
 	$(call codesign,$@)
 	@echo msi "ok"
+endif
 
 dist/wazero_$(VERSION)_%.zip: build/wazero_%/wazero.exe.signed
 	@echo zip "zipping $@"
@@ -343,4 +348,4 @@ sha256sum := $(if $(findstring darwin,$(shell go env GOOS)),shasum -a 256,sha256
 $(checksum_txt):
 	@cd $(@D); touch $(@F); $(sha256sum) * >> $(@F)
 
-dist: $(non_windows_archives) $(windows_archives) $(checksum_txt)
+dist: $(non_windows_archives) $(if $(findstring Windows_NT,$(OS)),$(windows_archives),) $(checksum_txt)
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
diff --git a/packaging/msi/wazero.wxs b/packaging/msi/wazero.wxs
index d12243cc..02b4183a 100644
--- a/packaging/msi/wazero.wxs
+++ b/packaging/msi/wazero.wxs
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+﻿<?xml version="1.0" encoding="UTF-8"?>
 <!--
     Copyright 2023 Tetrate
 
@@ -15,18 +15,15 @@
     limitations under the License.
 -->
 <!-- This builds a minimal package of wazero. The only required variable is Version
-     Ex. `wixl -D Version=0.6.1`
+     Ex. `candle.exe -dVersion=0.6.1`
 
      While all options used are documented, the initial version of this file was helped by:
      * cloudflared - https://github.com/cloudflare/cloudflared/commit/38af26e232d6a5fd981d40981b702a1d06a3b8ee
-     * podmad - https://github.com/containers/podman/commit/b6b0b6e8bd0f8c75fe9411f722a43f9305c6137a
-
-     In order to allow single-host builds, we restrict to syntax that works with GNOME msitools.
-     Raise issues on incompatibilies here https://gitlab.gnome.org/GNOME/msitools/-/issues -->
+     * podman - https://github.com/containers/podman/commit/b6b0b6e8bd0f8c75fe9411f722a43f9305c6137a -->
 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
 
     <?ifndef var.Version?>
-    <?error Version must be defined. Ex. wixl -D Version=0.6.1?>
+    <?error Version must be defined. Ex. candle -dVersion=0.6.1?>
     <?endif?>
 
     <?ifndef var.Bin?>
@@ -36,7 +33,7 @@
     <!-- UpgradeCode needs to be constant until an incompatible change occurs
          See https://docs.microsoft.com/en-us/windows/win32/msi/upgrade-table -->
     <?ifndef var.UpgradeCode?>
-    <?define UpgradeCode = "FC6C326B-1DB7-4D48-8F05-E13E79DF4D0E" ?>
+    <?define UpgradeCode = "bef1bfc9-a5fc-4819-988f-807e47312f57" ?>
     <?endif?>
 
     <!-- Id="*" means it will be automatically generated: TODO: Consider if we should version-correlate the Id
@@ -47,17 +44,15 @@
          -->
     <Product Id="*" Name="wazero" Manufacturer="Tetrate"  Version="$(var.Version)" UpgradeCode="$(var.UpgradeCode)"
              Language="1033" Codepage="1252">
-        <!-- TODO: set <Property Id="ALLUSERS"... after https://gitlab.gnome.org/GNOME/msitools/-/issues/40 -->
 
-             InstallScope="perUser" is default (Id="ALLUSERS" doesn't work on msitools 0.101) -->
         <Package Id="*" Keywords="Installer" Description="wazero installer" Manufacturer="Tetrate"
-                 InstallerVersion="310" Languages="1033" Compressed="yes" SummaryCodepage="1252"/>
+                 InstallerVersion="310" Languages="1033" Compressed="yes" SummaryCodepage="1252" Platform="x64" />
 
         <!-- wazero.exe is embedded inside the installer: wazero.msi
              Cabinet="Wazero.cab" because light.exe fails on "wazero.cab" -->
         <Media Id="1" Cabinet="Wazero.cab" EmbedCab="yes"/>
 
-        <!-- We allow the special version 0.0.1 in order to run tests. Releases will always be real versions. -->
+        <!-- We allow the special version 0.0.1.1 in order to run tests. Releases will always be real versions. -->
         <Upgrade Id="$(var.UpgradeCode)">
             <UpgradeVersion Minimum="$(var.Version)" OnlyDetect="yes" Property="NEWERVERSIONDETECTED"/>
             <UpgradeVersion Minimum="0.0.0" Maximum="$(var.Version)" IncludeMinimum="yes" IncludeMaximum="no"
@@ -73,6 +68,8 @@
         <Property Id="ARPNOREPAIR" Value="1"/>
         <Property Id="ARPNOMODIFY" Value="1"/>
 
+        <Property Id="ALLUSERS" Secure="yes"/>
+
         <!-- On Windows, when Id="TARGETDIR" Name="SourceDir": it will error otherwise -->
         <Directory Id="TARGETDIR" Name="SourceDir">
             <Directory Id="ProgramFiles64Folder">
@@ -84,6 +81,11 @@
                     </Component>
                 </Directory>
             </Directory>
+
+            <Component Id="Path" Guid="{fb130dec-3828-4ebc-b253-e7368cba14e8}">
+                <Environment Id="PATH" Name="PATH" Value="[INSTALLDIR]" Permanent="no" Part="last" Action="set" System="no" />
+            </Component>
+
         </Directory>
 
         <!-- Use most common size 48x48 for the Add/Remove programs -->
@@ -92,17 +94,11 @@
 
         <Feature Id="Complete" Level="1">
             <ComponentRef Id="WazeroBin"/>
+            <ComponentRef Id="Path" />
         </Feature>
 
-        <!-- Add wazero to the default PATH once installation succeeds.
-             Replace with <Component><Environment> after https://gitlab.gnome.org/GNOME/msitools/-/merge_requests/42 -->
-        <Property Id="setx" Value="setx.exe"/>
-        <CustomAction Id="ChangePath" ExeCommand="PATH &quot;%PATH%;[INSTALLDIR]&quot;" Property="setx"
-                      Execute="deferred"/>
         <InstallExecuteSequence>
             <RemoveExistingProducts Before="InstallInitialize"/>
-            <!-- Add PATH remove after after https://gitlab.gnome.org/GNOME/msitools/-/merge_requests/42 -->
-            <Custom Action="ChangePath" After="InstallServices">NOT Installed</Custom>
         </InstallExecuteSequence>
     </Product>
 </Wix>
diff --git a/packaging/msi/winget_manifest.sh b/packaging/msi/winget_manifest.sh
deleted file mode 100755
index 7c5c2302..00000000
--- a/packaging/msi/winget_manifest.sh
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/bin/sh -ue
-# Copyright 2023 Tetrate
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# This script echos the winget manifest for a wazero Windows Installer (msi).
-#
-# Ex.
-# msi_file=/path/to/wazero.msi
-# manifest_path=./manifests/t/Tetrate/wazero/${version}/Tetrate.wazero.yaml
-# mkdir -p $(dirname "${manifest_path}")
-# packaging/msi/winget_manifest.sh ${version} ${msi_file} > ${manifest_path}
-# winget validate --manifest ${manifest_path}
-
-version=${1:-0.0.1}
-msi_file=${2:-dist/wazero_dev_windows_amd64.msi}
-
-case $(uname -s) in
-CYGWIN* | MSYS* | MINGW*)
-  installer_sha256=$(certutil -hashfile "${msi_file}" SHA256 | sed -n 2p)
-  product_code=$(powershell -File ./packaging/msi/msi_product_code.ps1 -msi "${msi_file}")
-  ;;
-*) # notably, this gets rid of the Windows carriage return (\r), which otherwise would mess up the heredoc.
-  msiinfo -h export >/dev/null
-  # shasum -a 256, not sha256sum as https://github.com/actions/virtual-environments/issues/90
-  installer_sha256=$(shasum -a 256 "${msi_file}" | awk '{print toupper($1)}' 2>&-)
-  product_code=$(msiinfo export "${msi_file}" Property | sed -n '/ProductCode/s/\r$//p' | cut -f2)
-  ;;
-esac
-
-cat <<EOF
-# yaml-language-server: \$schema=https://aka.ms/winget-manifest.singleton.1.0.0.schema.json
----
-PackageIdentifier: Tetrate.wazero
-PackageVersion: ${version}
-PackageName: wazero
-Publisher: Tetrate
-Copyright: Copyright 2023 Tetrate
-License: Apache 2.0
-LicenseUrl: https://github.com/tetratelabs/wazero/blob/main/LICENSE
-Moniker: wazero
-Commands:
-  - wazero
-Tags:
-  - wazero
-  - webassembly
-  - wasm
-  - tetrate
-ShortDescription: wazero runs WebAssembly modules
-Description: wazero is a command line utility to run WebAssembly modules. Specifically, this runs .wasm files compiled with WASI or Go, and has zero platform dependencies.
-PackageUrl: https://wazero.io
-Installers:
-  - Architecture: x64
-    InstallerUrl: https://github.com/tetratelabs/wazero/releases/download/v${version}/wazero_${version}_windows_amd64.msi
-    InstallerSha256: $installer_sha256
-    InstallerType: msi
-    ProductCode: "${product_code}"
-PackageLocale: en-US
-ManifestType: singleton
-ManifestVersion: 1.0.0
-EOF


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)