// 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 #include #include #include #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 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::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 disabled{false}; std::atomic freed{false}; std::atomic stop{false}; std::atomic guarded{0}; std::atomic 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; }