Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

clang++ 14.0.2 compiled code crash when calling coroutine handle.destroy() in await suspend() #6724

Open
FunMiles opened this issue Apr 26, 2023 · 2 comments

Comments

@FunMiles
Copy link

I have an example of code that works with G++ 12, older Clang++ (v14.0.0 on macOS with experimental coroutines) and Clang++ in unoptimized compilation but crashes with -O2 or -O3 turned on. The crash happens when calling destroy on the handle of the suspended coroutine. The docs on cppreference.com states that when await_resume is called:

the coroutine is suspended (its coroutine state is populated with local variables and current suspension point). awaiter.await_suspend(handle) is called, where handle is the coroutine handle representing the current coroutine. Inside that function, the suspended coroutine state is observable via that handle, and it's this function's responsibility to schedule it to resume on some executor, or to be destroyed.

My code does just that. It destroys the calling subroutine and resumes another coroutine. Unfortunately, in macOS, Clang++ 14.0.2 (recent update) makes this code crash but only when -O2 or -O3 are used.

The below code is the minimum code I could build to demonstrate the problem. Note that almost any change will not be able to demonstrate the crash. For example, removing the loop in main (the part inside is called only once) will avoid the crash. The compiler does very aggressive optimization such that running the debugger does not allow to stop in several places that might be useful.

#include <iostream>
#if __has_include(<coroutine>)
#include <coroutine>
namespace crt = std;
#else
#include <experimental/coroutine>
namespace crt = std::experimental::coroutines_v1;
#endif

struct Timer {
    auto die_or_return(int t, bool destroy_handle=false)
    {
        struct awaiter {
            bool await_ready() const noexcept { return false; }

            void await_resume() const noexcept {}
            /* https://en.cppreference.com/w/cpp/language/coroutines states:
             * The coroutine is suspended (its coroutine state is populated with local variables and current suspension point).
             * awaiter.await_suspend(handle) is called, where handle is the coroutine handle representing the current coroutine.
             * Inside that function, the suspended coroutine state is observable via that handle,
             * and it's this function's responsibility to schedule it to resume on some executor,
             * or to be destroyed (returning false counts as scheduling)
             */
            crt::coroutine_handle<> await_suspend(crt::coroutine_handle<> h) noexcept
            {
                auto n = next;
                if (destroy_handle) {
                    h.destroy();
                }
                return n;
            }
            crt::coroutine_handle<> next;
            bool destroy_handle;
        };

        return awaiter{ender, destroy_handle};
    }

    crt::coroutine_handle<> ender = crt::noop_coroutine();
};

inline static bool active{true};

struct [[nodiscard]] autodestruct_task {
    struct promise_type {
        ~promise_type() { active = false; std::cout << "Dying" << std::endl; }
        autodestruct_task get_return_object() noexcept
        {
            return autodestruct_task{crt::coroutine_handle<promise_type>::from_promise(*this)};
        }

        crt::suspend_always initial_suspend() noexcept { return {}; }

        /// If the coroutine does not suspend at the end, its handle is automatically
        /// destroyed.
        crt::suspend_never final_suspend() noexcept {return {}; }

        void return_void() noexcept {}

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

    crt::coroutine_handle<promise_type> handle;

    explicit autodestruct_task(crt::coroutine_handle<promise_type> h) noexcept : handle(h) {}

};

autodestruct_task test_death(Timer &timer) {
    co_await timer.die_or_return(0,true);
}

autodestruct_task end_task() {
    std::cout << "Ending" << std::endl;
    co_return;
}

int main() {
    Timer timer;
    timer.ender = end_task().handle;
    auto tsk_handle = test_death(timer).handle;
    while (active && !tsk_handle.done()) {
        std::cout << "Resume" << std::endl;
        tsk_handle();
    }
}

The expected output:

Resume
Dying
Ending
Dying

Actual output:

Resume
Dying
**seg fault**
@FunMiles FunMiles changed the title clang++ code crash when calling coroutine handle.destroy() in await suspend() clang++ 14.0.2 compiled code crash when calling coroutine handle.destroy() in await suspend() Apr 27, 2023
@FunMiles
Copy link
Author

FunMiles commented Jul 4, 2023

The problem remains under 14.0.3.
Changing the one line auto n = next; to thread_local auto n = next; appears to be a workaround. But without somebody in the know figuring out why there is this bug, it may be a fragile workaround.

@FunMiles
Copy link
Author

FunMiles commented Feb 8, 2024

This problem exist in the original LLVM and is fixed by llvm@c467245
I will close this issue when the patch is brought into this fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant