c++c++20coroutinememory-corruptionaggregate-initialization

Double-free error in coroutine that takes aggregate parameter


The following code works correctly in the newest versions of GCC and Clang, but results in a double-free in GCC 11 to GCC 12, as illustrated at https://godbolt.org/z/41zcdGv93.

#include <coroutine>
#include <iostream>
#include <string>

struct Coro{
    struct Promise;
    struct Awaiter{
        bool await_ready(){return true;}
        void await_resume(){}
        void await_suspend(std::coroutine_handle<Promise> coro){}
    };
    struct Promise{
        Coro get_return_object(){return {};}
        auto initial_suspend() noexcept{return std::suspend_never{};}
        auto final_suspend() noexcept{return std::suspend_never{};}
        void unhandled_exception(){}
        Awaiter await_transform(const Coro&){
            return {};
        }
        void return_void(){}
    };
    using promise_type = Promise;
};

struct Aggregate{
    //(*)
    //Aggregate(const std::string& s):s{s}{}
    std::string s;
};

static_assert(std::is_aggregate_v<Aggregate>);

Coro f1(Aggregate){
    co_return;
}

Coro f2(std::string){
    co_return;
}

Coro g(){
    std::string foo{"foo"};
    // 1.
    co_await f1(Aggregate{"/"+foo});
    // 2.
    //co_await f2("/" + foo);
    // 3.
    //Coro c = f1(Aggregate{"/"+foo});
    //co_await c;
}

int main(){
    g();
}

Is this due to a compiler bug or undefined behavior, and in the former case, what are bug reports associated with it, and in the latter case, why is it undefined behavior?

The double-free goes away under any of the following modifications:

Changing f1 to take its parameter by reference instead of by value does not help.

The double free can clearly be seen from the assembly generated by gcc 12.3 starting from line 825 in the Godbolt link:

        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
        jmp     .L168
        mov     rbx, rax
.L168:
        mov     rax, QWORD PTR [rbp-40]
        add     rax, 40
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
        mov     rax, rbx

Solution

  • After hitting the bug again in December, I looked deeper into the link provided by the comment by @康桓瑋, and I found a more relevant bug report, which is bug 107288. The snippet in the bug report is

    #include <boost/asio/awaitable.hpp>
    #include <boost/asio/co_spawn.hpp>
    #include <boost/asio/detached.hpp>
    #include <boost/asio/io_context.hpp>
    
    namespace asio = boost::asio;
    
    struct foo
    {
        std::string s;
        int         i;
    };
    
    struct bar : foo
    {
        bar(std::string s, int i)
        : foo { .s = std::move(s), .i = i }
        {
        }
    };
    
    asio::awaitable< void >
    co_foo(foo)
    {
        std::printf("%s\n", __func__);
        co_return;
    };
    
    asio::awaitable< void >
    co_bar(foo)
    {
        std::printf("%s\n", __func__);
        co_return;
    };
    
    asio::awaitable< void >
    co_test()
    {
        // this works
        co_await co_bar(bar("Hello, World!", 1));
        // this works but this crashes
        co_await co_foo({ .s = "Hello, World!", .i = 1 });
    }
    
    int
    main()
    {
        asio::io_context ioc;
        asio::co_spawn(ioc, co_test(), asio::detached);
        ioc.run();
    }
    

    So yes, this is a gcc bug, and it has been fixed in gcc 13. There are comments in the bug report stating that this bug and bug 103909 commented by @康桓瑋 are related and I believe it is actually the case.