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