SIGSEGV with `co_await (stdexec::get_scheduler() | ...);`

Aperta
#2,098 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
cpp
Ambito
backend

Direzione di ricerca

Esegui il riproduttore Godbolt e segui il percorso await attraverso include/stdexec/__detail/__as_awaitable.hpp, in particolare __sender_awaiter::await_suspend e __get_continuation. Confronta la regressione segnalata con il workaround e con l'esempio dell'ordinamento previsto. È completato quando sync_wait non restituisce prematuramente, la coroutine task non viene distrutta prima della ripresa e il riproduttore non raggiunge più SIGSEGV.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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();
        }
      }
Lingua principale
C++
Stelle
2.4k
Fork
270
Merge medio
2g 17h
PR unite (30g)
37

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di NVIDIA/stdexec

Tutte le issue di NVIDIA/stdexec

Issue simili

Altre issue su C++

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.