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 //
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 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 The gate deliberately excludes Clang. Both `clang-cl` and
61 `clang++` targeting Windows define `_MSC_VER` for ABI
62 compatibility, but generate a correct tail-call.
63
64 Note that a probe which merely poisons the destroyed frame
65 cannot validate this gate. Routing the return through this
66 function moves the frame write to after `destroy()`, which
67 repairs the poison pattern and hides the defect. The
68 regression test in
69 test/unit/detail/await_suspend_helper.cpp unmaps the frame
70 instead, so any post-destroy access faults.
71
72 On unaffected compilers the handle is returned directly for
73 proper symmetric transfer.
74
75 Callers must use `auto` return type on their `await_suspend`
76 so the return type adapts per platform.
77
78 @param h The coroutine handle to transfer to.
79 */
80 #if BOOST_CAPY_WORKAROUND(_MSC_VER, < 1950) && !defined(__clang__)
81 inline void symmetric_transfer(std::coroutine_handle<> h) noexcept
82 {
83 // safe_resume is not needed here: the calling coroutine is
84 // about to suspend unconditionally. When it later resumes,
85 // await_resume restores TLS from the promise's environment.
86 h.resume();
87 }
88 #else
89 inline std::coroutine_handle<>
90 1881x symmetric_transfer(std::coroutine_handle<> h) noexcept
91 {
92 1881x return h;
93 }
94 #endif
95
96 // Helper to normalize await_suspend return types to std::coroutine_handle<>
97 template<typename Awaitable>
98 251x std::coroutine_handle<> call_await_suspend(
99 Awaitable* a,
100 std::coroutine_handle<> h,
101 io_env const* env)
102 {
103 using R = decltype(a->await_suspend(h, env));
104 if constexpr (std::is_void_v<R>)
105 {
106 1x a->await_suspend(h, env);
107 1x return std::noop_coroutine();
108 }
109 else if constexpr (std::is_same_v<R, bool>)
110 {
111 244x if(a->await_suspend(h, env))
112 1x return std::noop_coroutine();
113 243x return h;
114 }
115 else
116 {
117 6x return a->await_suspend(h, env);
118 }
119 }
120
121 } // namespace detail
122 } // namespace capy
123 } // namespace boost
124
125 #endif
126