Fix stale removal comments (persistent, not destroy) + add deterministic install test
- The remove_* comments still described the superseded "disable -> drain -> destroy" flow; the code keeps hooks alive (persistent) and re-enables on re-install. Updated the comments to match, and corrected the XInput note (its detours return synthesized state and never call the trampoline, so destroying its vector is safe -- unlike the trampoline-calling present/MKB/focus-cursor hooks). - hook_install_test: a fast, single-threaded contract test for hook_install.hpp -- install_inline creates the hook once and reuses the SAME trampoline across 50 install/remove cycles (never freed -> no stale-detour UAF), toggling enable/disable cleanly. Fills the guard the removed (flaky, concurrency-bound) reproducer left, with no threads so it can't flake on SafetyHook's enable/disable atomicity. x64 23/23, x86 3/3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,14 @@ add_executable(detour_gate_test detour_gate_test.cpp)
|
||||
target_include_directories(detour_gate_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
|
||||
add_test(NAME detour_gate_test COMMAND detour_gate_test)
|
||||
|
||||
# Deterministic contract test for the persistent inline-hook model (hook/src/hook_install.hpp):
|
||||
# install_inline creates a hook once and re-enables it on re-install, reusing the trampoline (never
|
||||
# freed) so a stale detour can't UAF. Single-threaded, so it can't flake on enable/disable atomicity.
|
||||
add_executable(hook_install_test hook_install_test.cpp)
|
||||
target_include_directories(hook_install_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
|
||||
target_link_libraries(hook_install_test PRIVATE safetyhook::safetyhook)
|
||||
add_test(NAME hook_install_test COMMAND hook_install_test)
|
||||
|
||||
# Unit test for the MKB event ring (SPSC push/pop, wrap-around, full/empty).
|
||||
add_executable(mkb_ring_test mkb_ring_test.cpp)
|
||||
target_link_libraries(mkb_ring_test PRIVATE coop_common)
|
||||
@@ -284,6 +292,7 @@ coop_output_subdir(tests
|
||||
dinput_hook_test
|
||||
audio_ring_test
|
||||
detour_gate_test
|
||||
hook_install_test
|
||||
mkb_ring_test
|
||||
mkb_map_test
|
||||
audio_mix_test
|
||||
|
||||
105
tests/hook_install_test.cpp
Normal file
105
tests/hook_install_test.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user