Skip to content

fix(wasi): resolve path_open against the preopen dir, not cwd - #30303

Closed
robobun wants to merge 2 commits into
mainfrom
farm/2f5944bb/wasi-path-open-preopen
Closed

fix(wasi): resolve path_open against the preopen dir, not cwd#30303
robobun wants to merge 2 commits into
mainfrom
farm/2f5944bb/wasi-path-open-preopen

Conversation

@robobun

@robobun robobun commented May 6, 2026

Copy link
Copy Markdown
Collaborator

What

node:wasi path_open was resolving the guest path relative to
process.cwd() instead of the preopen directory entry it was
given. With preopens: { "/work": hostDir }, a WASM program that
opened /work/file.txt fell back to <cwd>/file.txt.

Reproduction

Issue #30302 — a Rust WASI program built with rustc --target wasm32-wasip1
that reads DUCKFLIX_WASI_REQUEST (= /work/request.json) fails with
ENOENT: no such file or directory, open '<cwd>/request.json'. The
issue reporter's workaround was process.chdir(workspace) before
wasi.start(), which lines process.cwd() up with the preopen and
papers over the bug.

Cause

src/js/node/wasi.ts, line 1515 in path_open:

const fullUnresolved = path.resolve(p);  // no base → process.cwd()

CHECK_FD(dirfd, ...) already returns the preopen dirfd entry, whose
stats.path is the host directory, but the return value was discarded.
Every other path_* handler in the same file uses the stats correctly:

  • path_create_directory, path_filestat_get, path_filestat_set_times
  • path_link, path_readlink, path_remove_directory
  • path_rename, path_symlink, path_unlink_file

All do path.resolve(stats.path, p); only path_open didn't.

Fix

Capture stats from CHECK_FD and resolve against stats.path:

const stats = CHECK_FD(dirfd, constants_1.WASI_RIGHT_PATH_OPEN);
// ...
const fullUnresolved = stats.path ? path.resolve(stats.path, p) : path.resolve(p);

The stats.path guard matches the defensive pattern used by the other
handlers (if (!stats.path) return WASI_EINVAL); preopen entries
always set path when registered at line 853.

Test

test/js/bun/wasm/wasi.test.js gains a regression test that runs a
tiny C/WASI program (test/js/bun/wasm/preopen-wasi.c, pre-built as
preopen-wasi.wasm) which path_opens input.txt under the preopen
and writes got: <contents> to output.txt under the same preopen.
The test plants a decoy input.txt at the parent process's cwd so
the broken cwd-relative path also resolves — guaranteeing the test
checks that the output lands in the mapped host dir, not just that
it happens to land somewhere.

Gate

  • USE_SYSTEM_BUN=1 bun test test/js/bun/wasm/wasi.test.js → fails
    (ENOENT on <workDir>/output.txt)
  • bun bd test test/js/bun/wasm/wasi.test.js → passes

Fixes #30302

Every other path_* handler in src/js/node/wasi.ts resolves the guest
path against the preopen dirfd's host path via path.resolve(stats.path, p).
path_open ignored the stats it fetched and did path.resolve(p), which
resolves relative to process.cwd(), so a WASM program reading
'/work/file' under preopens: { '/work': hostDir } would fall back to
<cwd>/file instead of <hostDir>/file.

Capture stats from CHECK_FD and use stats.path as the base when present.

Fixes #30302.
@robobun

robobun commented May 6, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:26 PM PT - May 5th, 2026

@autofix-ci[bot], your commit a3f6051 has 1 failures in Build #51941 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30303

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

bun-30303 --bun

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fix(wasi): resolve path_open paths against preopen directory, not cwd #27724 - Also fixes path_open in node:wasi to resolve paths against the preopen directory instead of cwd

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75dcc94a-0806-46ae-a8bc-429c707c259e

📥 Commits

Reviewing files that changed from the base of the PR and between f0e0326 and a3f6051.

📒 Files selected for processing (1)
  • test/js/bun/wasm/wasi.test.js

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


Walkthrough

This PR updates WASI path_open to resolve guest paths against preopened host directories by using the directory FD's recorded path when available, and adds C and JS tests validating resolution against a preopened directory.

Changes

WASI Path Resolution Against Preopened Directories

