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>
This commit is contained in:
79
tools/sh_concurrency_repro/main.cpp
Normal file
79
tools/sh_concurrency_repro/main.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user