Repository navigation
Conversation
…m check The "client abort of a streaming Response releases the body stream it held" cases assert that all 8 streams are collected. On the x64 ASAN lane one stream, always the last, survives: its address is left in the live JSC::runInternalMicrotask frame that runs the test's own await continuation, and the conservative stack scan keeps it. A heap snapshot shows the cell with no incoming edge and no root. The leak this test guards (#41080) kept every stream, so assert alive <= 1 instead of 0. With the release in finalize_without_deinit disabled the test still fails with 8 of 8 alive.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review. WalkthroughThe streaming-response garbage-collection test now permits one surviving stream reference after repeated collection attempts. It still fails when more than one stream remains. ChangesStreaming response cleanup
Suggested reviewers: Merge Risk: ⚪ Minimal · up to This test-only adjustment accommodates one conservatively retained stream without masking the actual leak; the affected tests pass and no production behavior changes. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 3:21 AM PT - Sep 10th, 2026
✅ @robobun, your commit 2690c08dca893eef83a55495ea33de0d2de17257 passed in 🧪 To try this PR locally: bunx bun-pr 42190That installs a local version of the PR into your bun-42190 --bun |
|
Status: ready for review. Test-only change, head 2690c08. Reproduced with Review: two optional suggestions, both answered and resolved. The tolerance stays unconditional (build 110257 hit it on an alpine x64 release lane), and the conservative-root attribution carries the two proofs CLAUDE.md asks for (heap snapshot with no retainer, gdb frame that owns the stack word), both in the PR body. |
|
Pushed b79a636: the code comment now says the one-survivor tolerance applies on every build, since build 110257 (alpine x64 release) hit the same |
|
Pushed 2690c08: the test comment is now three lines and points here for the proof. Both review threads are answered and resolved. No change to the assertion. |
|
Independent check on main at 3d40e50. This change still fixes the failure, and the failure now blocks most PRs. Impact. Of the last 110 failed builds (2026-09-11 07:00 to 2026-09-12 04:00 UTC), 87 carry the error annotation for this file on Repro.
Same stack word. The heap snapshot shows the stream cell with no incoming edge and no Two more probes:
|
|
Fresh numbers from CI, read 2026-09-16 03:30Z. The failure is frequent again.
This change still covers all 26 failures, because each one has exactly one survivor. |
Rate of this failure on PR builds since #42775Source: the Buildkite annotations of 219 finished PR builds, 115781 to 116305 (2026-09-15 03:02Z to 2026-09-16 04:41Z). A build counts when one of the two
|
Problem
test/js/bun/http/serve-pending-promise-abort-leak.test.ts, bothreleases the body stream it heldcases, fail on thedebian 13 x64-asanlane:expect(alive).toBe(0),Received: 1. Builds 113711, 113722, 113732, 113727 (3 of 3 attempts), and once on alpine x64 release (110257).rootsentry. At conservative root gathering, its address is on the main thread stack once:rbp-0xa60in the liveJSC::runInternalMicrotaskframe under the test's ownawaitcontinuation.Fix
alive <= 1, on every build (a release lane hit it too). The leak that Bun.serve: release the body stream of a Response whose client aborted mid-stream #41080 fixed kept every stream: withrelease_body_streamdisabled infinalize_without_deinit, the test reportsReceived: 8.release-asanbuild, whole file, 3 runs each: 25 pass 2 fail before, 27 pass after.bun bd test: 27 pass.Background
sanitizeStackzeroes only dead stack below the stack pointer, so a stale value in a live caller frame survives eachBun.gc(true)from under it.awaitcontinuation is a microtask and runs insiderunInternalMicrotask. One drain earlier, the pump for the last response ran at that frame position and wrote the stream there.Notes
Main does not run the x64-asan test lane, so the failure only shows on PR builds.
Repro:
bun run build:asan, then./build/release-asan/bun-asan test test/js/bun/http/serve-pending-promise-abort-leak.test.ts. The whole file fails 3 of 3 (25 pass, 2 fail).-t "releases the body stream"alone passes.FOO=1in the environment makes the whole file pass. The microtask that pumped the last response is what left the address in the frame: the stale code addresses around the slot are that response's teardown (see below).Heap snapshot, taken with
generateHeapSnapshotForDebugging()frombun:jscafter the 20 GC rounds, with each stream tagged by a property at creation so noderef()is needed to find it:The async case is the same (index 7,
roots=[] incoming=[], one stack hit at the same stack address).Stack word, from gdb as the parent (
startup-with-shell off,LINES/COLUMNSunset, suspend signals passed through), with a breakpoint onJSC::MachineThreads::gatherConservativeRootsarmed by the test right before one moreBun.gc(true)from the same continuation. Frame-pointer walk of the main thread at that point, newest first (GC frames 0 to 16 elided):Stale code addresses next to that slot (symbolized):
WebCore::JSReadableHTTPResponseSinkController__endImpl,RequestContext::end_request_streaming,uWS::HttpContext<false>::onClose,us_internal_socket_close_raw, a return address insideJSC::runInternalMicrotask, and__interceptor_free/__sanitizer::BufferedStackTrace::UnwindImpl. That is the teardown of the last aborted response.#41409 adds a retainer report to this test and lists the file as flaky because the cause was not known then. This is the answer that report was meant to find. #40539 rewrites the file for speed and does not touch this assertion's meaning.
[auto-merge] gate passed · iteration 0 · 1 files touched
passes on PR (with fix)
diff hotspot
gate history · 3 passed · 0 rejected · iteration 0
evidence per changed file