Mock game: drive input/focus/MKB hooks + window title; add DetourGate test
- poll_input() each frame (XInputGetState / GetAsyncKeyState / GetKeyboardState / GetForegroundWindow), like a real game, so mock_game_test's hook/unhook storm actually exercises removing the input/focus/MKB hooks while their detours are in flight -- the coverage gap that let those removal races go untested. - Window title shows the backend + a once-per-second-smoothed fps. - A vectored-exception crash logger prints the faulting module+offset (named the storm's intermittent crashes during this work; inert otherwise). - detour_gate_test: fast, deterministic guard for DetourGate -- drain() must block while a Guard is in flight and return promptly otherwise, plus a concurrency stress that asserts no body runs against freed state. (A synthetic install-race unit test was tried but flaked on SafetyHook's own enable/disable atomicity under ~30M calls/s, unrelated to our code, so the storm is the install/remove guard.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
122
tests/detour_gate_test.cpp
Normal file
122
tests/detour_gate_test.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
// Unit test for coop::hook::DetourGate (hook/src/hook_guard.hpp) -- the safe-unhook coordination
|
||||
// every removable hook relies on. The integration-level guard is mock_game_test's hook/unhook storm
|
||||
// (now driven by an uncapped, input-polling mock), but that's slow and timing-dependent; this is a
|
||||
// fast, deterministic check of the core contract: drain() must NOT return while a detour body
|
||||
// (a Guard) is in flight, and must return promptly once none are. A regression that made drain()
|
||||
// return early would reintroduce the use-after-free the gate exists to prevent.
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
|
||||
#include "hook_guard.hpp"
|
||||
|
||||
using coop::hook::DetourGate;
|
||||
|
||||
namespace
|
||||
{
|
||||
int g_failures = 0;
|
||||
void check(bool ok, const char* what)
|
||||
{
|
||||
std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what);
|
||||
if (!ok)
|
||||
{
|
||||
++g_failures;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
// --- Contract 1: drain() blocks while a Guard is held, and returns after it's released. --------
|
||||
{
|
||||
DetourGate gate;
|
||||
std::atomic<bool> drained{false};
|
||||
// Hold a Guard (a detour body "in flight") before the drainer starts.
|
||||
auto* guard = new DetourGate::Guard(gate);
|
||||
check(gate.active() == 1, "active count reflects a held Guard");
|
||||
|
||||
std::thread drainer([&] {
|
||||
gate.drain();
|
||||
drained.store(true, std::memory_order_release);
|
||||
});
|
||||
|
||||
// While the Guard is held, drain() must not have returned.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(120));
|
||||
check(!drained.load(std::memory_order_acquire), "drain() blocks while a detour is in flight");
|
||||
|
||||
// Release the Guard; drain() must then return promptly.
|
||||
delete guard;
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
|
||||
while (!drained.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < deadline)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
check(drained.load(std::memory_order_acquire), "drain() returns once the in-flight detour finishes");
|
||||
drainer.join();
|
||||
check(gate.active() == 0, "active count back to zero");
|
||||
}
|
||||
|
||||
// --- Contract 2: drain() returns promptly when nothing is in flight. ---------------------------
|
||||
{
|
||||
DetourGate gate;
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
gate.drain();
|
||||
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - t0)
|
||||
.count();
|
||||
check(ms < 100, "drain() with no in-flight detours returns quickly");
|
||||
}
|
||||
|
||||
// --- Contract 3: concurrency stress -- disable -> drain -> "free" -> re-enable, while workers run
|
||||
// the guarded body. With the gate honoured, no worker ever runs its body while "freed" is set
|
||||
// (that would be the use-after-free). Models remove_*_hooks vs the game's detour threads. -----
|
||||
{
|
||||
DetourGate gate;
|
||||
std::atomic<bool> disabled{false};
|
||||
std::atomic<bool> freed{false};
|
||||
std::atomic<bool> stop{false};
|
||||
std::atomic<long long> guarded{0};
|
||||
std::atomic<long long> violations{0};
|
||||
|
||||
auto worker = [&] {
|
||||
while (!stop.load(std::memory_order_relaxed))
|
||||
{
|
||||
if (disabled.load(std::memory_order_acquire))
|
||||
{
|
||||
continue; // "hook removed" -> no new detour starts
|
||||
}
|
||||
DetourGate::Guard g(gate);
|
||||
if (freed.load(std::memory_order_acquire))
|
||||
{
|
||||
violations.fetch_add(1, std::memory_order_relaxed); // ran the body on freed state
|
||||
}
|
||||
guarded.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
};
|
||||
|
||||
std::thread workers[6];
|
||||
for (auto& w : workers)
|
||||
{
|
||||
w = std::thread(worker);
|
||||
}
|
||||
for (int c = 0; c < 3000; ++c)
|
||||
{
|
||||
disabled.store(true, std::memory_order_release); // disable: no new detours
|
||||
gate.drain(); // wait for in-flight detours
|
||||
freed.store(true, std::memory_order_release); // "free" the shared state
|
||||
freed.store(false, std::memory_order_release); // "rebuild"
|
||||
disabled.store(false, std::memory_order_release);
|
||||
}
|
||||
stop.store(true, std::memory_order_relaxed);
|
||||
for (auto& w : workers)
|
||||
{
|
||||
w.join();
|
||||
}
|
||||
std::printf(" stress: guarded=%lld violations=%lld\n", guarded.load(), violations.load());
|
||||
check(guarded.load() > 0, "workers actually ran guarded bodies");
|
||||
check(violations.load() == 0, "no detour body ran against freed state (drain + disable hold)");
|
||||
}
|
||||
|
||||
std::printf(g_failures == 0 ? "PASS detour_gate_test\n" : "FAILED detour_gate_test (%d)\n", g_failures);
|
||||
return g_failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user