Skip to content

WASIX Slice 3.5: Module-instantiation surface (env imports + v2 stubs + COOP/COEP)#370

Merged
taybenlor merged 12 commits into
taybenlor:mainfrom
liverpoolie:wasix/slice-3.5-instantiation-surface
May 10, 2026
Merged

WASIX Slice 3.5: Module-instantiation surface (env imports + v2 stubs + COOP/COEP)#370
taybenlor merged 12 commits into
taybenlor:mainfrom
liverpoolie:wasix/slice-3.5-instantiation-surface

Conversation

@liverpoolie

Copy link
Copy Markdown
Contributor

Implements issue #22.

Closes #22

Summary

Make every buildable wasmer-suite binary instantiate under the WASIX runtime so the FS-only tests can run end-to-end against the Slice 3 filesystem provider. Carve-out from Slice 6 (memory auto-detect) and Slice 4 (COOP/COEP) — neither the threads provider nor the worker host ships here.

What landed

  • WASIX-PLAN.md sequencing note explaining why env-import auto-detect ships ahead of the threads / futex provider work (every wasix-libc binary imports env.memory declared shared regardless of whether it uses threads).
  • env.memory / env.__indirect_function_table auto-detect. WebAssembly.Module.imports() only exposes { module, name, kind } — descriptors (limits, shared flag, table element type) are not standard, confirmed against V8 (Chrome / Node 22). Implementation walks the import section on the raw bytes (tolerates leading custom sections, varuint32 / memory64 limits, funcref / externref tables), then WebAssembly.compile + instantiate. Memory + table can be overridden via new WASIXContextOptions.memory / .indirectFunctionTable.
  • wasix_32v1 v2 import stubs. fd_dup2, path_open2 (wired via existing pathOpen ignoring the v2 fdflags2 arg), fd_fdflags_get/_set, proc_exit2 (throws WASIXExit, same exit semantics as proc_exit), proc_exec3, proc_spawn2, proc_fork_env, proc_signals_get/_sizes_get — names taken from wasm-objdump of the wasmer-suite binaries. proc_signals_get/_sizes_get return SUCCESS with size=0 instead of ENOSYS so wasix-libc's _start reaches main (ENOSYS is a fatal init error there — exits 71).
  • COOP/COEP headers on the Vite dev server (Playwright reuses it via test:server).
  • One-shot console.warn on WASIX construction when crossOriginIsolated === false. Without it, the shared-memory import failure looks like a generic engine error.
  • Inner-WASI drive sharing. wasix-libc reaches for both wasi_snapshot_preview1 and wasix_32v1 filesystem imports — preopen discovery (fd_prestat_get) is preview1. The internal WASI now reuses the WASIDriveFileSystemProvider's drive when one is supplied; opaque providers keep an empty inner drive.
  • TextDecoder shared-buffer fix in both WASI and WASIX text-decoding paths. TextDecoder rejects views over a SharedArrayBuffer; copying via .slice() before decode unblocks every preview1 stdout/path decode for WASIX guests.
  • CWD plumbing. WASIDrive accepts WASIDrivePreopen[]; harness wires /home at fd 4 to match wasmer run --volume .. getcwd / chdir bound on the wasix_32v1 surface (with resolveAbsolute + longest-preopen match + ENOTDIR/ENOENT shape). Preview1 fd_prestat_get / _dir_name overridden so wasix-libc's preopen walk sees fd 4.
  • Drive-level fixes (closes the gaps Ben flagged after CWD plumbing landed): ./ normalisation, parent-component validation on open/pathCreateDir, preopen retention on close, . / .. synthesis + .runno filtering in fdReaddir, preview1 path syscalls routed through the WASIX provider, ENOTCAPABLE → ENOENT mapping for WASIX callers, and harness --volume host:guest parsing so absolute mount points are pre-seeded.
  • Skip-map re-classification. Drop the ENV_IMPORTS_BUILT_TESTS block; classify each former entry per current failure mode (full list in the commit messages). Tokens stay grep-stable for downstream slice work.

Test status

  • Slice 1/2/3 specs: 57/57 chromium pass.
  • Wasix-suite (chromium / firefox / webkit): 204 pass / 45 skip. All 10 in-scope FS tests flip green; the 11th (fd-close) stays carved out as a TCP socket test (requires-provider-sockets).
  • Remaining skips are non-FS-drive (sockets, proc, mmap, threads, fd-table, mount syscall, symlinks, TTY) — match the original issue's out-of-scope list.

