Skip to content

Commit 9d79af8

Browse files
Fix GCC 14 coroutine lambda capture corruption
GCC 14 does not properly copy the lambda's implicit object parameter into the coroutine frame, storing a dangling stack reference instead. When the coroutine is later resumed, accessing lambda captures through the dangling reference causes a segfault. Fix by storing the callable on the heap in CoroTaskRunner::init() via a type-erased FuncStore wrapper. This ensures the lambda (and its captures) outlive the coroutine frame. Also update CoroTask_test.cpp to use explicit pointer-by-value captures instead of [&] as a defense-in-depth measure, and add 3 new tests for CoroTask<T> value-returning coroutines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 768694f commit 9d79af8

File tree

3 files changed

+87
-49
lines changed

3 files changed

+87
-49
lines changed

include/xrpl/core/CoroTaskRunner.ipp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ template <class F>
1515
void
1616
JobQueue::CoroTaskRunner::init(F&& f)
1717
{
18-
task_ = std::forward<F>(f)(shared_from_this());
18+
// Store the callable on the heap so it outlives the coroutine frame.
19+
// GCC 14 does not copy the lambda object into the coroutine frame;
20+
// it stores a dangling reference instead. Keeping the callable
21+
// alive here ensures the coroutine's captures remain valid.
22+
using Fn = std::decay_t<F>;
23+
auto store = std::make_unique<FuncStore<Fn>>(std::forward<F>(f));
24+
task_ = store->func(shared_from_this());
25+
storedFunc_ = std::move(store);
1926
}
2027

2128
inline JobQueue::CoroTaskRunner::~CoroTaskRunner()

include/xrpl/core/JobQueue.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,25 @@ class JobQueue : private Workers::Callback
134134
std::mutex mutex_run_;
135135
std::condition_variable cv_;
136136
CoroTask<void> task_;
137+
138+
// Type-erased storage to keep the coroutine callable alive.
139+
// GCC 14 does not copy the lambda's implicit object parameter
140+
// into the coroutine frame, so we must ensure the callable
141+
// outlives the coroutine by storing it on the heap here.
142+
struct FuncBase
143+
{
144+
virtual ~FuncBase() = default;
145+
};
146+
template <class F>
147+
struct FuncStore : FuncBase
148+
{
149+
F func;
150+
explicit FuncStore(F&& f) : func(std::move(f))
151+
{
152+
}
153+
};
154+
std::unique_ptr<FuncBase> storedFunc_;
155+
137156
#ifndef NDEBUG
138157
bool finished_ = false;
139158
#endif

src/test/core/CoroTask_test.cpp

Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ class CoroTask_test : public beast::unit_test::suite
3939
}
4040
};
4141

42+
// NOTE: All coroutine lambdas passed to postCoroTask use explicit
43+
// pointer-by-value captures instead of [&] to work around a GCC 14
44+
// bug where reference captures in coroutine lambdas are corrupted
45+
// in the coroutine frame.
46+
4247
// Test: CoroTask<void> runs to completion
4348
void
4449
testVoidCompletion()
@@ -55,8 +60,8 @@ class CoroTask_test : public beast::unit_test::suite
5560

