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:
51
hook/src/hook_install.hpp
Normal file
51
hook/src/hook_install.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
// Safe INSTALL of a SafetyHook inline hook -- the symmetric partner of hook_guard.hpp's safe removal.
|
||||
//
|
||||
// Two problems this avoids, both surfaced by mock_game_test's hook/unhook storm (a game polling a
|
||||
// hooked API at thousands/s while the subsystem is toggled on/off every 60 ms):
|
||||
//
|
||||
// 1. Enable-before-assign. create_inline() builds the hook AND enables it (patches the target's bytes
|
||||
// to jump to the detour), THEN the result is move-assigned into the global the detour reads to
|
||||
// reach the trampoline. A call landing in the detour during that assign reads a torn global -> AV.
|
||||
// So we create StartDisabled, let dst be populated, and only THEN enable().
|
||||
//
|
||||
// 2. Trampoline use-after-free on re-install. The old model recreated the hook on every install and
|
||||
// DESTROYED it on every remove (= {}), which frees the trampoline. A detour that has entered but
|
||||
// not yet reached its DetourGate::Guard (the unguarded prologue) can then call a freed trampoline
|
||||
// -> AV at a garbage address. drain() narrows that window but can't fully close it under heavy
|
||||
// preemption. So instead we treat hooks as PERSISTENT: create each once and thereafter only
|
||||
// enable()/disable() it across install/remove cycles. The trampoline is allocated once and never
|
||||
// freed during the session, so a stale detour always calls a live trampoline (which, when
|
||||
// disabled, simply runs the original). No churn, no leak (it's reused), no UAF. Removal therefore
|
||||
// disable()s the hook (and drains) but does NOT destroy it (see hook_guard.hpp).
|
||||
#pragma once
|
||||
|
||||
#include <safetyhook.hpp>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace coop::hook
|
||||
{
|
||||
|
||||
// Arm `detour` over `target` in `dst`: create it once (StartDisabled) if empty, then enable. Calling
|
||||
// this again after a remove just re-enables the SAME hook (no recreate -> the trampoline is never
|
||||
// freed). enable()'s [[nodiscard]] result is surfaced, not discarded; enabling an already-enabled
|
||||
// hook is a no-op success.
|
||||
inline void install_inline(safetyhook::InlineHook& dst, void* target, void* detour)
|
||||
{
|
||||
if (!dst) // create only the first time; reuse across enable/disable cycles
|
||||
{
|
||||
dst = safetyhook::create_inline(target, detour, safetyhook::InlineHook::StartDisabled);
|
||||
}
|
||||
if (dst && !dst.enable())
|
||||
{
|
||||
OutputDebugStringA("coop: SafetyHook InlineHook::enable() failed during install\n");
|
||||
}
|
||||
}
|
||||
|
||||
template <class T, class D>
|
||||
void install_inline(safetyhook::InlineHook& dst, T target, D detour)
|
||||
{
|
||||
install_inline(dst, reinterpret_cast<void*>(target), reinterpret_cast<void*>(detour));
|
||||
}
|
||||
|
||||
} // namespace coop::hook
|
||||
Reference in New Issue
Block a user