fix(coro): protect selected completion wakes - #1050
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes completion-waiter use-after-free races by introducing a generation-checked wake lease before scheduling.
Changes:
- Adds slot-owned wake selection, claiming, and abandonment.
- Migrates all completion-waiter consumers.
- Adds lifetime regressions and updates documentation.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
include/elio/coro/detail/completion_waiter.hpp |
Implements wake leases and generation transitions. |
include/elio/coro/task.hpp |
Claims join completion wakes. |
include/elio/coro/task_handle.hpp |
Claims task completion wakes. |
include/elio/coro/task_group.hpp |
Preserves routed scheduling with leases. |
include/elio/sync/object_cache.hpp |
Protects release handoff wakes. |
include/elio/rdma_ibverbs/endpoint.hpp |
Protects pump-exit wakes. |
tests/unit/test_task.cpp |
Tests lease and task lifetime transitions. |
tests/unit/test_task_group.cpp |
Tests task-group abandonment. |
tests/unit/test_object_cache.cpp |
Tests release-wait abandonment. |
wiki/API-Contracts.md |
Documents the ownership boundary. |
CHANGELOG.md |
Records the lifetime fix. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@copilot Please review the updated pull request only. The new commit adds the requested deterministic RDMA pump-exit destroy-after-selection regression. |
Reviewed ffe253a. The new section in |
Description
Fixes a dequeue-to-schedule use-after-free in the common completion-waiter path. Completion previously copied a raw coroutine handle out of
completion_waiter_slot, cleared the registration, and scheduled the copied handle after unlocking. A concurrent destruction of the suspended coroutine could then reclaim the frame before scheduling used that handle.This change makes
take()select a slot-owned wake and return a move-only lease. The producer must claim that lease before it obtains the coroutine handle. Destruction of the registered waiter before claim abandons the selected generation, so claim returns an empty handle instead of scheduling freed frame storage.The supported boundary remains explicit: waiter destruction can revoke a registered or selected-but-unclaimed wake. Once
claim()transfers scheduling ownership, external forced destruction is outside this contract.Type of Change
Related Issues
Closes #1049
Related to #390
Related to #762
Related to #1046
Changes Made
Core Changes
completion_wake_leasebacked by slot-owned selected-handle state and an odd/even generation.completion_waiter_slot::take()from returning a raw coroutine handle to selecting one wake under the slot mutex and returning its lease.claim()and abandonment transitions. Waiter destruction and lease destruction can invalidate a selected wake before scheduling ownership transfers, while stale leases cannot affect a reused slot generation.take()while a generation is already selected return an empty lease without disturbing the first producer.API Changes (if applicable)
No public API changes. Public awaiter signatures and exception specifications are unchanged. The return-type change is confined to the internal
coro::detail::completion_waiter_slotprotocol.Before:
After:
Migration Guide (if breaking change)
Not applicable.
Testing
Unit Tests
Integration Tests
Sanitizer Testing
Test Results
All builds were out of source with
--parallel 2. The normal all-target run included enabled RDMA stub/CUDA lifetime and fork-helper targets. A direct libibverbs syntax build was unavailable because the host does not provideinfiniband/verbs.h.Checklist
Code Quality
Documentation
Testing
Compatibility
Performance (if applicable)
Screenshots / Diagrams
Not applicable.
Additional Notes
The fix adds no allocation. A same-source Release mechanism benchmark measured 5,000,000
register -> take -> ownershiptransitions per sample, pinned to one CPU, with 12 interleaved pairs. Both implementations reported zero allocations.The approximately 54% direct mechanism cost is expected: eliminating the UAF requires a second arbitration after selection so waiter destruction can abandon the selected generation before scheduling ownership transfers. This is a deliberately narrow mechanism diagnostic, not an end-to-end workload result. A one-child task-group spawn/join control measured approximately +0.8% by median (+1.2% paired median), where scheduling and state management amortize the extra arbitration.
On the tested GCC 12 x86_64 ABI:
completion_waiter_slotcompletion_waitercompletion_wake_leasejoin_state_basetask_state<void>/task_state<int>task_group_completion_stateThe cache-line-aligned join/task states absorb the slot growth without changing total size. Other one-shot states embedding an unaligned slot may grow by 16 bytes depending on ABI and surrounding layout.
Reviewer Guidance
Areas requiring special attention:
completion_waiter.hpp.claim().Questions for reviewers:
Thank you for contributing to Elio! 🎉