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