Skip to content

Resume a posted waiter only while its awaiter is alive - #63

Merged
otamachan merged 1 commit into
mainfrom
guard-posted-resume
Oct 18, 2026
Merged

otamachan merged 1 commit into
mainfrom
guard-posted-resume

Conversation

@otamachan

@otamachan otamachan commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Channel::send / Channel::close, TfBuffer's transform arrival and send_goal's goal response post the waiting coroutine's resumption with a bare handle (ctx.post([h]() { h.resume(); })). If the task is destroyed after the post but before the executor runs it, the posted callback resumes a freed frame. The awaiter destructors from #56 cannot help, because the handle is already in the queue.

Add Waiting (executor.hpp), a coroutine handle owned by the awaiter, and post_resume, which resumes it through a weak_ptr when the posted callback runs. These awaiters now hand out a weak_ptr<Waiting> instead of the bare handle, so a resumption that runs late finds it expired with the awaiter. No awaiter needs a new destructor for this. Double resumption is still prevented by each awaiter's existing state (Channel's waiter_ under its mutex, TfBuffer's active, send_goal's done).

TimerStream is unchanged: its timer resumes synchronously, and its stop callback clears waiter_ on the spot.

Tests:

  • ChannelTest.SendThenWaiterDestroyed, ChannelTest.CloseThenWaiterDestroyed, TfBufferTest.TransformThenWaiterDestroyed, TaskDestroyTest.GoalResponseThenRequesterDestroyed: each posts the resumption and then destroys the task. Built with -fsanitize=address, all four report a heap-use-after-free on main and pass with this change.
  • TaskDestroyTest.OwnerDestroyedWhileItsTaskWaits: a node-like object that owns the CoContext, an Event, a TopicStream and the Task is destroyed while the task waits on the event, a sleep or the topic, and the frame must be freed with it. This already passes on main; it guards the destroy-on-drop behaviour.

colcon test passes (352 tests, ASan build, LeakSanitizer off).

This is an alternative to #60, which instead stops destroying a running frame. Under #60 the second test leaks the frame (the cancellation is posted to the context that is destroyed with the owner), and a task whose context outlives its owner unwinds into the destroyed owner.

Channel::send/close, TfBuffer's transform arrival and send_goal's goal
response post the waiting coroutine's resumption without checking that
it is still waiting. A task destroyed after the post but before the
executor runs it is then resumed from freed memory.

Add Waiting, owned by the awaiter, and post_resume, which resumes it
through a weak_ptr when the posted callback runs. A late resumption finds
the weak_ptr expired with the awaiter.

Add tests for the three cases, which hit a heap-use-after-free on main
under ASan, and one that destroys a node-like owner of the context and
its task while the task waits on an Event, a sleep or a topic.
@otamachan
otamachan merged commit 4ea20a3 into main Oct 18, 2026
6 checks passed
@otamachan
otamachan deleted the guard-posted-resume branch October 18, 2026 06:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants