Repository navigation
Resume a held handle only while its awaiter is alive - #64
Merged
Merged
Conversation
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.
Two awaiters hand a bare handle to something that outlives them: - GoalStream::cancel_goal() gives `this` and the handle to the action client's cancel callback. Destroying the waiting task before the cancel response arrives makes the callback write to the freed awaiter and resume the freed frame. - co_await on a Task stores the awaiting handle as the task's continuation. When the awaited Task is owned elsewhere and the awaiting task is destroyed first, the awaited task resumes the freed frame when it finishes. Both now go through Waiting, as the posted resumptions do. Task gets an Awaiter that lives in the awaiting frame and owns the Waiting, and the continuation becomes a weak_ptr to it; shield() does the same. Add a test for each, which hit a heap-use-after-free under ASan without this change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #63 (uses its
Waiting).#63 covered resumptions that are posted to the executor. Two awaiters instead hand a bare handle to something that outlives them, and resume it directly:
GoalStream::cancel_goal()gives[this, h]to the action client's cancel callback (co_context.hpp).CancelAwaiteris not cancellable and has no destructor, so destroying the waiting task before the cancel response arrives makes the callback writeresponseinto the freed awaiter and resume the freed frame.co_await taskstores the awaiting handle as the task'scontinuation. That is fine forco_await child(), where the child lives in the awaiting frame. When the awaitedTaskis owned elsewhere (e.g. a member, ortask.cancel(); co_await task;) and the awaiting task is destroyed first, the awaited task resumes the freed frame fromFinalAwaiterwhen it finishes.Both now go through
Waiting:CancelAwaiterowns aWaiting, and the callback holds aweak_ptrto it. It writes the response and resumes only while that is alive.Taskgets anAwaiter(viaoperator co_await) that lives in the awaiting frame and owns theWaiting.promise_type::continuationbecomes aweak_ptr<Waiting>, so it expires with the awaiting frame even when theTaskoutlives it.shield()owns itsWaitingthe same way.when_all/when_anykeep their own continuation and are unchanged.Tests (
test_task_destroy):CancelGoalResponseAfterRequesterDestroyed: the client answers in its own group; the task awaitingcancel_goal()is destroyed before that group spins.AwaitedTaskFinishesAfterAwaiterDestroyed: a task awaiting anothercreate_tasked task is destroyed while it waits.Built with
-fsanitize=address, both report a heap-use-after-free without this change and pass with it.colcon testpasses (354 tests, ASan build, LeakSanitizer off).