Skip to content

node:wasi: validate options and implement start()/initialize()/getImportObject() - #34475

Open
robobun wants to merge 8 commits into
mainfrom
farm/9fda569e/wasi-lifecycle-nodecompat
Open

robobun wants to merge 8 commits into
mainfrom
farm/9fda569e/wasi-lifecycle-nodecompat

Conversation

@robobun

@robobun robobun commented Jul 17, 2026 •

Copy link
Copy Markdown
Collaborator

What

node:wasi's WASI class looked like Node's but diverged at every lifecycle edge:

Node v26.3.0 Bun (before)
new WASI({}) / new WASI(42) / new WASI({version:"bogus"}) TypeError accepted
typeof w.initialize / typeof w.getImportObject "function" "undefined"
"sock_accept" in w.wasiImport true false
w.start(ok) returns 0 returns undefined
w.start(no_start) / w.start(reactor) TypeError returns undefined
w.start(i); w.start(i) ERR_WASI_ALREADY_STARTED runs twice
new WASI({stdout: fd}), guest writes fd 1 bytes land in fd bytes land on host stdout

The last one is the bad one: the stdin/stdout/stderr fd options are the documented way to capture guest output. Ignoring them sends guest bytes into the embedder's own stdio.

Fix

In src/js/node/wasi.ts:

  • Constructor validates options (object), options.version (required, "preview1"/"unstable"), args (array, elements coerced to strings), env/preopens (objects), stdin/stdout/stderr (non-negative int32), returnOnExit (boolean, default true), using the same error codes as Node.
  • stdin/stdout/stderr options are wired into the fd map so guest fds 0/1/2 back onto the supplied host fds.
  • getImportObject() returns {wasi_snapshot_preview1: wasiImport} / {wasi_unstable: wasiImport} by version.
  • finalizeBindings(instance, {memory}) validates instance/instance.exports/memory, guards with ERR_WASI_ALREADY_STARTED, and records the instance.
  • start(instance) requires _start to be a function, _initialize to be undefined, catches the proc_exit sentinel when returnOnExit is on, and returns the exit code.
  • initialize(instance) requires _start to be undefined, optionally calls _initialize.
  • wasiImport.sock_accept is present (returns ENOSYS, like sock_recv/sock_send).
  • ERR_WASI_ALREADY_STARTED is added to the error-code table.

src/js/wasi-runner.js (the bun file.wasm entrypoint) now passes version: "preview1" and returnOnExit: false to preserve its existing exit-code behavior.

Overlap

#34474 implements the returnOnExit / start()-returns-exit-code piece in isolation; this PR covers that as part of the wider lifecycle contract.

Verification

test/js/node/wasi/wasi.test.ts (new, 28 tests) covers constructor validation (including fd boundaries and error codes), getImportObject(), start() return/validation/once semantics, returnOnExit in both modes, initialize() reactor handling, args coercion, and the stdin/stdout/stderr fd options (including a spawned child to assert guest fd 1 bytes do not reach host stdout). All fail on current main, all pass with this change. test/js/bun/wasm/wasi.test.js (updated to pass version, plus a new bun reactor.wasm CLI test) still passes.

CI status: every lane is green except darwin x64, which fails only on test/js/web/url/url.test.ts (ICU/IDNA). That test fails the same way on main at 69c6138 and is unrelated to this change. It is reported for main-break triage.


no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/wasm/wasi.test.js

Fixes #12755
Fixes #28534

Fixes #40774

@robobun

robobun commented Jul 17, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:42 AM PT - Aug 28th, 2026

❌ @robobun, your commit 8264f6a has 1 failures in Build #107678 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34475

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

bun-34475 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. TypeError: wasi.initialize is not a function #12755 - PR implements the missing initialize() method that was causing "TypeError: wasi.initialize is not a function"
  2. WASI.start(...) fails where bun run ... succeeds #28534 - PR implements the missing getImportObject() method needed for programmatic WebAssembly instantiation with WASI

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

Fixes #12755
Fixes #28534

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The WASI shim now validates configuration, selects import namespaces, supports configured standard streams, exposes sock_accept, and manages start/initialize lifecycle behavior. The runner and tests cover reactor execution, argument coercion, exit handling, and file-descriptor integration.

