Skip to content

fix use-after-free in inplace_stop_source::request_stop() - #2303

Open
eklavya072 wants to merge 1 commit into
NVIDIA:mainfrom
eklavya072:fix/inplace-stop-source-lifetime
Open

eklavya072 wants to merge 1 commit into
NVIDIA:mainfrom
eklavya072:fix/inplace-stop-source-lifetime

Conversation

@eklavya072

Copy link
Copy Markdown

Fixes #1889.

When a stop callback destroys the inplace_stop_source it is registered with (which is what when_all does 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:

ERROR: AddressSanitizer: heap-use-after-free
    #0 in stdexec::inplace_stop_source::request_stop() stop_token.hpp:302

Following @RobertLeahy's suggestion in the issue, ~inplace_stop_source() now handles this the same way __remove_callback_ handles callbacks:

  • On the notifying thread (the source is destroyed from inside a callback), it sets a flag that lives on request_stop()'s stack, and request_stop() returns without touching *this again.
  • On any other thread, it waits until request_stop() is done with the source.
  • If stop was never requested, the destructor only does one atomic load, so the common case doesn't take the lock.

Tests:

  • a stop callback can destroy the inplace_stop_source destroys the source from a callback while another callback is still registered.
  • inplace_stop_source destructor waits for request_stop on another thread destroys it from another thread while request_stop() is still running.
  • Both fail under ASan on main. The second one also catches a version that only sets the flag (TSan reports a race on it).
  • The full test suite passes with the CI settings for gcc 14 Debug ASan and TSan, and clang 16 Release ASan (libc++) and Debug TSan.

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.cuh in CCCL, I can send the same change there too.

@copy-pr-bot

copy-pr-bot Bot commented Oct 5, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

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
eklavya072 force-pushed the fix/inplace-stop-source-lifetime branch from dbb74ae to 5cc004a Compare October 6, 2026 07:31

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

inplace_stop_source is not well-behaved when the lifetime of the stop source ends during the dispatch of the final stop callback

1 participant