LCOV - code coverage report
Current view: top level - capy/detail - await_suspend_helper.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 100.0 % 9 9
Test Date: 2026-10-23 20:10:35 Functions: 72.0 % 25 18 7

           TLA  Line data    Source code
       1                 : //
       2                 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
       3                 : // Copyright (c) 2026 Steve Gerbino
       4                 : // Copyright (c) 2026 Michael Vandeberg
       5                 : //
       6                 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
       7                 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
       8                 : //
       9                 : // Official repository: https://github.com/cppalliance/capy
      10                 : //
      11                 : 
      12                 : #ifndef BOOST_CAPY_DETAIL_AWAIT_SUSPEND_HELPER_HPP
      13                 : #define BOOST_CAPY_DETAIL_AWAIT_SUSPEND_HELPER_HPP
      14                 : 
      15                 : #include <coroutine>
      16                 : #include <boost/capy/ex/await_context.hpp>
      17                 : #include <boost/capy/detail/config.hpp>
      18                 : 
      19                 : #include <type_traits>
      20                 : 
      21                 : namespace boost {
      22                 : namespace capy {
      23                 : namespace detail {
      24                 : 
      25                 : /** Perform symmetric transfer, working around MSVC codegen bugs.
      26                 : 
      27                 :     MSVC stores the `std::coroutine_handle<>` returned from
      28                 :     `await_suspend` in a hidden `__$ReturnUdt$` variable located
      29                 :     on the coroutine frame. When another thread resumes or destroys
      30                 :     the frame between the store and the read-back for the
      31                 :     symmetric-transfer tail-call, the read hits freed memory.
      32                 : 
      33                 :     This occurs in two scenarios:
      34                 : 
      35                 :     @li `await_suspend` calls `h.destroy()` then returns a handle
      36                 :         (e.g. `when_all_runner` and `when_any_runner` final_suspend).
      37                 :         The return value is written to the now-destroyed frame.
      38                 : 
      39                 :     @li `await_suspend` hands the continuation to another thread
      40                 :         via an executor handoff (e.g. `post()` or `dispatch()`),
      41                 :         which may resume the parent. The parent can destroy this
      42                 :         frame before the runtime reads `__$ReturnUdt$` (e.g.
      43                 :         `boundary_trampoline` final_suspend).
      44                 : 
      45                 :     On affected compilers this function calls `h.resume()` on the
      46                 :     current stack and returns `void`, causing unconditional
      47                 :     suspension. The trade-off is O(n) stack growth instead of
      48                 :     O(1) tail-calls.
      49                 : 
      50                 :     This defect affects MSVC 19.34 through 19.44 on every target
      51                 :     and is fixed in MSVC 19.50 (VS 2026 / 18.0). Measured on
      52                 :     19.44 the caller builds the hidden return slot at
      53                 :     `__coro_frame_ptr$ + 0xC0`, on the coroutine frame; on 19.51
      54                 :     it is an `rsp`-relative stack temporary, so destroying the
      55                 :     frame no longer invalidates it.
      56                 : 
      57                 :     Do not widen this gate on the basis of Developer Community
      58                 :     ticket 10251975, tagged "Fixed in VS 2022 17.9 Preview 2";
      59                 :     19.39 reproduces the fault identically to 19.34.
      60                 : 
      61                 :     On ARM64 the workaround does not retire at 19.50, because
      62                 :     that target has a second, unrelated defect. With a real
      63                 :     symmetric transfer, MSVC 19.51 release loses the handler for
      64                 :     a `try` region that spans the suspend point: after the
      65                 :     coroutine is resumed from another call stack, a `throw`
      66                 :     inside that region is not caught by the `catch` beside it and
      67                 :     escapes to the promise's `unhandled_exception`. A `catch(...)`
      68                 :     misses it too, so the region is not found at all rather than
      69                 :     the handler failing to match. Debug builds are unaffected, as
      70                 :     is x64 at the same toolset. `testCatchAfterDeferredResume` in
      71                 :     test/unit/task.cpp covers this.
      72                 : 
      73                 :     `_M_ARM64EC` is included conservatively. It generates ARM64
      74                 :     code and has not been tested here; keeping the workaround on
      75                 :     is the safe direction, since it costs stack depth rather than
      76                 :     correctness.
      77                 : 
      78                 :     The gate deliberately excludes Clang by testing
      79                 :     `BOOST_CAPY_MSVC` rather than `_MSC_VER`. Both `clang-cl` and
      80                 :     `clang++` targeting Windows define `_MSC_VER` for ABI
      81                 :     compatibility, but generate a correct tail-call.
      82                 : 
      83                 :     Note that a probe which merely poisons the destroyed frame
      84                 :     cannot validate this gate. Routing the return through this
      85                 :     function moves the frame write to after `destroy()`, which
      86                 :     repairs the poison pattern and hides the defect. The
      87                 :     regression test in
      88                 :     test/unit/detail/await_suspend_helper.cpp unmaps the frame
      89                 :     instead, so any post-destroy access faults.
      90                 : 
      91                 :     On unaffected compilers the handle is returned directly for
      92                 :     proper symmetric transfer.
      93                 : 
      94                 :     Callers must use `auto` return type on their `await_suspend`
      95                 :     so the return type adapts per platform.
      96                 : 
      97                 :     @param h The coroutine handle to transfer to.
      98                 : */
      99                 : #if BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND
     100                 : inline void symmetric_transfer(std::coroutine_handle<> h) noexcept
     101                 : {
     102                 :     // safe_resume is not needed here: the calling coroutine is
     103                 :     // about to suspend unconditionally. When it later resumes,
     104                 :     // await_resume restores TLS from the promise's environment.
     105                 :     h.resume();
     106                 : }
     107                 : #else
     108                 : inline std::coroutine_handle<>
     109 HIT      107446 : symmetric_transfer(std::coroutine_handle<> h) noexcept
     110                 : {
     111          107446 :     return h;
     112                 : }
     113                 : #endif
     114                 : 
     115                 : // Helper to normalize await_suspend return types to std::coroutine_handle<>
     116                 : template<typename Awaitable>
     117             256 : std::coroutine_handle<> call_await_suspend(
     118                 :     Awaitable* a,
     119                 :     std::coroutine_handle<> h,
     120                 :     await_context const* env)
     121                 : {
     122                 :     using R = decltype(a->await_suspend(h, env));
     123                 :     if constexpr (std::is_void_v<R>)
     124                 :     {
     125               1 :         a->await_suspend(h, env);
     126               1 :         return std::noop_coroutine();
     127                 :     }
     128                 :     else if constexpr (std::is_same_v<R, bool>)
     129                 :     {
     130             232 :         if(a->await_suspend(h, env))
     131               1 :             return std::noop_coroutine();
     132             231 :         return h;
     133                 :     }
     134                 :     else
     135                 :     {
     136              23 :         return a->await_suspend(h, env);
     137                 :     }
     138                 : }
     139                 : 
     140                 : } // namespace detail
     141                 : } // namespace capy
     142                 : } // namespace boost
     143                 : 
     144                 : #endif
        

Generated by: LCOV version 2.3