https://gcc.gnu.org/bugzilla/show_bug.cgi?id=121219
Bug ID: 121219 Summary: Coroutine `operator new` heap-use-after-free on trunk (16.0), regression from 15.1 Product: gcc Version: 16.0 Status: UNCONFIRMED Severity: normal Priority: P3 Component: c++ Assignee: unassigned at gcc dot gnu.org Reporter: lesha at meta dot com Target Milestone: --- See: https://godbolt.org/z/4vGY61bva -- the same code is included below. The code is `-Wall` / `-Wextra`-clean. Its logging shows that something very weird happens to control flow on GCC trunk. The godbolt link shows the behavior on: - gcc trunk (16.0) -- heap-use-after-free - gcc 15.1 -- works - clang trunk -- works - MSVC 19.latest + /std:c++20 also works, I omitted it to keep clutter down The root cause of the crash is the presence of a custom `operator new` on the coroutine promise type. Switching `#if 1` to `#if 0` eliminates the crash. Custom coroutine allocation is used in production code, e.g. the Pigweed library include non-heap-allocating coros for embedded systems. I have not tested whether Pigweed is affected, but it seems likely. ``` #include <coroutine> #include <iostream> #include <stdexcept> struct Task { struct promise_type; using handle_type = std::coroutine_handle<promise_type>; struct promise_type { Task* task_; int result_; #if 1 // change to 0 to fix crash static void* operator new(std::size_t size) noexcept { void* p = ::operator new(size, std::nothrow); std::cerr << "operator new (no arg) " << size << " -> " << p << std::endl; return p; } static void operator delete(void* ptr) noexcept { return ::operator delete(ptr, std::nothrow); } static Task get_return_object_on_allocation_failure() { std::cerr << "get_return_object_on_allocation_failure" << std::endl; return Task(nullptr); } #endif auto get_return_object() { std::cerr << "get_return_object" << std::endl; return Task{handle_type::from_promise(*this)}; } auto initial_suspend() { std::cerr << "initial_suspend" << std::endl; return std::suspend_always{}; } auto final_suspend() noexcept { std::cerr << "final_suspend" << std::endl; return std::suspend_never{}; // Coroutine auto-destructs } ~promise_type() { if (task_) { std::cerr << "promise_type destructor: Clearing Task handle" << std::endl; task_->h_ = nullptr; } } void unhandled_exception() { std::cerr << "unhandled_exception" << std::endl; std::terminate(); } void return_value(int value) { std::cerr << "return_value: " << value << std::endl; result_ = value; if (task_) { task_->result_ = value; task_->completed_ = true; } } }; handle_type h_; int result_; bool completed_ = false; Task(handle_type h) : h_(h) { std::cerr << "Task constructor" << std::endl; if (h_) { h_.promise().task_ = this; // Link promise to Task } } ~Task() { std::cerr << "~Task destructor" << std::endl; // Only destroy handle if still valid (coroutine not completed) if (h_) { std::cerr << "Destroying coroutine handle" << std::endl; h_.destroy(); } } bool done() const { return completed_ || !h_ || h_.done(); } void resume() { std::cerr << "Resuming task" << std::endl; if (h_) h_.resume(); } int result() const { if (!done()) throw std::runtime_error("Result not available"); return result_; } }; Task my_coroutine() { std::cerr << "Inside my_coroutine" << std::endl; co_return 42; } int main() { auto task = my_coroutine(); while (!task.done()) { std::cerr << "Resuming task in main" << std::endl; task.resume(); } std::cerr << "Task completed in main, printing result" << std::endl; return task.result(); } ```