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

@@ -17,6 +17,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
@@ -946,9 +947,9 @@ bool install_present_hooks(IpcClient& ipc)
{
g_ipc = &ipc;
g_pid = GetCurrentProcessId();
if (g_hk_present)
if (g_hk_present.enabled())
{
return true; // already installed
return true; // already installed (persistent hook; the re-install path below re-enables it)
}
g_id_present = hook_register("IDXGISwapChain::Present", HookSubsys_Video);
@@ -963,10 +964,10 @@ bool install_present_hooks(IpcClient& ipc)
hook_set_installed(g_id_present1, false);
return false;
}
g_hk_present = safetyhook::create_inline(present, reinterpret_cast<void*>(&hk_Present));
install_inline(g_hk_present, present, &hk_Present);
if (present1 != nullptr)
{
g_hk_present1 = safetyhook::create_inline(present1, reinterpret_cast<void*>(&hk_Present1));
install_inline(g_hk_present1, present1, &hk_Present1);
}
g_unsupported_logged = false;
hook_set_installed(g_id_present, static_cast<bool>(g_hk_present));
@@ -980,7 +981,7 @@ bool install_present_hooks(IpcClient& ipc)
void* ecl = grab_execute_command_lists_address();
if (ecl != nullptr)
{
g_hk_ecl = safetyhook::create_inline(ecl, reinterpret_cast<void*>(&hk_ExecuteCommandLists));
install_inline(g_hk_ecl, ecl, &hk_ExecuteCommandLists);
hook_set_installed(g_id_ecl, static_cast<bool>(g_hk_ecl));
logf("install_present_hooks: d3d12 ExecuteCommandLists=%p hooked=%d", ecl,
static_cast<bool>(g_hk_ecl) ? 1 : 0);
@@ -1000,16 +1001,15 @@ void remove_present_hooks()
// Destroying here (= {}) frees the trampoline immediately; at a few hundred presents/s that race
// was rarely hit, but the uncapped mock-game storm (thousands/s) hits it reliably (0xC0000005).
// So: disable -> drain (in-flight detours finish on the live trampoline) -> only THEN destroy.
(void)g_hk_present.disable();
(void)g_hk_present1.disable();
(void)g_hk_ecl.disable();
disable_for_removal(g_hk_present);
disable_for_removal(g_hk_present1);
disable_for_removal(g_hk_ecl);
hook_set_installed(g_id_present, false);
hook_set_installed(g_id_present1, false);
hook_set_installed(g_id_ecl, false);
g_gate.drain();
g_hk_present = {}; // no detour is in-flight or can start now -> freeing the trampoline is safe
g_hk_present1 = {};
g_hk_ecl = {};
// Persistent hooks: keep g_hk_present/present1/ecl ALIVE (disabled), so the trampoline a stale
// detour may still call is never freed -- re-install re-enables them (see hook_install.hpp).
g_present_queue.store(nullptr, std::memory_order_relaxed);
g_logged_presents_n = 0; // let a fresh injection re-log the present pattern
g_logged_swapchains_n = 0;