Skip to content

node: implement node:wasi v26 WASI class surface (+23 tests) - #35709

Draft
cirospaciari wants to merge 15 commits into
claude/node-v26-fix-tlsfrom
claude/node-wasi
Draft

cirospaciari wants to merge 15 commits into
claude/node-v26-fix-tlsfrom
claude/node-wasi

Conversation

@cirospaciari

@cirospaciari cirospaciari commented Jul 25, 2026 •

Copy link
Copy Markdown
Member

What this does

Implements Node v26's node:wasi WASI class contract on top of Bun's existing JS preview1 host functions, and vendors Node's upstream test/wasi/ suite. 23 of Node's 26 wasi tests pass verbatim (0 passed before — there was no Node-compatible WASI surface).

What changed

src/js/node/wasi.ts — new WASI class wrapping the existing wasi-js engine, mirroring lib/wasi.js from Node v26.3.0:

  • constructor validation (options object, required version of 'preview1'|'unstable', args array, env/preopens objects, int32 stdin/stdout/stderr, boolean returnOnExit) with Node's exact error codes and check ordering
  • start() / initialize() / finalizeBindings() / getImportObject(); _start/_initialize/memory export validation with Node's exact messages; ERR_WASI_ALREADY_STARTED on double start
  • returnOnExit defaults to true: proc_exit records the code and throws a sentinel WebAssembly can't catch, start() returns the exit code
  • host functions throw ERR_WASI_NOT_STARTED (wasi.start() has not been called) until instance memory is attached
  • failed preopen opens surface as UVWASI_<errno> / uvwasi_init errors, matching uvwasi's failure shape
  • ExperimentalWarning on module load, matching Node (suppressed when Bun itself runs a .wasm entry point — the built-in runner promises clean stderr, covered by test/js/bun/wasm/wasi.test.js)

Engine bugs found by the upstream tests (each covered by a vendored test or a new unit test in test/js/bun/wasm/wasi.test.js):

  • fd_renumber had dup2 backwards — it moved to into from and closed the freshly opened fd, breaking wasi-libc freopen() (test-wasi-io)
  • poll_oneoff wrote 16-byte event records; preview1 events are 32 bytes, so any multi-subscription poll corrupted the guest event array (unit test)
  • poll_oneoff threw Invalid mix of BigInt and other type on every clock subscription, breaking sleep()/nanosleep() (unit test)
  • clock_res_get wrote through an absent DataView (test-wasi-clock_getres); sock_accept was missing — accept() on a bad fd now reports EBADF (test-wasi-sock)

src/jsc/bindings/ErrorCode.cpp — the single-expected-string ERR_INVALID_ARG_TYPE overload now follows Node's formatList buckets: 'undefined' renders must be undefined (required by test-wasi-start-validation / test-wasi-initialize-validation), class names render must be an instance of X. Pre-flattened "X, Y, or Z" strings keep the legacy of type rendering, so existing list-style call sites are unchanged. No in-repo test asserts the old renderings (grepped).

src/js/wasi-runner.js — the bun foo.wasm runner passes version: "preview1" and returnOnExit: false explicitly and calls initialize() for modules without _start, preserving its historical exit-code and stderr behavior.

scripts/runner.node.mjs — js/node/test/wasi/ added to the node-test inclusion predicate. Verified with a deliberately failing canary that the directory is collected and executed by the runner (then removed the canary), and that a real wasi test passes under the runner.

Tests

Vendored byte-verbatim from Node v26.3.0 (test/wasi/ + wasm fixtures + refreshed fixtures/wasi-preview-1.js): 23 files, all passing CI-style (BUN_GARBAGE_COLLECTOR_LEVEL=1, bunfig.node-test.toml, ASAN debug build). The diff adds 47 files under test/js/node/test/wasi/: the 23 test files plus the 24 .wasm binaries they load from wasi/wasm/, which are not tests themselves. test/js/bun/wasm/wasi.test.js updated for the required version option and extended with 2 regression tests for the poll/renumber fixes; all 7 pass.

