include/boost/capy/detail/await_suspend_helper.hpp

100.0% Lines (9 / 9) 100.0% Functions (2 / 2)
await_suspend_helper.hpp
f(x) Functions (2)
Line TLA Hits 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 107407x symmetric_transfer(std::coroutine_handle<> h) noexcept
119 {
120 107407x 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 When `completed` is given, a returned handle equal to `h` is
154 treated as a synchronous completion as well: it is not resumed,
155 `*completed` is set, and `false` is returned. `*completed` is set
156 on every synchronous completion and is never written after a real
157 suspension, when the frame may already be running elsewhere.
158 `quitter` uses this to hand over a synchronous result even if a
159 stop arrives. `task` passes nothing and keeps the inline resume.
160
161 @param a The wrapped awaitable.
162
163 @param h The coroutine performing the `co_await`.
164
165 @param env The execution environment to pass to the wrapped
166 awaitable's `await_suspend`.
167
168 @param completed If not null, set to `true` when the wrapped
169 awaitable completes synchronously.
170
171 @return `false` to continue without suspending, otherwise `true`.
172 */
173 template<class Awaitable>
174 bool
175 await_ready_then_suspend(
176 Awaitable& a,
177 std::coroutine_handle<> h,
178 await_context const* env,
179 bool* completed = nullptr) noexcept
180 {
181 // Called exactly once: await_ready may consume state.
182 if(a.await_ready())
183 {
184 if(completed)
185 *completed = true;
186 return false;
187 }
188 using R = decltype(a.await_suspend(h, env));
189 if constexpr (std::is_same_v<R, bool>)
190 {
191 if(a.await_suspend(h, env))
192 return true;
193 if(completed)
194 *completed = true;
195 return false;
196 }
197 else if constexpr (std::is_void_v<R>)
198 {
199 a.await_suspend(h, env);
200 return true;
201 }
202 else
203 {
204 std::coroutine_handle<> next = a.await_suspend(h, env);
205 if(completed && next == h)
206 {
207 *completed = true;
208 return false;
209 }
210 symmetric_transfer(next);
211 return true;
212 }
213 }
214 #endif
215
216 // Helper to normalize await_suspend return types to std::coroutine_handle<>
217 template<typename Awaitable>
218 256x std::coroutine_handle<> call_await_suspend(
219 Awaitable* a,
220 std::coroutine_handle<> h,
221 await_context const* env)
222 {
223 using R = decltype(a->await_suspend(h, env));
224 if constexpr (std::is_void_v<R>)
225 {
226 1x a->await_suspend(h, env);
227 1x return std::noop_coroutine();
228 }
229 else if constexpr (std::is_same_v<R, bool>)
230 {
231 232x if(a->await_suspend(h, env))
232 1x return std::noop_coroutine();
233 231x return h;
234 }
235 else
236 {
237 23x return a->await_suspend(h, env);
238 }
239 }
240
241 } // namespace detail
242 } // namespace capy
243 } // namespace boost
244
245 #endif
246