From f4f9db69239713d7939ec899912c5bf34dc90e0b Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Wed, 11 Mar 2026 12:08:12 -0600 Subject: [PATCH] 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. --- include/boost/capy/ex/run_async.hpp | 40 ++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/include/boost/capy/ex/run_async.hpp b/include/boost/capy/ex/run_async.hpp index bc268602..42cef1a0 100644 --- a/include/boost/capy/ex/run_async.hpp +++ b/include/boost/capy/ex/run_async.hpp @@ -154,9 +154,25 @@ struct run_async_trampoline return {}; } - std::suspend_never final_suspend() noexcept + auto final_suspend() noexcept { - return {}; + // Use an explicit destroyer awaiter instead of suspend_never. + // MSVC <= 19.39 misallocates the await_suspend return buffer + // inside the coroutine frame; destroying the frame from + // await_suspend then causes use-after-free. A void-returning + // await_suspend avoids the problem. See: + // https://developercommunity.visualstudio.com/t/destroy-coroutine-from-final_suspend-r/10096047 + struct destroyer + { + bool await_ready() noexcept { return false; } + void await_suspend( + std::coroutine_handle<> h) noexcept + { + h.destroy(); + } + void await_resume() noexcept {} + }; + return destroyer{}; } void return_void() noexcept @@ -245,9 +261,25 @@ struct run_async_trampoline return {}; } - std::suspend_never final_suspend() noexcept + auto final_suspend() noexcept { - return {}; + // Use an explicit destroyer awaiter instead of suspend_never. + // MSVC <= 19.39 misallocates the await_suspend return buffer + // inside the coroutine frame; destroying the frame from + // await_suspend then causes use-after-free. A void-returning + // await_suspend avoids the problem. See: + // https://developercommunity.visualstudio.com/t/destroy-coroutine-from-final_suspend-r/10096047 + struct destroyer + { + bool await_ready() noexcept { return false; } + void await_suspend( + std::coroutine_handle<> h) noexcept + { + h.destroy(); + } + void await_resume() noexcept {} + }; + return destroyer{}; } void return_void() noexcept