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();
}
```

Reply via email to