1

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 :

  1. Multiple threads in a thread_group are needed to reproduce the crash.
  2. 2 coroutine must be running simultaneously.
  3. When yielding outside the catch block, everything works as expected.
  4. Issue reproduced consistently on both Mac/Win platforms (linux wasn't tested).
  5. When debugging the crash, the call stack was pointing on the yield inside the catch block.
  6. I've found this old forum that mentions a similar issue, but the explanation is unclear to me : https://lists.boost.org.cpp.al/boost-bugs/2016/02/44257.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;
}
2
  • Wait what. Is it suddenly about stackful coroutines? Your previous question had 0 (zero) mention of that, if I remember. Seems the linked article explains that Microsoft's ABI documentation (if not proprietary, I dunno) doesn't specify the behavior, so they cannot support it fully. Best bet is to indeed move the offending code out of the catch block? Commented Feb 17 at 22:06
  • On my Boost the default completion token fucks with your spawn calls, and it creates deferred operations that you discard? Also, is the difference between the comments (async_wait(yield)) and the actual code (async_wait(yield[ec])) relevant? I cannot reproduce the issue so I'm having to guess a little here. Of course, risking throwing from the catch block seems pushing the limits Commented Feb 17 at 22:56

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.