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 18:56:46 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 ARM64 release loses the handler for
      64                 :     a `try` region that spans the suspend point: a `throw`
      65                 :     inside that region is not caught by the `catch` beside it and
      66                 :     escapes to the promise's `unhandled_exception`. A `catch(...)`
      67                 :     misses it too, so the region is not found at all rather than
      68                 :     the handler failing to match. Any `await_suspend` that returns
      69                 :     a handle triggers it, whether the coroutine resumes at once
      70                 :     or later from another call stack, and whether the throw comes
      71                 :     from `await_resume` or from the body after it. A standalone
      72                 :     program without capy reproduces it on 19.44 and 19.51; the
      73                 :     first clause of the gate already covers 19.44. Debug builds
      74                 :     are unaffected, as is x64 at the same toolset.
      75                 :     `testCatchAfterDeferredResume` in test/unit/task.cpp covers
      76                 :     this.
      77                 : 
      78                 :     The void-returning transfer alone does not cover every case on
      79                 :     19.51. See `await_ready_then_suspend` below for the second
      80                 :     safeguard that `task` and `quitter` apply where this one is on.
      81                 : 
      82                 :     `_M_ARM64EC` is included conservatively. It generates ARM64
      83                 :     code and has not been tested here; keeping the workaround on
      84                 :     is the safe direction, since it costs stack depth rather than
      85                 :     correctness.
      86                 : 
      87                 :     The gate deliberately excludes Clang by testing
      88                 :     `BOOST_CAPY_MSVC` rather than `_MSC_VER`. Both `clang-cl` and
      89                 :     `clang++` targeting Windows define `_MSC_VER` for ABI
      90                 :     compatibility, but generate a correct tail-call.
      91                 : 
      92                 :     Note that a probe which merely poisons the destroyed frame
      93                 :     cannot validate this gate. Routing the return through this
      94                 :     function moves the frame write to after `destroy()`, which
      95                 :     repairs the poison pattern and hides the defect. The
      96                 :     regression test in
      97                 :     test/unit/detail/await_suspend_helper.cpp unmaps the frame
      98                 :     instead, so any post-destroy access faults.
      99                 : 
     100                 :     On unaffected compilers the handle is returned directly for
     101                 :     proper symmetric transfer.
     102                 : 
     103                 :     Callers must use `auto` return type on their `await_suspend`
     104                 :     so the return type adapts per platform.
     105                 : 
     106                 :     @param h The coroutine handle to transfer to.
     107                 : */
     108                 : #if BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND
     109                 : inline void symmetric_transfer(std::coroutine_handle<> h) noexcept
     110                 : {
     111                 :     // safe_resume is not needed here: the calling coroutine is
     112                 :     // about to suspend unconditionally. When it later resumes,
     113                 :     // await_resume restores TLS from the promise's environment.
     114                 :     h.resume();
     115                 : }
     116                 : #else
     117                 : inline std::coroutine_handle<>
     118 HIT      107465 : symmetric_transfer(std::coroutine_handle<> h) noexcept
     119                 : {
     120          107465 :     return h;
     121                 : }
     122                 : #endif
     123                 : 
     124                 : #if BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND
     125                 : /** Ask whether an awaitable is ready, and suspend on it if not.
     126                 : 
     127                 :     Used by the `transform_awaiter` of `task` and `quitter` where the
     128                 :     symmetric-transfer workaround is on. There the awaiter's own
     129                 :     `await_ready` returns `false`, and its `await_suspend` calls this
     130                 :     instead, so the wrapped awaitable's `await_ready` runs inside
     131                 :     `await_suspend` rather than at the start of the `co_await`.
     132                 : 
     133                 :     This avoids a second MSVC ARM64 release defect. When the wrapped
     134                 :     `await_ready` is evaluated at the start of the `co_await`, and
     135                 :     performs an atomic read-modify-write (as
     136                 :     `async_waker::wait_awaiter` does to consume a latched wakeup), a
     137                 :     `try` region spanning the `co_await` can lose its handler if a
     138                 :     call precedes the `try`. This happens even with the workaround
     139                 :     above in place. Observed on 19.51; not on 19.44. A plain or
     140                 :     atomic load in `await_ready` was not affected. The regression
     141                 :     tests are `testCatchAfterRmwAwaitReady` and
     142                 :     `testCatchAroundWakerWait` in test/unit/task.cpp, and
     143                 :     `testCatchAfterRmwAwaitReady` and
     144                 :     `testCatchStopAfterRmwAwaitReady` in test/unit/quitter.cpp.
     145                 : 
     146                 :     The awaiting coroutine continues without suspending when the
     147                 :     wrapped awaitable is ready, or when its `await_suspend` returns
     148                 :     `false`. When the wrapped `await_suspend` returns a handle, that
     149                 :     handle is resumed through `symmetric_transfer` and the awaiting
     150                 :     coroutine stays suspended. Returning `false` instead of resuming
     151                 :     inline keeps immediate completions from growing the stack.
     152                 : 
     153                 :     @param a The wrapped awaitable.
     154                 : 
     155                 :     @param h The coroutine performing the `co_await`.
     156                 : 
     157                 :     @param env The execution environment to pass to the wrapped
     158                 :     awaitable's `await_suspend`.
     159                 : 
     160                 :     @return `false` to continue without suspending, otherwise `true`.
     161                 : */
     162                 : template<class Awaitable>
     163                 : bool
     164                 : await_ready_then_suspend(
     165                 :     Awaitable& a,
     166                 :     std::coroutine_handle<> h,
     167                 :     await_context const* env) noexcept
     168                 : {
     169                 :     // Called exactly once: await_ready may consume state.
     170                 :     if(a.await_ready())
     171                 :         return false;
     172                 :     using R = decltype(a.await_suspend(h, env));
     173                 :     if constexpr (std::is_same_v<R, bool>)
     174                 :     {
     175                 :         return a.await_suspend(h, env);
     176                 :     }
     177                 :     else if constexpr (std::is_void_v<R>)
     178                 :     {
     179                 :         a.await_suspend(h, env);
     180                 :         return true;
     181                 :     }
     182                 :     else
     183                 :     {
     184                 :         symmetric_transfer(a.await_suspend(h, env));
     185                 :         return true;
     186                 :     }
     187                 : }
     188                 : #endif
     189                 : 
     190                 : // Helper to normalize await_suspend return types to std::coroutine_handle<>
     191                 : template<typename Awaitable>
     192             256 : std::coroutine_handle<> call_await_suspend(
     193                 :     Awaitable* a,
     194                 :     std::coroutine_handle<> h,
     195                 :     await_context const* env)
     196                 : {
     197                 :     using R = decltype(a->await_suspend(h, env));
     198                 :     if constexpr (std::is_void_v<R>)
     199                 :     {
     200               1 :         a->await_suspend(h, env);
     201               1 :         return std::noop_coroutine();
     202                 :     }
     203                 :     else if constexpr (std::is_same_v<R, bool>)
     204                 :     {
     205             232 :         if(a->await_suspend(h, env))
     206               1 :             return std::noop_coroutine();
     207             231 :         return h;
     208                 :     }
     209                 :     else
     210                 :     {
     211              23 :         return a->await_suspend(h, env);
     212                 :     }
     213                 : }
     214                 : 
     215                 : } // namespace detail
     216                 : } // namespace capy
     217                 : } // namespace boost
     218                 : 
     219                 : #endif
        

Generated by: LCOV version 2.3