From 3deeb6452b41e3d419eaf366015c3a64bf810ad1 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:13:44 +0000 Subject: [PATCH 1/4] bundler: allow relative FileMap keys without tripping absolute-path debug asserts Bun.build's `files` option accepts user-supplied keys that may be relative (the docs show "./src/generated.ts"). Two debug assertions assumed file-namespace paths are always absolute: - enqueue_entry_item asserted every file-namespace path is absolute, but FileMap keys are lookup identities, not real fs paths. Skip the assertion when the path is a FileMap key. - generate_isolated_hash asserted path_with_pretty_initialized always yields pretty.ptr != text.ptr, but for a bare relative key like "e.js" the computed pretty equals text and dupe_alloc aliases them. Relax the assertion to allow this for non-absolute text. Release builds were unaffected (both are debug_assert); only CI_ASSERT builds panicked. Fuzzer repro: Bun.build({entrypoints:["./e.js"], files:{"./e.js": ")"}, throw:false}) --- src/bundler/LinkerContext.rs | 10 ++-- src/bundler/bundle_v2.rs | 8 +++- test/bundler/bundler_files.test.ts | 74 +++++++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index e8a16aae5a91..08a842bbe303 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -1774,10 +1774,12 @@ impl<'a> LinkerContext<'a> { .path_with_pretty_initialized(&source.path, arena) .expect("OOM"); } - // Note: `Path::assert_pretty_is_valid` lives on the - // resolver-side `Path<'a>`; the logger `Path` has no - // such debug hook yet. - debug_assert!(source.path.text.as_ptr() != source.path.pretty.as_ptr()); + // FileMap keys may be relative; when the computed pretty + // equals text, `dupe_alloc` aliases them and that's fine. + debug_assert!( + source.path.text.as_ptr() != source.path.pretty.as_ptr() + || !bun_paths::is_absolute(source.path.text) + ); break 'brk source.path.pretty; } else { diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 16de9421663f..3ae0f92c0c8a 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -2583,7 +2583,13 @@ pub mod bv2_impl { None => return Ok(None), }; - path.assert_file_path_is_absolute(); + // FileMap keys are user-supplied and may be relative; they are lookup + // identities, not real fs paths, so the absolute invariant does not apply. + if bun_core::Environment::CI_ASSERT + && !self.file_map.is_some_and(|fm| fm.contains(path.text)) + { + path.assert_file_path_is_absolute(); + } // borrowck: get-then-put instead of a single get-or-put. if self .path_to_source_index_map(target) diff --git a/test/bundler/bundler_files.test.ts b/test/bundler/bundler_files.test.ts index 81a5d904576b..5fa4eaea4af0 100644 --- a/test/bundler/bundler_files.test.ts +++ b/test/bundler/bundler_files.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; describe("bundler files option", () => { test("basic in-memory file bundling", async () => { @@ -582,4 +582,76 @@ describe("bundler files option", () => { const output = await result.outputs[0].text(); expect(output).toContain("injected by plugin"); }); + + // The debug assertion that file-namespace paths are absolute does not apply + // to FileMap keys, which are user-supplied lookup identities and may be + // relative. Run in a subprocess so a regression (assertion panic in the + // bundle thread) fails this test instead of taking down the runner. + test.each(["./e.js", "e.js", "./src/e.js"])( + "relative key %j as entry point does not trip the absolute-path assertion", + async key => { + const script = ` + const r = await Bun.build({ + entrypoints: [${JSON.stringify(key)}], + files: { ${JSON.stringify(key)}: 'console.log("from relative key")' }, + target: "bun", + throw: false, + }); + if (!r.success) { + for (const l of r.logs) console.error(l.message ?? l); + process.exit(1); + } + const out = await r.outputs[0].text(); + if (!out.includes("from relative key")) { + console.error("missing content:", out); + process.exit(1); + } + console.log("ok"); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + proc.stdout.text(), + proc.stderr.text(), + proc.exited, + ]); + expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({ + stdout: "ok", + stderr: "", + exitCode: 0, + }); + }, + ); + + test("relative key as entry point surfaces parse errors without crashing", async () => { + const script = ` + const r = await Bun.build({ + entrypoints: ["./e.js"], + files: { "./e.js": ")" }, + target: "bun", + throw: false, + }); + console.log(JSON.stringify({ success: r.success, logs: r.logs.length })); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + proc.stdout.text(), + proc.stderr.text(), + proc.exited, + ]); + expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({ + stdout: JSON.stringify({ success: false, logs: 1 }), + stderr: "", + exitCode: 0, + }); + }); }); From a074ce6adf8b752602b143f77dfd5b2d3200778e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:16:14 +0000 Subject: [PATCH 2/4] [autofix.ci] apply automated fixes --- test/bundler/bundler_files.test.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/test/bundler/bundler_files.test.ts b/test/bundler/bundler_files.test.ts index 5fa4eaea4af0..007759e5daad 100644 --- a/test/bundler/bundler_files.test.ts +++ b/test/bundler/bundler_files.test.ts @@ -614,11 +614,7 @@ describe("bundler files option", () => { stdout: "pipe", stderr: "pipe", }); - const [stdout, stderr, exitCode] = await Promise.all([ - proc.stdout.text(), - proc.stderr.text(), - proc.exited, - ]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({ stdout: "ok", stderr: "", @@ -643,11 +639,7 @@ describe("bundler files option", () => { stdout: "pipe", stderr: "pipe", }); - const [stdout, stderr, exitCode] = await Promise.all([ - proc.stdout.text(), - proc.stderr.text(), - proc.exited, - ]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({ stdout: JSON.stringify({ success: false, logs: 1 }), stderr: "", From 89a51df5240303a89e71d1adca795bb271094edf Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:28:25 +0000 Subject: [PATCH 3/4] test: trim comment to 2 lines per review --- test/bundler/bundler_files.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/bundler/bundler_files.test.ts b/test/bundler/bundler_files.test.ts index 007759e5daad..de60c99b528f 100644 --- a/test/bundler/bundler_files.test.ts +++ b/test/bundler/bundler_files.test.ts @@ -583,10 +583,8 @@ describe("bundler files option", () => { expect(output).toContain("injected by plugin"); }); - // The debug assertion that file-namespace paths are absolute does not apply - // to FileMap keys, which are user-supplied lookup identities and may be - // relative. Run in a subprocess so a regression (assertion panic in the - // bundle thread) fails this test instead of taking down the runner. + // FileMap keys are user-supplied identities and may be relative; run in a + // subprocess so an assertion panic fails the test instead of killing the runner. test.each(["./e.js", "e.js", "./src/e.js"])( "relative key %j as entry point does not trip the absolute-path assertion", async key => { From 0791891198ae8d3bf2290c7214107b8e49b1b260 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:57:20 +0000 Subject: [PATCH 4/4] test: make new subprocess tests concurrent per review --- test/bundler/bundler_files.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/bundler/bundler_files.test.ts b/test/bundler/bundler_files.test.ts index de60c99b528f..eea2dd3a65ff 100644 --- a/test/bundler/bundler_files.test.ts +++ b/test/bundler/bundler_files.test.ts @@ -585,7 +585,7 @@ describe("bundler files option", () => { // FileMap keys are user-supplied identities and may be relative; run in a // subprocess so an assertion panic fails the test instead of killing the runner. - test.each(["./e.js", "e.js", "./src/e.js"])( + test.concurrent.each(["./e.js", "e.js", "./src/e.js"])( "relative key %j as entry point does not trip the absolute-path assertion", async key => { const script = ` @@ -621,7 +621,7 @@ describe("bundler files option", () => { }, ); - test("relative key as entry point surfaces parse errors without crashing", async () => { + test.concurrent("relative key as entry point surfaces parse errors without crashing", async () => { const script = ` const r = await Bun.build({ entrypoints: ["./e.js"],