5661
gate g;
5762
auto runner = env.app().getJobQueue().postCoroTask(
58-
jtCLIENT, "CoroTaskTest", [&](auto) -> CoroTask<void> {
59-
g.signal();
63+
jtCLIENT, "CoroTaskTest", [gp = &g](auto) -> CoroTask<void> {
64+
gp->signal();
6065
co_return;
6166
});
6267
BEAST_EXPECT(runner);
@@ -83,11 +88,13 @@ class CoroTask_test : public beast::unit_test::suite
8388
gate g1, g2;
8489
std::shared_ptr<JobQueue::CoroTaskRunner> r;
8590
auto runner = env.app().getJobQueue().postCoroTask(
86-
jtCLIENT, "CoroTaskTest", [&](auto runner) -> CoroTask<void> {
87-
r = runner;
88-
g1.signal();
91+
jtCLIENT,
92+
"CoroTaskTest",
93+
[rp = &r, g1p = &g1, g2p = &g2](auto runner) -> CoroTask<void> {
94+
*rp = runner;
95+
g1p->signal();
8996
co_await runner->suspend();
90-
g2.signal();
97+
g2p->signal();
9198
co_return;
9299
});
93100
BEAST_EXPECT(runner);
@@ -115,10 +122,10 @@ class CoroTask_test : public beast::unit_test::suite
115122

116123
gate g;
117124
env.app().getJobQueue().postCoroTask(
118-
jtCLIENT, "CoroTaskTest", [&](auto runner) -> CoroTask<void> {
125+
jtCLIENT, "CoroTaskTest", [gp = &g](auto runner) -> CoroTask<void> {
119126
runner->post();
120127
co_await runner->suspend();
121-
g.signal();
128+
gp->signal();
122129
co_return;
123130
});
124131
BEAST_EXPECT(g.wait_for(5s));
@@ -141,13 +148,13 @@ class CoroTask_test : public beast::unit_test::suite
141148
gate g;
142149
int step = 0;
143150
env.app().getJobQueue().postCoroTask(
144-
jtCLIENT, "CoroTaskTest", [&](auto runner) -> CoroTask<void> {
145-
step = 1;
151+
jtCLIENT, "CoroTaskTest", [sp = &step, gp = &g](auto runner) -> CoroTask<void> {
152+
*sp = 1;
146153
co_await JobQueueAwaiter{runner};
147-
step = 2;
154+
*sp = 2;
148155
co_await JobQueueAwaiter{runner};
149-
step = 3;
150-
g.signal();
156+
*sp = 3;
157+
gp->signal();
151158
co_return;
152159
});
153160
BEAST_EXPECT(g.wait_for(5s));
@@ -185,20 +192,23 @@ class CoroTask_test : public beast::unit_test::suite
185192

186193
for (int i = 0; i < N; ++i)
187194
{
188-
jq.postCoroTask(jtCLIENT, "CoroTaskTest", [&, id = i](auto runner) -> CoroTask<void> {
189-
a[id] = runner;
190-
g.signal();
191-
co_await runner->suspend();
192-
193-
this->BEAST_EXPECT(*lv == -1);
194-
*lv = id;
195-
this->BEAST_EXPECT(*lv == id);
196-
g.signal();
197-
co_await runner->suspend();
198-
199-
this->BEAST_EXPECT(*lv == id);
200-
co_return;
201-
});
195+
jq.postCoroTask(
196+
jtCLIENT,
197+
"CoroTaskTest",
198+
[this, ap = &a, gp = &g, lvp = &lv, id = i](auto runner) -> CoroTask<void> {
199+
(*ap)[id] = runner;
200+
gp->signal();
201+
co_await runner->suspend();
202+
203+
this->BEAST_EXPECT(**lvp == -1);
204+
**lvp = id;
205+
this->BEAST_EXPECT(**lvp == id);
206+
gp->signal();
207+
co_await runner->suspend();
208+
209+
this->BEAST_EXPECT(**lvp == id);
210+
co_return;
211+
});
202212
BEAST_EXPECT(g.wait_for(5s));
203213
a[i]->join();
204214
}
@@ -238,8 +248,8 @@ class CoroTask_test : public beast::unit_test::suite
238248

239249
gate g;
240250
auto runner = env.app().getJobQueue().postCoroTask(
241-
jtCLIENT, "CoroTaskTest", [&](auto) -> CoroTask<void> {
242-
g.signal();
251+
jtCLIENT, "CoroTaskTest", [gp = &g](auto) -> CoroTask<void> {
252+
gp->signal();
243253
throw std::runtime_error("test exception");
244254
co_return;
245255
});
@@ -269,16 +279,18 @@ class CoroTask_test : public beast::unit_test::suite
269279
int counter = 0;
270280
std::shared_ptr<JobQueue::CoroTaskRunner> r;
271281
auto runner = env.app().getJobQueue().postCoroTask(
272-
jtCLIENT, "CoroTaskTest", [&](auto runner) -> CoroTask<void> {
273-
r = runner;
274-
++counter;
275-
g.signal();
282+
jtCLIENT,
283+
"CoroTaskTest",
284+
[rp = &r, cp = &counter, gp = &g](auto runner) -> CoroTask<void> {
285+
*rp = runner;
286+
++(*cp);
287+
gp->signal();
276288
co_await runner->suspend();
277-
++counter;
278-
g.signal();
289+
++(*cp);
290+
gp->signal();
279291
co_await runner->suspend();
280-
++counter;
281-
g.signal();
292+
++(*cp);
293+
gp->signal();
282294
co_return;
283295
});
284296
BEAST_EXPECT(runner);
@@ -316,10 +328,10 @@ class CoroTask_test : public beast::unit_test::suite
316328
gate g;
317329
int result = 0;
318330
auto runner = env.app().getJobQueue().postCoroTask(
319-
jtCLIENT, "CoroTaskTest", [&](auto) -> CoroTask<void> {
331+
jtCLIENT, "CoroTaskTest", [rp = &result, gp = &g](auto) -> CoroTask<void> {
320332
auto inner = []() -> CoroTask<int> { co_return 42; };
321-
result = co_await inner();
322-
g.signal();
333+
*rp = co_await inner();
334+
gp->signal();
323335
co_return;
324336
});
325337
BEAST_EXPECT(runner);
@@ -346,7 +358,7 @@ class CoroTask_test : public beast::unit_test::suite
346358
gate g;
347359
bool caught = false;
348360
auto runner = env.app().getJobQueue().postCoroTask(
349-
jtCLIENT, "CoroTaskTest", [&](auto) -> CoroTask<void> {
361+
jtCLIENT, "CoroTaskTest", [cp = &caught, gp = &g](auto) -> CoroTask<void> {
350362
auto inner = []() -> CoroTask<int> {
351363
throw std::runtime_error("inner error");
352364
co_return 0;
@@ -357,9 +369,9 @@ class CoroTask_test : public beast::unit_test::suite
357369
}
358370
catch (std::runtime_error const& e)
359371
{
360-
caught = true;
372+
*cp = true;
361373
}
362-
g.signal();
374+
gp->signal();
363375
co_return;
364376
});
365377
BEAST_EXPECT(runner);
@@ -386,14 +398,14 @@ class CoroTask_test : public beast::unit_test::suite
386398
gate g;
387399
int result = 0;
388400
auto runner = env.app().getJobQueue().postCoroTask(
389-
jtCLIENT, "CoroTaskTest", [&](auto) -> CoroTask<void> {
401+
jtCLIENT, "CoroTaskTest", [rp = &result, gp = &g](auto) -> CoroTask<void> {
390402
auto add = [](int a, int b) -> CoroTask<int> { co_return a + b; };
391-
auto mul = [&](int a, int b) -> CoroTask<int> {
403+
auto mul = [add](int a, int b) -> CoroTask<int> {
392404
int sum = co_await add(a, b);
393405
co_return sum * 2;
394406
};
395-
result = co_await mul(3, 4);
396-
g.signal();
407+
*rp = co_await mul(3, 4);
408+
gp->signal();
397409
co_return;
398410
});
399411
BEAST_EXPECT(runner);
@@ -421,7 +433,7 @@ class CoroTask_test : public beast::unit_test::suite
421433
env.app().getJobQueue().stop();
422434

423435
auto runner = env.app().getJobQueue().postCoroTask(
424-
jtCLIENT, "CoroTaskTest", [&](auto) -> CoroTask<void> { co_return; });
436+
jtCLIENT, "CoroTaskTest", [](auto) -> CoroTask<void> { co_return; });
425437
BEAST_EXPECT(!runner);
426438
}
427439

0 commit comments

Comments
 (0)