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 107407 : symmetric_transfer(std::coroutine_handle<> h) noexcept
119 : {
120 107407 : 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 256 : 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 1 : a->await_suspend(h, env);
227 1 : return std::noop_coroutine();
228 : }
229 : else if constexpr (std::is_same_v<R, bool>)
230 : {
231 232 : if(a->await_suspend(h, env))
232 1 : return std::noop_coroutine();
233 231 : return h;
234 : }
235 : else
236 : {
237 23 : return a->await_suspend(h, env);
238 : }
239 : }
240 :
241 : } // namespace detail
242 : } // namespace capy
243 : } // namespace boost
244 :
245 : #endif
|