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:
2026-06-23 11:17:13 +02:00
parent 8a43d2f568
commit 79582f9fa6
10 changed files with 197 additions and 93 deletions

View File

@@ -15,6 +15,7 @@
#include "coop/shared_memory.hpp"
#include "debug_log.hpp"
#include "hook_guard.hpp"
#include "hook_install.hpp"
#include "hook_registry.hpp"
namespace coop::hook
@@ -345,9 +346,9 @@ bool install_d3d9_hooks(IpcClient& ipc)
{
g_ipc = &ipc;
g_pid = GetCurrentProcessId();
if (g_hk_present9)
if (g_hk_present9.enabled())
{
return true; // already installed
return true; // already installed (persistent hook; re-install below re-enables it)
}
g_id_present9 = hook_register("IDirect3DDevice9::Present", HookSubsys_Video);
g_unsupported_logged = false;
@@ -358,7 +359,7 @@ bool install_d3d9_hooks(IpcClient& ipc)
hook_set_installed(g_id_present9, false); // not a D3D9 game (or no probe device)
return false;
}
g_hk_present9 = safetyhook::create_inline(present, reinterpret_cast<void*>(&hk_Present9));
install_inline(g_hk_present9, present, &hk_Present9);
hook_set_installed(g_id_present9, static_cast<bool>(g_hk_present9));
logf("install_d3d9_hooks: Present=%p hooked=%d", present, static_cast<bool>(g_hk_present9) ? 1 : 0);
return static_cast<bool>(g_hk_present9);
@@ -371,10 +372,11 @@ void remove_d3d9_hooks()
// (the trampoline) doesn't have it freed under it. Destroying (= {}) before the drain frees the
// trampoline immediately -- a UAF the uncapped mock-game storm (thousands of presents/s) hits
// reliably (0xC0000005). Disable -> drain -> only then destroy.
(void)g_hk_present9.disable();
disable_for_removal(g_hk_present9);
hook_set_installed(g_id_present9, false);
g_gate.drain();
g_hk_present9 = {}; // no detour in-flight or able to start now -> safe to free the trampoline
// Persistent hook: keep g_hk_present9 ALIVE (disabled) so a stale detour's trampoline call is
// never freed -- re-install re-enables it (see hook_install.hpp).
release_shared();
release_sysmem();
if (g_ctx != nullptr)