Conversation
|
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:41 PM PT - Aug 11th, 2026
✅ @robobun, your commit b864f7638cdc9ed1e1054bd5361b0f6eec7885ec passed in 🧪 To try this PR locally: bunx bun-pr 37549That installs a local version of the PR into your bun-37549 --bun |
There was a problem hiding this comment.
LGTM — mechanical swap from a hand-rolled SignalRef Drop guard + raw *mut AbortSignal fields to the existing AbortSignalRef (ExternalShared<AbortSignal>) already used by Request, Response::BodyAbortListener, and node_fs.
What was reviewed:
ref_from_jsisfrom_js+ref_()+adopt, andreq.signal.get().clone()onJsCell<Option<AbortSignalRef>>bumps viaExternalShared::clone→ same +1 taken at the same three extraction sites.clear_abort_signalorder preserved (clean_native_bindings→pending_activity_unref→ drop =unref);.take()leavesNoneso field drop-glue is a no-op, no double-unref.bun_opaqueandNonNullimports remain live in both files; no dead code introduced.
Extended reasoning...
Overview
This PR replaces the private SignalRef(Option<NonNull<AbortSignal>>) Drop guard in src/runtime/webcore/fetch.rs and the raw Option<*mut AbortSignal> fields on FetchOptions/FetchTasklet with Option<AbortSignalRef>, the ExternalShared<AbortSignal> type alias that already models this exact C++ intrusive-refcounted object throughout the codebase (Request.signal, Response::BodyAbortListener, node_fs, node_fs_watcher). Three opaque_deref conversions and the local guard type are deleted; the accessor becomes as_deref().
Refcount equivalence
I traced every changed site against the underlying FFI calls. AbortSignal::ref_from_js (src/jsc/AbortSignal.rs:249) is exactly from_js + ref_() + ExternalShared::adopt, matching the old from_js → opaque_deref → ref_() → NonNull::new. req.signal.get() returns &Option<AbortSignalRef> (JsCell::get at src/jsc/JSCell.rs:104), and .clone() routes through ExternalShared::clone → ext_ref → WebCore__AbortSignal__ref, matching the old signal_.ref_(). ExternalShared::Drop calls ext_deref → WebCore__AbortSignal__unref, matching what SignalRef::drop did via BackRef::unref(). In clear_abort_signal, the taken AbortSignalRef is dropped after clean_native_bindings and pending_activity_unref, so the unref() still fires last; .take() leaves None in the field so the struct's own drop glue does nothing. Neither FetchTasklet nor FetchOptions has a manual Drop impl, so the only new behavior is that a tasklet/options dropped while still holding a signal now releases it instead of leaking — strictly an improvement.
Security risks
None. This is an ownership-encoding refactor over an intrusive C++ refcount; no user input parsing, no new FFI surface, no unsafe blocks added or removed.
Level of scrutiny
Medium — it touches the fetch AbortSignal lifetime, but the change is a type substitution onto an established, already-tested wrapper (ExternalShared) with identical ref/unref semantics. The diff is small, net-negative, and every behavioral claim in the PR description checks out against the source. No dead imports were introduced (NonNull is still used by FetchHeadersRef; bun_opaque is still used at FetchTasklet.rs:2607/2619).
Other factors
The PR follows an established pattern the repo already uses for this object, matching the "grep for the in-tree helper" and "one implementation, in the right place" rules. Existing abort/leak test coverage (fetch-abort-stream-body, fetch-abort-queued, fetch-leak) passes per the PR description. One finder-raised nit about a stale comment listing signal among locals declared above was ruled out — the updated comment still accurately describes the set of owning locals whose Drop runs on early return.
fetch_impl kept the +1 ref on the request's AbortSignal in a private SignalRef guard around a NonNull, then passed the raw pointer through FetchOptions.signal and FetchTasklet.signal as Option<*mut AbortSignal>, with the matching unref written by hand in clear_abort_signal. All three now hold Option<AbortSignalRef>, the ExternalShared<AbortSignal> that Request, Response and node_fs already use: the two options paths take the ref through AbortSignal::ref_from_js, the Request path clones the Request's own ref, and clear_abort_signal releases it by dropping the taken value after clean_native_bindings and pending_activity_unref. Every early return in fetch_impl releases the ref through the same Drop, so SignalRef and its take() are deleted. The same ref()/unref() calls are issued at the same points: clear_data always runs clear_abort_signal before the tasklet box is freed, so the field's drop glue sees None. Option<AbortSignalRef> is one pointer through the NonNull niche where Option<*mut AbortSignal> was two words, and the Deref reads replace the null-checked opaque_deref calls.
160e994 to
b864f76
Compare
There was a problem hiding this comment.
LGTM — mechanical swap from a hand-rolled SignalRef Drop guard + raw *mut AbortSignal fields to the canonical Option<AbortSignalRef> (ExternalShared<AbortSignal>) already used by Request, BodyAbortListener, and node_fs.
What was reviewed:
- Refcount balance:
ref_from_js/.cloned()take the same +1 the oldfrom_js+ref_()did; every early return infetch_implafter extraction drops the local (wasSignalRef::Drop);FetchOptionsmove andFetchTasklet::gettransfer are 1:1. clear_abort_signalordering unchanged:take()→clean_native_bindings→pending_activity_unref→ drop (=unref()), matching the old explicit sequence.- Checked that
Request::abort_signal()returnsOption<&AbortSignalRef>so.cloned()bumps the intrusive refcount viaExternalShared::Clone.
Extended reasoning...
Overview
This PR replaces the private SignalRef(Option<NonNull<AbortSignal>>) Drop guard in fetch.rs and the two Option<*mut AbortSignal> fields on FetchOptions/FetchTasklet with Option<AbortSignalRef>, the ExternalShared<AbortSignal> type alias that the rest of the codebase already uses for the same C++ object. Three opaque_deref conversions and one hand-written unref() become as_deref()/Drop; the extraction sites use AbortSignal::ref_from_js and req.abort_signal().cloned().
Security risks
None introduced. The change is refcount plumbing for an intrusively-refcounted C++ WebCore::AbortSignal; no parsing, validation, or trust-boundary logic is touched. The TypeError rejection for non-AbortSignal values and the pre-aborted synchronous rejection are byte-identical.
Level of scrutiny
fetch is a hot, memory-safety-sensitive path, so I traced every ref/unref pair explicitly rather than trusting the shape. The +1 acquisition sites (ref_from_js = from_js + ref_() + adopt; .cloned() = ext_ref) and release sites (local drop on early return, clear_abort_signal's take() → drop) map one-to-one onto the old code. FetchTasklet::get still moves fetch_options.signal into the tasklet field, and clear_data still runs clear_abort_signal before the box is freed, so the field's Drop glue sees None on the normal path — and if a FetchOptions/FetchTasklet were ever dropped early, the ref is now released instead of leaked, which is strictly safer.
Other factors
This is exactly the "use the in-tree helper instead of hand-rolling" pattern REVIEW.md asks for, follows the established AbortSignalRef usage in Request.rs/node_fs, deletes ~40 lines of raw-pointer commentary, and adds no unsafe. The PR is part of a stated series of small type-system hardening changes; cargo check/clippy are clean and the abort-signal test files pass on the debug build.
|
Closing: main already has this change.
I compared each hunk of this PR with main at 08a2340. Two small differences remain, and neither changes behavior:
|
What
fetch_impl(src/runtime/webcore/fetch.rs) took a+1on the request'sAbortSignalwith hand-writtenref_()calls and parked the raw pointer in a privateSignalRef(Option<NonNull<AbortSignal>>)Drop guard, whosetake()disarmed it when the pointer was moved intoFetchOptions.signal: Option<*mut AbortSignal>.FetchTasklet::getmoved that pointer intoFetchTasklet.signal: Option<*mut AbortSignal>(src/runtime/webcore/fetch/FetchTasklet.rs), every reader went throughbun_opaque::opaque_deref, andclear_abort_signalreleased the ref with an explicitunref()afterclean_native_bindingsandpending_activity_unref.All three now hold
Option<AbortSignalRef>, theExternalShared<AbortSignal>thatRequest,Response::BodyAbortListener,node_fsandnode_fs_watcheralready use for the same C++ object.In
fetch.rsthe three extraction sites useAbortSignal::ref_from_js(options object and request-init object; thesignal is not of type AbortSignalrejection is unchanged) or clone theRequest's ownAbortSignalRef, the pre-abort check borrows theOption, and theFetchOptionsliteral moves it. InFetchTasklet.rsthe two fields change type,abort_signal()becomesas_deref(), the listener registration ingetborrows the field, andclear_abort_signaldrops the taken value instead of callingunref()by hand.SignalRef, itstake(), and the threeopaque_derefconversions are deleted; the two comments describing the raw-pointer protocol go with them. Nounsafeblocks are added or removed.Why
Who owns the
+1on the signal is now stated by the field types:fetch_implholds it for the duration of argument processing (every one of the early returns after extraction releases it throughAbortSignalRef's Drop, whichSignalRefre-implemented locally),FetchOptionscarries it, andFetchTaskletowns it untilclear_abort_signaltakes it out; aFetchTaskletorFetchOptionsthat is dropped while still holding the pointer can no longer leak it. This is zero-cost:Option<ExternalShared<AbortSignal>>is one word through theNonNullniche whereOption<*mut AbortSignal>was two, so both structs shrink; the sameref()/unref()/pendingActivityFFI calls are issued at the same program points (clear_dataalways runsclear_abort_signalbefore the tasklet box is freed, so the field's drop glue seesNone); and theDerefreads replaceopaque_derefcalls that each carried a null check, whileref_from_jsdrops theNonNull::newcheck on the valueref_()returned.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/web/fetch/fetch.test.ts fetch-abort-stream-body.test.ts fetch-abort-queued.test.ts fetch-leak.test.ts: 354 pass, 1 skip, 42 fail (fetch-abort-stream-body 6 pass/1 skip and fetch-abort-queued 1 pass are fully green; all AbortSignal tests in fetch.test.ts and the abort/leak tests in fetch-leak.test.ts pass).The 42 failures (41 in fetch.test.ts: sandbox
localhost/IPv6 ConnectionRefused or egress-proxy responses, chmod-000 reads succeeding as root, debug-build "utf16 ... (with gc)" and redirect timeouts; 1 in fetch-leak.test.ts: "Sending URLSearchParams > does not leak" 120s debug timeout) are the identical set that fails on main in this environment, so they are pre-existing and unrelated to this change.