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:
14
README.md
14
README.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user