From 687dd51527a717113bf45bb1c95752fbf8ab5b6e Mon Sep 17 00:00:00 2001 From: alwaysprince05 Date: Wed, 7 Oct 2026 03:55:13 +0530 Subject: [PATCH] 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