// 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 #include #include #include #include #include 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(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(&target_fn), reinterpret_cast(&detour_fn)); // created ONCE, persistent std::atomic stop{false}; std::atomic calls{0}; std::thread c1([&] { volatile int s = 0; while (!stop.load(std::memory_order_relaxed)) s = target_fn(static_cast(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(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; }