Files
CoopAllTheThings/tests/hook_install_test.cpp
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

102 lines
3.5 KiB
C++

// 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 <cstdint>
#include <cstdio>
#include <safetyhook.hpp>
#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<int>(x) + kMark; // reach the original through the trampoline
}
void install()
{
coop::hook::install_inline(g_hook, reinterpret_cast<void*>(&target_fn), reinterpret_cast<void*>(&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<void*>();
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<bool>(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<void*>() == 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<void*>() == 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;
}