Skip to content

Commit b237a61

Browse files
Fix repeat receiver lifetime after cleanup (NVIDIA#2187)
* Fix repeat receiver lifetime after cleanup * remove unused #include, add deduction guide for the sake of clang-16 * clang-format --------- Co-authored-by: Eric Niebler <eniebler@nvidia.com>
1 parent c62dd57 commit b237a61

5 files changed

Lines changed: 230 additions & 30 deletions

File tree

include/exec/repeat_n.hpp

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,25 +76,27 @@ namespace experimental::execution
7676
template <class _Error>
7777
constexpr void set_error(_Error &&__err) noexcept
7878
{
79+
auto *__state = __state_;
7980
STDEXEC_TRY
8081
{
8182
auto __err_copy = static_cast<_Error &&>(__err); // make a copy of the error...
82-
__state_->__cleanup(); // ... because this could potentially invalidate it.
83-
STDEXEC::set_error(std::move(__state_->__rcvr_), std::move(__err_copy));
83+
__state->__cleanup(); // ... because this could potentially invalidate it.
84+
STDEXEC::set_error(std::move(__state->__rcvr_), std::move(__err_copy));
8485
}
8586
STDEXEC_CATCH_ALL
8687
{
8788
if constexpr (!__nothrow_decay_copyable<_Error>)
8889
{
89-
STDEXEC::set_error(std::move(__state_->__rcvr_), std::current_exception());
90+
STDEXEC::set_error(std::move(__state->__rcvr_), std::current_exception());
9091
}
9192
}
9293
}
9394

9495
constexpr void set_stopped() noexcept
9596
{
96-
__state_->__cleanup();
97-
STDEXEC::set_stopped(std::move(__state_->__rcvr_));
97+
auto *__state = __state_;
98+
__state->__cleanup();
99+
STDEXEC::set_stopped(std::move(__state->__rcvr_));
98100
}
99101

100102
[[nodiscard]]

include/exec/repeat_until.hpp

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,17 @@ namespace experimental::execution
7070
template <class... _Booleans>
7171
constexpr void set_value(_Booleans &&...__bools) noexcept
7272
{
73+
auto *__state = __state_;
7374
if constexpr ((__is_bool_constant<_Booleans, true> && ...))
7475
{
7576
// Always done:
76-
__state_->__cleanup();
77-
STDEXEC::set_value(std::move(__state_->__rcvr_));
77+
__state->__cleanup();
78+
STDEXEC::set_value(std::move(__state->__rcvr_));
7879
}
7980
else if constexpr ((__is_bool_constant<_Booleans, false> && ...))
8081
{
8182
// Never done:
82-
__state_->__repeat();
83+
__state->__repeat();
8384
}
8485
else
8586
{
@@ -91,20 +92,20 @@ namespace experimental::execution
9192
bool const __done = (static_cast<bool>(static_cast<_Booleans &&>(__bools)) && ...);
9293
if (__done)
9394
{
94-
__state_->__cleanup();
95-
STDEXEC::set_value(std::move(__state_->__rcvr_));
95+
__state->__cleanup();
96+
STDEXEC::set_value(std::move(__state->__rcvr_));
9697
}
9798
else
9899
{
99-
__state_->__repeat();
100+
__state->__repeat();
100101
}
101102
}
102103
STDEXEC_CATCH_ALL
103104
{
104105
if constexpr (!__is_nothrow)
105106
{
106-
__state_->__cleanup();
107-
STDEXEC::set_error(std::move(__state_->__rcvr_), std::current_exception());
107+
__state->__cleanup();
108+
STDEXEC::set_error(std::move(__state->__rcvr_), std::current_exception());
108109
}
109110
}
110111
}
@@ -113,26 +114,28 @@ namespace experimental::execution
113114
template <class _Error>
114115
constexpr void set_error(_Error &&__err) noexcept
115116
{
117+
auto *__state = __state_;
116118
STDEXEC_TRY
117119
{
118120
auto __err_copy = static_cast<_Error &&>(__err); // make a local copy of the error...
119-
__state_->__cleanup(); // ... because this could potentially invalidate it.
120-
STDEXEC::set_error(std::move(__state_->__rcvr_), static_cast<_Error &&>(__err_copy));
121+
__state->__cleanup(); // ... because this could potentially invalidate it.
122+
STDEXEC::set_error(std::move(__state->__rcvr_), static_cast<_Error &&>(__err_copy));
121123
}
122124
STDEXEC_CATCH_ALL
123125
{
124126
if constexpr (!__nothrow_decay_copyable<_Error>)
125127
{
126-
__state_->__cleanup();
127-
STDEXEC::set_error(std::move(__state_->__rcvr_), std::current_exception());
128+
__state->__cleanup();
129+
STDEXEC::set_error(std::move(__state->__rcvr_), std::current_exception());
128130
}
129131
}
130132
}
131133

132134
constexpr void set_stopped() noexcept
133135
{
134-
__state_->__cleanup();
135-
STDEXEC::set_stopped(std::move(__state_->__rcvr_));
136+
auto *__state = __state_;
137+
__state->__cleanup();
138+
STDEXEC::set_stopped(std::move(__state->__rcvr_));
136139
}
137140

138141
[[nodiscard]]

test/exec/test_repeat_n.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,36 @@
2525

2626
#include <test_common/catch2.hpp>
2727

28+
#include "test_repeat_receiver_lifetime.hpp"
29+
2830
using namespace STDEXEC;
2931

3032
namespace
3133
{
34+
namespace lifetime_test = repeat_receiver_lifetime_test;
35+
36+
struct send_error
37+
{
38+
using signature = ex::set_error_t(int);
39+
40+
template <class Receiver>
41+
void operator()(Receiver &&rcvr) const noexcept
42+
{
43+
ex::set_error(static_cast<Receiver &&>(rcvr), 42);
44+
}
45+
};
46+
47+
struct send_stopped
48+
{
49+
using signature = ex::set_stopped_t();
50+
51+
template <class Receiver>
52+
void operator()(Receiver &&rcvr) const noexcept
53+
{
54+
ex::set_stopped(static_cast<Receiver &&>(rcvr));
55+
}
56+
};
57+
3258
TEST_CASE("repeat_n returns a sender", "[adaptors][repeat_n]")
3359
{
3460
auto snd = exec::repeat_n(ex::just() | then([] {}), 10);
@@ -115,6 +141,29 @@ namespace
115141
CHECK(count == 1);
116142
}
117143

144+
TEST_CASE("repeat_n does not access its child receiver after cleanup", "[adaptors][repeat_n]")
145+
{
146+
SECTION("set_error")
147+
{
148+
bool invalidated = false;
149+
auto snd = lifetime_test::invalidate_on_destroy_sender{send_error{}, &invalidated}
150+
| exec::repeat_n(1);
151+
auto op = ex::connect(std::move(snd), expect_error_receiver{42});
152+
ex::start(op);
153+
CHECK(invalidated);
154+
}
155+
156+
SECTION("set_stopped")
157+
{
158+
bool invalidated = false;
159+
auto snd = lifetime_test::invalidate_on_destroy_sender{send_stopped{}, &invalidated}
160+
| exec::repeat_n(1);
161+
auto op = ex::connect(std::move(snd), expect_stopped_receiver{});
162+
ex::start(op);
163+
CHECK(invalidated);
164+
}
165+
}
166+
118167
TEST_CASE("running deeply recursing algo on repeat_n doesn't blow the stack",
119168
"[adaptors][repeat_n]")
120169
{
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (c) 2026 NVIDIA Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 with LLVM Exceptions
5+
* (the "License"); you may not use this file except in compliance with
6+
* the License. You may obtain a copy of the License at
7+
*
8+
* https://llvm.org/LICENSE.txt
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
#pragma once
17+
18+
#include "stdexec/execution.hpp"
19+
20+
namespace repeat_receiver_lifetime_test
21+
{
22+
namespace ex = STDEXEC;
23+
24+
template <class Completion>
25+
struct invalidate_on_destroy_sender
26+
{
27+
using sender_concept = ex::sender_tag;
28+
using completion_signatures = ex::completion_signatures<typename Completion::signature>;
29+
30+
template <class Receiver>
31+
struct operation
32+
{
33+
operation(Receiver rcvr, Completion completion, bool *invalidated) noexcept
34+
: rcvr_(static_cast<Receiver &&>(rcvr))
35+
, completion_(static_cast<Completion &&>(completion))
36+
, invalidated_(invalidated)
37+
{}
38+
39+
~operation()
40+
{
41+
if constexpr (requires { rcvr_.__self_->__rcvr_.__state_; })
42+
{
43+
if (started_)
44+
{
45+
rcvr_.__self_->__rcvr_.__state_ = nullptr;
46+
*invalidated_ = true;
47+
}
48+
}
49+
}
50+
51+
void start() & noexcept
52+
{
53+
started_ = true;
54+
completion_(static_cast<Receiver &&>(rcvr_));
55+
}
56+
57+
Receiver rcvr_;
58+
Completion completion_;
59+
bool *invalidated_;
60+
bool started_ = false;
61+
};
62+
63+
template <ex::receiver_of<completion_signatures> Receiver>
64+
auto connect(Receiver rcvr) const -> operation<Receiver>
65+
{
66+
return {static_cast<Receiver &&>(rcvr), completion_, invalidated_};
67+
}
68+
69+
Completion completion_;
70+
bool *invalidated_;
71+
};
72+
73+
template <class Completion>
74+
STDEXEC_HOST_DEVICE_DEDUCTION_GUIDE
75+
invalidate_on_destroy_sender(Completion, bool *) -> invalidate_on_destroy_sender<Completion>;
76+
} // namespace repeat_receiver_lifetime_test

0 commit comments

Comments
 (0)