Preserve SafetyHook concurrency finding as a committed upstream repro

A multithreaded test that tight-looped InlineHook enable()/disable() while
other threads called the hooked function flaked ~1/10. Isolation proved this is
a SafetyHook limitation, not our code: with the hook created once (no install
race, no trampoline UAF), tight-loop toggling AVs ~1/3 of runs in Debug
(0xC0000005, faulting RIP in the target body), while a no-toggle control is
clean at ~60M calls. enable()/disable() re-patch the prologue in place under a
VEH page-trap that only relocates a thread parked ON the prologue; a thread in
the function body faults on the briefly-non-exec page and relies on instruction
retry, which under rapid toggling races a half-rewritten prologue.

Rather than silently drop the flaky test, preserve the finding:
- tools/sh_concurrency_repro/: minimal, committed, non-CI reproducer
  (coop_sh_concurrency_repro; --callonly is the control). Surfaces 5/16 AVs.
- docs/safetyhook-concurrency.md: upstream-ready write-up (mechanism + fix
  directions + why it does not affect us).
- README lessons-learned + memory updated; tests/CMakeLists cross-references it.

Our code stays in SafetyHook's safe envelope (install/remove reconciled from a
single tick-bounded worker thread, never a tight loop), so the mock_game_test
storm is reliably green; the persistent-trampoline contract is covered
deterministically by hook_install_test + detour_gate_test. Removes the temp
_sh_probe wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 12:23:06 +02:00
parent f843c56f5b
commit 51e2c4f4cd
6 changed files with 213 additions and 0 deletions

View File

@@ -719,6 +719,20 @@ Non-obvious things that cost time and constrain the design:
original) — no code patching, pristine stack regardless of prologue. Inline
hooking stays fine for `Present`/`SwapBuffers` (clean prologues). Guarded by the
x86 hook tests.
- **SafetyHook's `enable()`/`disable()` aren't safe to call in a *tight loop* concurrent with calls to
the hooked function.** They re-patch the prologue in place under a VEH "trap" that reliably relocates
a thread *parked on the prologue*, but a thread executing the function **body** faults when the whole
page is briefly made non-executable during the patch and relies on a retry — and under rapid toggling
that retry races a half-rewritten prologue → AV in the caller. A minimal reproducer (one thread
tight-looping enable/disable, two hammering the target; hook created once, so no install race / UAF)
faults ~1/3 of runs in Debug while the no-toggle control is clean. This is a SafetyHook limitation,
not our code: we reconcile install/remove from a **single, tick-bounded worker thread**, never a
tight loop, so the `mock_game_test` storm (a single enable/disable concurrent with thousands of
calls/s — SafetyHook's designed-safe case) is reliably crash-free. The finding is preserved as a
committed, non-CI reproducer (`tools/sh_concurrency_repro/`) and written up for upstream in
[docs/safetyhook-concurrency.md](docs/safetyhook-concurrency.md); an earlier flaky tight-loop CTest
that hit this was replaced by the deterministic `hook_install_test` + `detour_gate_test` (our
contract) plus the storm (realistic concurrency).
- **Steam Input init suppresses XInput.** Initializing Steam Input turns on Steam's
in-process XInput interception, which hides controllers from `XInputGetState`
unless they're bound to the running appid's action set — defaulting to it