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