Search code examples
c++stack-overflowc++20coroutinec++-coroutine

c++ coroutine continuous loop causes stack overflow


I got a stack overflow exception in visual studio inside a coroutine in a loop and found that the loop had a bug that prevented it from termination but I wondered why the stack was overflowed ? the coroutine might not even was using the stack but the heap instead and even if the stack was used there was no any recursive calls at all

after some experiments I could reproduce the crash with :

  • msvc 19.28
  • g++-10 on wsl and mingw64
  • clang-cl 10 on windows and clang++-10 on linux

this code causes the stack overflow:

#include <stdexcept>
#include <utility>
#include <cstdio>

#ifdef __clang__
#ifdef _WIN32
#pragma message "using clang coroutine header"
#include "clang-cl-coro.h"
#else
#pragma message "using coroutine experimental header"
#include <experimental/coroutine>
#endif

namespace std
{
    template<class P = void>
    using coroutine_handle = experimental::coroutine_handle<P>;

    using suspend_never = experimental::suspend_never;

    using suspend_always = experimental::suspend_always;

}

#else
#pragma message "using coroutine header"
#include <coroutine>
#endif

class vtask
{
    inline static size_t task_count = 0;

public:

    struct promise_type
    {
        inline static size_t promise_count = 0;

        std::coroutine_handle<> waiter;
        std::exception_ptr ex_ptr = nullptr;

        struct resume_waiter
        {
            inline static size_t awaiter_count = 0;

            std::coroutine_handle<> waiter;

            resume_waiter(std::coroutine_handle<> waiter) noexcept : waiter{ waiter }
            {
                ++awaiter_count;
                printf("[%zu] resume_waiter(std::coroutine_handle<> waiter)\n", awaiter_count);
            }

            ~resume_waiter()
            {
                --awaiter_count;
                printf("[%zu] ~resume_waiter()\n", awaiter_count);
            }

            bool await_ready() const noexcept { return false; }

            auto await_suspend(std::coroutine_handle<>) noexcept
            {
                return waiter;
            }

            void await_resume() const noexcept {}
        };

        promise_type()
        {
            ++promise_count;
            printf("[%zu] vtask::promise_type()\n", promise_count);

        }

        ~promise_type()
        {
            --promise_count;
            printf("[%zu] ~vtask::promise_type()\n", promise_count);
        }

        vtask get_return_object() { return { *this }; }

        constexpr std::suspend_always initial_suspend() noexcept { return {}; }

        resume_waiter final_suspend() const noexcept { return { waiter }; }

        void unhandled_exception() noexcept
        {
            ex_ptr = std::current_exception();
        }

        void return_void() const noexcept {}
    };

    vtask(promise_type& p) : coro{ std::coroutine_handle<promise_type>::from_promise(p) }
    {
        ++task_count;
        printf("[%zu] vtask(promise_type& p)\n", task_count);
    }

    vtask(vtask&& other) noexcept : coro{ std::exchange(other.coro, nullptr) }
    {
        ++task_count;
        printf("[%zu] vtask(vtask&& other)\n", task_count);
    }

    ~vtask()
    {
        if (coro)
            coro.destroy();
        --task_count;
        printf("[%zu] ~vtask()\n", task_count);
    }

    bool await_ready() const noexcept
    {
        return false;
    }

    void await_suspend(std::coroutine_handle<> waiter)
    {
        coro.promise().waiter = waiter;
        coro.resume();
    }

    void await_resume() noexcept {}

private:
    std::coroutine_handle<promise_type> coro;
};

struct detached_task
{
    struct promise_type
    {
        inline static size_t promise_count = 0;

        promise_type()
        {
            ++promise_count;
            printf("[%zu] detached_task::promise_type()\n", promise_count);
        }

        ~promise_type()
        {
            --promise_count;
            printf("[%zu] ~detached_task::promise_type()\n", promise_count);
        }

        detached_task get_return_object() { return {}; }

        std::suspend_never initial_suspend() noexcept { return {}; }

        std::suspend_never final_suspend() noexcept { return {}; }

        void unhandled_exception() noexcept
        {
            std::terminate();
        }

        constexpr void return_void() const noexcept {}
    };

    inline static size_t task_count = 0;

    detached_task()
    {
        ++task_count;
        printf("[%zu] detached_task()\n", task_count);
    }

    ~detached_task()
    {
        --task_count;
        printf("[%zu] ~detached_task()\n", task_count);
    }
};

vtask do_stackoverflow() { co_return; }

detached_task stackoverflow()
{
    for (;;)
        co_await do_stackoverflow();
}

int main()
{
    stackoverflow();
}

command lines used:

cl /std:c++latest coro-stackoverflow.cpp /EHsc for msvc