WASI runtime behavior

Layer / File(s) Summary
Configuration and import contracts
src/js/node/wasi.ts, test/js/node/wasi/wasi.test.ts
WASI options are validated and normalized, binding namespaces follow the selected version, standard streams use configured descriptors, and sock_accept is exposed.
Execution lifecycle and exit handling
src/js/node/wasi.ts, src/jsc/bindings/ErrorCode.ts, test/js/node/wasi/wasi.test.ts
Instance finalization, _start/_initialize validation, exit-code propagation, and single-use lifecycle errors are implemented and tested.
Runner integration and module fixtures
src/js/wasi-runner.js, test/js/node/wasi/wasi.test.ts, test/js/bun/wasm/wasi.test.js
The runner selects start() or initialize(), while tests construct minimal WASI modules and verify reactor execution, imports, and argument coercion.
Standard-stream integration
test/js/bun/wasm/wasi.test.js, test/js/node/wasi/wasi.test.ts
Tests verify guest reads and writes through configured stdin, stdout, and stderr descriptors, including child-process behavior.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement both linked objectives: WASI.initialize/getImportObject support and compatible start() behavior.
Out of Scope Changes check ✅ Passed No clear unrelated changes stand out; the edits all support WASI lifecycle, validation, compatibility, or tests.
Title check ✅ Passed The title clearly summarizes the primary changes: WASI option validation and implementation of the start(), initialize(), and getImportObject() APIs.
Description check ✅ Passed The description explains the problem, implementation, overlap, linked issues, and verification results. The section headings differ from the template, but equivalent content is present and complete.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/node/wasi.ts`:
- Around line 2009-2040: Move the kStarted state transition out of
finalizeBindings and into start and initialize after their _start/_initialize
export validation succeeds but before guest code is invoked. Keep
finalizeBindings responsible for memory and instance binding only, and ensure
validation failures leave the WASI instance reusable. Add a regression test
covering a failed export validation followed by a successful valid invocation.
- Around line 817-831: Cache the wasiConfig properties args, env, preopens, and
returnOnExit in local variables before any undefined checks or validation within
the option-processing flow. Use those locals for validateArray, validateObject,
validateBoolean, and args mapping so each getter is invoked only once, while
preserving the existing defaults and validation behavior.

In `@test/js/node/wasi/wasi.test.ts`:
- Around line 104-118: Expand the WASI option tests around the existing
stdin/stdout/stderr and returnOnExit cases: verify FD values 0 and 2 ** 31 - 1
are accepted while NaN, infinities, and 2 ** 31 are rejected; add a
returnOnExit: false proc_exit assertion covering the host-exit path; and add
stderr guest FD 2 coverage confirming writes use the configured host descriptor,
matching stdin/stdout tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 40461bf3-090c-46aa-8662-70fffc1dd947

📥 Commits

Reviewing files that changed from the base of the PR and between 6618e7f and 14f78df.

📒 Files selected for processing (5)
  • src/js/node/wasi.ts
  • src/js/wasi-runner.js
  • src/jsc/bindings/ErrorCode.ts
  • test/js/bun/wasm/wasi.test.js
  • test/js/node/wasi/wasi.test.ts

Comment thread src/js/node/wasi.ts Outdated
Comment thread src/js/node/wasi.ts
Comment thread test/js/node/wasi/wasi.test.ts
@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. node:wasi: honor returnOnExit so proc_exit does not kill the host process #34474 - Both implement returnOnExit option, proc_exit sentinel handling, and start() returning exit code; node:wasi: honor returnOnExit so proc_exit does not kill the host process #34474 is fully subsumed by this PR
  2. fix(wasi): fix random_get and implement WASI#getImportObject #34036 - Both implement WASI#getImportObject(); the getImportObject() portion is subsumed by this PR (though fix(wasi): fix random_get and implement WASI#getImportObject #34036 also fixes random_get independently)

🤖 Generated with Claude Code

Comment thread src/js/wasi-runner.js
Comment thread src/js/node/wasi.ts Outdated
Comment thread src/js/node/wasi.ts
Comment thread test/js/node/wasi/wasi.test.ts Outdated
Comment thread src/js/node/wasi.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No further findings — all four earlier rounds of nits have been addressed. This is a moderate-scope Node compat change (new public methods, constructor now requires version, bun file.wasm now runs _initialize for reactors) with overlap against #34474/#34036, so leaving the merge decision to a maintainer.

Checked: constructor validation ordering vs Node v26 finalizeBindings; returnOnExit sentinel doesn't leak when _start throws a user error; stdio fd wiring reaches fd_write/fd_read via stats.real; wasi-runner.js reactor branch is covered by the new CLI test.

Extended reasoning...

Overview

Touches src/js/node/wasi.ts (constructor validation, start()/initialize()/getImportObject()/finalizeBindings(), sock_accept, stdio fd mapping, returnOnExit sentinel), src/js/wasi-runner.js (pass version/returnOnExit, branch on _start vs reactor), ErrorCode.ts (adds ERR_WASI_ALREADY_STARTED), plus a new 22-test file and updates to the existing WASI test to pass version.

Security risks

None introduced. The stdio fd options are validated with validateInt32(..., 0) before being stored; the WASI preopen sandbox logic is untouched. No new privilege surface.

Level of scrutiny

Medium. node:wasi is a JS-only compat shim (file header calls it a "quick hack"), not a hot path or memory-safety surface. But this is user-facing API: new WASI()/new WASI({}) now throw where they previously didn't (matching Node), start() now validates exports strictly, and bun reactor.wasm now calls _initialize where it used to silently no-op. Those are intentional and Node-aligned, but they change observable behavior for existing callers, which is worth a maintainer's sign-off.

Other factors

  • Four prior review rounds from me (reactor handling in wasi-runner, kEmptyObject reuse, kStarted ordering vs Node v26, bare .toThrow(), ArrayPrototypeMap idiom) — all addressed in de0bed0 → 1e088a5.
  • The kStarted-ordering question was resolved against Node v26.3.0's actual finalizeBindings (sets the flag after instance/exports/memory validation), which this PR matches.
  • Test coverage is thorough: exact error codes, fd boundary at 2**31-1, returnOnExit:false via spawned child, stdout/stderr/stdin fd routing including a subprocess assertion that guest fd 1 does not leak to host stdout, reactor via CLI.
  • Duplicate-PR bot flagged #34474 (fully subsumed) and #34036 (partial overlap on getImportObject) — reconciling those is a maintainer call, not a code issue here.

robobun and others added 8 commits August 28, 2026 11:20
…ortObject()

The WASI class previously accepted any constructor input and silently
diverged from Node's lifecycle contract: version/options were never
validated, initialize() and getImportObject() didn't exist, start()
returned undefined, ran on reactor modules, and could be called twice,
and the documented stdin/stdout/stderr fd options were ignored so guest
writes to fd 1 landed on the host's real stdout.

Constructor now validates options/version/args/env/preopens/stdio/
returnOnExit the same way Node does; start() requires _start, rejects
_initialize, runs once, and returns the exit code; initialize() and
getImportObject() are implemented; the stdin/stdout/stderr options are
wired into the fd map so guest stdio lands on the caller-supplied fds;
returnOnExit (default true) captures proc_exit; sock_accept is present
in wasiImport; ERR_WASI_ALREADY_STARTED is added.
bun file.wasm previously silently no-op'd on reactor-style modules
(export _initialize, not _start) because the old start() tolerated a
missing _start. With start() now matching Node's validation, branch on
the module shape so reactors are driven via initialize() instead.
…n random_get test

The dead-code sweep on main removed validateUndefined from
internal/validators because nothing used it yet. WASI.start() and
WASI.initialize() now use it. The new random_get test on main also
constructs WASI without a version, which the constructor now rejects.
@robobun
robobun force-pushed the farm/9fda569e/wasi-lifecycle-nodecompat branch from 1e088a5 to 8264f6a Compare August 28, 2026 11:24

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code review found no issues

No high-confidence issues detected in this change.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

node:wasi: WASI.prototype.getImportObject is missing WASI.start(...) fails where bun run ... succeeds TypeError: wasi.initialize is not a function

1 participant