// Deterministic, single-threaded contract test for the PERSISTENT inline-hook model // (hook/src/hook_install.hpp + hook_guard.hpp): install_inline() creates a hook once and thereafter // only re-enables it, so the trampoline is allocated ONCE and never freed across install/remove // cycles -- which is what makes spamming a subsystem on/off safe (a stale detour can never jump // through a freed trampoline). The mock_game_test storm covers the concurrent behaviour; this guards // the contract with no threads, so it can't flake on SafetyHook's enable/disable atomicity. #include #include #include #include "hook_guard.hpp" #include "hook_install.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; } } safetyhook::InlineHook g_hook; DetourGate g_gate; // A target with a real (relocatable) body, never inlined. __declspec(noinline) int target_fn(int x) { volatile int a = x; a = a * 3 + 7; a ^= (a >> 2); a += (a << 1); return a; } constexpr int kMark = 100000; // the detour adds this so "did the detour run" is observable int detour_fn(int x) { DetourGate::Guard guard(g_gate); return g_hook.call(x) + kMark; // reach the original through the trampoline } void install() { coop::hook::install_inline(g_hook, reinterpret_cast(&target_fn), reinterpret_cast(&detour_fn)); } void remove() { if (g_hook && !g_hook.disable()) { // surface, don't discard } g_gate.drain(); // persistent model: disable + drain, but do NOT destroy } } // namespace int main() { const int probe = 5; const int original = target_fn(probe); // unhooked baseline // First install: hook enabled, detour runs and reaches the original through the trampoline. install(); check(g_hook.enabled(), "install_inline enables the hook"); void* const tramp1 = g_hook.original(); check(tramp1 != nullptr, "trampoline allocated"); check(target_fn(probe) == original + kMark, "detour runs and reaches the original via the trampoline"); // Remove (persistent): disable + drain, but keep the hook ALIVE. remove(); check(!g_hook.enabled(), "remove disables the hook"); check(static_cast(g_hook), "remove keeps the hook alive (persistent -- not destroyed)"); check(target_fn(probe) == original, "disabled -> the original runs"); // Re-install: SAME trampoline reused (not recreated/freed), detour runs again. install(); check(g_hook.enabled(), "re-install re-enables the hook"); check(g_hook.original() == tramp1, "re-install REUSES the same trampoline (never freed -> no UAF)"); check(target_fn(probe) == original + kMark, "detour runs again after re-enable"); // Many cycles: the trampoline must never change and enable/disable must toggle cleanly. bool reuse = true, toggles = true; for (int i = 0; i < 50; ++i) { remove(); toggles = toggles && !g_hook.enabled(); install(); reuse = reuse && (g_hook.original() == tramp1); toggles = toggles && g_hook.enabled(); } check(reuse, "trampoline stays identical across 50 install/remove cycles (no churn, no leak)"); check(toggles, "enable/disable toggles cleanly across the cycles"); check(target_fn(probe) == original + kMark, "hook still works after the cycles"); g_hook = {}; // teardown (process is single-threaded here, so destroying is fine) std::printf(g_failures == 0 ? "PASS hook_install_test\n" : "FAILED hook_install_test (%d)\n", g_failures); return g_failures == 0 ? 0 : 1; }