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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user