Conversation
get_latest_version took a const SILENT generic plus Option<&mut Progress> and Option<&mut Node>; its only caller instantiated it with SILENT = false and both Some, so its five error paths each unwrapped both Options at runtime (ten expect calls) and the progress_node assignment unwrapped the Node once more. To produce the two references the caller leaked a Box<Progress> and kept two raw pointers into it, and the download block in _exec repeated the same dance, plus an expect on NonNull::new of the leaked pointer. The function now takes &mut Progress and reaches the node through progress.root; its one caller and the download block in _exec each own a plain stack Progress. Its return type becomes Result<Version> because, with the SILENT branches gone, every path that does not return a version exits the process, so the Ok(None) arm and the caller's else branch were unreachable. This removes ten unsafe blocks, two intentionally leaked heap allocations, eleven expect() calls and one unwrap(). The HTTP client still receives the same NonNull<Node>, Progress has no Drop impl, and the sequence of start/end/refresh calls is unchanged, so there is no runtime difference beyond the two allocations no longer happening.
|
Warning Review limit reached
Next review available in: 6 minutes 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 (2)
Comment |
|
Updated 4:59 PM PT - Aug 11th, 2026
✅ @robobun, your commit 6e71b61600c08ccfbba31fa53f7aa7d38a0c0948 passed in 🧪 To try this PR locally: bunx bun-pr 37674That installs a local version of the PR into your bun-37674 --bun |
There was a problem hiding this comment.
LGTM — mechanical ownership refactor that swaps leaked Box<Progress> + raw pointers for a stack local, matching the pattern already used in StandaloneModuleGraph.rs.
What was reviewed:
- Confirmed
get_latest_versionhas a single caller withSILENT=false, so the removedOptionreturn,Ok(None)arms, and const generic were all dead. - Confirmed
Progress::startwrites intopub rootand there is noDropforProgress/Node, so the stack local +progress.root.end()shape is equivalent. - Checked
send_syncblocks until the HTTP callback completes, soNonNull::from(&mut progress.root)into a stack frame is sound (same as the existingdownload_to_pathusage).
Extended reasoning...
Overview
Refactors UpgradeCommand::get_latest_version and the download block in _exec (src/runtime/cli/upgrade_command.rs) to own Progress as a plain stack local instead of a leaked Box with two raw pointers. Drops the SILENT const generic and the Option<Version> return, since the only instantiation was ::<false> and every non-version path already ended in Global::exit. The test change is a one-word comment update in bun-upgrade.test.ts removing a now-stale reference to the progress object being leaked.
Security risks
None. This is the bun upgrade CLI path; the change is purely internal ownership plumbing with no effect on the download URL, digest verification, TLS settings, or archive extraction logic.
Level of scrutiny
Low-to-medium. The interesting question is whether moving Progress from a leaked heap allocation to the stack changes the lifetime seen by async_http.client.progress_node (a raw NonNull<Node>). I traced send_sync in src/http/AsyncHTTP.rs: it schedules the request and blocks on read_item() until the callback fires, so the HTTP thread's accesses to progress_node are strictly bracketed by send_sync. The stack progress outlives that call in every path (including the ? early returns, where it lives in the caller's frame). This is the exact shape StandaloneModuleGraph::download_to_path and several src/install/ sites already use with a stack refresher and refresher.root.end().
Other factors
- Verified via grep that
get_latest_versionhas no other callers, so removing theSILENTgeneric andOptionwrapper cannot break anything. ProgressandNodehave noDropimpl, so the switch from leaked-forever to stack-dropped is a no-op at teardown.- Net removal of 10
unsafeblocks, 11expects, 1unwrap, 2 leaked allocations, and one impossible return state — a strict simplification with no new control flow. - CI build #92597 is green and
bun-upgrade.test.ts(8 tests) passes per the PR description. - No CODEOWNERS entry covers this file.
Problem
bun upgradeleaks its two progress bars on purpose and drives them through raw pointers, 10unsafeblocks in all.&mutparameters that borrow the same object, so the caller could only satisfy it by leaking the object.SILENTmode and anOption<Version>return that nothing uses: the one caller is non-silent, and every non-silent path without a version exits the process, soOk(None)was unreachable and theOptionparameters were unwrapped 11 times.Fix
&mut Progressand reaches the node through it; both call sites keep the progress bar as a stack local, so the leaks, raw pointers andunsafego away.Progresshas noDrop, so the only runtime change is two allocations that no longer happen.SILENTparameter, theOptionreturn and the caller's deadelsebranch are deleted; every remaining path returns a version or exits. A test comment that called the progress object leaked is updated.cargo checkandcargo clippyare clean.Background
Progressis bun's terminal progress bar.start()fills its publicrootnode,refresh()redraws,root.end()finishes it. Sincerootis public, holding theProgressis enough to reach the node.NonNull<Node>and updates it from the HTTP thread whilesend_syncblocks; its soundness rests on the node outliving that call, before and after this change.Global::exitends the process, so anyreturnafter it is dead code; that is what made theOptionreturn unreachable.Original description
What
UpgradeCommand::get_latest_versionwas declared asbut its only instantiation is
get_latest_version::<false>with both optionsSome. Of its six!SILENTblocks, five are error paths that each didprogress.expect(..).end(); refresher.expect(..).refresh();(10expectcalls standing in for a type invariant) and the sixth setprogress_nodefromprogress.as_deref_mut().unwrap(). To produce both references at once the caller leaked aBox<Progress>viaheap::into_rawand kept two raw pointers into it, and the download block in_execrepeated the same pattern, plus anexpectonNonNull::newof the leaked pointer (10unsafeblocks between the two sites).Progress::startwrites the node into thepub rootfield, so a single&mut Progressreaches both objects. The function now takesand the error paths are
progress.root.end(); progress.refresh();. Its single caller and the download block in_execeach own a plain stackProgress:The return type loses its
Option: with theSILENTbranches gone, every path that does not return a version ends inGlobal::exit, so theOk(None)returns and the caller'selse { return Ok(()) }were unreachable and are deleted. Net: removes 10unsafeblocks, 2 leaked heap allocations, 11expectcalls and 1unwrap, one const generic, and one impossible return state; 1 Rust file plus a one-word update to a comment inbun-upgrade.test.tsthat described the progress object as intentionally leaked. This is the same shapestandalone_graph/StandaloneModuleGraph.rs::download_to_pathalready uses (refresher.root.end(),refresher.refresh()). No dependency changes.Why
Ownership of the progress bar is now visible in the types: it is a local of
_exec, borrowed for the duration ofget_latest_version, and the "no progress bar" state theOptionparameters andOk(None)return allowed is no longer representable. It is zero-cost:Progresshas noDropimpl, the HTTP client receives the sameNonNull<Node>as before, the start/end/refresh sequence is unchanged, and the only runtime difference is that two allocations that were never freed no longer happen.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 on upgrade-stack-progress; bun bd test test/cli/install/bun-upgrade.test.ts: 8 pass, 0 fail.