Files
CoopAllTheThings/tools/sh_concurrency_repro/main.cpp
BlackMark 51e2c4f4cd Preserve SafetyHook concurrency finding as a committed upstream repro
A multithreaded test that tight-looped InlineHook enable()/disable() while
other threads called the hooked function flaked ~1/10. Isolation proved this is
a SafetyHook limitation, not our code: with the hook created once (no install
race, no trampoline UAF), tight-loop toggling AVs ~1/3 of runs in Debug
(0xC0000005, faulting RIP in the target body), while a no-toggle control is
clean at ~60M calls. enable()/disable() re-patch the prologue in place under a
VEH page-trap that only relocates a thread parked ON the prologue; a thread in
the function body faults on the briefly-non-exec page and relies on instruction
retry, which under rapid toggling races a half-rewritten prologue.

Rather than silently drop the flaky test, preserve the finding:
- tools/sh_concurrency_repro/: minimal, committed, non-CI reproducer
  (coop_sh_concurrency_repro; --callonly is the control). Surfaces 5/16 AVs.
- docs/safetyhook-concurrency.md: upstream-ready write-up (mechanism + fix
  directions + why it does not affect us).
- README lessons-learned + memory updated; tests/CMakeLists cross-references it.

Our code stays in SafetyHook's safe envelope (install/remove reconciled from a
single tick-bounded worker thread, never a tight loop), so the mock_game_test
storm is reliably green; the persistent-trampoline contract is covered
deterministically by hook_install_test + detour_gate_test. Removes the temp
_sh_probe wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:23:06 +02:00

80 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;
}