100.00% Lines (9/9) 100.00% Functions (2/2)
TLA Baseline Branch
Line Hits Code Line Hits Code
1   // 1   //
2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) 2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3   // Copyright (c) 2026 Steve Gerbino 3   // Copyright (c) 2026 Steve Gerbino
4   // 4   //
5   // Distributed under the Boost Software License, Version 1.0. (See accompanying 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) 6   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7   // 7   //
8   // Official repository: https://github.com/cppalliance/capy 8   // Official repository: https://github.com/cppalliance/capy
9   // 9   //
10   10  
11   #ifndef BOOST_CAPY_DETAIL_AWAIT_SUSPEND_HELPER_HPP 11   #ifndef BOOST_CAPY_DETAIL_AWAIT_SUSPEND_HELPER_HPP
12   #define BOOST_CAPY_DETAIL_AWAIT_SUSPEND_HELPER_HPP 12   #define BOOST_CAPY_DETAIL_AWAIT_SUSPEND_HELPER_HPP
13   13  
14   #include <coroutine> 14   #include <coroutine>
  15 + #include <boost/capy/detail/config.hpp>
15   #include <boost/capy/ex/io_env.hpp> 16   #include <boost/capy/ex/io_env.hpp>
16   17  
17   #include <type_traits> 18   #include <type_traits>
18   19  
19   namespace boost { 20   namespace boost {
20   namespace capy { 21   namespace capy {
21   namespace detail { 22   namespace detail {
22   23  
23   /** Perform symmetric transfer, working around an MSVC codegen bug. 24   /** Perform symmetric transfer, working around an MSVC codegen bug.
24   25  
25   MSVC stores the `std::coroutine_handle<>` returned from 26   MSVC stores the `std::coroutine_handle<>` returned from
26   `await_suspend` in a hidden `__$ReturnUdt$` variable located 27   `await_suspend` in a hidden `__$ReturnUdt$` variable located
27   on the coroutine frame. When another thread resumes or destroys 28   on the coroutine frame. When another thread resumes or destroys
28   the frame between the store and the read-back for the 29   the frame between the store and the read-back for the
29   symmetric-transfer tail-call, the read hits freed memory. 30   symmetric-transfer tail-call, the read hits freed memory.
30   31  
31   This occurs in two scenarios: 32   This occurs in two scenarios:
32   33  
33   @li `await_suspend` calls `h.destroy()` then returns a handle 34   @li `await_suspend` calls `h.destroy()` then returns a handle
34   (e.g. `when_all_runner` and `when_any_runner` final_suspend). 35   (e.g. `when_all_runner` and `when_any_runner` final_suspend).
35   The return value is written to the now-destroyed frame. 36   The return value is written to the now-destroyed frame.
36   37  
37   @li `await_suspend` hands the continuation to another thread 38   @li `await_suspend` hands the continuation to another thread
38   via an executor handoff (e.g. `post()` or `dispatch()`), 39   via an executor handoff (e.g. `post()` or `dispatch()`),
39   which may resume the parent. The parent can destroy this 40   which may resume the parent. The parent can destroy this
40   frame before the runtime reads `__$ReturnUdt$` (e.g. 41   frame before the runtime reads `__$ReturnUdt$` (e.g.
41   `boundary_trampoline` final_suspend). 42   `boundary_trampoline` final_suspend).
42   43  
43 - On MSVC this function calls `h.resume()` on the current stack 44 + On affected compilers this function calls `h.resume()` on the
44 - and returns `void`, causing unconditional suspension. The 45 + current stack and returns `void`, causing unconditional
45 - trade-off is O(n) stack growth instead of O(1) tail-calls. 46 + suspension. The trade-off is O(n) stack growth instead of
  47 + O(1) tail-calls.
46   48  
47 - On other compilers the handle is returned directly for proper 49 + On x64 the workaround applies to MSVC 19.34 through 19.44 and
48 - symmetric transfer. 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.
49   91  
50   Callers must use `auto` return type on their `await_suspend` 92   Callers must use `auto` return type on their `await_suspend`
51   so the return type adapts per platform. 93   so the return type adapts per platform.
52   94  
53   @param h The coroutine handle to transfer to. 95   @param h The coroutine handle to transfer to.
54   */ 96   */
55 - #if BOOST_CAPY_WORKAROUND(_MSC_VER, >= 1) 97 + #if (BOOST_CAPY_WORKAROUND(_MSC_VER, < 1950) || \
  98 + defined(_M_ARM64) || defined(_M_ARM64EC)) && \
  99 + !defined(__clang__)
56   inline void symmetric_transfer(std::coroutine_handle<> h) noexcept 100   inline void symmetric_transfer(std::coroutine_handle<> h) noexcept
57   { 101   {
58   // safe_resume is not needed here: the calling coroutine is 102   // safe_resume is not needed here: the calling coroutine is
59   // about to suspend unconditionally. When it later resumes, 103   // about to suspend unconditionally. When it later resumes,
60   // await_resume restores TLS from the promise's environment. 104   // await_resume restores TLS from the promise's environment.
61   h.resume(); 105   h.resume();
62   } 106   }
63   #else 107   #else
64   inline std::coroutine_handle<> 108   inline std::coroutine_handle<>
HITCBC 65   1847 symmetric_transfer(std::coroutine_handle<> h) noexcept 109   1845 symmetric_transfer(std::coroutine_handle<> h) noexcept
66   { 110   {
HITCBC 67   1847 return h; 111   1845 return h;
68   } 112   }
69   #endif 113   #endif
70   114  
71   // Helper to normalize await_suspend return types to std::coroutine_handle<> 115   // Helper to normalize await_suspend return types to std::coroutine_handle<>
72   template<typename Awaitable> 116   template<typename Awaitable>
HITCBC 73   239 std::coroutine_handle<> call_await_suspend( 117   239 std::coroutine_handle<> call_await_suspend(
74   Awaitable* a, 118   Awaitable* a,
75   std::coroutine_handle<> h, 119   std::coroutine_handle<> h,
76   io_env const* env) 120   io_env const* env)
77   { 121   {
78   using R = decltype(a->await_suspend(h, env)); 122   using R = decltype(a->await_suspend(h, env));
79   if constexpr (std::is_void_v<R>) 123   if constexpr (std::is_void_v<R>)
80   { 124   {
HITCBC 81   1 a->await_suspend(h, env); 125   1 a->await_suspend(h, env);
HITCBC 82   1 return std::noop_coroutine(); 126   1 return std::noop_coroutine();
83   } 127   }
84   else if constexpr (std::is_same_v<R, bool>) 128   else if constexpr (std::is_same_v<R, bool>)
85   { 129   {
HITCBC 86   232 if(a->await_suspend(h, env)) 130   232 if(a->await_suspend(h, env))
HITCBC 87   1 return std::noop_coroutine(); 131   1 return std::noop_coroutine();
HITCBC 88   231 return h; 132   231 return h;
89   } 133   }
90   else 134   else
91   { 135   {
HITCBC 92   6 return a->await_suspend(h, env); 136   6 return a->await_suspend(h, env);
93   } 137   }
94   } 138   }
95   139  
96   } // namespace detail 140   } // namespace detail
97   } // namespace capy 141   } // namespace capy
98   } // namespace boost 142   } // namespace boost
99   143  
100   #endif 144   #endif