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:
@@ -82,4 +82,19 @@ private:
|
||||
std::atomic<int> m_active{0};
|
||||
};
|
||||
|
||||
// Disable a SafetyHook inline hook as the first step of removal (restore the original bytes, keep the
|
||||
// trampoline alive for the drain). disable() returns a [[nodiscard]] std::expected: a failure leaves
|
||||
// the function patched while removal goes on to free the trampoline -- a use-after-free -- so it must
|
||||
// not be silently discarded. There's no clean recovery mid-unhook, but surface it so it's
|
||||
// diagnosable. Templated so this header needn't depend on SafetyHook (the type is deduced at the
|
||||
// call site, where it's already included).
|
||||
template <class InlineHook>
|
||||
void disable_for_removal(InlineHook& hook)
|
||||
{
|
||||
if (!hook.disable())
|
||||
{
|
||||
OutputDebugStringA("coop: SafetyHook InlineHook::disable() failed during removal -- unhook may be unsafe\n");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace coop::hook
|
||||
|
||||
Reference in New Issue
Block a user