Skip to content

bundler: fix a segfault in bun build --sourcemap when the link step fails - #44502

Merged
dylan-conway merged 3 commits into
mainfrom
claude/bundler-build-error-segfault-dc780d
Oct 3, 2026
Merged

dylan-conway merged 3 commits into
mainfrom
claude/bundler-build-error-segfault-dc780d

Conversation

@dylan-conway

@dylan-conway dylan-conway commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

bun build with source maps on sometimes crashes when the build fails in the link step. It should print the error and exit with code 1.

// a.js
import { b } from "./b.js";
console.log(b, require("./b.js"));

// b.js
export const b = await 0;
$ bun build --sourcemap=inline a.js
panic(main thread): Segmentation fault at address 0x18

Expected, and what the other runs print:

error: This require call is not allowed because the transitive dependency "b.js" contains a top-level await

The crash is not tied to top-level await. import { nope } from "./b.js" (No matching export) crashes the same way. It needs --sourcemap; without it there were 0 crashes in 40 runs.

Cause

  1. LinkerContext::link calls compute_data_for_source_map, which puts two tasks per reachable file on the worker pool. The only waits for them are in generate_chunks_in_parallel.
  2. Every ? and return Err in link after that point returns with the tasks still on the pool. The quickest is return Err(ImportResolutionFailed) in step 4 of scan_imports_and_exports. It comes before the blocking worker_pool.each of step 5, which otherwise gives the tasks time to start.
  3. generate_from_cli then calls deinit_without_freeing_arena, which walks workers_assignments and reads Worker.thread of every entry.
  4. At that moment a pool thread that runs its first task of the build is in SourceMapDataTask::run_* → Worker::get → get_worker_slow. It has inserted its Box::<Worker>::new_uninit() pointer into the map and has not reached worker.write(...).

So the main thread reads a Worker that was never written:

  • In a release build the fresh memory is zero. thread reads as None, Worker::deinit runs on the main thread, and the drop glue of Option<WorkerData> follows a null Box<Define>. That is the fault at 0x18.
  • In a debug + ASAN build the memory is 0xbe…be. thread reads as Some(0xbebebebebebebebe) and Queue::push faults at 0xbebebebebebebee6.

A debugger stopped at the ASAN fault shows the main thread in deinit_soon and pool threads at ThreadPool.rs:448 and :451, inside the Worker { .. } literal of get_worker_slow, under run_line_offset and run_quoted_source_contents.

Bun.build does not crash because its caller waits on the two groups on its error path. generate_from_cli and generate_from_bake_production_cli run the same teardown without that wait.

Fix

deinit_without_freeing_arena frees what the tasks use, so it now waits for them, before anything else. The first thing it did was to run the finalizers of native plugins, which free source text that these tasks read. The two waits in Bun.build's caller are now redundant and are removed. That leaves the two arms of its match the same, so they are folded into one call. source_maps and the two groups have no user outside the crate any more and become pub(crate). Waiting on a group that is already at zero takes a lock and returns.

The fixing lines are the two wait() calls in src/bundler/bundle_v2.rs.

Which build errors this covers

  • Errors from before the link step (resolve errors, syntax errors) never schedule these tasks. They did not crash before (0 of 10 each on the unfixed ASAN build).
  • Errors from the link step all leave through the same teardown, so the one wait covers them.
  • Two Ok returns also skip the waits: the dependency scanner branch of generate_from_cli, and chunks.is_empty() in generate_from_bake_production_cli. They are covered too.

Not changed

enqueue_entry_points_*(...)? in generate_from_cli, generate_from_bake_production_cli and run_from_js_in_new_thread returns before wait_for_parse, so an error there would reach the teardown with parse tasks on the pool. Every error I traced on that path is an allocation failure. I found no input that reaches it, so there is no test to write. Waiting there is not a drop-in fix either: enqueue_entry_item increments pending_items before its two fallible steps, so after such a failure wait_for_parse would never return. It is left as it is, and the SAFETY comment in get_worker_slow names the exception.

How did you verify your code works?

New test default/TopLevelAwaitForbiddenRequireSourceMapCLI in test/bundler/esbuild/default.test.ts, next to the existing test for this error. That test uses the Bun.build backend without source maps, which is why it never saw the crash. The new test runs the CLI build 40 times, four at a time, and expects the error line and exit code 1 from each. It checks the error line because an ASAN crash also exits with code 1.

binary runs of the test result
release, this commit's parent (272ff43) 40 40 fail
release, this PR 40 40 pass
debug + ASAN, parent 10 10 fail (time out: each ASAN report is slow)
debug + ASAN, this PR (bun bd test) 5 5 pass, 0.91 s to 0.96 s each
Bun 1.4.2 (USE_SYSTEM_BUN=1) 40 40 fail

The whole file run against the parent release build five times: 152 pass and this test fails, each time. With bun bd test and both new tests: 154 pass, 0 fail.

default/TopLevelAwaitForbiddenRequireSourceMapAPI covers Bun.build, which now relies on the shared wait: 40 builds of the same two files in one subprocess. It passes on main, where the caller has its own wait, so its "before" is this branch with the two shared wait() calls removed. There ASAN reports a heap-use-after-free in compute_quoted_source_contents, which reads the freed linker graph.

