fix: Guard Coro::resume() against completed coroutines - #6608
Conversation
A documented race condition (JobQueue.h:354-377) allows post() to schedule a resume() job that executes after the coroutine has already completed. With boost::coroutine v1, calling operator() on a completed coroutine was technically UB but happened to be benign in Release builds (BOOST_ASSERT compiled out). After the switch to boost::coroutine2, the same call becomes genuine UB — the underlying boost::context::continuation is in a moved-from state, leading to crashes or memory corruption. The XRPL_ASSERT at Coro.ipp:79 caught this in CI Release builds with -Dassert=ON, surfacing as an intermittent assertion failure. Replace the assertion with an early-return guard: if coro_ is already exhausted when resume() acquires the mutex, clean up and return. This makes the documented race condition safe by design rather than relying on undefined behavior being benign. Update the race condition documentation and resume() doc comment to reflect the new behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #6608 +/- ##
=========================================
- Coverage 81.5% 81.5% -0.0%
=========================================
Files 998 998
Lines 74456 74456
Branches 7578 7558 -20
=========================================
- Hits 60648 60645 -3
- Misses 13808 13811 +3
🚀 New features to boost your workflow:
|
|
/ai-review |
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
There was a problem hiding this comment.
Two low-severity correctness concerns flagged inline: a potential LocalValues TLS lifecycle violation on the early-return path (line 76), and loss of diagnostic coverage from removing the assertion (line 86).
Review by Claude Opus 4.6 · Prompt: V12
|
@pratikmankawde are there any action items from the AI reviewer or can they be dismissed? |
No action needed. |
There was a problem hiding this comment.
Pull request overview
This PR hardens JobQueue::Coro::resume() against being invoked after a coroutine has already completed, addressing an intermittent CI assertion/UB triggered by the documented post()-before-yield() race in JobQueue.
Changes:
- Update
Coro::resume()to avoid invokingboost::coroutines2pull coroutines once they are completed. - Update
JobQueuecoroutine documentation to describe the completed-coroutine behavior and the boost::coroutine2 UB risk.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| include/xrpl/core/JobQueue.h | Updates API/race-condition documentation to reflect safe handling of late resume(). |
| include/xrpl/core/Coro.ipp | Adds a runtime guard so resume() does not call operator() on a completed coroutine. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
i see that you’re now suffering from the same pain I had yesterday |
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
No pain no gain ;-) |
There was a problem hiding this comment.
One observability concern flagged inline — the silent if (coro_) guard correctly prevents UB from the documented race, but also silently swallows unrelated double-resume bugs that the removed assert would have caught. A warning log in the else branch is recommended.
Review by Claude Opus 4.6 · Prompt: V12
| // completion. Calling operator() on a completed boost::coroutine2 is | ||
| // undefined behavior, so we must check and skip invoking the coroutine | ||
| // body if it has already completed. | ||
| if (coro_) |
There was a problem hiding this comment.
Silent guard swallows double-resume bugs too — add a diagnostic log in the else branch to preserve observability:
if (coro_)
{
coro_();
}
else
{
JLOG(j_.warn()) << "Coro::resume() called on completed coroutine — possible double-resume bug";
}
There was a problem hiding this comment.
@pratikmankawde I'll go ahead and merge this change, but if this comment is valid it will be easy to address in a follow-up PR.
There was a problem hiding this comment.
A double resume is quite rare as per the current architecture. So the else part is an extraneous detail to have. And it won't be needed after switching to std::coroutine. So, we can ignore this.
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
High Level Overview of Change
Guard
Coro::resume()against being called on an already-completed coroutine, fixing an intermittent assertion failure in CI.Context of Change
A documented race condition (
JobQueue.h:354-377) allowspost()to schedule aresume()job that executes after the coroutine has already completed. With boost::coroutine v1, callingoperator()on a completed coroutine was technically UB but happened to be benign in Release builds (BOOST_ASSERTcompiled out). After the switch to boost::coroutine2 (PR #6372), the same call becomes genuine UB — the underlyingboost::context::continuationis in a moved-from state.The
XRPL_ASSERTatCoro.ipp:79caught this in CI Release builds with-Dassert=ON, surfacing as an intermittent crash that killed the test process.Source analysis:
pull_coroutine_impl::pull(): guards withBOOST_ASSERT(!is_complete())— compiled out in Releasepull_control_block::resume(): no guard at all — unconditionally callsstd::move(c).resume()on completed continuation (UB)Type of Change
API Impact
libxrplchange (any change that may affectlibxrplor dependents oflibxrpl)Before / After
Before: A late
resume()call (from the documented race) hitsXRPL_ASSERT→ process abort in assert-enabled builds, or UB (callingoperator()on completed coroutine2) in non-assert builds.After: A late
resume()detects!coro_, cleans up (LocalValues,running_,cv_), and returns early. The race condition is handled safely by design.Test Plan
Jira: RIPD-5379