Surfaced env imports (vs the proposed list)

Inspected via wasm-objdump -x public/bin/wasix-tests/*.wasm:

  • env.memory (shared, initial=129, max=65536) — ✓ matched the hypothesis.
  • env.__indirect_function_tablenot present in any of the 25 wasmer-suite binaries at the pinned SHA. The runtime still handles it (constructed from the descriptor when surfaced) so the surface is forward-compatible.
  • v2 surface beyond the proposed fd_dup2 / path_open2 / proc_exit2: also proc_signals_get, proc_signals_sizes_get, fd_fdflags_get, fd_fdflags_set, proc_exec3, proc_spawn2, proc_fork_env. All stubbed, all mapped to TODO(slice-N) pointers.

Test plan

  • npm run test:prepare:wasix-suite — every binary builds (25/25).
  • npx tsc --noEmit clean.
  • npx playwright test tests/wasix-suite.spec.ts --project=chromium --project=firefox --project=webkit — 204 pass / 45 skip.
  • npx playwright test tests/core.spec.ts tests/libc.spec.ts tests/libstd.spec.ts tests/reactor.spec.ts tests/wasix-clock-random.spec.ts tests/wasix-fs-provider.spec.ts --project=chromium — 57 passed.
  • args.spec.ts / stdio.spec.ts / wasix-smoke.spec.ts need cargo-built *.wasi.wasm binaries; not exercised in this slice.

🤖 Generated with Claude Code

liverpoolie and others added 12 commits May 3, 2026 15:32
Slice 3.5 carves the env.memory auto-detect path out of Slice 6 because
every wasix-libc binary imports a shared env.memory regardless of whether
it uses threads, so FS-only tests can't instantiate without it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WASIX.start now compiles the module first, walks
WebAssembly.Module.imports for env.memory / env.__indirect_function_table,
and constructs a matching WebAssembly.Memory / WebAssembly.Table from the
import descriptor (or validates the host-supplied override against it).
The resolved values land in imports.env so wasix-libc binaries — which
import a shared env.memory — can instantiate.

Adds memory and indirectFunctionTable overrides to WASIXContextOptions
for hosts that need to share the import surface across sibling instances
(threaded configurations, Slice 6).

Emits a one-shot console.warn when crossOriginIsolated is false: the
shared-memory import otherwise fails with a generic engine error that
doesn't point at COOP/COEP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wasix-libc binaries built with the upstream sysroot link against the
flag-extended v2 surface alongside v1. Stub the v2 entries that surface
on the wasmer integration suite so the binaries can instantiate:

- proc_exit2 → throws WASIXExit (same exit path as proc_exit).
- path_open2, fd_dup2, fd_fdflags_get / _set → ENOSYS, deferred to the
  fd-table extraction in Slice 9.
- proc_exec3, proc_spawn2, proc_fork_env → ENOSYS, deferred to the
  proc provider in Slice 7.
- proc_signals_get / _sizes_get → ENOSYS, deferred to the signals
  provider in Slice 8.

Stubs live in the import-table assembly, not in wasix-32v1.ts (the ABI
table stays for spec-level constants).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wasix-libc binaries import a shared env.memory. Browsers reject the
underlying SharedArrayBuffer unless the host page is
cross-origin-isolated, so the dev server (used by Playwright via
test:server) now sends COOP same-origin + COEP require-corp.

Replaces the Slice-4 NOTE comment that flagged this as needed once the
worker host landed; the import surface in Slice 3.5 needs the same
headers without the worker host.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WASIX guests import a SharedArrayBuffer-backed env.memory.
TextDecoder rejects views over a SharedArrayBuffer with
"The provided ArrayBufferView value must not be shared", so the
preview1 fd_write stdio path and every path_* helper that decodes a
guest string blew up before reaching the file system.

Slice into a fresh non-shared Uint8Array before decoding. Cheap for
single-iov stdio writes and short paths; preview1 binaries (non-shared
memory) keep the same shape with one extra .slice() per decode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier env-auto-detect commit assumed
WebAssembly.Module.imports() exposed each entry's descriptor (memory
limits, shared flag, table element type), but the standard surface
returns only { module, name, kind } — engines do not expose limits.
Confirmed against V8 (Chrome / Node 22): the import section parse
has to happen on the raw bytes.

Replace the static-API approach with a small import-section parser
(tolerant of leading custom sections, varuint32 / memory64 limits,
funcref / externref tables). WASIX.start now buffers the response
bytes, walks the import section for env.memory / env.__indirect_function_table,
constructs matching memory + table from the descriptors, then compiles.

Other plumbing this slice picks up so wasix-libc binaries reach main:

- Share the inner WASI's drive with WASIDriveFileSystemProvider.drive
  when the host hands one over. wasix-libc reaches for both
  wasi_snapshot_preview1 and wasix_32v1 fs imports — the inner WASI
  must see the same files. Other (opaque) FileSystemProvider
  implementations keep an empty inner drive (those hosts shouldn't
  have wasix-libc binaries in the first place).
- Wire path_open2 to the existing WASIDriveFileSystemProvider.pathOpen,
  ignoring the v2 fdflags2 arg until the fd-table extraction (Slice 9).
  wasix-libc routes every open()/opendir() through path_open2 so the
  ENOSYS stub blocked the entire suite at the first stat.
- proc_signals_get / _sizes_get return SUCCESS with size=0 instead of
  ENOSYS. wasix-libc's _start treats ENOSYS as a fatal init error and
  exits 71 before reaching main; reporting "no signals" lets early
  init finish. Real signal delivery lands in Slice 8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the ENV_IMPORTS_BUILT_TESTS block — every binary in that list now
instantiates under the new env-import surface. Re-classify each former
entry per current failure mode:

- closing-pre-opened-dirs, create-and-remove-dirs, create-dir-at-cwd*,
  cross-fs-rename, cwd-to-home, distinct-inodes-same-basename,
  fstatat-with-chdir, open-under-file → requires-future-feature with
  REQUIRES_CWD_PLUMBING note. wasix-libc's default cwd is /home and the
  wasmer runner mounts the test dir there; the binaries instantiate and
  reach main but stat / mkdir / opendir fail at the first cwd-relative
  resolution because the WASIX runtime doesn't yet implement
  getcwd / chdir or the /home preopen.
- pwrite-and-size → requires-future-feature, distinct cause: the test
  opens absolute /data/my_file.txt, which needs the wasmer
  --volume=.:/data mount. WASIDrive's single preopen can't model it.
- fd-close → requires-provider-sockets (test opens a TCP socket).
- popen, posix_spawn → requires-provider-proc.
- vfork → requires-provider-proc (note distinguishing wasix's vfork
  semantics from POSIX).
- udp → requires-provider-sockets.
- fs-mount, mount-tmp-locally → requires-future-feature (mount syscall).
- msync-* / munmap-* / read-after-munmap → requires-future-feature
  (mmap / file-backed mappings not modelled).
- symlink-open-read-write → requires-future-feature (symlinks).

No proposed-pass test currently flips green: the env-import surface lets
every binary instantiate but the cwd / chdir plumbing the wasmer runner
provides is out of scope for this slice. Surface + classification ship
here so downstream slices can wire individual provider tokens
(grep `requires-provider-proc` etc.) atomically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wasix-libc's compiled-in default cwd is /home and the wasmer test runner
mounts each test dir there (`--volume .` is shorthand for `.=/home`).
The flat-path WASIDrive only modelled the implicit fd 3 = "/" preopen,
so wasix-libc binaries that called open("foo") had nowhere to resolve
the cwd-relative path against.

Extend the drive to accept additional preopens at construction (each
binds a guest-visible name like "/home" to a drive-internal prefix),
and surface them through WASIDriveFileSystemProvider so the WASIX
runtime can answer `fd_prestat_get(4)` with the new mount.

Also fix a latent bug in WASIDrive's `open` / `pathStat` where opening
a sub-directory under any non-root preopen used `prefix = /<path>/`
instead of `<parent.prefix><path>/` — fine for the implicit fd 3 = "/"
case (the strings collapse), but it would have routed every sub-dir
lookup under fd 4 = "/home/" to the wrong subtree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wasix-libc reads `getcwd` from `__wasilibc_resolve_path` to turn
relative paths absolute before walking the preopen table; without a
real implementation every cwd-relative open fell through with
ENOTCAPABLE before reaching the FS provider. Wire `getcwd` and `chdir`
to a runtime-side cwd string (default `/home`, matching wasix-libc's
compiled-in default and the wasmer runner's mount point), and override
preview1's `fd_prestat_get` / `fd_prestat_dir_name` so wasix-libc's
preopen discovery sees the FS provider's full preopen map (fd 4 =
/home alongside fd 3 = ".") rather than wasi.ts's hardcoded fd-3-only
view.

`chdir` validates the resolved target lives under a known preopen and
resolves to a directory via the FS provider; cwd-relative path
resolution further down the syscall stack is unnecessary because
wasix-libc handles that itself before calling `path_*`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mount each per-test input set under /home/<rel-path> instead of
/<rel-path>, configure the FS provider with a /home preopen at fd 4,
and prime PWD=/home so wasix-libc's startup resolver sees the cwd
before falling back to getcwd. This mirrors what `wasmer run --volume .`
does (the runner's default mount point is /home because that's
wasix-libc's default cwd).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cwd / chdir / /home preopen unblock four of the previous
REQUIRES_CWD_PLUMBING tests (cross-fs-rename, cwd-to-home,
distinct-inodes-same-basename, fstatat-with-chdir). The remaining
five surface drive-level limitations rather than cwd gaps; replace
the cwd carve-out with notes pointing at the real root cause:

- create-dir-at-cwd, create-dir-at-cwd-with-chdir: WASIDrive does not
  normalise `./` segments so mkdirat(cwd_fd, "./testN") writes a
  path subsequent stat cannot find.
- create-and-remove-dirs: drive does not enforce parent-dir-exists
  on mkdir.
- closing-pre-opened-dirs: closing a preopen drops the fd entry,
  breaking wasmer-style libc preopen retention across close.
- open-under-file: drive does not validate parent type, so
  open("file/child") never returns ENOTDIR.

All five lift with the WASIDrive extraction in a later slice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…aps (refs #22)

Brings the remaining FS-category wasix-suite tests to green by fixing
the drive-level bugs that were carved out in the previous skip-map
pass, plus the harness-side gaps that the carve-outs were masking:

  * `WASIDrive`: normalize `./` and empty path segments via a
    `normalizeRelative` helper so `mkdirat(cwd, "./foo")` and
    `stat("foo")` resolve to the same flat-path key. `..` is rejected
    (the flat-path map can't model traversal).
  * `WASIDrive.pathCreateDir` / `open`: reject paths whose parent
    component is missing (ENOENT) or a regular file (ENOTDIR), via a
    shared `validateParent` guard. POSIX-correct and lets
    `create-and-remove-dirs` / `open-under-file` enforce their
    negative assertions.
  * `WASIDrive.close`: keep preopen fds in the openMap on
    user-initiated close. wasix-libc's preopen cache continues to
    resolve cwd-relative paths via the "closed" fds — matches
    wasmer's runtime behaviour and unblocks `closing-pre-opened-dirs`.
  * `WASIDriveFileSystemProvider.fdReaddir`: synthesize POSIX `.` /
    `..` entries and filter the `.runno` directory sentinel.
  * `WASIX` preview1 overrides: route `path_filestat_get`, `path_open`,
    `path_create_directory`, `path_remove_directory`, `path_unlink_file`,
    `path_rename`, and `fd_readdir` through the WASIX provider.
    wasix-libc imports several of these from preview1 (notably
    `rmdir` → `path_remove_directory`, `stat` → `path_filestat_get`),
    where the preview1 stubs in wasi.ts either return ENOSYS or skip
    the provider's POSIX translation layer.
  * Provider error mapping: rewrite drive-side `ENOTCAPABLE` to
    `ENOENT` for WASIX callers so wasix-libc's `errno == ENOENT`
    checks fire on missing paths. Preview1 callers go through wasi.ts
    directly and still see the original code (the WASI test suite
    asserts on it explicitly).
  * Harness: parse `--volume host:guest` mounts from each test's
    `run.sh` and pre-seed the guest mount points (plus the implicit
    `/tmp` MemFS) as empty directories in the in-memory FS. Also
    seed `main.c` / `run.sh` and synthesize empty `main.wasm` /
    `output` placeholders so cwd-listing tests see the same layout
    wasmer's runner provides.
  * `WASIX_SUITE_SKIPS`: drop the FS-category entries that now pass
    (`create-dir-at-cwd`, `create-dir-at-cwd-with-chdir`,
    `create-and-remove-dirs`, `closing-pre-opened-dirs`,
    `open-under-file`, `pwrite-and-size`).

204 tests pass across chromium / firefox / webkit; 45 skipped (all
non-FS provider gaps — sockets, threads, mmap, proc, fd-table).

@taybenlor taybenlor left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@taybenlor taybenlor merged commit 3c32435 into taybenlor:main May 10, 2026
2 checks passed
@liverpoolie liverpoolie deleted the wasix/slice-3.5-instantiation-surface branch May 10, 2026 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Emscripten Binaries

2 participants