TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : // Copyright (c) 2026 Steve Gerbino
4 : //
5 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 : //
8 : // Official repository: https://github.com/cppalliance/capy
9 : //
10 :
11 : #ifndef BOOST_CAPY_DETAIL_AWAIT_SUSPEND_HELPER_HPP
12 : #define BOOST_CAPY_DETAIL_AWAIT_SUSPEND_HELPER_HPP
13 :
14 : #include <coroutine>
15 : #include <boost/capy/detail/config.hpp>
16 : #include <boost/capy/ex/io_env.hpp>
17 :
18 : #include <type_traits>
19 :
20 : namespace boost {
21 : namespace capy {
22 : namespace detail {
23 :
24 : /** Perform symmetric transfer, working around an MSVC codegen bug.
25 :
26 : MSVC stores the `std::coroutine_handle<>` returned from
27 : `await_suspend` in a hidden `__$ReturnUdt$` variable located
28 : on the coroutine frame. When another thread resumes or destroys
29 : the frame between the store and the read-back for the
30 : symmetric-transfer tail-call, the read hits freed memory.
31 :
32 : This occurs in two scenarios:
33 :
34 : @li `await_suspend` calls `h.destroy()` then returns a handle
35 : (e.g. `when_all_runner` and `when_any_runner` final_suspend).
36 : The return value is written to the now-destroyed frame.
37 :
38 : @li `await_suspend` hands the continuation to another thread
39 : via an executor handoff (e.g. `post()` or `dispatch()`),
40 : which may resume the parent. The parent can destroy this
41 : frame before the runtime reads `__$ReturnUdt$` (e.g.
42 : `boundary_trampoline` final_suspend).
43 :
44 : On affected compilers this function calls `h.resume()` on the
45 : current stack and returns `void`, causing unconditional
46 : suspension. The trade-off is O(n) stack growth instead of
47 : O(1) tail-calls.
48 :
49 : On x64 the workaround applies to MSVC 19.34 through 19.44 and
50 : self-retires on MSVC 19.50 (VS 2026 / 18.0). Measured on
51 : 19.44 the caller builds the hidden return slot at
52 : `__coro_frame_ptr$ + 0xC0`, on the coroutine frame; on 19.51
53 : it is an `rsp`-relative stack temporary, so destroying the
54 : frame no longer invalidates it.
55 :
56 : Do not widen this gate on the basis of Developer Community
57 : ticket 10251975, tagged "Fixed in VS 2022 17.9 Preview 2";
58 : 19.39 reproduces the fault identically to 19.34.
59 :
60 : On ARM64 the workaround does not retire at 19.50, because
61 : that target has a second, unrelated defect. With a real
62 : symmetric transfer, MSVC 19.51 release loses the handler for
63 : a `try` region that spans the suspend point: after the
64 : coroutine is resumed from another call stack, 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. Debug builds are unaffected, as
69 : is x64 at the same toolset. `testCatchAfterDeferredResume` in
70 : test/unit/task.cpp covers this.
71 :
72 : `_M_ARM64EC` is included conservatively. It generates ARM64
73 : code and has not been tested here; keeping the workaround on
74 : is the safe direction, since it costs stack depth rather than
75 : correctness.
76 :
77 : The gate deliberately excludes Clang. Both `clang-cl` and
78 : `clang++` targeting Windows define `_MSC_VER` for ABI
79 : compatibility, but generate a correct tail-call.
80 :
81 : Note that a probe which merely poisons the destroyed frame
82 : cannot validate this gate. Routing the return through this
83 : function moves the frame write to after `destroy()`, which
84 : repairs the poison pattern and hides the defect. The
85 : regression test in
86 : test/unit/detail/await_suspend_helper.cpp unmaps the frame
87 : instead, so any post-destroy access faults.
88 :
89 : On unaffected compilers the handle is returned directly for
90 : proper symmetric transfer.
91 :
92 : Callers must use `auto` return type on their `await_suspend`
93 : so the return type adapts per platform.
94 :
95 : @param h The coroutine handle to transfer to.
96 : */
97 : #if (BOOST_CAPY_WORKAROUND(_MSC_VER, < 1950) || \
98 : defined(_M_ARM64) || defined(_M_ARM64EC)) && \
99 : !defined(__clang__)
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 1845 : symmetric_transfer(std::coroutine_handle<> h) noexcept
110 : {
111 1845 : return h;
112 : }
113 : #endif
114 :
115 : // Helper to normalize await_suspend return types to std::coroutine_handle<>
116 : template<typename Awaitable>
117 239 : std::coroutine_handle<> call_await_suspend(
118 : Awaitable* a,
119 : std::coroutine_handle<> h,
120 : io_env 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 6 : return a->await_suspend(h, env);
137 : }
138 : }
139 :
140 : } // namespace detail
141 : } // namespace capy
142 : } // namespace boost
143 :
144 : #endif
|