Dropped (fail on Bun, not vendored):

  • test-wasi-poll.js — fd readiness (POLLOUT / stdin POLLHUP) needs real poll(2); a JS host over node:fs cannot detect pipe hangup without consuming data
  • test-wasi-pthread.js — wasm shared memory not enabled in JSC here (WebAssembly.Module doesn't parse: shared memory is not enabled); flaky upstream on Linux
  • test-wasi-worker-terminate.js — worker.terminate() cannot interrupt a wasm busy-loop; the process wedges and ignores SIGTERM

Behavior notes

new WASI() without version now throws ERR_INVALID_ARG_TYPE, matching Node ≥20 (previously Bun ignored version entirely). test-wasi-options-validation.js requires this exact behavior.

Rework the node:wasi entry point around Node v26's lib/wasi.js surface,
keeping the existing JS preview1 host functions as the backing engine:

- constructor: options/version/args/env/preopens/stdin/stdout/stderr/
  returnOnExit validation with Node's exact error codes and check order;
  version is now required ('preview1' or 'unstable')
- start()/initialize()/finalizeBindings()/getImportObject() with Node's
  validation messages, ERR_WASI_ALREADY_STARTED on double start, and
  returnOnExit (default true) implemented via a thrown sentinel so
  start() returns the exit code
- host functions throw ERR_WASI_NOT_STARTED until an instance memory is
  attached; failed preopen opens surface as UVWASI_<errno> errors like
  uvwasi_init
- module load emits the WASI ExperimentalWarning (suppressed for the
  built-in .wasm runner, which promises clean stderr)

Engine fixes found by Node's upstream wasi tests:
- fd_renumber had dup2 backwards: it moved 'to' into 'from' and closed
  the freshly opened fd, breaking freopen()
- poll_oneoff wrote 16-byte event records; preview1 events are 32 bytes,
  so multi-subscription polls corrupted the guest event array
- poll_oneoff mixed BigInt and Number when charging elapsed time,
  throwing on any clock subscription (sleep/nanosleep)
- clock_res_get wrote through a stale/absent DataView; sock_accept was
  missing (accept() on a bad fd now reports EBADF)

ERR_INVALID_ARG_TYPE with a single expected string now follows Node's
formatList buckets: 'undefined' renders 'must be undefined', class names
render 'must be an instance of X'. Pre-flattened list strings keep the
legacy 'of type' rendering.

The bun .wasm runner passes version/returnOnExit explicitly and calls
initialize() for reactor modules, preserving its exit-code behavior.
Vendor test/wasi from Node v26.3.0 byte-verbatim, including the wasm
fixtures compiled from C, and refresh fixtures/wasi-preview-1.js to the
v26 version (the previously vendored copy predated the threads target
and parseArgs handling). Add js/node/test/wasi/ to the node-test
runner's inclusion predicate; verified with a deliberately failing
canary that the directory is collected and executed.

Not vendored (fail on Bun):
- test-wasi-poll.js: fd readiness (POLLOUT/POLLHUP) needs real poll(2);
  the JS host over node:fs cannot detect pipe hangup without consuming
- test-wasi-pthread.js: wasm shared memory is not enabled in JSC here
  (WebAssembly.Module rejects shared memories); flaky upstream on Linux
- test-wasi-worker-terminate.js: worker.terminate() cannot interrupt a
  wasm busy-loop, the process wedges and ignores SIGTERM
@robobun

robobun commented Jul 25, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 11:45 PM PT - Aug 21st, 2026

❌ @robobun, your commit 1d293dc has 38 failures in Build #103294 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35709

That installs a local version of the PR into your bun-35709 executable, so you can run:

bun-35709 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. Bug in wasi.poll_oneoff #20857 - PR fixes the poll_oneoff BigInt mixing bug (TypeError: Invalid mix of BigInt and other type) and corrects 16-byte → 32-byte event records
  2. WASI.start(...) fails where bun run ... succeeds #28534 - PR implements getImportObject() and start() on the node:wasi WASI class, which this issue reports as missing
  3. TypeError: wasi.initialize is not a function #12755 - PR implements initialize() on the node:wasi WASI class, which this issue reports as not a function

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #20857
Fixes #28534
Fixes #12755

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

robobun added 6 commits August 3, 2026 20:48
…aude/node-wasi

# Conflicts:
#	src/jsc/bindings/ErrorCode.ts
…aude/node-wasi

# Conflicts:
#	src/jsc/bindings/ErrorCode.cpp
- restore src/jsc/bindings/BunHeapProfiler.h (deleted by #36500 on main,
  still needed by $newCppFunction sites in src/js/node/v8.ts)
- web_worker.rs: parent_ref was removed by #36571; deref parent directly
- path.rs: re-expose resolve_{posix,windows}_t as pub(crate) for permission.rs
- clap: re-expose Diagnostic fields as pub for Arguments.rs node-compat errors
- permission.rs: switch std::sync::RwLock -> bun_threading::RwLock,
  std::env::var -> bun_core::env_var::NODE_OPTIONS (disallowed_types/methods)
- run_command.rs: exec_check pub -> pub(crate) (unreachable_pub)
- BunHeapProfiler.rs: then(|| ..) -> then_some (unnecessary_lazy_evaluations)
- Timer.rs: move SAFETY comment onto the unsafe block it documents
…aude/node-wasi

# Conflicts:
#	src/jsc/bindings/BunHeapProfiler.h

This branch has not been deployed

No deployments
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.

2 participants