Files
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

90 lines
2.9 KiB
C++

// Minimal reproducer for a SafetyHook concurrency limitation (see docs/safetyhook-concurrency.md).
//
// Claim: safetyhook::InlineHook::enable()/disable() are NOT safe to call in a tight loop while other
// threads call the hooked function. The hook here is created ONCE (no install race, no create/destroy,
// no trampoline churn), so a crash is purely SafetyHook's enable/disable-vs-concurrent-call behaviour.
//
// Modes:
// (default) one thread tight-loops disable()/enable(); two threads hammer the target.
// --callonly control: the two caller threads hammer, NO toggling. Must never crash.
//
// Observed on Windows x64 (MSVC): default mode faults ~1 in 8 runs (~30M toggle cycles) with the
// faulting RIP inside the target function BODY (e.g. target_fn+0x4) -- a caller thread that the VEH
// trap relocation failed to rescue while the prologue was being re-patched. --callonly is 3/3 clean
// at 60-75M calls. This is a regression REPRODUCER for an upstream report, deliberately excluded from
// the CTest suite (it is probabilistic by nature); run it directly when validating an upstream fix.
//
// Run it in a loop to see the flake, e.g. (PowerShell):
// 1..16 | % { & .\coop_sh_concurrency_repro.exe; "exit=$LASTEXITCODE" }
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <thread>
#include <safetyhook.hpp>
namespace {
safetyhook::InlineHook g_hook;
// A real, relocatable, never-inlined target so SafetyHook steals a genuine prologue.
__declspec(noinline) int target_fn(int x)
{
volatile int a = x;
a = a * 3 + 7;
a ^= (a >> 2);
a += (a << 1);
return a;
}
int detour_fn(int x)
{
return g_hook.call<int>(x) + 100000; // reach the original through the trampoline
}
} // namespace
int main(int argc, char** argv)
{
const bool call_only = argc > 1 && std::strcmp(argv[1], "--callonly") == 0;
g_hook = safetyhook::create_inline(reinterpret_cast<void*>(&target_fn),
reinterpret_cast<void*>(&detour_fn)); // created ONCE, persistent
std::atomic<bool> stop{false};
std::atomic<long long> calls{0};
std::thread c1([&] {
volatile int s = 0;
while (!stop.load(std::memory_order_relaxed))
s = target_fn(static_cast<int>(calls.fetch_add(1)));
(void)s;
});
std::thread c2([&] {
volatile int s = 0;
while (!stop.load(std::memory_order_relaxed))
s = target_fn(static_cast<int>(calls.fetch_add(1)));
(void)s;
});
std::thread tog;
if (!call_only) {
tog = std::thread([&] {
long long n = 0;
while (!stop.load(std::memory_order_relaxed)) {
if (!g_hook.disable()) {
}
if (!g_hook.enable()) {
}
++n;
}
std::printf("toggles=%lld\n", n);
});
}
std::this_thread::sleep_for(std::chrono::seconds(3));
stop.store(true);
c1.join();
c2.join();
if (tog.joinable())
tog.join();
g_hook = {};
std::printf("survived %lld calls (mode=%s)\n", calls.load(), call_only ? "callonly" : "toggle");
return 0;
}