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 19.51 release loses the handler for
64 : a `try` region that spans the suspend point: after the
65 : coroutine is resumed from another call stack, a `throw`
66 : inside that region is not caught by the `catch` beside it and
67 : escapes to the promise's `unhandled_exception`. A `catch(...)`
68 : misses it too, so the region is not found at all rather than
69 : the handler failing to match. Debug builds are unaffected, as
70 : is x64 at the same toolset. `testCatchAfterDeferredResume` in
71 : test/unit/task.cpp covers this.
72 :
73 : `_M_ARM64EC` is included conservatively. It generates ARM64
74 : code and has not been tested here; keeping the workaround on
75 : is the safe direction, since it costs stack depth rather than
76 : correctness.
77 :
78 : The gate deliberately excludes Clang by testing
79 : `BOOST_CAPY_MSVC` rather than `_MSC_VER`. Both `clang-cl` and
80 : `clang++` targeting Windows define `_MSC_VER` for ABI
81 : compatibility, but generate a correct tail-call.
82 :
83 : Note that a probe which merely poisons the destroyed frame
84 : cannot validate this gate. Routing the return through this
85 : function moves the frame write to after `destroy()`, which
86 : repairs the poison pattern and hides the defect. The
87 : regression test in
88 : test/unit/detail/await_suspend_helper.cpp unmaps the frame
89 : instead, so any post-destroy access faults.
90 :
91 : On unaffected compilers the handle is returned directly for
92 : proper symmetric transfer.
93 :
94 : Callers must use `auto` return type on their `await_suspend`
95 : so the return type adapts per platform.
96 :
97 : @param h The coroutine handle to transfer to.
98 : */
99 : #if BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND
100 : inline void symmetric_transfer(std::coroutine_handle<> h) noexcept
101 : {
102 : // safe_resume is not needed here: the calling coroutine is
103 : // about to suspend unconditionally. When it later resumes,
104 : // await_resume restores TLS from the promise's environment.
105 : h.resume();
106 : }
107 : #else
108 : inline std::coroutine_handle<>
109 HIT 107446 : symmetric_transfer(std::coroutine_handle<> h) noexcept
110 : {
111 107446 : return h;
112 : }
113 : #endif
114 :
115 : // Helper to normalize await_suspend return types to std::coroutine_handle<>
116 : template<typename Awaitable>
117 256 : std::coroutine_handle<> call_await_suspend(
118 : Awaitable* a,
119 : std::coroutine_handle<> h,
120 : await_context const* env)
121 : {
122 : using R = decltype(a->await_suspend(h, env));
123 : if constexpr (std::is_void_v<R>)
124 : {
125 1 : a->await_suspend(h, env);
126 1 : return std::noop_coroutine();
127 : }
128 : else if constexpr (std::is_same_v<R, bool>)
129 : {
130 232 : if(a->await_suspend(h, env))
131 1 : return std::noop_coroutine();
132 231 : return h;
133 : }
134 : else
135 : {
136 23 : return a->await_suspend(h, env);
137 : }
138 : }
139 :
140 : } // namespace detail
141 : } // namespace capy
142 : } // namespace boost
143 :
144 : #endif
|