Layer / File(s) Summary
Core Implementation
src/js/node/wasi.ts
path_open now calls CHECK_FD(dirfd, WASI_RIGHT_PATH_OPEN) into stats, and computes fullUnresolved as stats.path ? path.resolve(stats.path, p) : path.resolve(p) so guest paths resolve against the preopened dir when present.
WASI Test Program (C)
test/js/bun/wasm/preopen-wasi.c
New WASI test program added: declares imports (wasi_path_open, wasi_fd_read, wasi_fd_write, wasi_fd_close, wasi_proc_exit), ciovec struct, rights/flags macros, and _start which opens input.txt from preopen dirfd=3, reads it, writes got: <contents> to output.txt, closes fds, and exits.
JS Test Harness
test/js/bun/wasm/wasi.test.js
Adds tempDir and path.join imports and a test "node:wasi path_open resolves against the preopen host dir, not cwd" that sets up a temp workspace, preopens the host dir, runs the WASI program via Bun, and asserts exit code 0, no stderr, and expected output file contents.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: fixing path_open to resolve against the preopen directory rather than cwd.
Description check ✅ Passed The description fully covers required sections with comprehensive context: What (the bug), Reproduction (issue reference), Cause (code analysis), Fix (implementation details), Test (verification approach), and Gate commands.
Linked Issues check ✅ Passed Code changes fully address issue #30302: path_open now resolves guest paths against preopen host directories instead of process.cwd(), matching expected behavior and fixing the reported WASI preopens resolution bug.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the path_open resolution issue: modifying wasi.ts logic, adding a test program, and introducing a regression test.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/js/node/wasi.ts (1)

1550-1554: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not swallow path_open errors and still return success.

The inner catch logs and then falls through to WASI_ESUCCESS, which can falsely report success on open/rights failures.

Suggested fix
-              } catch (e) {
-                console.error(e);
-              }
+              } catch (e) {
+                throw e;
+              }
               return constants_1.WASI_ESUCCESS;
🤖 Prompt for 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.

In `@src/js/node/wasi.ts` around lines 1550 - 1554, The catch inside the path_open
implementation currently just console.error(e) and then falls through to return
constants_1.WASI_ESUCCESS, falsely reporting success; update the catch in the
path_open function to not swallow the error but instead map the thrown Node
error to the proper WASI errno and return that (or rethrow to be handled
upstream). Locate the path_open handler (the block that returns
constants_1.WASI_ESUCCESS) and replace the bare console.error(e) with logic that
derives a WASI errno from the error (use any existing errno-mapping helper or
inspect e.code/e.errno) and return that constant instead of WASI_ESUCCESS, or
rethrow the original error if callers expect exceptions.
🤖 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 `@test/js/bun/wasm/preopen-wasi.c`:
- Around line 77-83: The computed total bytes variable can exceed the actual
buffer capacity because total = plen + nread doesn't account for the clamp in
the copy loop; update the calculation of total before creating write_iov so it
is clamped to the number of bytes actually copied (e.g., total = min(plen +
nread, (u32)sizeof(out_buf)) or compute total = plen + actual_copied) so
fd_write / write_iov only sees the valid byte count; adjust the total variable
used by struct ciovec write_iov and any downstream uses (variables: total, plen,
nread, out_buf, write_iov, fd_write).

In `@test/js/bun/wasm/wasi.test.js`:
- Around line 72-81: The test should ignore the known ASAN startup warning in
the subprocess stderr and assert the process exit code after validating
filesystem/content; update the block that reads proc.stderr.text() and
proc.exited (variables stderr, exitCode, proc) to filter out lines that start
with or contain "WARNING: ASAN interferes" (or similar ASAN startup text) from
stderr before asserting it, perform the file content check against
Bun.file(join(workDir, "output.txt")).text() first, and only then assert
expect(exitCode).toBe(0); ensure the stderr assertion uses the filtered value
(expect(filteredStderr).toBe("")).

---

Outside diff comments:
In `@src/js/node/wasi.ts`:
- Around line 1550-1554: The catch inside the path_open implementation currently
just console.error(e) and then falls through to return
constants_1.WASI_ESUCCESS, falsely reporting success; update the catch in the
path_open function to not swallow the error but instead map the thrown Node
error to the proper WASI errno and return that (or rethrow to be handled
upstream). Locate the path_open handler (the block that returns
constants_1.WASI_ESUCCESS) and replace the bare console.error(e) with logic that
derives a WASI errno from the error (use any existing errno-mapping helper or
inspect e.code/e.errno) and return that constant instead of WASI_ESUCCESS, or
rethrow the original error if callers expect exceptions.
🪄 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: 72f73b7a-42ef-4cbf-ad0e-5511d7f2475b

📥 Commits

Reviewing files that changed from the base of the PR and between b009453 and f0e0326.

