Harden hook removal: disable -> drain -> destroy, and drain settles first
Uncapping the mock game (next commit) turned mock_game_test's hook/unhook storm
into a real stress test (thousands of presents/s instead of tens), which reliably
crashed the game on remove (0xC0000005) for dx9/dx11/dx12. Two races the slow
vsync'd mock had masked:
1. Trampoline use-after-free. remove_*_hooks did `hook = {}` (destroy) BEFORE the
DetourGate drain. Destroying a SafetyHook InlineHook frees its trampoline
immediately, but an in-flight detour about to call the original via .stdcall()
(the trampoline) then used freed memory. Fix: disable() first (restores the
original bytes under thread suspension, but KEEPS the trampoline alive) -> drain
-> only then destroy. Applied to present/d3d9/opengl/vk/xinput/mkb/focus.
2. Entry-window race in DetourGate::drain(). It returned the instant the active
count read zero, but a thread can be inside the detour yet not have reached its
Guard constructor (the prologue is unguarded), so the count reads zero while a
detour is about to run -- and the freed state is then used. Fix: Sleep(1) BEFORE
each zero-check; with the hook disabled no new detour starts, so any
already-entered thread registers within that window. This alone fixed dx11 (the
highest present rate, ~11000/s, which hit the window every storm).
Audio is unaffected (it uses vtable swaps, which keep a real original pointer, not
a trampoline). Full suite 21/21, and the storm now survives on every backend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -53,15 +53,23 @@ public:
|
||||
// Wait (bounded ~400 ms) for all in-flight detours to finish. Call AFTER the hook is
|
||||
// restored / the gate is closed, so no new detour can start -- otherwise this may never
|
||||
// reach zero on a busy render thread.
|
||||
//
|
||||
// We Sleep(1) *before* each zero-check (not after) to close an entry-window race: a thread can
|
||||
// have already jumped into the detour but not yet reached its Guard constructor (the few-
|
||||
// instruction prologue is unguarded), so m_active reads 0 even though a detour is about to run.
|
||||
// Returning then would free the state out from under it. With the hook disabled no NEW detour can
|
||||
// start, so any such thread reaches its Guard within nanoseconds; a 1 ms settle before concluding
|
||||
// "zero" lets it register. Without this, a backend presenting at thousands/s (the uncapped
|
||||
// mock-game storm) reliably crashed on remove (0xC0000005); with it, the count is accurate.
|
||||
void drain()
|
||||
{
|
||||
for (int spins = 0; spins < 400; ++spins)
|
||||
{
|
||||
Sleep(1);
|
||||
if (m_active.load(std::memory_order_acquire) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user