From 687dd51527a717113bf45bb1c95752fbf8ab5b6e Mon Sep 17 00:00:00 2001 From: alwaysprince05 Date: Wed, 7 Oct 2026 03:55:13 +0530 Subject: [PATCH 1/3] fix(exec): own the written env in sequence write_env's operation state Subscribing to a write_env sender that wraps a sequence sender takes the transparent-adaptor path in exec::subscribe: the adaptor's data is joined with the receiver's environment and stored in the environment presented to the child. The joined environment held a reference to the data member of the sender expression itself, so once the sender expression is destroyed, the operation state is left reading dangling memory. Have __write_env_t's __child_env_fn own a decayed copy of the data -- moving out of an rvalue sender, copying from an lvalue -- and forward the sender's value category to the transformation instead of the value category of the data member binding. The unconditional noexcept on __child_env_fn::operator() becomes conditional on constructing the owned data, and the transparent branch of the subscribe machinery accounts for that construction in its computed noexcept. Refs #2305. --- include/exec/sequence_senders.hpp | 21 ++- .../exec/sequence/test_write_env_sequence.cpp | 152 ++++++++++++++++++ 2 files changed, 168 insertions(+), 5 deletions(-) diff --git a/include/exec/sequence_senders.hpp b/include/exec/sequence_senders.hpp index 17dd704a5..c5a716a83 100644 --- a/include/exec/sequence_senders.hpp +++ b/include/exec/sequence_senders.hpp @@ -264,10 +264,15 @@ namespace experimental::execution template struct __child_env_fn { - auto operator()(_Env __env, _Data const & __data) const noexcept - -> STDEXEC::__join_env_t<_Data const &, _Env> + static_assert(STDEXEC::__nothrow_move_constructible<_Data>); + + template + auto operator()(_Env __env, _DataFwd&& __data) const + noexcept(STDEXEC::__nothrow_constructible_from<_Data, _DataFwd&&>) + -> STDEXEC::__join_env_t<_Data, _Env> { - return STDEXEC::__env::__join(__data, static_cast<_Env&&>(__env)); + return STDEXEC::__env::__join(_Data(static_cast<_DataFwd&&>(__data)), + static_cast<_Env&&>(__env)); } }; }; @@ -1010,8 +1015,14 @@ namespace experimental::execution using __rcvr_t = __adaptor_rcvr<_Receiver, __child_env_t>; using __result_t = STDEXEC::__call_result_t; __check_operation_state<__result_t>(); + using __xform_t = __adaptor_child_env_fn_t<__tag_t, env_of_t<_Receiver>, __data_t>; + using __data_fwd_t = decltype(STDEXEC::__forward_like<__tfx_seq_t>( + __declval<__data_t&>())); constexpr bool __nothrow_subscribe = __nothrow_callable; - return __declfn<__result_t, __nothrow_subscribe && __nothrow_tfx_seq>(); + constexpr bool __nothrow_child_env = + __nothrow_callable<__xform_t, env_of_t<_Receiver>, __data_fwd_t>; + return __declfn<__result_t, + __nothrow_subscribe && __nothrow_child_env && __nothrow_tfx_seq>(); } else if constexpr (__subscribable_with_static_member<__tfx_seq_t, _Receiver>) { @@ -1084,7 +1095,7 @@ namespace experimental::execution __adaptor_rcvr<_Receiver, __child_env_t>{ static_cast<_Receiver&&>(__rcvr), __xform_t{}(static_cast(__env), - STDEXEC::__forward_like(__data))}); + STDEXEC::__forward_like<__tfx_seq_t>(__data))}); } else if constexpr (__subscribable_with_static_member<__tfx_seq_t, _Receiver>) { // NOLINT(bugprone-branch-clone) diff --git a/test/exec/sequence/test_write_env_sequence.cpp b/test/exec/sequence/test_write_env_sequence.cpp index 9b8aea31d..f6b5ea923 100644 --- a/test/exec/sequence/test_write_env_sequence.cpp +++ b/test/exec/sequence/test_write_env_sequence.cpp @@ -29,6 +29,7 @@ #include #include +#include #include namespace @@ -150,4 +151,155 @@ namespace | exec::ignore_all_values()); CHECK(value == 42); } + + // Lifetime tests for issue #2305: `write_env` over a sequence sender injects + // its data into the environment the child is subscribed with, and the + // resulting operation state must own that data rather than reference the + // (long-dead) sender. + + struct read_env_query : STDEXEC::__query + { + static consteval auto query(STDEXEC::forwarding_query_t) noexcept -> bool + { + return true; + } + }; + + inline constexpr int poisoned_value = -1; + + struct injected_env_data + { + int value; + + explicit injected_env_data(int value) noexcept + : value(value) + {} + + injected_env_data(injected_env_data const &) = default; + injected_env_data(injected_env_data&&) noexcept = default; + + // Poison the value on destruction so that a read through a dangling + // reference after the sender is gone fails deterministically. + ~injected_env_data() + { + value = poisoned_value; + } + + auto query(read_env_query) const noexcept -> int + { + return value; + } + }; + + struct move_only_env_data + { + int value; + + explicit move_only_env_data(int value) noexcept + : value(value) + {} + + move_only_env_data(move_only_env_data&&) noexcept = default; + move_only_env_data(move_only_env_data const &) = delete; + + ~move_only_env_data() + { + value = poisoned_value; + } + + auto query(read_env_query) const noexcept -> int + { + return value; + } + }; + + // A sequence sender that reads an injected query out of its receiver's + // environment when started. + struct env_reading_sequence + { + using sender_concept = exec::sequence_sender_tag; + using item_types = exec::item_types; + using completion_signatures = + STDEXEC::completion_signatures; + + int* observed_; + + template + struct op + { + using operation_state_concept = STDEXEC::operation_state_t; + Rcvr rcvr_; + int* observed_; + + void start() noexcept + { + *observed_ = read_env_query{}(STDEXEC::get_env(rcvr_)); + STDEXEC::set_value(static_cast(rcvr_)); + } + }; + + template + auto subscribe(Rcvr rcvr) const -> op + { + return op{static_cast(rcvr), observed_}; + } + }; + + struct test_sequence_rcvr + { + using receiver_concept = STDEXEC::receiver_tag; + + template + auto set_next(Item&& item) noexcept + { + return static_cast(item); + } + + void set_value() noexcept {} + + void set_stopped() noexcept {} + + template + void set_error(Error&&) noexcept + {} + + auto get_env() const noexcept + { + return STDEXEC::prop(STDEXEC::get_stop_token, STDEXEC::inplace_stop_token{}); + } + }; + + TEST_CASE("write_env's env data outlives a temporary sequence sender", "[sequence][write_env]") + { + int observed = 0; + auto op = exec::subscribe(STDEXEC::write_env(env_reading_sequence{&observed}, + injected_env_data{42}), + test_sequence_rcvr{}); + STDEXEC::start(op); + CHECK(observed == 42); + } + + TEST_CASE("write_env with a move-only env over a sequence sender", "[sequence][write_env]") + { + int observed = 0; + auto op = exec::subscribe(STDEXEC::write_env(env_reading_sequence{&observed}, + move_only_env_data{42}), + test_sequence_rcvr{}); + STDEXEC::start(op); + CHECK(observed == 42); + } + + TEST_CASE("write_env's env data outlives a named sequence sender", "[sequence][write_env]") + { + int observed = 0; + using sndr_t = decltype(STDEXEC::write_env(env_reading_sequence{}, injected_env_data{0})); + using op_t = exec::subscribe_result_t; + std::optional sndr; + sndr.emplace(STDEXEC::write_env(env_reading_sequence{&observed}, injected_env_data{42})); + std::optional op; + op.emplace(exec::subscribe(*sndr, test_sequence_rcvr{})); + sndr.reset(); + STDEXEC::start(*op); + CHECK(observed == 42); + } } // namespace From e55ee1abc0206d55db0f65bf636eb884cc1a653f Mon Sep 17 00:00:00 2001 From: alwaysprince05 Date: Sat, 10 Oct 2026 12:18:29 +0530 Subject: [PATCH 2/3] fix(exec): detect transparent adaptors' child env fn on gcc 12 gcc 12 mishandles a `typename T::template X<...>` type-requirement written directly inside a requires-expression: it is silently unsatisfied when T is dependent, and ill-formed when it is not. The detection in __adaptor_child_env_fn therefore always fell through to __identity_child_env_fn, and the child of a transparent sequence adaptor was subscribed with the receiver's environment alone instead of the adaptor's data joined into it. Name the nested type through an alias, which gcc 12 handles, so the detection picks the adaptor's __child_env_fn as written. The new write_env sequence tests are what exposed this: they read the injected query back from the child's environment, which cannot compile while the identity branch is taken. --- include/exec/sequence_senders.hpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/include/exec/sequence_senders.hpp b/include/exec/sequence_senders.hpp index c5a716a83..5a0149b1a 100644 --- a/include/exec/sequence_senders.hpp +++ b/include/exec/sequence_senders.hpp @@ -285,6 +285,17 @@ namespace experimental::execution STDEXEC::__minvocable_q> && __sequence_adaptor_traits>>::__transparent; + // The custom child-environment transformation an adaptor's traits define + // (`__sequence_adaptor_traits<_Tag>::__child_env_fn<_Env, _Data>`), if any. + // Named through an alias because gcc 12 mishandles a `typename T::template + // X<...>` type-requirement written directly inside a requires-expression: + // it silently reports such a requirement as unsatisfied for a dependent T + // (and rejects a non-dependent one outright), which would silently fall + // every transparent adaptor back to the identity transformation. + template + using __custom_child_env_fn_t = + typename __sequence_adaptor_traits<_Tag>::template __child_env_fn<_Env, _Data>; + // The function object that transforms the environment of a transparent // sequence adaptor: the adaptor's traits may define a nested // `__child_env_fn<_Env, _Data>` function object type; otherwise, the @@ -292,11 +303,9 @@ namespace experimental::execution template consteval auto __adaptor_child_env_fn() { - if constexpr (requires { - typename __sequence_adaptor_traits<_Tag>::template __child_env_fn<_Env, _Data>; - }) + if constexpr (requires { typename __custom_child_env_fn_t<_Tag, _Env, _Data>; }) { - return typename __sequence_adaptor_traits<_Tag>::template __child_env_fn<_Env, _Data>{}; + return __custom_child_env_fn_t<_Tag, _Env, _Data>{}; } else { From 85e1f827f9eb78ca9d89d8793fcc49a77dab0a90 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Sat, 10 Oct 2026 13:13:42 -0700 Subject: [PATCH 3/3] remove superfluous typename keyword --- include/exec/sequence_senders.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/exec/sequence_senders.hpp b/include/exec/sequence_senders.hpp index 5a0149b1a..e62992ac2 100644 --- a/include/exec/sequence_senders.hpp +++ b/include/exec/sequence_senders.hpp @@ -294,7 +294,7 @@ namespace experimental::execution // every transparent adaptor back to the identity transformation. template using __custom_child_env_fn_t = - typename __sequence_adaptor_traits<_Tag>::template __child_env_fn<_Env, _Data>; + __sequence_adaptor_traits<_Tag>::template __child_env_fn<_Env, _Data>; // The function object that transforms the environment of a transparent // sequence adaptor: the adaptor's traits may define a nested