Fix generation races in SharedTaskRaceGuard#148
Draft
ruccho wants to merge 1 commit into
Draft
Conversation
the operation continuation unconditionally unlinked its source before the SelfVersion claim. when the shared task won first, the pooled source could be re-rented and re-armed by another guard, and the stale unlink then removed it from the new guard's list without taking that guard's gate, so the new guard's drain never completed the race and its awaiter hung forever (e.g. CompleteAsync/StopAndExportAsync). the shared-task drain also claimed sources by re-reading SelfVersion at completion time, racing the operation continuation's claim from the arm-time baseline: it could observe the already-bumped version, claim the next generation and double-complete the source (InvalidOperationException in ManualResetValueTaskSourceCore). now both sides claim a generation with a CAS from the same arm-time baseline before touching anything: the operation continuation unlinks only after winning its claim (a won claim implies the source was never recycled), and the drain claims each node under _gate using the recorded arm version and completes only the nodes it won. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
SharedTaskRaceGuardhad two generation (SelfVersion) races. Because the Source pool isstaticand shared across all guards, a recycled Source can be corrupted across guards.Bug 1: The operation-side continuation calls
Unlink(source)unconditionally before the CAS-based winner check on SelfVersion. If the shared-task side wins first, the Source is returned to the pool, another guard rents and re-arms it, and then the stale continuation fires — removing the re-armed Source from the new guard's linked list without holding the new guard's_gate. The Source is then missed by the new guard's drain, and the caller awaiting itsPushAsyncnever completes (CompleteAsync/StopAndExportAsynchang).Bug 2 (found while fixing):
TryCompleteFromSharedTaskacquires its CAS baseline via a plain read of SelfVersion at call time — a TOCTOU. If the operation side wins the CAS and calls SetResult after the drain snapshots but before it completes, the drain claims the next generation incorrectly and double-completes (SignalCompletionthrowsInvalidOperationException→ crash).Fix
Unified both sides to touch the Source only when they win the claim via a CAS from the same generation baseline captured at arm time.
Unlink→ SetResult/SetException (winning the CAS implies no recycle happened, so Unlink is safe). If it loses, do nothing.armVersionto Source (written under_gate), and claim each node under the_gatelock via a CAS on thearmVersionbaseline. Only won nodes complete after the lock is released.TryCompleteFromSharedTasknow takes the already-capturedselfVersionas an argument.Verification
InvalidOperationExceptionwithin seconds; post-fix ran 30M rounds over 60s + 16M rounds over 30s with hangs=0, faults=0.🤖 Generated with Claude Code