⛔ Files ignored due to path filters (1)
  • test/js/bun/wasm/preopen-wasi.wasm is excluded by !**/*.wasm
📒 Files selected for processing (3)
  • src/js/node/wasi.ts
  • test/js/bun/wasm/preopen-wasi.c
  • test/js/bun/wasm/wasi.test.js

Comment on lines +77 to +83
for (u32 i = 0; i < nread && plen + i < sizeof(out_buf); i++) {
out_buf[plen + i] = read_buf[i];
}
u32 total = plen + nread;

struct ciovec write_iov = { out_buf, total };
u32 nwritten = 0;

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clamp total to the number of bytes actually copied into out_buf.

nread is bounded during copy, but total = plen + nread is not. This can cause fd_write to read past valid output bytes.

Suggested fix
-    for (u32 i = 0; i < nread && plen + i < sizeof(out_buf); i++) {
+    u32 copied = 0;
+    for (u32 i = 0; i < nread && plen + i < sizeof(out_buf); i++) {
         out_buf[plen + i] = read_buf[i];
+        copied++;
     }
-    u32 total = plen + nread;
+    u32 total = plen + copied;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (u32 i = 0; i < nread && plen + i < sizeof(out_buf); i++) {
out_buf[plen + i] = read_buf[i];
}
u32 total = plen + nread;
struct ciovec write_iov = { out_buf, total };
u32 nwritten = 0;
u32 copied = 0;
for (u32 i = 0; i < nread && plen + i < sizeof(out_buf); i++) {
out_buf[plen + i] = read_buf[i];
copied++;
}
u32 total = plen + copied;
struct ciovec write_iov = { out_buf, total };
u32 nwritten = 0;
🤖 Prompt for 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.

In `@test/js/bun/wasm/preopen-wasi.c` around lines 77 - 83, The computed total
bytes variable can exceed the actual buffer capacity because total = plen +
nread doesn't account for the clamp in the copy loop; update the calculation of
total before creating write_iov so it is clamped to the number of bytes actually
copied (e.g., total = min(plen + nread, (u32)sizeof(out_buf)) or compute total =
plen + actual_copied) so fd_write / write_iov only sees the valid byte count;
adjust the total variable used by struct ciovec write_iov and any downstream
uses (variables: total, plen, nread, out_buf, write_iov, fd_write).

Comment on lines +72 to +81
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);

// The WASM program proc_exits(0) on success; non-zero encodes which step
// failed. See test/js/bun/wasm/preopen-wasi.c.
expect(stderr).toBe("");
expect(exitCode).toBe(0);

// The preopen points at <cwd>/work, so the output file must land there,
// with "got: " prefixed to the host-dir input's contents.
expect(await Bun.file(join(workDir, "output.txt")).text()).toBe("got: hello from host file");

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize ASAN stderr noise and assert exitCode last.

This subprocess test uses bunEnv, so stderr should ignore the known ASAN startup warning, and exitCode should be asserted after content/filesystem checks.

Suggested fix
-  const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
+  const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
+  const filteredStderr = stderr
+    .split("\n")
+    .filter(line => !line.startsWith("WARNING: ASAN interferes"))
+    .filter(Boolean)
+    .join("\n");
@@
-  expect(stderr).toBe("");
-  expect(exitCode).toBe(0);
+  expect(filteredStderr).toBe("");
@@
   expect(await Bun.file(join(workDir, "output.txt")).text()).toBe("got: hello from host file");
+  expect(exitCode).toBe(0);

Based on learnings: assert command exit code last in Bun tests, and filter WARNING: ASAN interferes lines from subprocess stderr when using bunEnv.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
// The WASM program proc_exits(0) on success; non-zero encodes which step
// failed. See test/js/bun/wasm/preopen-wasi.c.
expect(stderr).toBe("");
expect(exitCode).toBe(0);
// The preopen points at <cwd>/work, so the output file must land there,
// with "got: " prefixed to the host-dir input's contents.
expect(await Bun.file(join(workDir, "output.txt")).text()).toBe("got: hello from host file");
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
const filteredStderr = stderr
.split("\n")
.filter(line => !line.startsWith("WARNING: ASAN interferes"))
.filter(Boolean)
.join("\n");
// The WASM program proc_exits(0) on success; non-zero encodes which step
// failed. See test/js/bun/wasm/preopen-wasi.c.
expect(filteredStderr).toBe("");
// The preopen points at <cwd>/work, so the output file must land there,
// with "got: " prefixed to the host-dir input's contents.
expect(await Bun.file(join(workDir, "output.txt")).text()).toBe("got: hello from host file");
expect(exitCode).toBe(0);
🤖 Prompt for 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.

In `@test/js/bun/wasm/wasi.test.js` around lines 72 - 81, The test should ignore
the known ASAN startup warning in the subprocess stderr and assert the process
exit code after validating filesystem/content; update the block that reads
proc.stderr.text() and proc.exited (variables stderr, exitCode, proc) to filter
out lines that start with or contain "WARNING: ASAN interferes" (or similar ASAN
startup text) from stderr before asserting it, perform the file content check
against Bun.file(join(workDir, "output.txt")).text() first, and only then assert
expect(exitCode).toBe(0); ensure the stderr assertion uses the filtered value
(expect(filteredStderr).toBe("")).

@robobun

robobun commented May 6, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #27724 (same 2-line fix, same test strategy). Letting the older PR go first.

@robobun robobun closed this May 6, 2026
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 preopens appears to resolve guest paths relative to process.cwd() instead of mapped host directory

1 participant