SIGSEGV with `co_await (stdexec::get_scheduler() | ...);`
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
Research direction
Run the Godbolt reproducer and trace the await path through include/stdexec/__detail/__as_awaitable.hpp, especially __sender_awaiter::await_suspend and __get_continuation. Compare the reported regression with the workaround and the intended ordering example. Done means sync_wait does not return prematurely, the task coroutine is not destroyed before resumption, and the reproducer no longer reaches SIGSEGV.
Written by the indexing model from the issue text.
Description
Summary
Using co_await (ex::get_scheduler() | ex::let_value([](auto sch) { return ex::schedule(sch); })); in a task leads to a premature exit of sync_wait and a subsequent use-after-free of the coroutine.
This is a regression introduced with 482c2605918d3aadbd472960e207126a21809140 #2078
Tested with Clang and GCC on x86_64
Workaround
auto sch = co_await ex::get_scheduler();
co_await (ex::just(sch) | ex::let_value([](auto sch) { return ex::schedule(sch); }));
Reproducer
https://godbolt.org/z/WE9fs5WPv
#include <coroutine>
#include <stdexec/execution.hpp>
namespace ex = STDEXEC;
void foo()
{
int order[2] = {0, 0};
size_t idx = 0;
ex::sync_wait(
[&]() -> ex::task<void> {
order[idx++] = 1;
co_await (ex::get_scheduler() | ex::let_value([](auto sch) { return ex::schedule(sch); }));
order[idx++] = 2;
}());
}
void foo_workaround()
{
int order[2] = {0, 0};
size_t idx = 0;
ex::sync_wait(
[&]() -> ex::task<void> {
order[idx++] = 1;
auto sch = co_await ex::get_scheduler();
co_await (ex::just(sch) | ex::let_value([](auto sch) { return ex::schedule(sch); }));
order[idx++] = 2;
}());
}
int main()
{
foo(); // crashes with SIGSEGV probaly use after free
foo_workaround();
}
/* my usage:
namespace Tools
{
// Give control back to the scheduler.
struct Yield : ex::sender_adaptor_closure<Yield>
{
auto operator()() const
{
return ex::get_scheduler() | ex::let_value([](auto sch) { return ex::schedule(sch); });
}
template<class Sndr>
auto operator()(Sndr sndr) const;
};
inline constexpr Yield yield{};
template<class Sndr>
auto Yield::operator()(Sndr sndr) const
{
return std::move(sndr) | ex::let_value([](auto... value) {
return ex::when_all(ex::just(std::move(value)...), Tools::yield());
});
}
}
void foo_intended_use()
{
int order[4] = {0, 0, 0, 0};
size_t idx = 0;
ex::sync_wait(
ex::when_all(
[&]() -> ex::task<void> {
order[idx++] = 1;
auto sch = co_await ex::get_scheduler();
co_await Tools::yield();
order[idx++] = 2;
}(),
[&]() -> ex::task<void> {
order[idx++] = 3;
auto sch = co_await ex::get_scheduler();
co_await Tools::yield();
order[idx++] = 4;
}()));
assert(order[0] == 1);
assert(order[1] == 3);
assert(order[2] == 2);
assert(order[3] == 4);
}
*/
Analysis
We analyzed this on 307b83c5689ea7c2e5b31561cdc428697705333e and fee4d651494014610a277540f209cae56011e47f with our intended code, but without the when_all.
We debugged it and it seems, that after the co_await Tools::yield() the sync_wait returns, because __coroutine_unhandled_stopped is called. This leads then to the destruction of the coroutine handle and then a use-after-free once the run_loop wants to execute the rest of the task:
include/stdexec/__detail/__as_awaitable.hpp
// When the sender is known to complete inline, we can connect and start the operation
// in await_suspend.
template <class _Promise, sender_in<env_of_t<_Promise&>> _Sender>
requires __completes_inline<_Sender, env_of_t<_Promise&>>
struct __sender_awaiter<_Promise, _Sender>
: __sender_awaiter_base<__value_t<_Sender, _Promise>, true>
{
...
auto await_suspend([[maybe_unused]] __std::coroutine_handle<> __continuation)
-> __std::coroutine_handle<>
{
STDEXEC_ASSERT(this->__continuation_.handle() == __continuation);
{
auto __opstate = STDEXEC::connect(static_cast<_Sender&&>(__sndr_), __receiver_t(*this));
// The following call to start will complete synchronously, writing its result
// into the __result_ variant.
STDEXEC::start(__opstate); // yields continuation was added here
}
return this->__get_continuation(); // sync_wait will return after this.
}
...
};
...
[[nodiscard]]
constexpr auto __get_continuation() const noexcept -> __std::coroutine_handle<>
{
// If the operation was stopped (__result_ is valueless), we should use the
// unhandled_stopped() continuation. Otherwise, should resume the __continuation_
// as normal.
if (__result_.__is_valueless()) // after Tools::yield() __result_ is considered valueless because set_value was not called
{
return STDEXEC::__coroutine_unhandled_stopped(__continuation_); // sync_wait returns because of this
}
else
{
return __continuation_.handle();
}
}
- Dominant language
- C++
- Stars
- 2.4k
- Forks
- 270
- Avg merge
- 2d 17h
- Merged PRs (30d)
- 37
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from NVIDIA/stdexec
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 3/5 1-2 days Newbie friendliness 68/100
-
Difficulty 4/5 3-5 days Newbie friendliness 45/100
-
Difficulty 4/5 3-5 days Newbie friendliness 66/100
-
Difficulty 3/5 1-2 days Newbie friendliness 74/100
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
AXERA-TECH/ax-llm#77 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
games-on-whales/wolf#509 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
-
bug-unconfirmed
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
NVIDIA/cuda-samples#453 ·