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 107465 : symmetric_transfer(std::coroutine_handle<> h) noexcept
119 : {
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 : }
187 : }
188 : #endif
189 :
190 : // Helper to normalize await_suspend return types to std::coroutine_handle<>
191 : template<typename Awaitable>
192 256 : std::coroutine_handle<> call_await_suspend(
193 : Awaitable* a,
194 : std::coroutine_handle<> h,
195 : await_context const* env)
196 : {
197 : using R = decltype(a->await_suspend(h, env));
198 : if constexpr (std::is_void_v<R>)
199 : {
200 1 : a->await_suspend(h, env);
201 1 : return std::noop_coroutine();
202 : }
203 : else if constexpr (std::is_same_v<R, bool>)
204 : {
205 232 : if(a->await_suspend(h, env))
206 1 : return std::noop_coroutine();
207 231 : return h;
208 : }
209 : else
210 : {
211 23 : return a->await_suspend(h, env);
212 : }
213 : }
214 :
215 : } // namespace detail
216 : } // namespace capy
217 : } // namespace boost
218 :
219 : #endif
|