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:
@@ -289,9 +289,17 @@ void remove_focus_spoof()
|
||||
SetWindowLongPtrA(g_game_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_orig_proc));
|
||||
}
|
||||
}
|
||||
g_focus_hooks.clear();
|
||||
g_hk_clipcursor = {}; // restore ClipCursor / SetCursorPos before clearing state
|
||||
g_hk_setcursorpos = {};
|
||||
// Disable (restore original bytes) the inline focus 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 a trampoline under a detour about to call it (the same
|
||||
// UAF class as the present-storm crash). The WNDPROC subclass uses a saved g_orig_proc pointer
|
||||
// (restored above), not a trampoline, so it has nothing to free early.
|
||||
for (auto& h : g_focus_hooks)
|
||||
{
|
||||
(void)h.disable();
|
||||
}
|
||||
(void)g_hk_clipcursor.disable();
|
||||
(void)g_hk_setcursorpos.disable();
|
||||
ClipCursor(nullptr); // leave the cursor free when the spoof is removed
|
||||
hook_set_installed(g_id_foreground, false);
|
||||
hook_set_installed(g_id_active, false);
|
||||
@@ -299,11 +307,14 @@ void remove_focus_spoof()
|
||||
hook_set_installed(g_id_wndproc, false);
|
||||
hook_set_installed(g_id_clipcursor, false);
|
||||
hook_set_installed(g_id_setcursorpos, false);
|
||||
// The WNDPROC is restored and the inline hooks reset above, so no NEW detour can start. Drain
|
||||
// The WNDPROC is restored and the inline hooks disabled above, so no NEW detour can start. Drain
|
||||
// any focus / WNDPROC detour still in-flight on the game's window thread before nulling the
|
||||
// state they read (g_orig_proc / g_game_hwnd / g_focus_ipc) -- otherwise a dispatch mid-flight
|
||||
// could call a null original WNDPROC or a dangling IPC pointer.
|
||||
g_gate.drain();
|
||||
g_focus_hooks.clear(); // no detour in-flight or able to start now -> safe to free the trampolines
|
||||
g_hk_clipcursor = {};
|
||||
g_hk_setcursorpos = {};
|
||||
if (g_focus_ipc != nullptr)
|
||||
{
|
||||
g_focus_ipc->mark_focus_spoof(false, 0);
|
||||
|
||||
Reference in New Issue
Block a user