Repository navigation
fix use-after-free in inplace_stop_source::request_stop() - #2303
Merged
ericniebler merged 1 commit intoOct 8, 2026
Merged
Conversation
If a stop callback destroys the stop source (for example when it completes a when_all and the receiver destroys the operation state), request_stop() still calls __lock_() and stores to __state_ on the freed object. Make the destructor work like __remove_callback_. On the notifying thread it sets a flag that lives on request_stop()'s stack, so request_stop() returns without touching *this again. On any other thread it waits until request_stop() has finished. Sources that never had stop requested skip all of this. Fixes NVIDIA#1889
eklavya072
force-pushed
the
fix/inplace-stop-source-lifetime
branch
from
October 6, 2026 07:31
dbb74ae to
5cc004a
Compare
ericniebler
approved these changes
Oct 8, 2026
Collaborator
|
/ok to test 5cc004a |
Collaborator
|
this is fantastic, thank you! |
ericniebler
pushed a commit
to ericniebler/stdexec
that referenced
this pull request
Oct 25, 2026
The stop callbacks that when_all (since NVIDIA#1718) and exec::when_any (since NVIDIA#2115) register on the receiver's stop token increment the count of outstanding children, request stop, and then arrive. If the last child arrives on another thread while such a callback is running, the count is already zero, so the callback's arrival completes the operation a second time. With inplace_stop_source the thread of the last child usually spins forever in ~inplace_stop_callback instead, waiting for a callback that the other thread has already destroyed. The extra count only worked around inplace_stop_source not tolerating its destruction from inside its own request_stop() (NVIDIA#1889), which NVIDIA#2303 fixed. Make both callbacks only forward the request to the internal stop source, like on-stop-request in [exec.when.all]. For when_all this also undoes the rest of the workaround from NVIDIA#1851: it completes with set_stopped only when a child does, and it no longer records values once a child has failed or been stopped. Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
Fixes #1889.
When a stop callback destroys the
inplace_stop_sourceit is registered with (which is whatwhen_alldoes when its last child completes and the receiver tears down the op-state),request_stop()keeps using the freed source after the callback returns. It calls__lock_()and later stores to__state_. ASan on main:Following @RobertLeahy's suggestion in the issue,
~inplace_stop_source()now handles this the same way__remove_callback_handles callbacks:request_stop()'s stack, andrequest_stop()returns without touching*thisagain.request_stop()is done with the source.Tests:
a stop callback can destroy the inplace_stop_sourcedestroys the source from a callback while another callback is still registered.inplace_stop_source destructor waits for request_stop on another threaddestroys it from another thread whilerequest_stop()is still running.The workaround from #1851 can probably be reverted once this is in, I left that for a separate PR. The same code is also in cudax's
stop_token.cuhin CCCL, I can send the same change there too.