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 + 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 + 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.
49   74  
50   Callers must use `auto` return type on their `await_suspend` 75   Callers must use `auto` return type on their `await_suspend`
51   so the return type adapts per platform. 76   so the return type adapts per platform.
52   77  
53   @param h The coroutine handle to transfer to. 78   @param h The coroutine handle to transfer to.
54   */ 79   */
55 - #if BOOST_CAPY_WORKAROUND(_MSC_VER, >= 1) 80 + #if BOOST_CAPY_WORKAROUND(_MSC_VER, < 1950) && !defined(__clang__)
56   inline void symmetric_transfer(std::coroutine_handle<> h) noexcept 81   inline void symmetric_transfer(std::coroutine_handle<> h) noexcept
57   { 82   {
58   // safe_resume is not needed here: the calling coroutine is 83   // safe_resume is not needed here: the calling coroutine is
59   // about to suspend unconditionally. When it later resumes, 84   // about to suspend unconditionally. When it later resumes,
60   // await_resume restores TLS from the promise's environment. 85   // await_resume restores TLS from the promise's environment.
61   h.resume(); 86   h.resume();
62   } 87   }
63   #else 88   #else
64   inline std::coroutine_handle<> 89   inline std::coroutine_handle<>
HITCBC 65   1855 symmetric_transfer(std::coroutine_handle<> h) noexcept 90   1881 symmetric_transfer(std::coroutine_handle<> h) noexcept
66   { 91   {
HITCBC 67   1855 return h; 92   1881 return h;
68   } 93   }
69   #endif 94   #endif
70   95  
71   // Helper to normalize await_suspend return types to std::coroutine_handle<> 96   // Helper to normalize await_suspend return types to std::coroutine_handle<>
72   template<typename Awaitable> 97   template<typename Awaitable>
HITCBC 73   251 std::coroutine_handle<> call_await_suspend( 98   251 std::coroutine_handle<> call_await_suspend(
74   Awaitable* a, 99   Awaitable* a,
75   std::coroutine_handle<> h, 100   std::coroutine_handle<> h,
76   io_env const* env) 101   io_env const* env)
77   { 102   {
78   using R = decltype(a->await_suspend(h, env)); 103   using R = decltype(a->await_suspend(h, env));
79   if constexpr (std::is_void_v<R>) 104   if constexpr (std::is_void_v<R>)
80   { 105   {
HITCBC 81   1 a->await_suspend(h, env); 106   1 a->await_suspend(h, env);
HITCBC 82   1 return std::noop_coroutine(); 107   1 return std::noop_coroutine();
83   } 108   }
84   else if constexpr (std::is_same_v<R, bool>) 109   else if constexpr (std::is_same_v<R, bool>)
85   { 110   {
HITCBC 86   244 if(a->await_suspend(h, env)) 111   244 if(a->await_suspend(h, env))
HITCBC 87   1 return std::noop_coroutine(); 112   1 return std::noop_coroutine();
HITCBC 88   243 return h; 113   243 return h;
89   } 114   }
90   else 115   else
91   { 116   {
HITCBC 92   6 return a->await_suspend(h, env); 117   6 return a->await_suspend(h, env);
93   } 118   }
94   } 119   }
95   120  
96   } // namespace detail 121   } // namespace detail
97   } // namespace capy 122   } // namespace capy
98   } // namespace boost 123   } // namespace boost
99   124  
100   #endif 125   #endif