Skip to content

Commit f4f9db6

Browse files
committed
Fix MSVC thread_pool join test failure (49 of 50 tasks)
The run_async trampoline coroutine used suspend_never for final_suspend, relying on automatic frame destruction when the coroutine falls through. MSVC's symmetric transfer implementation (which uses an internal trampoline loop rather than true tail calls) can mishandle this pattern, potentially double-destroying the frame. When the work_guard destructor fires twice, outstanding_work_ reaches zero one task early, stop_ is set, and the remaining queued task is abandoned without its handler running. Replace suspend_never with an explicit destroyer awaiter that calls h.destroy() in await_suspend and returns void. This gives MSVC's symmetric transfer loop a clean exit point and avoids the problematic auto-destruction codepath. Both trampoline specializations (allocator-based and memory_resource*) are updated.
1 parent 55efc7f commit f4f9db6

1 file changed

Lines changed: 36 additions & 4 deletions

File tree

include/boost/capy/ex/run_async.hpp

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,25 @@ struct run_async_trampoline
154154
return {};
155155
}
156156

157-
std::suspend_never final_suspend() noexcept
157+
auto final_suspend() noexcept
158158
{
159-
return {};
159+
// Use an explicit destroyer awaiter instead of suspend_never.
160+
// MSVC <= 19.39 misallocates the await_suspend return buffer
161+
// inside the coroutine frame; destroying the frame from
162+
// await_suspend then causes use-after-free. A void-returning
163+
// await_suspend avoids the problem. See:
164+
// https://developercommunity.visualstudio.com/t/destroy-coroutine-from-final_suspend-r/10096047
165+
struct destroyer
166+
{
167+
bool await_ready() noexcept { return false; }
168+
void await_suspend(
169+
std::coroutine_handle<> h) noexcept
170+
{
171+
h.destroy();
172+
}
173+
void await_resume() noexcept {}
174+
};
175+
return destroyer{};
160176
}
161177

162178
void return_void() noexcept
@@ -245,9 +261,25 @@ struct run_async_trampoline<Ex, Handlers, std::pmr::memory_resource*>
245261
return {};
246262
}
247263

248-
std::suspend_never final_suspend() noexcept
264+
auto final_suspend() noexcept
249265
{
250-
return {};
266+
// Use an explicit destroyer awaiter instead of suspend_never.
267+
// MSVC <= 19.39 misallocates the await_suspend return buffer
268+
// inside the coroutine frame; destroying the frame from
269+
// await_suspend then causes use-after-free. A void-returning
270+
// await_suspend avoids the problem. See:
271+
// https://developercommunity.visualstudio.com/t/destroy-coroutine-from-final_suspend-r/10096047
272+
struct destroyer
273+
{
274+
bool await_ready() noexcept { return false; }
275+
void await_suspend(
276+
std::coroutine_handle<> h) noexcept
277+
{
278+
h.destroy();
279+
}
280+
void await_resume() noexcept {}
281+
};
282+
return destroyer{};
251283
}
252284

253285
void return_void() noexcept

0 commit comments

Comments
 (0)