Conversation
FileSink.rs carried a private FileSinkRef(*mut FileSink) guard whose new_ref, adopt and Drop re-implemented bun_ptr::ScopedRef for a single type. FileSink already derives CellRefCounted, which emits the AnyRefCounted bridge that ScopedRef requires, so the nine guard sites now call ScopedRef::new and ScopedRef::adopt directly and the local type is deleted. assign_to_stream also stops casting the AnyPromise back to a raw JSPromise and calls AnyPromise::status and AnyPromise::result, which the enum provides; the comment claiming otherwise was stale. ScopedRef is a NonNull<FileSink>, the same size as the raw pointer it replaces, its constructor is the same Cell<u32> increment and its Drop is the same CellRefCounted::deref that FileSink::deref forwards to, and AnyPromise::status/result reach the same JSC__JSPromise__status/__result calls (JSInternalPromise is an alias of JSPromise in bun_jsc). This removes four unsafe blocks and two unsafe fns from FileSink.rs.
|
Warning Review limit reached
Next review available in: 9 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 (1)
Comment |
|
Updated 6:27 PM PT - Aug 11th, 2026
✅ @robobun, your commit 214c065ef27d664b1245ff3d8d3e3daf75b753ae passed in 🧪 To try this PR locally: bunx bun-pr 37695That installs a local version of the PR into your bun-37695 --bun |
There was a problem hiding this comment.
LGTM — mechanical swap of a file-local ref guard for the shared bun_ptr::ScopedRef, plus removal of a stale AnyPromise workaround.
What was reviewed:
- Verified
ScopedRef::new/adopt/Dropinsrc/ptr/ref_count.rsare semantically identical to the deletedFileSinkRef(sameCell<u32>bump viarc_ref, sameCellRefCounted::derefon drop — the exact path the inherentFileSink::derefforwards to). - Confirmed the
#[derive(CellRefCounted)]onFileSinkemits theAnyRefCountedbridge withDestructorCtx = (), satisfyingScopedRef's bound. - Confirmed
AnyPromise::status()/result()dispatch to the sameJSC__JSPromise__status/__resultFFI calls the removed raw-pointer cast reached; therelease_unrunshape matchesFileResponseStream's.
Extended reasoning...
Overview
Single-file refactor in src/runtime/webcore/FileSink.rs (+12/-61). Two independent simplifications:
- Deletes the private
FileSinkRefRAII guard (a file-local reimplementation ofbun_ptr::ScopedRef) and rewrites its 9 call sites to useScopedRefdirectly — 5new_ref→ScopedRef::new, 4adopt→ScopedRef::adopt. - In
assign_to_stream, replaces a manualmatchthat recovered*mut JSPromisefromAnyPromise(plus twounsafederefs) with direct calls toAnyPromise::status()/result(), removing a stale comment claiming those methods didn't exist.
Security risks
None. No new inputs, no new I/O, no auth/crypto/permissions surface. The change reduces unsafe surface (drops 4 unsafe blocks and 2 unsafe fns) without adding any.
Level of scrutiny
This is refcount-lifetime code on a re-entrant callback path, so I traced the full equivalence rather than trusting the shape:
ScopedRef::new(ptr)→T::rc_ref(ptr)→ (per theCellRefCountedderive insrc/bun_core_macros/lib.rs) increments theCell<u32>field. The oldFileSinkRef::new_refcalled(*this).ref_(), the inherent forwarder to the same trait method.ScopedRef::adopt(ptr)wraps without bumping — identical to oldFileSinkRef::adopt.ScopedRef::Drop→T::rc_deref→rc_deref_with_context→<Self as CellRefCounted>::deref(this). The oldFileSinkRef::DropcalledFileSink::deref(self.0), which is the derive-emitted inherent forwarder to that sameCellRefCounted::deref. The destroy hook (Self::deinit) is wired via#[ref_count(destroy = Self::deinit)]on the derive, unchanged.ScopedRefstoresNonNull<T>(vianew_unchecked) vs. the old*mut FileSink; every call site passes the canonical heap pointer orptr::from_mut(self), so non-null holds.AnyPromise::status()/result()(checked insrc/jsc/AnyPromise.rs) macro-dispatch toJSPromise::status()/result(), which callJSC__JSPromise__status/__result— the same FFI the removed cast reached. The Internal-variant cast is now handled inside the safe wrapper.
Every substitution is provably behavior-preserving; there is no judgment call on ordering, ref balance, or error paths.
Other factors
- The
drop(ScopedRef::<FileSink>::adopt(sink))shape inrelease_unrunmatches the existing sibling inFileResponseStream::release_unrun(src/runtime/server/FileResponseStream.rs:667), and theAnyPromiseusage matches siblingassign_to_streamcallers cited in the description. - PR reports
filesink.test.ts,spawn-stdin-readable-stream.test.ts, andspawn-streaming-stdin.test.tsall pass (92 tests) on the debug build. - No prior reviewer comments to address; no CODEOWNERS gating this path.
- Aligns with the repo's "grep for the in-tree helper before hand-writing anything" and "delete dead code in the same PR" review rules.
|
Closing. #40478 removed The second change, |
Problem
FileSink.rshas a private ref guard,FileSinkRef, that is a one-type copy ofbun_ptr::ScopedRef, whichFileSinkalready qualifies for.assign_to_streamcasts itsAnyPromiseto a raw*mut JSPromiseand reads it in twounsafeblocks, under a comment sayingAnyPromiselacksstatus()/result(). It has both; the cast and comment are stale.Fix
FileSinkRef; its 9 sites useScopedRefin the same mode as before (5new, which takes a ref; 4adopt, which takes over one).assign_to_streamcallspromise.status()andpromise.result(vm), as the otherassign_to_streamcallers already do.Cell<u32>and drop through the sameCellRefCounted::deref; the promise accessors reach the same JSC calls the cast did.cargo check/clippyclean, debug build passes, existing filesink and spawn stdin tests pass (92). Removes 4unsafeblocks and 2unsafe fns, adds none.Background
FileSinkis the native sink behind file and pipe writers. It is intrusively refcounted: the count lives in the struct (bun_ptr::CellRefCountedderive) and it is freed at zero. JS holds raw pointers into it, so it is not anRc.on_write,run_pending, promise reactions) re-enter JS, which can drop the last outside ref mid-function, so each holds a local ref until it returns.bun_ptr::ScopedRef<T>is the shared guard for that:new(p)takes a ref,unsafe adopt(p)takes over a ref taken earlier (when a task or promise reaction was queued) without bumping the count, and drop releases one ref.bun_jsc::AnyPromisewraps aJSPromiseorJSInternalPromise(an alias ofJSPromiseinbun_jsc) and exposesstatus()/result(vm)for both.Original description
What
src/runtime/webcore/FileSink.rsdefined a privatestruct FileSinkRef(*mut FileSink)withunsafe fn new_ref(bumps the count),unsafe fn adopt(does not) and aDropthat callsFileSink::deref. That is a one-type copy ofbun_ptr::ScopedRef, andFileSinkalready derivesbun_ptr::CellRefCounted, which emits theAnyRefCountedbridgeScopedRefrequires. The local type is deleted and its 9 call sites use the shared guard: 5new_refsites becomeScopedRef::new, 4adoptsites becomeScopedRef::adopt(theFlushPendingTask::release_unrunsite becomesdrop(ScopedRef::<FileSink>::adopt(sink)), the same shape asFileResponseStream'sTaskable::release_unrun).FileSink::assign_to_streamalso matched on theAnyPromiseto recover a raw*mut JSPromiseand dereferenced it in twounsafeblocks, under a comment sayingAnyPromisehad nostatus()/result(). It has both, so the function now callspromise.status()andpromise.result(vm)like the siblingassign_to_streamcallers inBlob.rs,s3/client.rs,FetchTasklet.rsandhtml_rewriter.rs, and the cast block and stale comment are gone.Net effect in
FileSink.rs: removes 4unsafeblocks and 2unsafe fns, no newunsafe. One file, +12/-61 lines.Why
The ref/deref bracket around FileSink's re-entrant callbacks (
on_write,run_pending,on_auto_flush,on_attached_process_exit, the promise reactions and the flush task) is now expressed with the sameScopedReftype used by the other intrusively refcounted types in the runtime, so a reader sees one guard with one documentednew/adoptcontract instead of a file-local variant. It is zero-cost:ScopedRef<FileSink>is aNonNull<FileSink>, the same size as the*mut FileSinkit replaces; the derive'src_refis the sameCell<u32>incrementnew_refperformed, andScopedRef'sDropcalls the sameCellRefCounted::derefthatFileSink::derefforwards to, all#[inline].AnyPromise::status()/result()reach the sameJSC__JSPromise__status/__resultcalls the removed cast did (JSInternalPromiseis an alias ofJSPromiseinbun_jsc); the only difference is the opaque handle's non-null assertion, whichas_any_promisehas already established for both variants, and it is the shape the four sibling sites already use.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/js/bun/util/filesink.test.ts test/js/bun/spawn/spawn-stdin-readable-stream.test.ts test/js/bun/spawn/spawn-streaming-stdin.test.ts: 92 pass, 0 fail (92 tests across 3 files).