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:
(*)
so that Aggregate
is no longer an aggregate2.
and call f2
instead)f1
in a different line from co_await
(uncommenting the lines near 3.
)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
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.