bundler: copy a BuildMessage's namespace out of the bundle arena - #40722
Conversation
Since #40640 a plugin module's namespace lives in the bundle's arena. Location copied it as a bare pointer, so a BuildMessage in result.logs read freed memory when JS read position.namespace after the build. Location.namespace is now a Cow that both clone impls deep-copy, like file and line_text.
|
Warning Review limit reached
On-demand reviews are free for the next 24 days. After that, they cost $0.25 per reviewed file. Or wait 1 minute for your next included review. View limit detailsLimit details: You’ve used all 5 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
Comment |
|
Status: diff ready, waiting on CI. Reproduced on main ( The new test in |
There was a problem hiding this comment.
LGTM — mechanical extension of the existing file/line_text deep-copy pattern to namespace.
What was reviewed:
Location.namespacebecomesCow<'static, [u8]>and is deep-copied in both the manualCloneimpl andclone_with_builder, mirroring howfileandline_textare already handled in the same struct.- Checked that
count(),Default,init, and bothinit_or_nullconstruction sites were all updated consistently; the three consumer sites (BuildMessage.rs,DevErrorPage.rs,css/error.rs) are trivial borrow adjustments. - Test follows harness conventions (tempDir, bunEnv spread, concurrent pipe drain, stderr/stdout asserted before exitCode) and uses
MIMALLOC_PURGE_DELAY=0so the stale-pointer read faults instead of passing by luck.
Extended reasoning...
Overview
This PR fixes a use-after-free in bun_ast::Location.namespace. Previously the field was a Str (&'static [u8]) that could borrow into the bundle's MimallocArena; when a BuildMessage from Bun.build().logs outlives the arena, reading position.namespace would touch freed memory. The fix promotes the field to Cow<'static, [u8]> and deep-copies it in the manual Clone and clone_with_builder impls — exactly the same treatment already applied to file and line_text in the same struct for the same reason. Three call sites (src/css/error.rs, src/jsc/BuildMessage.rs, src/runtime/server/DevErrorPage.rs) are updated to borrow/deref the Cow. A subprocess test in test/bundler/bun-build-api.test.ts reproduces the crash by forcing mimalloc to return arena pages to the OS immediately.
Security risks
None. This is an internal representation change to fix a UAF; no user-facing input parsing, auth, or crypto paths are touched. The only externally observable effect is that log.position.namespace now returns the correct string instead of garbage or crashing.
Level of scrutiny
Moderate — memory safety in native code — but the change is small (~30 lines of Rust), applies an already-established pattern from the neighboring fields in the same struct, and the PR description correctly identifies the mechanism (msg_to_js clones the Msg while the bundle is still alive, so the deep-copy is taken from valid memory). All construction sites (Default, init, init_or_null's two branches, the CSS error builder) were updated to Cow::Borrowed, and both clone paths were updated to Cow::Owned(...to_vec()). The count() helper was updated to match file's treatment. No sites were missed based on a grep of location.namespace usages.
Other factors
The test is well-constructed per the repo's review rules: it uses tempDir, spreads bunEnv, drains stdout/stderr/exited concurrently, asserts output before exit code, uses test.concurrent, and sets MIMALLOC_PURGE_DELAY=0 / MIMALLOC_ABANDONED_PAGE_PURGE=1 so the freed-page read is a hard SEGV rather than a lucky pass on the old bytes. No CODEOWNERS entries cover the changed files. The bug hunt exited with dry_streak and no findings. No prior reviews or outstanding objections in the timeline.
|
Updated 11:40 PM PT - Aug 27th, 2026
❌ @robobun, your commit ba22a15 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 40722That installs a local version of the PR into your bun-40722 --bun |
ast/lib.rs: #40722 makes Location own its namespace on the lines where this branch computes line and column from the tracker counts; main's Cow with this branch's conversions. parse_entry.rs: #40594 tells a parser-generated import record apart by its empty range, which on this branch is range.is_some().
Problem
Bun.build()with a plugin module in a non-filenamespace: if that module has a build error, readinglog.position.namespacefromresult.logsafter the build reads freed memory. WithMIMALLOC_PURGE_DELAY=0it crashes:SEGV in simdutf::validate_asciifromBuildMessage::generate_position_object(src/jsc/BuildMessage.rs:133).MimallocArena(src/resolver/lib.rs, the disjointtext/prettybranch ofdupe_alloc).bun_ast::Locationstoresnamespaceas a bare&'static [u8], and its clone impls copy the pointer. TheBuildMessageholds thatMsgafter the arena is destroyed.Fix
Location.namespacebecomes aCow<'static, [u8]>. Both clone impls (Cloneandclone_with_builder) deep-copy it, the same way they already copyfileandline_text. The four constructors borrow, as before.msg_to_js(src/jsc/lib.rs) clones theMsgwhile the bundle is still alive. The copy is taken from valid memory, and theBuildMessagethen owns its bytes.test/bundler/bun-build-api.test.ts, "a BuildMessage keeps the namespace of a plugin module after the build". On main it crashes with the SEGV above. Also green: the rest ofbun-build-api.test.ts,test/js/bun/plugin/plugins.test.ts,test/bake/dev/html.test.ts.Background
Path::dupe_allocturns a resolver's or a plugin'sPathinto the one the bundle graph stores.textis the absolute path,prettythe display path,namespaceisfileor a plugin namespace. Since dev server: keep out-of-root watched paths alive across bundles #40640 the display path, and the namespace of a plugin module, live in the bundle's arena, which is destroyed when the bundle is done.bun_ast::Locationis the position attached to a log message.Bun.build()results and the dev server keep messages after the bundle is gone, which is whyLocationdoes not deriveCloneand deep-copies instead.prettyis<namespace>:<text>, which never containstext, so every module in a non-filenamespace takes the arena branch.Notes
Split out of #39456, which stops
dupe_allocfrom growing theFilenameStoreon every bundle and makes more of thePatharena-backed. This change is needed on its own since #40640 and is the smaller fix, so it lands first. #39456 is stacked on it.MIMALLOC_PURGE_DELAY=0andMIMALLOC_ABANDONED_PAGE_PURGE=1make mimalloc return the destroyed heap's pages to the OS at once. Without them the stale pointer reads the old bytes and the test passes by luck. ASAN does not see the arena (mimalloc manages its own segments), so the crash is a plain SEGV.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/bun-build-api.test.ts