Description
In the code that is based on cobalt, crashes occur. When this code is run under address sanitizer, it detects a "heap-use-after-free" error. I managed to prepare a simplified scenario (maybe not the simplest possible) that leads to this memory violation.
#include <cassert>
#include <thread>
#include <boost/asio.hpp>
#include <boost/cobalt.hpp>
using namespace boost;
void runContext(asio::io_context& context)
{
cobalt::this_thread::set_executor(context.get_executor());
context.run();
}
class SingleThreadedWithContext
{
public:
SingleThreadedWithContext()
: executorWorkGuard(boost::asio::make_work_guard(ioContext)),
contextThread(std::jthread{runContext, std::ref(ioContext)})
{
}
~SingleThreadedWithContext() { executorWorkGuard.reset(); }
auto getExecutor() { return ioContext.get_executor(); }
private:
asio::io_context ioContext;
asio::executor_work_guard<decltype(ioContext)::executor_type> executorWorkGuard;
std::jthread contextThread;
};
cobalt::task<void> emptyTask()
{
co_return;
}
cobalt::promise<void> promisThatSpawnsEmptyTaskToAnotherThread(auto executorToSpawn)
{
co_await cobalt::spawn(executorToSpawn, emptyTask(), cobalt::use_op);
}
cobalt::task<void> makePromiseThatSpawnsToAnotherThreadAndCancelIt(auto executorToSpawn)
{
auto prom = promisThatSpawnsEmptyTaskToAnotherThread(executorToSpawn);
assert(prom);
prom.cancel();
co_await prom;
}
int main(int /*argc*/, const char** /*argv*/)
{
SingleThreadedWithContext threadA;
SingleThreadedWithContext threadB;
cobalt::spawn(threadA.getExecutor(),
makePromiseThatSpawnsToAnotherThreadAndCancelIt(threadB.getExecutor()),
asio::use_future)
.get();
return 0;
}
My analysis led me to the conclusion that when we call the spawn method, one of the variables (recs
) is allocated as a shared_ptr, but on a memory block that is probably saved on the coroutine frame (cobalt/include/boost/cobalt/detail/spawn.hpp:111 -> auto recs = std::allocate_shared<detail::task_receiver<void>>(alloc, std::move(a.receiver_));
)
When the coroutine ends, the frame is freed, along with the memory pointed to by the aforementioned shared_ptr. The problem is that access to this shared_ptr is done by another thread and can occur after the frame has been removed. From the point of view of shared_ptr, everything seems to be ok, because the reference count is positive, but because we allocated the object itself (with the counter) on a "local" piece of memory, the whole thing can lead to access to the freed memory.
if (sl.is_connected())
sl.assign(
[ex = exec, recs](asio::cancellation_type ct)
{
asio::dispatch(ex, [recs, ct] {recs->cancel(ct); // crash here -> recs point to freed memory });
});
e.g. changing allocation to auto recs = std::make_shared<detail::task_receiver<void>>(std::move(a.receiver_));
resolves a problem
gcc 14.1, example build with address sanitizer