Repository navigation
Run every wasi-testsuite case on Linux, macOS and Windows - #499
Draft
kateinoigakukun wants to merge 15 commits into
Draft
kateinoigakukun wants to merge 15 commits into
kateinoigakukun wants to merge 15 commits into
Conversation
fdReaddirDoesNotLeakFileDescriptors lowered RLIMIT_NOFILE to 32 above the descriptors open at the time. The limit is process-wide, so any test opening files in parallel could fail with EMFILE while the loop ran, and fileDescriptorLeakTest and the wasi-testsuite cases sometimes did. Count the open descriptors before and after the loop instead; a leak adds one per call, far more than parallel tests hold at once.
Darwin's poll supports few devices besides ttys, and flags the others, /dev/null among them, POLLNVAL even though they are open. poll_oneoff passed that on as BADF, so a guest polling stdin failed when it was redirected from /dev/null, as poll_oneoff_stdio does under a test runner. select(2) reports such a device as always ready, so report it ready too when the descriptor is still open.
With SYMLINK_FOLLOW, path_filestat_get resolved all but the last component in the sandbox and then let the host's fstatat follow a symlink there. A symlink pointing out of the preopen, such as `../../etc/passwd` or `/`, therefore reported the size, times and inode of a host file outside it. Resolve the final symlink in the sandbox too: while the last component names a symlink, read it and resolve its target relative to the same preopen, refusing absolute targets and stopping after as many links as path_open allows. The host then only ever stats something that is not a symlink.
fd_readdir stat'ed every entry it returned for its inode, and stat'ed the entries it skipped to reach the cookie too, then threw those results away. Listing a directory of n entries in chunks therefore cost O(n^2) fstatat calls, since each call starts over from the first entry. Use the inode readdir already reports, as wasmtime does, and skip entries without building them.
The pinned revision was from July 2025. Upstream has since moved each suite's binaries under a per-WASI-version directory, preopens a test's `root` as the guest's `/` instead of listing `dirs`, and checks stdout and stderr. Point at the newest prebuilt binaries and follow the current specification: run the wasm32-wasip1 suites, preopen the root, and give the guest files for stdio so that its output can be checked and none of them is a tty, as under the upstream runner. A trap now reports what the guest printed to stderr, which is where a Rust test explains its panic. Cases run one at a time, as under the upstream runner, since several create the same names in the shared root. Of the eleven tests skipped on Linux and macOS, six pass against the new binaries. The other five stay skipped until the following commits fix them. fd_advise no longer needs NO_FD_ALLOCATE, since the test now accepts NOTSUP from fd_allocate.
fd_allocate answered NOTSUP for any open descriptor, including a directory, where dir_fd_op_failures expects ISDIR, BADF or NOTCAPABLE like every other file-only operation. Check that the descriptor is a file first, and answer BADF otherwise as wasmtime does.
path_open passed only APPEND of the requested fdflags on to the host, so a file opened with SYNC, DSYNC, RSYNC or NONBLOCK did not get them, and fd_fdstat_get did not report them back, which path_filestat checks. Pass them all on. Reading the flags back also tested O_SYNC and O_RSYNC for any of their bits. Glibc defines both to include O_DSYNC, so a DSYNC file was reported as SYNC too. Require every bit instead.
path_filestat_set_times opened its target to call futimens, with O_NOFOLLOW when the guest did not ask to follow symlinks. Opening a symlink that way fails with ELOOP, so the times of a symlink itself could not be set, which symlink_filestat does. Call utimensat with AT_SYMLINK_NOFOLLOW on the last component instead, as wasmtime does; openParent still resolves the rest of the path inside the sandbox. When following, resolve the final symlink inside the sandbox as path_filestat_get does, and set the times of what it names the same way. Opening the target is no longer needed at all: it opened for writing, which failed on every directory, and it blocked on a FIFO.
path_link answered NOTSUP. Create the link with linkat on the parents that openParent resolves inside the sandbox, without AT_SYMLINK_FOLLOW, so that a symlink is linked rather than its target. A guest asking to follow is refused with INVAL, as in wasmtime, since the host would otherwise resolve the symlink outside the sandbox. WASIDir gains a link requirement. Its default answers NOTSUP, so the memory file system and SPI implementations outside this package keep building.
path_symlink created a symlink to an absolute path, which sandboxed resolution then refuses to follow anyway, and symlink_create expects the creation itself to fail. Answer PERM up front, as wasmtime does. This was the last wasi-testsuite case skipped on Linux and macOS.
On Windows a guest could not open a directory: preopens went through _wsopen_s, which refuses directories, so a preopen failed with EACCES, and every directory-relative primitive of the platform layer (openat, fstatat, readlinkat, mkdirat, unlinkat, renameat, linkat, symlinkat, utimensat, readdir) answered NOTSUP. Most of the 53 wasi-testsuite cases skipped on Windows failed for this; 48 of them pass now. Open directories with backup semantics and wrap the HANDLE in a CRT descriptor, so FileDescriptor stays a CRT descriptor everywhere above the platform layer. Win32 has no openat, but the NT layer does: NtCreateFile opens a name relative to a directory HANDLE, and NtSetInformationFile renames or links an open file into one. Every primitive goes through those, never through a path rebuilt from the HANDLE, so a guest renaming directories or planting symlinks from another thread cannot redirect it. The primitives never follow a symlink: the sandbox walker resolves those itself, and an open that lands on one fails with ELOOP, as with O_NOFOLLOW. A name with a backslash or a colon, which Windows would read as a separator or a stream, is refused, and so is a symlink target with either. They give POSIX outcomes where Windows differs: - O_CREAT or O_TRUNC on a directory fails with EISDIR, and an existing symlink is never truncated. - A hard link to a symlink links the symlink. - Deleting and renaming use POSIX semantics, so a name disappears at once even while open, and renaming onto an empty directory replaces it. - `.` cannot be renamed, so a preopen cannot be moved through itself. - Only symlinks and junctions count as symlinks; other reparse points, such as cloud-file placeholders, are ordinary files. Windows records whether a symlink points to a directory, so the symlinkat primitive takes a closure that looks the target up in the sandbox. Creating a symlink by handle needs the symlink privilege; a non-elevated process falls back to CreateSymbolicLinkW on the directory's path, as cap-std does for wasmtime.
The CRT offers neither pread nor pwrite, so fd_pread and fd_pwrite seeked to the requested offset and left the descriptor there, while POSIX leaves the offset alone. pwrite-with-append checks that. Seek back to where the descriptor was afterwards.
The CRT has no F_GETFL or F_SETFL, so fd_fdstat_get reported no flags on Windows, where path_filestat expects the APPEND it opened with, and fd_fdstat_set_flags was ignored, so fd_flags_set could not turn APPEND off. Record the open options per descriptor in the platform layer instead, update them on F_SETFL as Linux does (append and nonblocking only), and seek to the end before each write while append is set, as the CRT's own _O_APPEND did. A pipe or console cannot seek but appends anyway, so a failed seek is ignored.
GetFileInformationByHandle fails on character devices such as NUL and the console, so fd_fdstat_get and fd_filestat_get failed on stdio redirected from NUL, which the stdio test does after renumbering it. Report such a handle as a character device with no other metadata instead, as fstat would.
poll_oneoff answered NOTSUP for any descriptor subscription on Windows, which has no poll for arbitrary handles, so poll_oneoff_stdio failed. Probe each handle by its kind instead, until one is ready or the clock runs out: disk files and devices such as NUL are always ready, as poll(2) reports regular files; a pipe is readable once it holds data or its writer is gone; console input once it has pending events; anything is writable. This was the last wasi-testsuite case skipped on any platform, so the skip lists go away.
kateinoigakukun
force-pushed
the
wasi-testsuite-compliance
branch
2 times, most recently
from
October 5, 2026 14:43
b69f6fb to
d8d3df0
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every case of the newest wasi-testsuite (72
wasm32-wasip1programs) now runs with no skip list on Linux, macOS and Windows. Before this PR, the suite was pinned to July 2025.Existing bugs fixed first
fdReaddirDoesNotLeakFileDescriptorsRLIMIT_NOFILEwhile other tests ran in parallel, so they failed with EMFILE at randompoll_oneoffon macOS/dev/nullis reportedPOLLNVAL, so polling stdin answered BADFPOLLNVALdescriptor as ready, asselectdoespath_filestat_get+ SYMLINK_FOLLOW (sandbox leak)link -> ../../etc/passwdexposed host metadatafd_readdirTest runner
The runner follows the upstream
doc/specification.md:testsuite/wasm32-wasip1/e0aa527f, the newestprod/testsuite-baseroot(preopened as/) replaceddirsrootas/file.cleanup(about 2 s in total)Linux / macOS fixes
dir_fd_op_failuresfd_allocateon a directory returns BADFpath_filestatpath_openpasses SYNC, DSYNC, RSYNC and NONBLOCK on to the host; O_SYNC is decoded correctly on glibcsymlink_filestatpath_filestat_set_timesusesutimensat(AT_SYMLINK_NOFOLLOW), following inside the sandboxpath_linkpath_linkis implemented withlinkat; SYMLINK_FOLLOW returns INVAL, as in wasmtimesymlink_createWindows
New file:
Sources/WASI/Platform/PAL/PALWindows.swift. No platform-layer API changes beyond one closure parameter, and no Foundation.FileDescriptoris unchanged*atoperationsNtCreateFile/NtSetInformationFilerelative to the directory HANDLE (looked up from ntdll at run time). No path is ever rebuilt, so concurrent renames cannot redirect an operation\or:are refused. Created relative to the handle; without the symlink privilege, falls back toCreateSymbolicLinkW, as cap-std doesO_CREAT/O_TRUNCon a directory; a hard link to a symlink links the symlink; POSIX-semantics delete and rename;.cannot be renamed; case-only renames work; only symlinks and junctions count as symlinksFollow-up commits, one per test:
pwrite-with-appendfd_flags_set,path_filestatstdioNULand the console are stat'ed as character devicespoll_oneoff_stdiopoll_oneoffprobes handles by kind (file, pipe, console)Not included
wasm32-wasip3suitesOVERLAPPEDI/O would need one_O_APPEND