Fix use-after-free in Bun.Transpiler.transform() error reporting#30309
Fix use-after-free in Bun.Transpiler.transform() error reporting#30309robobun wants to merge 6 commits into
Conversation
TransformTask.run() uses a MimallocArena for parsing that is destroyed when run() returns. Parser/lexer error messages have their text allocated in that arena, so when then() later calls log.toJS() on the JS thread and clones the messages, it reads freed memory. Clone the log messages into bun.default_allocator before the arena is destroyed so they survive until then() runs.
|
Updated 4:58 AM PT - May 6th, 2026
❌ @robobun, your commit ba94fd0 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 30309That installs a local version of the PR into your bun-30309 --bun |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughTransformTask now clones log messages during run and frees cloned location strings during deinit via a new helper to avoid use-after-free; a new concurrent test spawns 20 invalid transpile tasks and asserts each returns a parse-related Error. ChangesTranspiler log message reallocation + concurrent error UAF test
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/transpiler/transpiler-transform-error-uaf.test.ts`:
- Around line 2-23: The test writes a one-off script to "index.js" then calls
Bun.spawnSync with cmd [bunExe(), "run", "index.js"]; replace that by invoking
bunExe() with the -e flag and passing the script body directly (i.e. use
bunExe(), "-e", "<script>") so this single-file subprocess test follows the
guideline; update the Bun.spawnSync cmd array accordingly (and you can remove
the temp file creation via tempDirWithFiles or keep the temp dir for other uses
if needed), referencing bunExe(), Bun.spawnSync, and tempDirWithFiles in the
change.
🪄 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: 9b0e2509-0d7d-4294-b2d0-3e383dfd9c2a
📒 Files selected for processing (1)
test/js/bun/transpiler/transpiler-transform-error-uaf.test.ts
There was a problem hiding this comment.
Both prior concerns are addressed and the clone/free pairing now looks correct to me; leaving the final call to a maintainer since this is cross-thread allocator ownership and there are three competing PRs (#29958/#30180/#30263) taking different approaches to the same UAF.
Extended reasoning...
Overview
This PR fixes a use-after-free in Bun.Transpiler#transform() (async variant) where parser diagnostics allocated in a worker-thread MimallocArena were read on the JS thread after the arena was destroyed. The fix adds a defer in TransformTask.run() that deep-clones each Msg into bun.default_allocator before arena.deinit(), and adds matching cleanup in TransformTask.deinit() (including a freeClonedLocation helper to compensate for Location.deinit being a no-op). A subprocess regression test is added.
Security risks
None. This is internal memory-lifetime management for diagnostic strings; no auth, crypto, or untrusted-input parsing changes beyond what already existed.
Level of scrutiny
Medium-high. The change is small (~20 lines) but touches manual allocator ownership across a thread boundary in runtime code. I verified against src/logger/logger.zig that Msg.clone deep-clones data.text, location.file, location.line_text, and recursively each note via bun.clone, and that the new deinit path frees exactly those allocations (freeClonedLocation for location strings since Location.deinit is a no-op at logger.zig:149, then msg.deinit for data.text, note texts, and the notes slice). Zero-length text/file slices are safe to free. Defer ordering is correct (clone runs before arena.deinit()).
Other factors
Both issues I raised earlier (the introduced leak, and the explicit test timeout) have been addressed. The remaining reason I'm not auto-approving is not a correctness concern but a merge-strategy one: github-actions already flagged three other open PRs fixing the same bug with different mechanisms (separate arena + StringBuilder, log.cloneToWithRecycled, and a near-identical defer-clone). A maintainer should decide which approach to land — and whether the freeClonedLocation workaround here is preferable to fixing Location.deinit itself. CI failures on this PR (Windows http/hot/jsc-stress) are unrelated to the change.
What does this PR do?
Fixes a use-after-free when
Bun.Transpiler#transform()(the async variant) rejects with parse errors.TransformTask.run()runs on a worker thread and uses aMimallocArenafor parsing. Any parser/lexer diagnostics added tothis.loghave their text (msg.data.text, notes) allocated from that arena. The arena is destroyed at the end ofrun(), but the log messages are read later on the JS thread inthen()viathis.log.toJS()→BuildMessage.create()→Msg.clone()→allocator.dupe(u8, this.text), which reads freed arena memory.Setting
this.log.msgs.allocator = bun.default_allocatoronly keeps theMsgarray alive; the text inside each message still lives in the arena.The fix clones each log message into
bun.default_allocatorin adeferthat runs beforearena.deinit(), so the message contents survive untilthen()runs.How did you verify your code works?
Added a regression test that runs 20 concurrent
transform()calls on source with many parse errors. Under ASAN without the fix this reliably triggersuse-after-poisonat the same PC as the fuzzer report; with the fix it passes cleanly and the error messages/positions are intact.All existing
test/js/bun/transpiler/andtest/bundler/transpiler/transpiler.test.jstests continue to pass.