boostorg / boostorg/cobalt

heap-use-after-free error when cancelling spawned awaitable

Open
#194 6 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
347
Forks
47
Avg merge
3d 8h
Merged PRs (30d)
1

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.
```cpp
#include
#include

#include
#include

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 executorWorkGuard;
std::jthread contextThread;
};

cobalt::task emptyTask()
{
co_return;
}

cobalt::promise promisThatSpawnsEmptyTaskToAnotherThread(auto executorToSpawn)
{
co_await cobalt::spawn(executorToSpawn, emptyTask(), cobalt::use_op);
}

cobalt::task 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>(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.

```cpp
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>(std::move(a.receiver_));` resolves a problem

gcc 14.1, example build with address sanitizer

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.