From 9a04fc92c17c3fa5341e1700fd9e68aa2d4f92cc Mon Sep 17 00:00:00 2001 From: alwaysprince05 Date: Tue, 13 Oct 2026 12:10:12 +0530 Subject: [PATCH] fix(stdexec): initialize stop-when's once-flag before its callbacks The fused token's callback_type declared __called_ after __cb1_ and __cb2_. When the first token is already stopped, __cb1_'s constructor invokes the function immediately, and the function reads and writes __called_ -- whose lifetime has not started: the flag then observes an indeterminate value and is subsequently overwritten with false. The function can therefore run twice (flag read as 0, then reset), or be skipped entirely (flag read as non-zero and the second token never stopped), both of which violate [exec.stop.when]. Regression from #2234. Declare __called_ before the two callback members so that any immediate invocation during construction sees a live, initialized flag. Fixes #2326. test.stdexec passes in Release and Debug with STDEXEC_ENABLE_EXTRA_TYPE_CHECKING on; a regression test covering the already-stopped-first-token case is included. --- include/stdexec/__detail/__stop_when.hpp | 6 ++- .../stdexec/algos/adaptors/test_stop_when.cpp | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/include/stdexec/__detail/__stop_when.hpp b/include/stdexec/__detail/__stop_when.hpp index abec35ec4..5c36758ae 100644 --- a/include/stdexec/__detail/__stop_when.hpp +++ b/include/stdexec/__detail/__stop_when.hpp @@ -120,10 +120,14 @@ namespace STDEXEC using __cb1_t = _Token1::template callback_type<__cb_t>; using __cb2_t = _Token2::template callback_type<__cb_t>; + // __called_ must be initialized before the callbacks: a callback + // registered with an already-stopped token runs its function as part + // of the callback's own construction, and that function reads and + // writes __called_. _Fn __fn_; + __std::atomic __called_{false}; __cb1_t __cb1_; __cb2_t __cb2_; - __std::atomic __called_{false}; }; _Token1 __tkn1_; diff --git a/test/stdexec/algos/adaptors/test_stop_when.cpp b/test/stdexec/algos/adaptors/test_stop_when.cpp index 4b76c8c84..feb4d1365 100644 --- a/test/stdexec/algos/adaptors/test_stop_when.cpp +++ b/test/stdexec/algos/adaptors/test_stop_when.cpp @@ -70,6 +70,43 @@ namespace wait_for_value(ex::__stop_when(isTokenStoppable(), source.get_token()), true); } +#if !STDEXEC_USE_MODULES() + // TODO: figure out why this fails in a modules build + TEST_CASE("callbacks registered with an already-stopped fused token run exactly once", + "[adaptors][stop-when]") + { + // Regression test for #2326: the first token is already stopped when the + // callback is registered, so the function must run once, at registration, + // and never again when the second token is stopped later. + ex::inplace_stop_source first, second; + first.request_stop(); + + int calls = 0; + int atReg = -1; + auto makeCallback = [](auto token, auto fn) noexcept + { + using cb_t = decltype(token)::template callback_type; + return std::optional(std::in_place, std::move(token), std::move(fn)); + }; + + ex::sync_wait(ex::__stop_when(ex::__stop_when(ex::read_env(ex::get_stop_token) + | ex::then( + [&](auto token) noexcept + { + auto cb = makeCallback(token, + [&]() noexcept + { ++calls; }); + atReg = calls; + second.request_stop(); + }), + first.get_token()), + second.get_token())); + + REQUIRE(atReg == 1); + REQUIRE(calls == 1); + } +#endif + #if !STDEXEC_USE_MODULES() // TODO: figure out why this fails in a modules build TEST_CASE("stop-when fuses its token with the receiver's when both are stoppable",