binary runs of the test result
release, shared waits removed 40 40 fail
debug + ASAN, shared waits removed 10 10 fail
release, this PR 40 40 pass
debug + ASAN, this PR 10 10 pass

Crashes of 40 runs. The two release builds are of the same commit with the same toolchain and differ only by the fix:

build before after
the two files above, --sourcemap=inline 12 0
No matching export, two files, --sourcemap=inline 10 0
ten-file graph the crash was found with, --target=node --sourcemap=inline 11 0
the same with --splitting 12 0

Crashes of 10 runs on the debug + ASAN builds, all with No matching export:

build before after
through export * 9 0
--splitting --sourcemap=external 10 0
--compile --sourcemap=inline 9 0
--minify --target=bun --sourcemap=linked 10 0

Released versions, crashes of 40 runs of the ten-file graph: 1.3.0: 0, 1.4.0: 15, 1.4.2: 10.

Bun.build, which lost its own wait: 540 failing builds with source maps in three processes, six at once, under ASAN. Before and after, every build rejects with the expected message and there is no report.

bun build --app with a link error, the other caller without the wait: 15 crashes of 40 runs before and 0 of 50 after on release, 10 of 10 before and 0 of 30 after under ASAN.

An independent A/B of the two release builds and the two ASAN builds:

  • Failing builds: 841 combinations of fixture and options. The fixed release build ran them 21,025 times with no crash, no hang, and the same stderr, exit code and output directory as the runs of the unfixed build that did not crash.
  • Successful builds: 433 builds with source maps on, output directories byte-identical.
  • Hangs: no timeout on one CPU (taskset -c 0), under nice -n 19, under CPU load, or with a graph of 10,000 files.
  • Cost: the two-file failing build has a median 0.13 ms to 0.32 ms higher, where the same binary against itself differed by 0.01 ms to 0.15 ms. Builds of 1,000 and 10,000 files, failing or not, are within that noise.

@robobun

robobun commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 8:22 PM PT - Oct 2nd, 2026

@dylan-conway, your commit 8f4e351 is building: #123119

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Essentials
  • Run ID: 818a0e20-7d0c-4803-a994-c2f1f26ffbf1
📥 Commits

Reviewing files that changed from the base of the PR and between eb5a317 and 8f4e351.

📒 Files selected for processing (2)
  • src/bundler/ThreadPool.rs
  • test/bundler/esbuild/default.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.


Walkthrough

Bundler teardown now waits for source-map line-offset and quoted-content tasks before running finalizers. Completion-task cleanup deinitializes the bundle after successful and failed builds. Regression tests check repeated CLI and Bun.build runs for the transitive top-level-await require error.

Changes

Source-map task lifecycle

Layer / File(s) Summary
Teardown synchronization and regression test
src/bundler/LinkerContext.rs, src/bundler/ThreadPool.rs, src/bundler/bundle_v2.rs, src/runtime/api/js_bundle_completion_task.rs, src/bundler/BundleThread.rs, test/bundler/esbuild/default.test.ts
Source-map wait-group fields are crate-visible. deinit_without_freeing_arena waits for both task groups before finalizers. init_and_run deinitializes bv2 after success or error. Comments describe teardown ordering. Regression tests run 40 CLI builds and 40 Bun.build API builds and check the transitive top-level-await require error.

Possibly related PRs

  • oven-sh/bun#39855: Both changes move source-map task waits into teardown and remove caller-side waits.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 8f4e3

This change adds regression tests and a clarifying comment for the source-map teardown fix. No actionable merge-blocking risk was found.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: fixing a segfault in bun build --sourcemap when linking fails.
Description check ✅ Passed The description includes both required template sections. It explains the cause and fix, identifies an unaddressed allocation-failure edge case, and provides detailed verification results.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked the --watch branch of generate_from_cli (it leaks the Box<BundleV2> instead of tearing down, so in-flight source-map tasks keep a live graph and the missing wait there is not a use-after-free), the folded Ok/Err match in js_bundle_completion_task.rs (set_result still runs before deinit_without_freeing_arena on success, and the error path now waits via the shared teardown instead of locally), and the cost of the unconditional waits (WaitGroup::wait on the default zero count is one lock, one atomic load and one unlock, so builds without source maps do not block).

Extended reasoning...

The change moves the two source-map WaitGroup waits into the shared BundleV2 teardown so every caller (CLI, bake, DevServer, bun test --changed, Bun.build) joins the pool tasks before finalizers and worker teardown, deletes the JS API's now-redundant local waits, tightens three fields to pub(crate), and adds a 40-run CLI stress test. It touches no security-sensitive surface. The inline findings (a SAFETY comment overstating the wait_for_parse invariant, and no Bun.build sibling coverage) already signal a human look; the remaining candidates were ruled out from the code as noted above.

Comment thread src/bundler/ThreadPool.rs Outdated
Comment thread test/bundler/esbuild/default.test.ts
…eue error exception in the Worker SAFETY comment

@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.

@dylan-conway
dylan-conway merged commit 8f6a13a into main Oct 3, 2026
9 of 10 checks passed
@dylan-conway
dylan-conway deleted the claude/bundler-build-error-segfault-dc780d branch October 3, 2026 03:40
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