Harden hook removal: disable -> drain -> destroy, and drain settles first

Uncapping the mock game (next commit) turned mock_game_test's hook/unhook storm
into a real stress test (thousands of presents/s instead of tens), which reliably
crashed the game on remove (0xC0000005) for dx9/dx11/dx12. Two races the slow
vsync'd mock had masked:

1. Trampoline use-after-free. remove_*_hooks did `hook = {}` (destroy) BEFORE the
   DetourGate drain. Destroying a SafetyHook InlineHook frees its trampoline
   immediately, but an in-flight detour about to call the original via .stdcall()
   (the trampoline) then used freed memory. Fix: disable() first (restores the
   original bytes under thread suspension, but KEEPS the trampoline alive) -> drain
   -> only then destroy. Applied to present/d3d9/opengl/vk/xinput/mkb/focus.

2. Entry-window race in DetourGate::drain(). It returned the instant the active
   count read zero, but a thread can be inside the detour yet not have reached its
   Guard constructor (the prologue is unguarded), so the count reads zero while a
   detour is about to run -- and the freed state is then used. Fix: Sleep(1) BEFORE
   each zero-check; with the hook disabled no new detour starts, so any
   already-entered thread registers within that window. This alone fixed dx11 (the
   highest present rate, ~11000/s, which hit the window every storm).

Audio is unaffected (it uses vtable swaps, which keep a real original pointer, not
a trampoline). Full suite 21/21, and the storm now survives on every backend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 09:44:42 +02:00
parent c480dfe152
commit 958c355126
8 changed files with 79 additions and 25 deletions

View File

@@ -514,12 +514,21 @@ void remove_mkb_hooks()
return;
}
g_active.store(false, std::memory_order_release);
g_hk_async = {}; // InlineHook destructor restores the original bytes -> no new detour starts
g_hk_kbstate = {};
g_hk_cursor = {};
g_hk_getrawinputdata = {}; // restore GetRawInputData
// Disable (restore original bytes) the inline hooks first so no new detour starts, but KEEP the
// trampolines alive for any in-flight detour calling its trampoline; destroy only after the
// drain. Destroying before the drain frees the trampoline under a detour about to call it -- the
// same UAF class as the present-storm crash. (The DI hook is a vtable swap: remove() restores the
// slot and keeps m_original valid, so it has no trampoline to free early.)
(void)g_hk_async.disable();
(void)g_hk_kbstate.disable();
(void)g_hk_cursor.disable();
(void)g_hk_getrawinputdata.disable();
g_vh_di_getstate.remove(); // restore the DI GetDeviceState slot (probe kept alive for re-enable)
g_gate.drain(); // wait for any in-flight polling / DI / raw detour before clearing state
g_hk_async = {}; // no detour in-flight or able to start now -> safe to free the trampolines
g_hk_kbstate = {};
g_hk_cursor = {};
g_hk_getrawinputdata = {};
hook_set_installed(g_id_di_getstate, false);
hook_set_installed(g_id_rawinput, false);
for (int vk = 0; vk < 256; ++vk)