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 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 107446x symmetric_transfer(std::coroutine_handle<> h) noexcept
110 {
111 107446x return h;
112 }
113 #endif
114
115 // Helper to normalize await_suspend return types to std::coroutine_handle<>
116 template<typename Awaitable>
117 256x 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 1x a->await_suspend(h, env);
126 1x return std::noop_coroutine();
127 }
128 else if constexpr (std::is_same_v<R, bool>)
129 {
130 232x if(a->await_suspend(h, env))
131 1x return std::noop_coroutine();
132 231x return h;
133 }
134 else
135 {
136 23x return a->await_suspend(h, env);
137 }
138 }
139
140 } // namespace detail
141 } // namespace capy
142 } // namespace boost
143
144 #endif
145