Repository navigation
Conversation
TranspilerStateGuard held a raw *mut Transpiler and a raw *mut Log, dereferenced the former through an unsafe accessor in Drop, and each of scan, transformSync and scanImports open-coded the same swap-in block with its own unsafe detach_lifetime_ref and a comment arguing that the guard was declared after the arena. The guard now holds &'a JSTranspiler plus a PhantomData<&'a Arena> borrow, restores through the safe JsCell::with_mut closure, and gets its macro_context policy from a two-variant MacroRestore enum instead of Option<Option<MacroContext>>. A single TranspilerStateGuard::enter performs the one remaining detach and the swap-in, so the three call sites shrink to one line each and dropck, rather than a comment, guarantees the guard is dropped before the arena it installed; the Log stays a raw pointer because scanImports keeps using the local while the guard is live. Net three unsafe blocks removed. No runtime change: the guard is a stack-only value that went from 48 to 40 bytes, MacroRestore has the same 24-byte layout as the nested Option it replaces, with_mut is the same UnsafeCell projection the raw dereference performed, and the swap-in and restore code is unchanged apart from no longer being triplicated.
|
Warning Review limit reached
Next review available in: 15 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
Comment |
There was a problem hiding this comment.
LGTM — mechanical refactor that consolidates three open-coded swap-in blocks into TranspilerStateGuard::enter and drops raw pointers for a borrowed owner.
What was reviewed:
- Traced
Dropbefore/after:owner.config_log_ptr()yields the same pointer each caller previously stored inrestore_log, andwith_mutonowner.transpilerperforms the sameUnsafeCellprojection as the old raw-pointer deref. - Checked
transform_sync's split ofmacro_context.take()into a separatewith_mutbeforeenter— no observable intermediate state, same net effect. - Verified declaration order at all three call sites still drops
_restorebeforelog/arena; the new'aborrow ofarenanow makes dropck enforce the arena half. MacroRestore::{Leave,Restore}maps 1:1 onto the oldNone/Some(prev)arms; thedeinit-before-restore of the fresh context is preserved verbatim.
Extended reasoning...
Overview
Single-file refactor of src/runtime/api/JSTranspiler.rs (+76/-105). TranspilerStateGuard — the RAII guard that swaps a per-call Arena/Log into the long-lived Transpiler for scan, transformSync, and scanImports — is reworked from two raw pointers (*mut Transpiler<'static>, *mut Log) plus Option<Option<MacroContext>> to a lifetime-parameterized struct that borrows &'a JSTranspiler and phantom-borrows &'a Arena, with a named MacroRestore enum. A new enter() constructor absorbs the swap-in logic that was triplicated at each call site; Drop restores via owner.transpiler.with_mut(..) and owner.config_log_ptr() instead of dereferencing stored raw pointers. Net -3 unsafe blocks.
Security risks
None. This is internal RAII plumbing for Bun.Transpiler; no input parsing, auth, crypto, or externally-reachable surface changes. The unsafe surface strictly shrinks.
Level of scrutiny
Moderate — it touches lifetime-laundering and Drop ordering around a stack Arena, which is memory-safety-relevant. But the change is behavior-preserving by construction: every statement in the old Drop and swap-in bodies reappears in the new enter/Drop, just routed through &'a JSTranspiler + JsCell::with_mut instead of a stored *mut. I traced each of the three call sites against the diff:
scan/scan_imports: oldprev_macro_context: None→MacroRestore::Leave;restore_log: self.config_log_ptr()at construction →self.owner.config_log_ptr()at drop (same field projection, same value). Declaration order (arena,log,_restore) unchanged, so reverse-drop still restores before the locals free — and now the'aborrow ofarenainentermakes dropck reject a reordering.transform_sync: previously didt.macro_context.take()inside the samewith_mutclosure as the arena/log swap; now it's a separatewith_mutimmediately beforeenter. Two closure-scoped projections back-to-back with no JS re-entry between them — identical resulting state._ast_scopewas already declared before_restorehere, unchanged.
The one thing dropck does not enforce (the log local, passed as *mut because scan_imports needs &mut log while the guard is live) is explicitly documented on the struct, and declaration order at all three sites is correct.
Other factors
The PR description reports the existing transpiler suite (191 pass across transpiler.test.js and the two UAF regression tests) passes on the debug build. No behavioral change means no new test is required per REVIEW.md. No CODEOWNERS entry covers this path. No prior human reviewer comments on the timeline. The change follows the repo's stated preferences: dedup within the diff, named enum over nested Option, fewer unsafe blocks with a SAFETY comment on the one that remains.
What
TranspilerStateGuardinsrc/runtime/api/JSTranspiler.rsis the RAII guard thatBun.Transpiler'sscan,transformSyncandscanImportsuse to point the long-livedTranspilerat a per-call stackArena/Logand put the resting state back on every exit. It held two raw pointers into the owningJSTranspiler(transpiler: *mut Transpiler<'static>andrestore_log: *mut Log), dereferenced the first through anunsafeaccessor inDrop, and encoded "restoremacro_contextor not" asOption<Option<MacroContext>>. Each of the three host functions also open-coded the same swap-in block: its ownunsafe { detach_lifetime_ref(&arena) }, its ownwith_mutswap, a struct literal building the guard, and a comment arguing that the guard is declared after the arena and therefore drops first.enterperforms the single remainingdetach_lifetime_refand the swap-in;Droprestores through the safeJsCell::with_mutclosure onowner.transpilerand matches onMacroRestore. The 3 call sites each collapse from 17 to 19 lines to oneTranspilerStateGuard::enter(self, &arena, &raw mut log, MacroRestore::..)line (transformSyncstill takes the previousmacro_contextitself and passes it asMacroRestore::Restore(prev)). The guard'stranspiler_mutandrestore_log_ptrhelpers and both raw fields are deleted;JSTranspiler::config_log_ptrkeeps its one remaining caller inDrop. Removes 4unsafeblocks and adds 1 (net 3 fewer); one file, +76/-105 lines.Why
The guard's type now answers what it restores and on whose behalf: it borrows the
JSTranspilerit will write back to and the arena it installed, so the requirement that previously lived in three comments (the guard must die before the arena it points the transpiler at) is enforced by dropck, and the two possible macro-context outcomes are the two named variants ofMacroRestoreinstead of a nestedOption. TheLogis deliberately not borrowed, becausescanImportspasses&mut logto the scanner while the guard is live; that one remaining ordering requirement is stated on the struct. It is zero-cost: the guard is a stack-only value (48 bytes before, 40 after, sincePhantomDatais zero-sized andMacroRestorehas the same 24-byte layout asOption<Option<MacroContext>>),with_mutis#[inline(always)]and performs the sameUnsafeCellprojection the raw dereference did, and the swap-in and restore bodies are the same statements as before, executed once per call instead of being triplicated in the source.Part of a series of small type-system hardening changes; each PR stands alone.
Verification
cargo checkandcargo clippyare clean for the touched crates. Debug build succeeds. bun bd test test/bundler/transpiler/transpiler.test.js test/js/bun/transpiler/transpiler-tsconfig-uaf.test.ts test/js/bun/transpiler/transpiler-error-gc-uaf.test.ts: 191 pass, 1 skip, 21 todo, 0 fail (213 tests across 3 files).