Files
CoopAllTheThings/hook/src/hook_guard.hpp
BlackMark 30eccf749d Apply clang-format across the whole tree
Run clang-format (the repo's .clang-format: LLVM base, 120 cols, tabs,
Allman functions) over every source file so the tree is formatter-clean.
Whitespace only -- no behavior change; full x64 + x86 suites pass.

Also set SortIncludes: false in .clang-format. Windows include order is
load-bearing (windows.h must precede tlhelp32.h / mmreg.h / xinput.h /
dinput.h; winsock2.h must precede windows.h), and the default
alphabetical sort reorders tlhelp32.h ahead of windows.h -- a build
break. Leaving order alone keeps the manual, correct grouping.
2026-07-12 11:52:53 +02:00

87 lines
4.0 KiB
C++

// Safe-unhook coordination between a hook's removal (worker thread) and the detours still
// running on the game's own threads (render / audio / window / input).
//
// The hazard: remove_*_hooks restores the hook and then frees the shared state the detour
// touches (D3D device/context, the keyed-mutex texture, Vulkan read-back resources, the IPC
// pointer). A capture detour mid-flight on the game's render thread then uses freed memory ->
// use-after-free -> the game crashes.
//
// The removal sequence that prevents it:
// 1. Disable the hook FIRST so no NEW detour can start. For a SafetyHook inline hook that's
// `disable_for_removal(hook)` (disable, NOT `= {}` destroy): it restores the original bytes but
// keeps the trampoline alive -- hooks are PERSISTENT, never destroyed mid-session, so an in-flight
// detour about to call the trampoline never finds it freed (see hook_install.hpp). For a hook the
// game reaches by a cached pointer (Vulkan present, the WNDPROC subclass) it's clearing an atomic
// gate / restoring the window proc instead.
// 2. drain() -- wait (bounded) for detour BODIES already running to finish, since the disable above
// does NOT wait for the part of the detour that runs before it calls the trampoline.
// 3. Only THEN free the shared state the detour was reading.
//
// Each detour wraps its whole body in a DetourGate::Guard (an RAII active-count). drain() spins
// until that count reaches zero. Detours are microseconds to a few milliseconds, so this returns
// almost immediately; the bound keeps a wedged game thread from hanging the worker.
#pragma once
#include <atomic>
#include <windows.h>
namespace coop::hook {
class DetourGate {
public:
// RAII: marks a detour body as in-flight for as long as it's on the stack.
class Guard {
public:
explicit Guard(DetourGate& gate) : m_gate(gate) { m_gate.m_active.fetch_add(1, std::memory_order_acq_rel); }
~Guard() { m_gate.m_active.fetch_sub(1, std::memory_order_acq_rel); }
Guard(const Guard&) = delete;
Guard& operator=(const Guard&) = delete;
private:
DetourGate& m_gate;
};
// 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. A backend presenting at thousands of frames/s hits this window
// reliably, so checking before the first sleep is not safe.
void drain()
{
for (int spins = 0; spins < 400; ++spins) {
Sleep(1);
if (m_active.load(std::memory_order_acquire) == 0) {
return;
}
}
}
int active() const { return m_active.load(std::memory_order_acquire); }
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