-
Notifications
You must be signed in to change notification settings - Fork 5k
fix(wasi): resolve path_open against the preopen dir, not cwd #30303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| // Minimal WASI program that opens a file from a preopen using path_open | ||
| // and reads it, then writes the contents to another file. | ||
| // | ||
| // We declare the WASI imports by hand so we don't depend on a WASI sysroot. | ||
| // | ||
| // Compile: | ||
| // clang --target=wasm32 -nostdlib -O2 -fno-builtin \ | ||
| // -Wl,--no-entry -Wl,--export=_start -Wl,--export=memory \ | ||
| // -Wl,--allow-undefined \ | ||
| // -o preopen-wasi.wasm preopen-wasi.c | ||
| // | ||
| // Regression fixture for oven-sh/bun#30302. | ||
|
|
||
| typedef unsigned int u32; | ||
| typedef unsigned long long u64; | ||
|
|
||
| __attribute__((import_module("wasi_snapshot_preview1"), import_name("path_open"))) | ||
| unsigned int wasi_path_open(u32 dirfd, u32 dirflags, const char *path, u32 path_len, | ||
| u32 oflags, u64 fs_rights_base, u64 fs_rights_inheriting, | ||
| u32 fdflags, u32 *opened_fd); | ||
|
|
||
| __attribute__((import_module("wasi_snapshot_preview1"), import_name("fd_read"))) | ||
| unsigned int wasi_fd_read(u32 fd, const void *iovs, u32 iovs_len, u32 *nread); | ||
|
|
||
| __attribute__((import_module("wasi_snapshot_preview1"), import_name("fd_write"))) | ||
| unsigned int wasi_fd_write(u32 fd, const void *iovs, u32 iovs_len, u32 *nwritten); | ||
|
|
||
| __attribute__((import_module("wasi_snapshot_preview1"), import_name("fd_close"))) | ||
| unsigned int wasi_fd_close(u32 fd); | ||
|
|
||
| __attribute__((import_module("wasi_snapshot_preview1"), import_name("proc_exit"))) | ||
| void wasi_proc_exit(u32 rval) __attribute__((noreturn)); | ||
|
|
||
| struct ciovec { | ||
| const void *buf; | ||
| u32 buf_len; | ||
| }; | ||
|
|
||
| // WASI rights for read/write files. Using the superset the Bun implementation | ||
| // grants to preopen directories keeps things simple. | ||
| #define RIGHTS_ALL ((u64)-1) | ||
|
|
||
| // O_CREAT | O_TRUNC in WASI oflags | ||
| #define WASI_O_CREAT (1 << 0) | ||
| #define WASI_O_TRUNC (1 << 3) | ||
|
|
||
| static char read_buf[256]; | ||
| static char out_buf[256]; | ||
|
|
||
| void _start() { | ||
| // Preopen dirfd is always 3 (first after stdin/stdout/stderr). | ||
| u32 dirfd = 3; | ||
|
|
||
| // Read "input.txt" from the preopen. | ||
| u32 in_fd = 0; | ||
| unsigned int err = wasi_path_open(dirfd, 0, "input.txt", 9, 0, | ||
| RIGHTS_ALL, RIGHTS_ALL, 0, &in_fd); | ||
| if (err != 0) wasi_proc_exit(10 + err); | ||
|
|
||
| struct ciovec read_iov = { read_buf, sizeof(read_buf) }; | ||
| u32 nread = 0; | ||
| err = wasi_fd_read(in_fd, &read_iov, 1, &nread); | ||
| if (err != 0) wasi_proc_exit(30 + err); | ||
| wasi_fd_close(in_fd); | ||
|
|
||
| // Write "output.txt" in the preopen with "got: " prefix + contents. | ||
| u32 out_fd = 0; | ||
| err = wasi_path_open(dirfd, 0, "output.txt", 10, | ||
| WASI_O_CREAT | WASI_O_TRUNC, | ||
| RIGHTS_ALL, RIGHTS_ALL, 0, &out_fd); | ||
| if (err != 0) wasi_proc_exit(50 + err); | ||
|
|
||
| // Build "got: <contents>" in out_buf. | ||
| const char *prefix = "got: "; | ||
| u32 plen = 5; | ||
| for (u32 i = 0; i < plen; i++) out_buf[i] = prefix[i]; | ||
| 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; | ||
| err = wasi_fd_write(out_fd, &write_iov, 1, &nwritten); | ||
| if (err != 0) wasi_proc_exit(70 + err); | ||
|
|
||
| wasi_fd_close(out_fd); | ||
| wasi_proc_exit(0); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { spawnSync } from "bun"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { expect, it } from "bun:test"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { bunEnv, bunExe } from "harness"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { bunEnv, bunExe, tempDir } from "harness"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { join } from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("Should support printing 'hello world'", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { stdout, stderr, exitCode } = spawnSync({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -20,3 +21,62 @@ it("Should support printing 'hello world'", () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| exitCode: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // node:wasi path_open must resolve the guest path against the preopen's | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // mapped host directory, not against process.cwd(). Every other path_* | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // handler in src/js/node/wasi.ts does this; path_open used to call | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // path.resolve(p) (no base), making a WASM program that path_opens an | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // entry under its preopen fail with ENOENT at cwd. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Regression guard for oven-sh/bun#30302. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("node:wasi path_open resolves against the preopen host dir, not cwd", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| using dir = tempDir("wasi-preopen", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "work/input.txt": "hello from host file", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Deliberately place a wrong-looking file at `cwd/input.txt` so that | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the buggy cwd-relative lookup would pick this up instead of erroring | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // — catches a regression that silently opens the wrong file. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "input.txt": "wrong file — should never be read", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "runner.mjs": ` | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fs from "node:fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { WASI } from "node:wasi"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const workDir = process.argv[2]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const wasmPath = process.argv[3]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const wasi = new WASI({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| version: "preview1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| preopens: { "/work": workDir }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const wasmBytes = fs.readFileSync(wasmPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const module = await WebAssembly.compile(wasmBytes); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const instance = await WebAssembly.instantiate(module, wasi.getImports(module)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| wasi.start(instance); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.stderr.write("wasi.start threw: " + (err?.message ?? err) + "\\n"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cwd = String(dir); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const workDir = join(cwd, "work"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const wasmPath = join(import.meta.dir, "preopen-wasi.wasm"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| await using proc = Bun.spawn({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| cmd: [bunExe(), "runner.mjs", workDir, wasmPath], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| env: bunEnv, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| cwd, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| stdout: "pipe", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| stderr: "pipe", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+72
to
+81
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize ASAN stderr noise and assert This subprocess test uses 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 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clamp
totalto the number of bytes actually copied intoout_buf.nreadis bounded during copy, buttotal = plen + nreadis not. This can causefd_writeto read past valid output bytes.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents