fix: correct the workaround gate for symmetric transfer on MSVC - #437
mvandeberg wants to merge 1 commit into
Conversation
`detail::symmetric_transfer` applied the workaround to each compiler
that defines `_MSC_VER`. This was too wide. MSVC has two different
defects, and the new gate keeps the workaround for each one:
#if (BOOST_CAPY_WORKAROUND(_MSC_VER, < 1950) || \
defined(_M_ARM64) || defined(_M_ARM64EC)) && \
!defined(__clang__)
The first defect occurs on MSVC 19.34 to 19.44, on all architectures.
The caller puts the hidden return slot of `await_suspend` on the
coroutine frame, at `__coro_frame_ptr$ + 0xC0`. When `await_suspend`
destroys that frame, the runtime then reads released memory. MSVC 19.50
puts the slot on the stack, thus the workaround retires there.
The second defect occurs on MSVC 19.51 for ARM64, in release builds
only. A `try` region that spans the suspend point loses its handler.
When the coroutine resumes from a different call stack, a `throw` in
that region does not go to the adjacent `catch`. The exception goes to
`unhandled_exception` of the promise. A `catch(...)` also does not get
it, thus the runtime does not find the region at all. Debug builds are
correct, and so are x64 builds at the same toolset. The gate includes
`_M_ARM64EC` because that target also emits ARM64 code.
The gate excludes Clang. `clang-cl` and `clang++` define `_MSC_VER` for
ABI compatibility, but they emit a correct tail-call.
Two tests cover the defects:
- `test/unit/detail/await_suspend_helper.cpp` destroys the coroutine
frame in `await_suspend` and then transfers. The test unmaps each
frame on destruction, thus every subsequent access gives a fault. A
poison pattern is not sufficient, because `symmetric_transfer` moves
the write to the frame to a point after the destruction.
- `test/unit/task.cpp` awaits a handle-returning awaitable in a `try`
block, resumes the parked handle from a different call stack, and
then throws. The test watches for an escape through the error handler
of `run_async`. A failure thus gives an assertion and not a
terminate, which keeps the other results.
Refs cppalliance#378
|
An automated preview of the documentation is available at https://437.capy.prtest3.cppalliance.org/index.html If more commits are pushed to the pull request, the docs will rebuild at the same URL. 2026-09-29 22:30:20 UTC |
|
GCOVR code coverage report https://437.capy.prtest3.cppalliance.org/gcovr/index.html Build time: 2026-09-29 22:42:48 UTC |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #437 +/- ##
========================================
Coverage 98.09% 98.09%
========================================
Files 130 130
Lines 6289 6289
========================================
Hits 6169 6169
Misses 120 120
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1 file with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
detail::symmetric_transferapplied the workaround to each compiler that defines_MSC_VER. This was too wide. MSVC has two different defects, and the new gate keeps the workaround for each one:The first defect occurs on MSVC 19.34 to 19.44, on all architectures. The caller puts the hidden return slot of
await_suspendon the coroutine frame, at__coro_frame_ptr$ + 0xC0. Whenawait_suspenddestroys that frame, the runtime then reads released memory. MSVC 19.50 puts the slot on the stack, thus the workaround retires there.The second defect occurs on MSVC 19.51 for ARM64, in release builds only. A
tryregion that spans the suspend point loses its handler. When the coroutine resumes from a different call stack, athrowin that region does not go to the adjacentcatch. The exception goes tounhandled_exceptionof the promise. Acatch(...)also does not get it, thus the runtime does not find the region at all. Debug builds are correct, and so are x64 builds at the same toolset. The gate includes_M_ARM64ECbecause that target also emits ARM64 code.The gate excludes Clang.
clang-clandclang++define_MSC_VERfor ABI compatibility, but they emit a correct tail-call.Two tests cover the defects:
test/unit/detail/await_suspend_helper.cppdestroys the coroutine frame inawait_suspendand then transfers. The test unmaps each frame on destruction, thus every subsequent access gives a fault. A poison pattern is not sufficient, becausesymmetric_transfermoves the write to the frame to a point after the destruction.test/unit/task.cppawaits a handle-returning awaitable in atryblock, resumes the parked handle from a different call stack, and then throws. The test watches for an escape through the error handler ofrun_async. A failure thus gives an assertion and not a terminate, which keeps the other results.Refs #378