g++ -std=c++20 coro-stackoverflow.cpp -fcoroutines for mingw64

clang-cl /std:c++latest coro-stackoverflow.cpp /EHsc

g++-10 -std=c++20 coro-stackoverflow.cpp -fcoroutines -o overflow.bug on wsl

clang++-10 -std=c++20 -stdlib=libc++ coro-stackoverflow.cpp -o overflow-clang.bug on wsl

and this is the clang coro header on windows:

#pragma once

namespace std { namespace experimental { inline namespace coroutines_v1 {

template <typename R, typename...> struct coroutine_traits {
  using promise_type = typename R::promise_type;
};

template <typename Promise = void> struct coroutine_handle;

template <> struct coroutine_handle<void> {
  static coroutine_handle from_address(void *addr) noexcept {
    coroutine_handle me;
    me.ptr = addr;
    return me;
  }
  void operator()() { resume(); }
  void *address() const { return ptr; }
  void resume() const { __builtin_coro_resume(ptr); }
  void destroy() const { __builtin_coro_destroy(ptr); }
  bool done() const { return __builtin_coro_done(ptr); }
  coroutine_handle &operator=(decltype(nullptr)) {
    ptr = nullptr;
    return *this;
  }
  coroutine_handle(decltype(nullptr)) : ptr(nullptr) {}
  coroutine_handle() : ptr(nullptr) {}
//  void reset() { ptr = nullptr; } // add to P0057?
  explicit operator bool() const { return ptr; }

protected:
  void *ptr;
};

template <typename Promise> struct coroutine_handle : coroutine_handle<> {
  using coroutine_handle<>::operator=;

  static coroutine_handle from_address(void *addr) noexcept {
    coroutine_handle me;
    me.ptr = addr;
    return me;
  }

  Promise &promise() const {
    return *reinterpret_cast<Promise *>(
        __builtin_coro_promise(ptr, alignof(Promise), false));
  }
  static coroutine_handle from_promise(Promise &promise) {
    coroutine_handle p;
    p.ptr = __builtin_coro_promise(&promise, alignof(Promise), true);
    return p;
  }
};

  template <typename _PromiseT>
  bool operator==(coroutine_handle<_PromiseT> const& _Left,
    coroutine_handle<_PromiseT> const& _Right) noexcept
  {
    return _Left.address() == _Right.address();
  }

  template <typename _PromiseT>
  bool operator!=(coroutine_handle<_PromiseT> const& _Left,
    coroutine_handle<_PromiseT> const& _Right) noexcept
  {
    return !(_Left == _Right);
  }

  template <typename _PromiseT>
  bool operator==(coroutine_handle<_PromiseT> const& _Left,
     std::nullptr_t) noexcept
  {
      return _Left.address() == nullptr;
  }

  template <typename _PromiseT>
  bool operator==(std::nullptr_t, coroutine_handle<_PromiseT> const& _Right) noexcept
  {
      return _Right.address() == nullptr;
  }

  template <typename _PromiseT>
  bool operator!=(coroutine_handle<_PromiseT> const& _Left,
     std::nullptr_t) noexcept
  {


    return !(_Left == nullptr);
  }

  template <typename _PromiseT>
  bool operator!=(std::nullptr_t, coroutine_handle<_PromiseT> const& _Right) noexcept
  {
      return _Right.address() != nullptr;
  }

struct suspend_always {
  bool await_ready() { return false; }
  void await_suspend(coroutine_handle<>) {}
  void await_resume() {}
};
struct suspend_never {
  bool await_ready() { return true; }
  void await_suspend(coroutine_handle<>) {}
  void await_resume() {}
};

}}}

on windows the crash occurs early for msvc and clang-cl builds but mingw64 and wsl builds takes more time

gcc 10.1 which I used seems to have a knows bug where the task is constructed twice but destroyed once leaking a task each iteration which seems to cause the overflow

but clang and msvc don't have this bug and they also crash !

Edit: tried gcc 10.3 mingw64 and it doesn't have the mentioned gcc bug but it also cause a stack overflow ! even faster the the bugged compiler ! maybe this behavior is expected ?

I can't figure out which is wrong with the code above


Solution

  • The problem was in this part of code :

    void await_suspend(std::coroutine_handle<> waiter)
    {
        coro.promise().waiter = waiter;
        coro.resume(); 
    }
    

    this asymmetric transfer caused the stackoverflow and changing the code to :

    auto await_suspend(std::coroutine_handle<> waiter)
    {
        coro.promise().waiter = waiter;
        return coro; 
    }
    

    eliminates the problem on clang and msvc but gcc 10.3 still crashes, I think it doesn't support symmetric transfer yet