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.
90 lines
2.9 KiB
C++
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;
|
|
}
|