fix: reset the handle when a suspended fiber is garbage-collected on V8 >= 10.4 - #4
Closed
jbaczuk-qualia wants to merge 1 commit into
Closed
jbaczuk-qualia wants to merge 1 commit into
jbaczuk-qualia wants to merge 1 commit into
Conversation
…V8 >= 10.4 f3cbba6 ("Make compatible with v20") registers the Fiber weak callback with WeakCallbackType::kParameter because V8 removed kFinalizer. The two types have different contracts: kFinalizer ran before the object was reclaimed and let the callback resurrect it, which Fiber::WeakCallback relied on for suspended fibers (ClearWeak(), unwind later in DestroyOrphans, MakeWeak() again). kParameter is a phantom callback: the object is already gone and V8 CHECKs that the callback reset its handle ("Handle not reset in first callback", global-handles.cc), so a yielded fiber whose JS object becomes unreachable aborted the process on node 20+: # Fatal error in , line 0 # Check failed: Handle not reset in first callback. See comments on |v8::WeakCallbackInfo|. On V8 >= 10.4 the orphan branch now resets the handle in the callback and DestroyOrphans deletes the fiber after unwinding it instead of re-weakening a handle that no longer exists. MakeWeak(), ClearWeak() and the Fiber.current getter tolerate the empty handle, which Fiber::Yield_ and JS code in the zombie's catch/finally blocks hit while the stack unwinds (the first version without the guards segfaulted in GlobalHandles::ClearWeakness). The node 18 (V8 10.2) code path is unchanged. test/orphan-gc.js garbage-collects 200 yielded fibers, forces DestroyOrphans and checks every fiber was unwound; it aborts on the unpatched build and passes here on node 24.21.0 (patched custom-v24, ucontext) together with the other 19 tests, and passes on node 18.16.1 with fibers 5.0.4. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Author
|
Closed 2026-09-21: carried into #5 as |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
f3cbba6("Make compatible with v20", PR #1) registers the Fiber weak callback withWeakCallbackType::kParameterbecause V8 removedkFinalizer. The two have different contracts:kFinalizer(node 18, V8 10.2): the callback runs before the object is reclaimed and may resurrect it.Fiber::WeakCallbackrelied on this for suspended fibers:ClearWeak(), unwind later inDestroyOrphans(),MakeWeak()again.kParameter(node 20+): a phantom callback. The object is already gone and V8CHECKs that the callback reset its handle (global-handles.cc: "Handle not reset in first callback").So on node 20+ a yielded fiber whose JS object becomes unreachable (its resuming callback was dropped: an abandoned request, a cleared timer, a socket closed without firing) aborts the process at the next GC:
Finished and never-started fibers were fine (
delete→ destructor resets the handle), which is why the suite and the GDC click-throughs never hit it. Node 18 is unaffected.Fix
On
V8_AT_LEAST(10, 4):WeakCallback: for a started (yielded) fiber, queue it as an orphan and reset the handle instead ofClearWeak().DestroyOrphans: afterUnwindStack(),deletethe fiber instead ofMakeWeak()on a handle that no longer exists.MakeWeak(),ClearWeak()and theFiber.currentgetter return early on an empty handle.Fiber::Yield_and user code in the zombie'scatch/finallyreach these while the stack unwinds; the first cut without the guards segfaulted inGlobalHandles::ClearWeakness.The node 18 code path is unchanged.
Test
test/orphan-gc.js: 200 yielded fibers dropped, two forced GCs, aFiber.run()to triggerDestroyOrphans, and a check that all 200 unwound (each touchesFiber.currentfrom itscatch).node24-compatunpatched, node 24.21.0 (patchedcustom-v24, ucontext)Check failed: Handle not reset in first callbackNotes
node24-compat(PR [CORO_UCONTEXT] Node 24 / V8 13 compatibility (ucontext coroutines; closed, superseded by #5) #2), so this lands with the V8 13 port. PR Async resource #1 carries the samekParameterchange and should not merge without this.kFinalizer; cleanup still runs, the regression is this crash.🤖 Generated with Claude Code