From 3e3af77e108f8db7d7ff1991807d1ab411707050 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Tue, 11 Aug 2026 01:59:48 +0200 Subject: [PATCH 1/3] Fix repeat receiver lifetime after cleanup --- include/exec/repeat_n.hpp | 12 ++-- include/exec/repeat_until.hpp | 31 +++++---- test/exec/test_repeat_n.cpp | 49 ++++++++++++++ test/exec/test_repeat_receiver_lifetime.hpp | 74 +++++++++++++++++++++ test/exec/test_repeat_until.cpp | 70 +++++++++++++++++++ 5 files changed, 217 insertions(+), 19 deletions(-) create mode 100644 test/exec/test_repeat_receiver_lifetime.hpp diff --git a/include/exec/repeat_n.hpp b/include/exec/repeat_n.hpp index e6af4c626..05ec07688 100644 --- a/include/exec/repeat_n.hpp +++ b/include/exec/repeat_n.hpp @@ -76,25 +76,27 @@ namespace experimental::execution template constexpr void set_error(_Error &&__err) noexcept { + auto *__state = __state_; STDEXEC_TRY { auto __err_copy = static_cast<_Error &&>(__err); // make a copy of the error... - __state_->__cleanup(); // ... because this could potentially invalidate it. - STDEXEC::set_error(std::move(__state_->__rcvr_), std::move(__err_copy)); + __state->__cleanup(); // ... because this could potentially invalidate it. + STDEXEC::set_error(std::move(__state->__rcvr_), std::move(__err_copy)); } STDEXEC_CATCH_ALL { if constexpr (!__nothrow_decay_copyable<_Error>) { - STDEXEC::set_error(std::move(__state_->__rcvr_), std::current_exception()); + STDEXEC::set_error(std::move(__state->__rcvr_), std::current_exception()); } } } constexpr void set_stopped() noexcept { - __state_->__cleanup(); - STDEXEC::set_stopped(std::move(__state_->__rcvr_)); + auto *__state = __state_; + __state->__cleanup(); + STDEXEC::set_stopped(std::move(__state->__rcvr_)); } [[nodiscard]] diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index 146750da1..0cfb494a5 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -70,16 +70,17 @@ namespace experimental::execution template constexpr void set_value(_Booleans &&...__bools) noexcept { + auto *__state = __state_; if constexpr ((__is_bool_constant<_Booleans, true> && ...)) { // Always done: - __state_->__cleanup(); - STDEXEC::set_value(std::move(__state_->__rcvr_)); + __state->__cleanup(); + STDEXEC::set_value(std::move(__state->__rcvr_)); } else if constexpr ((__is_bool_constant<_Booleans, false> && ...)) { // Never done: - __state_->__repeat(); + __state->__repeat(); } else { @@ -91,20 +92,20 @@ namespace experimental::execution bool const __done = (static_cast(static_cast<_Booleans &&>(__bools)) && ...); if (__done) { - __state_->__cleanup(); - STDEXEC::set_value(std::move(__state_->__rcvr_)); + __state->__cleanup(); + STDEXEC::set_value(std::move(__state->__rcvr_)); } else { - __state_->__repeat(); + __state->__repeat(); } } STDEXEC_CATCH_ALL { if constexpr (!__is_nothrow) { - __state_->__cleanup(); - STDEXEC::set_error(std::move(__state_->__rcvr_), std::current_exception()); + __state->__cleanup(); + STDEXEC::set_error(std::move(__state->__rcvr_), std::current_exception()); } } } @@ -113,26 +114,28 @@ namespace experimental::execution template constexpr void set_error(_Error &&__err) noexcept { + auto *__state = __state_; STDEXEC_TRY { auto __err_copy = static_cast<_Error &&>(__err); // make a local copy of the error... - __state_->__cleanup(); // ... because this could potentially invalidate it. - STDEXEC::set_error(std::move(__state_->__rcvr_), static_cast<_Error &&>(__err_copy)); + __state->__cleanup(); // ... because this could potentially invalidate it. + STDEXEC::set_error(std::move(__state->__rcvr_), static_cast<_Error &&>(__err_copy)); } STDEXEC_CATCH_ALL { if constexpr (!__nothrow_decay_copyable<_Error>) { - __state_->__cleanup(); - STDEXEC::set_error(std::move(__state_->__rcvr_), std::current_exception()); + __state->__cleanup(); + STDEXEC::set_error(std::move(__state->__rcvr_), std::current_exception()); } } } constexpr void set_stopped() noexcept { - __state_->__cleanup(); - STDEXEC::set_stopped(std::move(__state_->__rcvr_)); + auto *__state = __state_; + __state->__cleanup(); + STDEXEC::set_stopped(std::move(__state->__rcvr_)); } [[nodiscard]] diff --git a/test/exec/test_repeat_n.cpp b/test/exec/test_repeat_n.cpp index 36f012562..9131406c5 100644 --- a/test/exec/test_repeat_n.cpp +++ b/test/exec/test_repeat_n.cpp @@ -25,10 +25,36 @@ #include +#include "test_repeat_receiver_lifetime.hpp" + using namespace STDEXEC; namespace { + namespace lifetime_test = repeat_receiver_lifetime_test; + + struct send_error + { + using signature = ex::set_error_t(int); + + template + void operator()(Receiver &&rcvr) const noexcept + { + ex::set_error(static_cast(rcvr), 42); + } + }; + + struct send_stopped + { + using signature = ex::set_stopped_t(); + + template + void operator()(Receiver &&rcvr) const noexcept + { + ex::set_stopped(static_cast(rcvr)); + } + }; + TEST_CASE("repeat_n returns a sender", "[adaptors][repeat_n]") { auto snd = exec::repeat_n(ex::just() | then([] {}), 10); @@ -115,6 +141,29 @@ namespace CHECK(count == 1); } + TEST_CASE("repeat_n does not access its child receiver after cleanup", "[adaptors][repeat_n]") + { + SECTION("set_error") + { + bool invalidated = false; + auto snd = lifetime_test::invalidate_on_destroy_sender{send_error{}, &invalidated} + | exec::repeat_n(1); + auto op = ex::connect(std::move(snd), expect_error_receiver{42}); + ex::start(op); + CHECK(invalidated); + } + + SECTION("set_stopped") + { + bool invalidated = false; + auto snd = lifetime_test::invalidate_on_destroy_sender{send_stopped{}, &invalidated} + | exec::repeat_n(1); + auto op = ex::connect(std::move(snd), expect_stopped_receiver{}); + ex::start(op); + CHECK(invalidated); + } + } + TEST_CASE("running deeply recursing algo on repeat_n doesn't blow the stack", "[adaptors][repeat_n]") { diff --git a/test/exec/test_repeat_receiver_lifetime.hpp b/test/exec/test_repeat_receiver_lifetime.hpp new file mode 100644 index 000000000..9afdd0349 --- /dev/null +++ b/test/exec/test_repeat_receiver_lifetime.hpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "stdexec/execution.hpp" + +#include + +namespace repeat_receiver_lifetime_test +{ + namespace ex = STDEXEC; + + template + struct invalidate_on_destroy_sender + { + using sender_concept = ex::sender_tag; + using completion_signatures = ex::completion_signatures; + + template + struct operation + { + operation(Receiver rcvr, Completion completion, bool *invalidated) noexcept + : rcvr_(static_cast(rcvr)) + , completion_(static_cast(completion)) + , invalidated_(invalidated) + {} + + ~operation() + { + if constexpr (requires { rcvr_.__self_->__rcvr_.__state_; }) + { + if (started_) + { + rcvr_.__self_->__rcvr_.__state_ = nullptr; + *invalidated_ = true; + } + } + } + + void start() & noexcept + { + started_ = true; + completion_(static_cast(rcvr_)); + } + + Receiver rcvr_; + Completion completion_; + bool *invalidated_; + bool started_ = false; + }; + + template Receiver> + auto connect(Receiver rcvr) const -> operation + { + return {static_cast(rcvr), completion_, invalidated_}; + } + + Completion completion_; + bool *invalidated_; + }; +} // namespace repeat_receiver_lifetime_test diff --git a/test/exec/test_repeat_until.cpp b/test/exec/test_repeat_until.cpp index 801c91c5b..a8c79958f 100644 --- a/test/exec/test_repeat_until.cpp +++ b/test/exec/test_repeat_until.cpp @@ -26,6 +26,8 @@ #include +#include "test_repeat_receiver_lifetime.hpp" + #include #include #include @@ -38,6 +40,40 @@ namespace ex = STDEXEC; namespace { + namespace lifetime_test = repeat_receiver_lifetime_test; + + struct send_true + { + using signature = ex::set_value_t(std::true_type); + + template + void operator()(Receiver &&rcvr) const noexcept + { + ex::set_value(static_cast(rcvr), std::true_type{}); + } + }; + + struct send_error + { + using signature = ex::set_error_t(int); + + template + void operator()(Receiver &&rcvr) const noexcept + { + ex::set_error(static_cast(rcvr), 42); + } + }; + + struct send_stopped + { + using signature = ex::set_stopped_t(); + + template + void operator()(Receiver &&rcvr) const noexcept + { + ex::set_stopped(static_cast(rcvr)); + } + }; struct boolean_sender { @@ -154,6 +190,40 @@ namespace ex::start(op); } + TEST_CASE("repeat_until does not access its child receiver after cleanup", + "[adaptors][repeat_until]") + { + SECTION("set_value") + { + bool invalidated = false; + auto snd = exec::repeat_until( + lifetime_test::invalidate_on_destroy_sender{send_true{}, &invalidated}); + auto op = ex::connect(std::move(snd), expect_void_receiver{}); + ex::start(op); + CHECK(invalidated); + } + + SECTION("set_error") + { + bool invalidated = false; + auto snd = exec::repeat_until( + lifetime_test::invalidate_on_destroy_sender{send_error{}, &invalidated}); + auto op = ex::connect(std::move(snd), expect_error_receiver{42}); + ex::start(op); + CHECK(invalidated); + } + + SECTION("set_stopped") + { + bool invalidated = false; + auto snd = exec::repeat_until( + lifetime_test::invalidate_on_destroy_sender{send_stopped{}, &invalidated}); + auto op = ex::connect(std::move(snd), expect_stopped_receiver{}); + ex::start(op); + CHECK(invalidated); + } + } + TEST_CASE("running deeply recursing algo on repeat_until doesn't blow the stack", "[adaptors][repeat_until]") { From bc767f69bb52387951a70dad0f9d42398ac22827 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 10 Aug 2026 17:47:26 -0700 Subject: [PATCH 2/3] remove unused #include, add deduction guide for the sake of clang-16 --- test/exec/test_repeat_receiver_lifetime.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/exec/test_repeat_receiver_lifetime.hpp b/test/exec/test_repeat_receiver_lifetime.hpp index 9afdd0349..8a7009a6a 100644 --- a/test/exec/test_repeat_receiver_lifetime.hpp +++ b/test/exec/test_repeat_receiver_lifetime.hpp @@ -17,8 +17,6 @@ #include "stdexec/execution.hpp" -#include - namespace repeat_receiver_lifetime_test { namespace ex = STDEXEC; @@ -71,4 +69,8 @@ namespace repeat_receiver_lifetime_test Completion completion_; bool *invalidated_; }; + + template + STDEXEC_HOST_DEVICE_DEDUCTION_GUIDE + invalidate_on_destroy_sender(Completion, bool *) -> invalidate_on_destroy_sender; } // namespace repeat_receiver_lifetime_test From fd72a809ba2136b26f750a11dbb8ee06d9c9c536 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 10 Aug 2026 19:40:46 -0700 Subject: [PATCH 3/3] clang-format --- test/exec/test_repeat_until.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/exec/test_repeat_until.cpp b/test/exec/test_repeat_until.cpp index a8c79958f..5512e6455 100644 --- a/test/exec/test_repeat_until.cpp +++ b/test/exec/test_repeat_until.cpp @@ -91,11 +91,11 @@ namespace { if (counter_ == 0) { - ex::set_value(static_cast(rcvr_), true); + ex::set_value(static_cast(rcvr_), true); } else { - ex::set_value(static_cast(rcvr_), false); + ex::set_value(static_cast(rcvr_), false); } } }; @@ -103,7 +103,7 @@ namespace template Receiver> auto connect(Receiver rcvr) const -> operation { - return {static_cast(rcvr), --*counter_}; + return {static_cast(rcvr), --*counter_}; } std::shared_ptr counter_ = std::make_shared(1000); @@ -325,10 +325,10 @@ namespace { struct error_type { - explicit error_type(unsigned& throw_after) noexcept + explicit error_type(unsigned &throw_after) noexcept : throw_after_(throw_after) {} - error_type(error_type const & other) + error_type(error_type const &other) : throw_after_(other.throw_after_) { if (!throw_after_) @@ -337,7 +337,7 @@ namespace } --throw_after_; } - unsigned& throw_after_; + unsigned &throw_after_; }; struct receiver { @@ -359,7 +359,7 @@ namespace CHECK(!done_); done_ = true; } - bool& done_; + bool &done_; }; unsigned throw_after = 0; bool done = false; @@ -435,15 +435,15 @@ namespace --throw_after_; } public: - explicit value_type(unsigned& throw_after) noexcept + explicit value_type(unsigned &throw_after) noexcept : throw_after_(throw_after) {} - value_type(value_type const & other) + value_type(value_type const &other) : throw_after_(other.throw_after_) { maybe_throw_(); } - unsigned& throw_after_; + unsigned &throw_after_; operator bool() && { maybe_throw_(); @@ -465,7 +465,7 @@ namespace { CHECK(!done_); } - bool& done_; + bool &done_; }; unsigned throw_after = 0; bool done = false;