I'm currently investigating a crash scenario that is caused by performing a Boost yield inside a C++ catch block. Here's a minimal reproducible example that leads to a crash. Notice the following points :
- Multiple threads in a thread_group are needed to reproduce the crash.
- 2 coroutine must be running simultaneously.
- When yielding outside the catch block, everything works as expected.
- Issue reproduced consistently on both Mac/Win platforms (linux wasn't tested).
- When debugging the crash, the call stack was pointing on the yield inside the catch block.
- I've found this old forum that mentions a similar issue, but the explanation is unclear to me : .php
Here's a standalone reproduction example. Unfortunately I wasn't to make it shorter, but you can simply run it as-is linked to boost version 1.86-1.83
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/thread.hpp>
#include <iostream>
namespace asio = boost::asio;
using namespace std::chrono_literals;
void TimerCatchYield(asio::steady_timer& timer,
const asio::yield_context& yield) {
boost::system::error_code ec;
try {
throw std::runtime_error("");
} catch (const std::exception&) {
// async_wait(yield) here - causes crash
timer.async_wait(yield[ec]);
}
// async_wait(yield) here - works as expected
}
void coroutineA(asio::steady_timer& timer, const asio::yield_context& yield) {
std::cout << "Coroutine A: Starting timer...\n";
timer.expires_after(500ms);
TimerCatchYield(timer, yield);
std::cout << "Coroutine A: 1\n";
timer.expires_after(500ms);
TimerCatchYield(timer, yield);
std::cout << "Coroutine A: 2\n";
timer.expires_after(500ms);
TimerCatchYield(timer, yield);
std::cout << "Coroutine A: 3\n";
std::cout << "Coroutine A: all timers expired\n";
}
void coroutineB(asio::steady_timer& timer, const asio::yield_context& yield) {
std::cout << "Coroutine B: Starting timer...\n";
boost::system::error_code ec;
timer.expires_after(500ms);
timer.async_wait(yield[ec]);
std::cout << "Coroutine B: 1\n";
timer.expires_after(500ms);
timer.async_wait(yield[ec]);
std::cout << "Coroutine B: 2\n";
timer.expires_after(500ms);
timer.async_wait(yield[ec]);
std::cout << "Coroutine B: 3\n";
std::cout << "Coroutine B: all timers expired\n";
}
int main() {
asio::io_context io;
asio::steady_timer timerA(io);
asio::steady_timer timerB(io);
auto strand = make_strand(io);
boost::thread_group threads;
spawn(strand,
[&](const asio::yield_context& yield) { coroutineA(timerA, yield); });
spawn(strand, [&](const asio::yield_context& yield_b) {
coroutineB(timerB, yield_b);
});
for (int i = 0; i < 4; ++i) {
threads.create_thread([&io]() { io.run(); });
}
threads.join_all();
std::cout << "Main finished\n";
return 0;
}