Make inline-hook install AND remove safe to spam
The uncapped, input-polling mock_game_test storm (thousands of presents/s, now
also driving the input/focus/MKB hooks) drove out a family of install/remove races
the slow vsync'd mock had masked. Fixes (hook/src/hook_install.hpp + hook_guard.hpp):
- Persistent hooks. The old model created a hook on install and DESTROYED it on
remove (= {}), freeing the trampoline; a detour about to call it (.stdcall) then
hit freed memory -> 0xC0000005. drain() can't fully close that window (a thread
can be inside the detour but not past its Guard ctor). So hooks are now created
ONCE and only enable()/disable()d across install/remove cycles -- never destroyed
during the session -- so a stale detour always calls a live trampoline (disabled,
it just runs the original). Reused, so no churn and no leak. remove_* therefore
disable()s + drain()s but does not destroy; install guards check .enabled().
- Install race. create_inline() enables the hook before the result is move-assigned
into the global the detour reads; a call landing in the detour mid-assign reads a
torn hook -> AV. install_inline() creates StartDisabled, assigns, then enable()s.
- drain() Sleep(1)s BEFORE each zero-check, so a thread that entered the detour but
hasn't reached its Guard registers before we conclude zero.
- Focus: publish g_orig_proc before SetWindowLongPtr activates the subclass (and
subclass_proc falls back to DefWindowProc if null); and disable the focus-query
hooks in reverse install order, because GetForegroundWindow shares user32 code
with GetActiveWindow (keep GFW hooked until GAW is unhooked).
- disable()/enable() [[nodiscard]] results are handled (logged), not (void)-discarded.
Storm now survives on every backend across repeated runs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
#include <safetyhook.hpp>
|
||||
|
||||
#include "hook_guard.hpp"
|
||||
#include "hook_install.hpp"
|
||||
#include "hook_registry.hpp"
|
||||
|
||||
namespace coop::hook
|
||||
@@ -101,8 +102,15 @@ LRESULT CALLBACK subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return g_unicode ? CallWindowProcW(g_orig_proc, hwnd, msg, wparam, lparam)
|
||||
: CallWindowProcA(g_orig_proc, hwnd, msg, wparam, lparam);
|
||||
// Read g_orig_proc once; if the subclass is live but the original isn't published yet (the tiny
|
||||
// install/remove window), fall back to DefWindowProc rather than call through a null pointer.
|
||||
const WNDPROC orig = g_orig_proc;
|
||||
if (orig == nullptr)
|
||||
{
|
||||
return g_unicode ? DefWindowProcW(hwnd, msg, wparam, lparam) : DefWindowProcA(hwnd, msg, wparam, lparam);
|
||||
}
|
||||
return g_unicode ? CallWindowProcW(orig, hwnd, msg, wparam, lparam)
|
||||
: CallWindowProcA(orig, hwnd, msg, wparam, lparam);
|
||||
}
|
||||
|
||||
HWND WINAPI hk_GetForegroundWindow()
|
||||
@@ -166,7 +174,8 @@ void hook_export(HMODULE module, const char* name, void* detour, int registry_id
|
||||
{
|
||||
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name)))
|
||||
{
|
||||
g_focus_hooks.emplace_back(safetyhook::create_inline(target, detour));
|
||||
g_focus_hooks.emplace_back();
|
||||
install_inline(g_focus_hooks.back(), target, detour); // assign-then-enable (no install race)
|
||||
hook_set_installed(registry_id, true);
|
||||
}
|
||||
}
|
||||
@@ -197,12 +206,20 @@ bool install_focus_spoof(IpcClient& ipc)
|
||||
g_game_hwnd = hwnd;
|
||||
g_unicode = IsWindowUnicode(hwnd) != FALSE;
|
||||
|
||||
// Replacing GWLP_WNDPROC from another thread is safe (the new proc runs on
|
||||
// the window's own thread); match A/W so CallWindowProc translates correctly.
|
||||
const LONG_PTR replaced = g_unicode
|
||||
? SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc))
|
||||
: SetWindowLongPtrA(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
|
||||
g_orig_proc = reinterpret_cast<WNDPROC>(replaced);
|
||||
// Publish g_orig_proc BEFORE activating the subclass, so a message that dispatches the instant the
|
||||
// subclass goes live finds a valid original (not the null/stale value from a prior install cycle)
|
||||
// -- the WNDPROC analogue of the inline-hook install race. Replacing GWLP_WNDPROC from another
|
||||
// thread is safe (the new proc runs on the window's own thread); match A/W for CallWindowProc.
|
||||
g_orig_proc = g_unicode ? reinterpret_cast<WNDPROC>(GetWindowLongPtrW(hwnd, GWLP_WNDPROC))
|
||||
: reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hwnd, GWLP_WNDPROC));
|
||||
if (g_unicode)
|
||||
{
|
||||
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
|
||||
}
|
||||
else
|
||||
{
|
||||
SetWindowLongPtrA(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
|
||||
}
|
||||
hook_set_installed(g_id_wndproc, true);
|
||||
|
||||
if (HMODULE user32 = GetModuleHandleW(L"user32.dll"))
|
||||
@@ -214,12 +231,12 @@ bool install_focus_spoof(IpcClient& ipc)
|
||||
|
||||
if (void* clip = reinterpret_cast<void*>(GetProcAddress(user32, "ClipCursor")))
|
||||
{
|
||||
g_hk_clipcursor = safetyhook::create_inline(clip, reinterpret_cast<void*>(&hk_ClipCursor));
|
||||
install_inline(g_hk_clipcursor, clip, &hk_ClipCursor);
|
||||
hook_set_installed(g_id_clipcursor, static_cast<bool>(g_hk_clipcursor));
|
||||
}
|
||||
if (void* setpos = reinterpret_cast<void*>(GetProcAddress(user32, "SetCursorPos")))
|
||||
{
|
||||
g_hk_setcursorpos = safetyhook::create_inline(setpos, reinterpret_cast<void*>(&hk_SetCursorPos));
|
||||
install_inline(g_hk_setcursorpos, setpos, &hk_SetCursorPos);
|
||||
hook_set_installed(g_id_setcursorpos, static_cast<bool>(g_hk_setcursorpos));
|
||||
}
|
||||
}
|
||||
@@ -289,17 +306,21 @@ void remove_focus_spoof()
|
||||
SetWindowLongPtrA(g_game_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_orig_proc));
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
// Disable the inline focus hooks in REVERSE install order. GetForegroundWindow/GetActiveWindow
|
||||
// share user32 code (the real GetForegroundWindow's path runs through GetActiveWindow's body), so
|
||||
// GetActiveWindow's inline patch corrupts that shared code. GetForegroundWindow is installed
|
||||
// first, so disabling in reverse unhooks GetActiveWindow (restoring the shared bytes) while
|
||||
// GetForegroundWindow is still hooked -- its detour returns g_game_hwnd and never reaches the
|
||||
// patched bytes. The reverse of the enable order (GFW first) keeps the invariant "GetActiveWindow
|
||||
// hooked => GetForegroundWindow hooked" across the whole install/remove cycle, so a call never
|
||||
// lands in a half-patched shared region (the intermittent storm crash: GetActiveWindow+0x8).
|
||||
// Disable (not destroy) keeps the trampolines alive for any in-flight detour; destroy after drain.
|
||||
for (auto it = g_focus_hooks.rbegin(); it != g_focus_hooks.rend(); ++it)
|
||||
{
|
||||
(void)h.disable();
|
||||
disable_for_removal(*it);
|
||||
}
|
||||
(void)g_hk_clipcursor.disable();
|
||||
(void)g_hk_setcursorpos.disable();
|
||||
disable_for_removal(g_hk_clipcursor);
|
||||
disable_for_removal(g_hk_setcursorpos);
|
||||
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);
|
||||
@@ -312,9 +333,11 @@ void remove_focus_spoof()
|
||||
// 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 = {};
|
||||
// The focus-query hooks (GetForegroundWindow/GetActiveWindow/GetFocus) return g_game_hwnd and
|
||||
// never call the trampoline, so destroying them is safe; recreate on re-install. The cursor hooks
|
||||
// DO call the trampoline, so keep them ALIVE (disabled) -- persistent, re-enabled on re-install
|
||||
// (see hook_install.hpp) -- so a stale detour never hits a freed trampoline.
|
||||
g_focus_hooks.clear();
|
||||
if (g_focus_ipc != nullptr)
|
||||
{
|
||||
g_focus_ipc->mark_focus_spoof(false, 0);
|
||||
|
||||
Reference in New Issue
Block a user