Skip to content

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

Open
@Pele44

Description

@Pele44

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions