Files
openfut-launcher/openfut-hook/src/sbc_trace.rs
T
funman300 94feaec63f Promote the FIFA17 SBC dispatch repair: armed by the build, not by env
Retail Gates A-G passed on the pinned CardsDLL build (4706a881), and the repair
has been live-proven repeatedly, so it is now a promoted feature. Arming it from
OPENFUT_SBC_DISPATCH meant any launch that did not export it (Steam, the launcher
Launch button, a bare umu-run) silently lost the SBC screen to the known
response-to-deserializer dispatch defect, leaving a harness script as the only
working entry point.

REPAIR_PROMOTED is now a build constant with a compile-time contract, and both
install sites derive from it: sbc_dispatch::install always arms, and
sbc_trace::install derives the parser/notifier/controller-registration traces from
it because those traces ARE the repair decision inputs, not optional diagnostics.

Promotion weakens no check. Safety stays in the runtime evidence gate rather than a
flag: the worker still validates the exact CardsDLL signatures before installing a
detour, and decide() still requires the transport sentinel status, the pinned
category-response vtable captured while the response object was provably live,
balanced parser counts on the one parser thread, this generation notifier having
entered AND returned, the captured controller/model identity, and one repair per
deserializer generation. An unrecognised build leaves native execution untouched.

Rollback is a file swap (restore the previous version.dll via the hook harness
backup), the documented client rollback path, deliberately not an env kill-switch.

fmt clean, strict clippy clean on default and fifa17 features, 22 tests pass,
release cross-build to x86_64-pc-windows-gnu produces artifact 3641d581.
2026-08-19 02:45:43 +00:00

1250 lines
47 KiB
Rust

//! Passive, behavior-preserving trace hooks for FIFA 17's SBC category response.
use core::ffi::c_void;
use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use windows_sys::Win32::Foundation::{
CloseHandle, GetLastError, ERROR_NO_MORE_FILES, HANDLE, INVALID_HANDLE_VALUE,
};
use windows_sys::Win32::System::Diagnostics::Debug::{
FlushInstructionCache, GetThreadContext, CONTEXT, CONTEXT_CONTROL_AMD64,
};
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Thread32First, Thread32Next, TH32CS_SNAPTHREAD, THREADENTRY32,
};
use windows_sys::Win32::System::LibraryLoader::{
GetModuleHandleA, GetModuleHandleExA, GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
GET_MODULE_HANDLE_EX_FLAG_PIN,
};
use windows_sys::Win32::System::Memory::{
VirtualAlloc, VirtualFree, VirtualProtect, VirtualQuery, MEMORY_BASIC_INFORMATION, MEM_COMMIT,
MEM_RELEASE, MEM_RESERVE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY,
PAGE_GUARD, PAGE_NOACCESS, PAGE_READWRITE,
};
use windows_sys::Win32::System::Threading::{
GetCurrentProcess, GetCurrentProcessId, GetCurrentThreadId, OpenThread, ResumeThread,
SuspendThread, THREAD_GET_CONTEXT, THREAD_SUSPEND_RESUME,
};
pub(crate) const CATEGORY_FACTORY_RVA: usize = 0x17aa10;
pub(crate) const CATEGORY_DESERIALIZER_RVA: usize = 0x17b2b0;
const COPY_LEN: usize = 19;
const ABS_JUMP_LEN: usize = 14;
const TRAMPOLINE_LEN: usize = COPY_LEN + ABS_JUMP_LEN;
const NOTIFIER_RVA: usize = 0x17aa80;
const NOTIFIER_COPY_LEN: usize = 15;
const CONTROLLER_REGISTER_RVA: usize = 0x1a4a70;
const CONTROLLER_REGISTER_COPY_LEN: usize = 15;
const FUT_SBS_CATEGORIES_EVENT: u32 = 0x756c;
const CONTROLLER_REGISTER_SIGNATURE: [u8; CONTROLLER_REGISTER_COPY_LEN] = [
0x89, 0x54, 0x24, 0x10, 0x48, 0x83, 0xec, 0x28, 0x4c, 0x8d, 0x81, 0xc0, 0x00, 0x00, 0x00,
];
const NOTIFIER_SIGNATURE: [u8; 32] = [
0x48, 0x89, 0x5c, 0x24, 0x08, 0x48, 0x89, 0x74, 0x24, 0x10, 0x57, 0x48, 0x83, 0xec, 0x20, 0x48,
0x8b, 0x59, 0x58, 0x48, 0x8b, 0x71, 0x60, 0x33, 0xff, 0x48, 0x2b, 0xf3, 0xc6, 0x81, 0x88, 0x00,
];
const MAX_PEERS: usize = 512;
const MAX_QUIESCE_PASSES: usize = 8;
const CONTROL_RVA: usize = 0x180d00;
const CONTROL_SIGNATURE: [u8; 12] = [
0x48, 0x83, 0xec, 0x28, 0x48, 0x85, 0xc9, 0x74, 0x50, 0x45, 0x33, 0xc0,
];
const FACTORY_SIGNATURE: [u8; 32] = [
0x48, 0x89, 0x4c, 0x24, 0x08, 0x53, 0x48, 0x83, 0xec, 0x30, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe,
0xff, 0xff, 0xff, 0x33, 0xc0, 0x89, 0x44, 0x24, 0x48, 0x48, 0x8d, 0x44, 0x24, 0x48, 0x48, 0x89,
];
const DESERIALIZER_SIGNATURE: [u8; 32] = [
0x48, 0x8b, 0xc4, 0x55, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x8d, 0xa8, 0xe8,
0xfd, 0xff, 0xff, 0x48, 0x81, 0xec, 0xf0, 0x02, 0x00, 0x00, 0x48, 0xc7, 0x45, 0x98, 0xfe, 0xff,
];
type FactoryFn = unsafe extern "system" fn(*mut c_void) -> *mut c_void;
type DeserializerFn = unsafe extern "system" fn(*mut c_void, *mut c_void) -> u8;
static ENABLED: AtomicBool = AtomicBool::new(false);
static STATE: AtomicUsize = AtomicUsize::new(TraceState::Disabled as usize);
static FACTORY_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
static DESERIALIZER_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
static FACTORY_ENTRIES: AtomicU64 = AtomicU64::new(0);
static FACTORY_EXITS: AtomicU64 = AtomicU64::new(0);
static FACTORY_LAST_THIS: AtomicUsize = AtomicUsize::new(0);
static FACTORY_LAST_RESULT: AtomicUsize = AtomicUsize::new(0);
static FACTORY_LAST_THREAD: AtomicUsize = AtomicUsize::new(0);
static DESERIALIZER_ENTRIES: AtomicU64 = AtomicU64::new(0);
static DESERIALIZER_EXITS: AtomicU64 = AtomicU64::new(0);
static DESERIALIZER_LAST_THIS: AtomicUsize = AtomicUsize::new(0);
static DESERIALIZER_LAST_READER: AtomicUsize = AtomicUsize::new(0);
static DESERIALIZER_LAST_RESULT: AtomicBool = AtomicBool::new(false);
static DESERIALIZER_LAST_THREAD: AtomicUsize = AtomicUsize::new(0);
static TRACE_BASE: AtomicUsize = AtomicUsize::new(0);
static DESERIALIZER_EXIT_M: AtomicUsize = AtomicUsize::new(0);
static DESERIALIZER_EXIT_COUNT: AtomicUsize = AtomicUsize::new(0);
static DESERIALIZER_EXIT_B_READY: AtomicUsize = AtomicUsize::new(usize::MAX);
static DESERIALIZER_EXIT_RESPONSE_VTABLE: AtomicUsize = AtomicUsize::new(0);
static NOTIFIER_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
static CONTROLLER_REGISTER_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
static NOTIFIER_ENTRIES: AtomicU64 = AtomicU64::new(0);
static NOTIFIER_EXITS: AtomicU64 = AtomicU64::new(0);
static NOTIFIER_CTX: AtomicUsize = AtomicUsize::new(0);
static NOTIFIER_BYTE_BEFORE: AtomicUsize = AtomicUsize::new(usize::MAX);
static NOTIFIER_BYTE_AFTER: AtomicUsize = AtomicUsize::new(usize::MAX);
static NOTIFIER_BEGIN: AtomicUsize = AtomicUsize::new(0);
static NOTIFIER_END: AtomicUsize = AtomicUsize::new(0);
static NOTIFIER_COUNT: AtomicUsize = AtomicUsize::new(usize::MAX);
static PATCH_INSTALLER_BUSY: AtomicBool = AtomicBool::new(false);
static CODE_PATCH_PENDING: AtomicUsize = AtomicUsize::new(0);
pub(crate) struct PatchInstallerGate;
impl Drop for PatchInstallerGate {
fn drop(&mut self) {
PATCH_INSTALLER_BUSY.store(false, Ordering::Release);
}
}
pub(crate) fn acquire_patch_installer_gate() -> Option<PatchInstallerGate> {
for _ in 0..200 {
if PATCH_INSTALLER_BUSY
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return Some(PatchInstallerGate);
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
None
}
pub(crate) struct CodeInstallerPending;
impl Drop for CodeInstallerPending {
fn drop(&mut self) {
CODE_PATCH_PENDING.fetch_sub(1, Ordering::AcqRel);
}
}
pub(crate) fn code_patch_installers_ready() -> bool {
CODE_PATCH_PENDING.load(Ordering::Acquire) == 0
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(usize)]
enum TraceState {
Disabled,
Requested,
Installed,
Failed,
DegradedHookActive,
DegradedProcessState,
DegradedHookAndProcess,
}
pub(crate) fn env_enabled(value: Option<&str>) -> bool {
matches!(value, Some("1"))
}
pub(crate) fn target_va(base: usize, rva: usize) -> Option<usize> {
base.checked_add(rva)
}
pub(crate) fn absolute_jump(destination: usize) -> [u8; ABS_JUMP_LEN] {
let mut jump = [0u8; ABS_JUMP_LEN];
jump[..6].copy_from_slice(&[0xff, 0x25, 0, 0, 0, 0]);
jump[6..].copy_from_slice(&(destination as u64).to_le_bytes());
jump
}
fn instruction_pointer_in_span(rip: usize, target: usize) -> bool {
target
.checked_add(COPY_LEN)
.map(|end| rip >= target && rip < end)
.unwrap_or(true)
}
pub(crate) struct SuspendedPeers {
handles: [HANDLE; MAX_PEERS],
tids: [u32; MAX_PEERS],
count: usize,
}
impl SuspendedPeers {
fn empty() -> Self {
Self {
handles: [core::ptr::null_mut(); MAX_PEERS],
tids: [0; MAX_PEERS],
count: 0,
}
}
fn contains_tid(&self, tid: u32) -> bool {
self.tids[..self.count].contains(&tid)
}
pub(crate) unsafe fn resume_all(&mut self) -> bool {
let mut all_resumed = true;
for index in (0..self.count).rev() {
let handle = self.handles[index];
if handle.is_null() {
continue;
}
if ResumeThread(handle) == u32::MAX {
all_resumed = false;
continue;
}
CloseHandle(handle);
self.handles[index] = core::ptr::null_mut();
}
all_resumed
}
}
impl Drop for SuspendedPeers {
fn drop(&mut self) {
// Emergency retry only. A handle whose thread still cannot be resumed is
// intentionally leaked rather than closed while its thread is suspended.
unsafe {
let _ = self.resume_all();
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum QuiesceFailure {
Acquire,
Resume,
}
/// Stop and inspect every peer thread before touching either entry point. Any
/// incomplete enumeration/access/context operation fails the transaction closed.
pub(crate) unsafe fn suspend_peers(
factory: usize,
deserializer: usize,
) -> Result<SuspendedPeers, QuiesceFailure> {
let process_id = GetCurrentProcessId();
let current_thread_id = GetCurrentThreadId();
let mut peers = SuspendedPeers::empty();
for _ in 0..MAX_QUIESCE_PASSES {
// Enumerate into fixed stack storage before suspending anything found in
// this pass. Later passes close the thread-creation race to a fixed point.
let mut candidates = [0u32; MAX_PEERS];
let mut candidate_count = 0usize;
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if snapshot == INVALID_HANDLE_VALUE {
break;
}
let mut entry: THREADENTRY32 = core::mem::zeroed();
entry.dwSize = core::mem::size_of::<THREADENTRY32>() as u32;
let mut available = Thread32First(snapshot, &mut entry) != 0;
if !available {
CloseHandle(snapshot);
break;
}
let mut enumeration_ok = true;
while available {
let tid = entry.th32ThreadID;
if entry.th32OwnerProcessID == process_id
&& tid != current_thread_id
&& !peers.contains_tid(tid)
{
if peers.count + candidate_count >= MAX_PEERS {
enumeration_ok = false;
break;
}
if !candidates[..candidate_count].contains(&tid) {
candidates[candidate_count] = tid;
candidate_count += 1;
}
}
entry.dwSize = core::mem::size_of::<THREADENTRY32>() as u32;
available = Thread32Next(snapshot, &mut entry) != 0;
if !available && GetLastError() != ERROR_NO_MORE_FILES {
enumeration_ok = false;
}
}
CloseHandle(snapshot);
if !enumeration_ok {
return Err(if peers.resume_all() {
QuiesceFailure::Acquire
} else {
QuiesceFailure::Resume
});
}
if candidate_count == 0 {
return Ok(peers);
}
for &tid in &candidates[..candidate_count] {
let handle = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT, 0, tid);
if handle.is_null() || SuspendThread(handle) == u32::MAX {
if !handle.is_null() {
CloseHandle(handle);
}
return Err(if peers.resume_all() {
QuiesceFailure::Acquire
} else {
QuiesceFailure::Resume
});
}
peers.handles[peers.count] = handle;
peers.tids[peers.count] = tid;
peers.count += 1;
let mut context: CONTEXT = core::mem::zeroed();
context.ContextFlags = CONTEXT_CONTROL_AMD64;
if GetThreadContext(handle, &mut context) == 0
|| instruction_pointer_in_span(context.Rip as usize, factory)
|| instruction_pointer_in_span(context.Rip as usize, deserializer)
{
return Err(if peers.resume_all() {
QuiesceFailure::Acquire
} else {
QuiesceFailure::Resume
});
}
}
}
Err(if peers.resume_all() {
QuiesceFailure::Acquire
} else {
QuiesceFailure::Resume
})
}
unsafe fn executable_range(address: usize, length: usize) -> bool {
let Some(end) = address.checked_add(length) else {
return false;
};
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
if VirtualQuery(
address as _,
&mut mbi,
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
) == 0
|| mbi.State != MEM_COMMIT
|| mbi.Protect & (PAGE_GUARD | PAGE_NOACCESS) != 0
{
return false;
}
let protection = mbi.Protect & 0xff;
matches!(
protection,
PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY
) && end <= mbi.BaseAddress as usize + mbi.RegionSize
}
pub(crate) unsafe fn executable_range_in_image(base: usize, address: usize, length: usize) -> bool {
let Some(end) = address.checked_add(length) else {
return false;
};
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
if VirtualQuery(
address as _,
&mut mbi,
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
) == 0
{
return false;
}
executable_range(address, length)
&& mbi.AllocationBase as usize == base
&& end <= mbi.BaseAddress as usize + mbi.RegionSize
}
pub(crate) unsafe fn readable_range(address: usize, length: usize) -> bool {
let Some(end) = address.checked_add(length) else {
return false;
};
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
VirtualQuery(
address as _,
&mut mbi,
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
) != 0
&& mbi.State == MEM_COMMIT
&& mbi.Protect & (PAGE_GUARD | PAGE_NOACCESS) == 0
&& end <= mbi.BaseAddress as usize + mbi.RegionSize
}
pub(crate) unsafe fn guarded_usize(address: usize) -> Option<usize> {
(address & 7 == 0 && readable_range(address, 8))
.then(|| core::ptr::read_volatile(address as *const usize))
}
pub(crate) unsafe fn guarded_u16(address: usize) -> Option<u16> {
readable_range(address, 2).then(|| core::ptr::read_volatile(address as *const u16))
}
pub(crate) unsafe fn guarded_u8(address: usize) -> Option<u8> {
readable_range(address, 1).then(|| core::ptr::read_volatile(address as *const u8))
}
pub(crate) unsafe fn valid_cards_image(base: usize) -> bool {
let Some(control) = base.checked_add(CONTROL_RVA) else {
return false;
};
if !readable_range(base, 0x1000) || !executable_range(control, CONTROL_SIGNATURE.len()) {
return false;
}
if *(base as *const u16) != 0x5a4d {
return false;
}
let pe_off = *((base + 0x3c) as *const u32) as usize;
if pe_off > 0xf00 {
return false;
}
let Some(pe) = base.checked_add(pe_off) else {
return false;
};
if *(pe as *const u32) != 0x0000_4550 {
return false;
}
let Some(size_field) = pe.checked_add(24 + 0x38) else {
return false;
};
let size = *(size_field as *const u32) as usize;
let required = CATEGORY_DESERIALIZER_RVA + DESERIALIZER_SIGNATURE.len();
size > required
&& core::slice::from_raw_parts(control as *const u8, CONTROL_SIGNATURE.len())
== CONTROL_SIGNATURE
}
pub(crate) unsafe fn validate_cards_build(base: usize) -> bool {
valid_cards_image(base)
}
unsafe fn signature_matches(target: usize, signature: &[u8; 32]) -> bool {
core::slice::from_raw_parts(target as *const u8, signature.len()) == signature
}
pub(crate) unsafe fn allocate_trampoline(target: usize, copy_len: usize) -> Option<usize> {
let trampoline_len = copy_len.checked_add(ABS_JUMP_LEN)?;
let memory = VirtualAlloc(
core::ptr::null(),
trampoline_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
) as usize;
if memory == 0 {
return None;
}
core::ptr::copy_nonoverlapping(target as *const u8, memory as *mut u8, copy_len);
let Some(resume) = target.checked_add(copy_len) else {
VirtualFree(memory as _, 0, MEM_RELEASE);
return None;
};
let jump = absolute_jump(resume);
core::ptr::copy_nonoverlapping(jump.as_ptr(), (memory + copy_len) as *mut u8, ABS_JUMP_LEN);
let mut old = 0u32;
if VirtualProtect(memory as _, trampoline_len, PAGE_EXECUTE_READ, &mut old) == 0
|| FlushInstructionCache(GetCurrentProcess(), memory as _, trampoline_len) == 0
{
VirtualFree(memory as _, 0, MEM_RELEASE);
return None;
}
Some(memory)
}
unsafe fn write_entry(
target: usize,
destination: usize,
original: &[u8; COPY_LEN],
published: &mut bool,
) -> Result<(), bool> {
let mut patch = [0x90u8; COPY_LEN];
patch[..ABS_JUMP_LEN].copy_from_slice(&absolute_jump(destination));
let mut old = 0u32;
if VirtualProtect(target as _, COPY_LEN, PAGE_EXECUTE_READWRITE, &mut old) == 0 {
return Err(true);
}
// From this point onward another processor may observe the detour, even if a
// later cache/protection operation fails and rollback restores original bytes.
*published = true;
core::ptr::copy_nonoverlapping(patch.as_ptr(), target as *mut u8, COPY_LEN);
let flushed = FlushInstructionCache(GetCurrentProcess(), target as _, COPY_LEN) != 0;
let mut ignored = 0u32;
let restored = VirtualProtect(target as _, COPY_LEN, old, &mut ignored) != 0;
if flushed && restored {
Ok(())
} else {
// `true` means the caller may safely free the trampoline. A false value
// means rollback itself failed, so executable backing must be retained.
Err(restore_entry(target, original))
}
}
unsafe fn restore_entry(target: usize, original: &[u8; COPY_LEN]) -> bool {
let mut old = 0u32;
if VirtualProtect(target as _, COPY_LEN, PAGE_EXECUTE_READWRITE, &mut old) == 0 {
return false;
}
core::ptr::copy_nonoverlapping(original.as_ptr(), target as *mut u8, COPY_LEN);
let flushed = FlushInstructionCache(GetCurrentProcess(), target as _, COPY_LEN) != 0;
let mut ignored = 0u32;
let restored = VirtualProtect(target as _, COPY_LEN, old, &mut ignored) != 0;
flushed && restored
}
unsafe fn restore_notifier_entry(target: usize) -> bool {
let original: [u8; NOTIFIER_COPY_LEN] =
NOTIFIER_SIGNATURE[..NOTIFIER_COPY_LEN].try_into().unwrap();
let mut old = 0u32;
if VirtualProtect(
target as _,
NOTIFIER_COPY_LEN,
PAGE_EXECUTE_READWRITE,
&mut old,
) == 0
{
return false;
}
core::ptr::copy_nonoverlapping(original.as_ptr(), target as *mut u8, NOTIFIER_COPY_LEN);
let flushed = FlushInstructionCache(GetCurrentProcess(), target as _, NOTIFIER_COPY_LEN) != 0;
let mut ignored = 0u32;
flushed && VirtualProtect(target as _, NOTIFIER_COPY_LEN, old, &mut ignored) != 0
}
unsafe fn write_notifier_entry(
target: usize,
destination: usize,
published: &mut bool,
) -> Result<(), bool> {
let mut patch = [0x90u8; NOTIFIER_COPY_LEN];
patch[..ABS_JUMP_LEN].copy_from_slice(&absolute_jump(destination));
let mut old = 0u32;
if VirtualProtect(
target as _,
NOTIFIER_COPY_LEN,
PAGE_EXECUTE_READWRITE,
&mut old,
) == 0
{
return Err(true);
}
*published = true;
core::ptr::copy_nonoverlapping(patch.as_ptr(), target as *mut u8, NOTIFIER_COPY_LEN);
let flushed = FlushInstructionCache(GetCurrentProcess(), target as _, NOTIFIER_COPY_LEN) != 0;
let mut ignored = 0u32;
if flushed && VirtualProtect(target as _, NOTIFIER_COPY_LEN, old, &mut ignored) != 0 {
Ok(())
} else {
Err(restore_notifier_entry(target))
}
}
unsafe fn restore_controller_register_entry(target: usize) -> bool {
let mut old = 0u32;
if VirtualProtect(
target as _,
CONTROLLER_REGISTER_COPY_LEN,
PAGE_EXECUTE_READWRITE,
&mut old,
) == 0
{
return false;
}
core::ptr::copy_nonoverlapping(
CONTROLLER_REGISTER_SIGNATURE.as_ptr(),
target as *mut u8,
CONTROLLER_REGISTER_COPY_LEN,
);
let flushed = FlushInstructionCache(
GetCurrentProcess(),
target as _,
CONTROLLER_REGISTER_COPY_LEN,
) != 0;
let mut ignored = 0u32;
flushed && VirtualProtect(target as _, CONTROLLER_REGISTER_COPY_LEN, old, &mut ignored) != 0
}
unsafe fn write_controller_register_entry(
target: usize,
destination: usize,
published: &mut bool,
) -> Result<(), bool> {
let mut patch = [0x90u8; CONTROLLER_REGISTER_COPY_LEN];
patch[..ABS_JUMP_LEN].copy_from_slice(&absolute_jump(destination));
let mut old = 0u32;
if VirtualProtect(
target as _,
CONTROLLER_REGISTER_COPY_LEN,
PAGE_EXECUTE_READWRITE,
&mut old,
) == 0
{
return Err(true);
}
*published = true;
core::ptr::copy_nonoverlapping(
patch.as_ptr(),
target as *mut u8,
CONTROLLER_REGISTER_COPY_LEN,
);
let flushed = FlushInstructionCache(
GetCurrentProcess(),
target as _,
CONTROLLER_REGISTER_COPY_LEN,
) != 0;
let mut ignored = 0u32;
if flushed && VirtualProtect(target as _, CONTROLLER_REGISTER_COPY_LEN, old, &mut ignored) != 0
{
Ok(())
} else {
Err(restore_controller_register_entry(target))
}
}
unsafe extern "system" fn controller_register_wrapper(controller: *mut c_void, event: u32) {
let original: unsafe extern "system" fn(*mut c_void, u32) =
core::mem::transmute(CONTROLLER_REGISTER_TRAMPOLINE.load(Ordering::Acquire));
original(controller, event);
if event == FUT_SBS_CATEGORIES_EVENT {
crate::sbc_dispatch::note_sbc_controller(
controller as usize,
TRACE_BASE.load(Ordering::Acquire),
);
}
}
unsafe extern "system" fn notifier_wrapper(ctx: *mut c_void) {
NOTIFIER_ENTRIES.fetch_add(1, Ordering::Relaxed);
let address = ctx as usize;
NOTIFIER_CTX.store(address, Ordering::Relaxed);
NOTIFIER_BYTE_BEFORE.store(
address
.checked_add(0x88)
.and_then(|p| guarded_u8(p))
.map(usize::from)
.unwrap_or(usize::MAX),
Ordering::Relaxed,
);
let begin = address
.checked_add(0x58)
.and_then(|p| guarded_usize(p))
.unwrap_or(0);
let end = address
.checked_add(0x60)
.and_then(|p| guarded_usize(p))
.unwrap_or(0);
let count = if end >= begin && (end - begin) & 7 == 0 {
(end - begin) / 8
} else {
usize::MAX
};
NOTIFIER_BEGIN.store(begin, Ordering::Relaxed);
NOTIFIER_END.store(end, Ordering::Relaxed);
NOTIFIER_COUNT.store(count, Ordering::Relaxed);
let original: unsafe extern "system" fn(*mut c_void) =
core::mem::transmute(NOTIFIER_TRAMPOLINE.load(Ordering::Acquire));
original(ctx);
NOTIFIER_BYTE_AFTER.store(
address
.checked_add(0x88)
.and_then(|p| guarded_u8(p))
.map(usize::from)
.unwrap_or(usize::MAX),
Ordering::Relaxed,
);
NOTIFIER_EXITS.fetch_add(1, Ordering::Release);
}
unsafe extern "system" fn factory_wrapper(this: *mut c_void) -> *mut c_void {
FACTORY_ENTRIES.fetch_add(1, Ordering::Relaxed);
FACTORY_LAST_THREAD.store(GetCurrentThreadId() as usize, Ordering::Relaxed);
FACTORY_LAST_THIS.store(this as usize, Ordering::Relaxed);
let address = FACTORY_TRAMPOLINE.load(Ordering::Acquire);
let original: FactoryFn = core::mem::transmute(address);
let result = original(this);
FACTORY_LAST_RESULT.store(result as usize, Ordering::Release);
FACTORY_EXITS.fetch_add(1, Ordering::Release);
result
}
unsafe extern "system" fn deserializer_wrapper(this: *mut c_void, reader: *mut c_void) -> u8 {
DESERIALIZER_ENTRIES.fetch_add(1, Ordering::Relaxed);
DESERIALIZER_LAST_THREAD.store(GetCurrentThreadId() as usize, Ordering::Relaxed);
DESERIALIZER_LAST_THIS.store(this as usize, Ordering::Relaxed);
DESERIALIZER_LAST_READER.store(reader as usize, Ordering::Relaxed);
let address = DESERIALIZER_TRAMPOLINE.load(Ordering::Acquire);
let original: DeserializerFn = core::mem::transmute(address);
let result = original(this, reader);
DESERIALIZER_LAST_RESULT.store(result != 0, Ordering::Release);
let base = TRACE_BASE.load(Ordering::Acquire);
let a = base
.checked_add(0x2e6398)
.and_then(|slot| guarded_usize(slot))
.unwrap_or(0);
let m = a
.checked_add(0x20a68)
.and_then(|slot| guarded_usize(slot))
.unwrap_or(0);
let count = m
.checked_add(0x50)
.and_then(|slot| guarded_u16(slot))
.map(usize::from)
.unwrap_or(usize::MAX);
let ready = a
.checked_add(0x1f9d8 + 0x28)
.and_then(|slot| guarded_u8(slot))
.map(usize::from)
.unwrap_or(usize::MAX);
let response_vtable = guarded_usize(this as usize).unwrap_or(0);
DESERIALIZER_EXIT_M.store(m, Ordering::Relaxed);
DESERIALIZER_EXIT_COUNT.store(count, Ordering::Relaxed);
DESERIALIZER_EXIT_B_READY.store(ready, Ordering::Relaxed);
DESERIALIZER_EXIT_RESPONSE_VTABLE.store(response_vtable, Ordering::Relaxed);
DESERIALIZER_EXITS.fetch_add(1, Ordering::Release);
result
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct DispatchEvidence {
pub(crate) base: usize,
pub(crate) factory_entries: u64,
pub(crate) factory_exits: u64,
pub(crate) factory_result: usize,
pub(crate) factory_thread: usize,
pub(crate) deserializer_entries: u64,
pub(crate) deserializer_exits: u64,
pub(crate) deserializer_this: usize,
pub(crate) deserializer_reader: usize,
pub(crate) deserializer_result: bool,
pub(crate) deserializer_thread: usize,
pub(crate) response_vtable: usize,
pub(crate) model: usize,
pub(crate) category_count: usize,
pub(crate) notifier_entries: u64,
pub(crate) notifier_exits: u64,
}
pub(crate) fn dispatch_evidence() -> DispatchEvidence {
DispatchEvidence {
base: TRACE_BASE.load(Ordering::Acquire),
factory_entries: FACTORY_ENTRIES.load(Ordering::Acquire),
factory_exits: FACTORY_EXITS.load(Ordering::Acquire),
factory_result: FACTORY_LAST_RESULT.load(Ordering::Acquire),
factory_thread: FACTORY_LAST_THREAD.load(Ordering::Relaxed),
deserializer_entries: DESERIALIZER_ENTRIES.load(Ordering::Acquire),
deserializer_exits: DESERIALIZER_EXITS.load(Ordering::Acquire),
deserializer_this: DESERIALIZER_LAST_THIS.load(Ordering::Relaxed),
deserializer_reader: DESERIALIZER_LAST_READER.load(Ordering::Relaxed),
deserializer_result: DESERIALIZER_LAST_RESULT.load(Ordering::Acquire),
deserializer_thread: DESERIALIZER_LAST_THREAD.load(Ordering::Relaxed),
response_vtable: DESERIALIZER_EXIT_RESPONSE_VTABLE.load(Ordering::Relaxed),
model: DESERIALIZER_EXIT_M.load(Ordering::Relaxed),
category_count: DESERIALIZER_EXIT_COUNT.load(Ordering::Relaxed),
notifier_entries: NOTIFIER_ENTRIES.load(Ordering::Acquire),
notifier_exits: NOTIFIER_EXITS.load(Ordering::Acquire),
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum InstallOutcome {
Installed,
PrePatchCleanFailure,
PostPatchCleanFailure,
DegradedHookActive,
DegradedProcessState,
DegradedHookAndProcess,
}
fn may_free_trampolines(outcome: InstallOutcome) -> bool {
outcome == InstallOutcome::PrePatchCleanFailure
}
unsafe fn install_pair(base: usize) -> InstallOutcome {
let Some(factory) = target_va(base, CATEGORY_FACTORY_RVA) else {
return InstallOutcome::PrePatchCleanFailure;
};
let Some(deserializer) = target_va(base, CATEGORY_DESERIALIZER_RVA) else {
return InstallOutcome::PrePatchCleanFailure;
};
TRACE_BASE.store(base, Ordering::Release);
// Preparation phase: validate the PE/ranges/signatures and allocate executable
// backing before suspending any peer.
if !valid_cards_image(base)
|| !executable_range(factory, FACTORY_SIGNATURE.len())
|| !executable_range(deserializer, DESERIALIZER_SIGNATURE.len())
|| !signature_matches(factory, &FACTORY_SIGNATURE)
|| !signature_matches(deserializer, &DESERIALIZER_SIGNATURE)
{
return InstallOutcome::PrePatchCleanFailure;
}
// Pin before quiescence so the image cannot unload for the lifetime of any
// published trampoline. This loader operation is intentionally outside the
// suspended phase.
let mut pinned = core::ptr::null_mut();
if GetModuleHandleExA(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
factory as *const u8,
&mut pinned,
) == 0
|| pinned as usize != base
{
return InstallOutcome::PrePatchCleanFailure;
}
let factory_original: [u8; COPY_LEN] = FACTORY_SIGNATURE[..COPY_LEN].try_into().unwrap();
let deserializer_original: [u8; COPY_LEN] =
DESERIALIZER_SIGNATURE[..COPY_LEN].try_into().unwrap();
let Some(factory_trampoline) = allocate_trampoline(factory, COPY_LEN) else {
return InstallOutcome::PrePatchCleanFailure;
};
let Some(deserializer_trampoline) = allocate_trampoline(deserializer, COPY_LEN) else {
VirtualFree(factory_trampoline as _, 0, MEM_RELEASE);
return InstallOutcome::PrePatchCleanFailure;
};
FACTORY_TRAMPOLINE.store(factory_trampoline, Ordering::Release);
DESERIALIZER_TRAMPOLINE.store(deserializer_trampoline, Ordering::Release);
let Some(_installer_gate) = acquire_patch_installer_gate() else {
VirtualFree(factory_trampoline as _, 0, MEM_RELEASE);
VirtualFree(deserializer_trampoline as _, 0, MEM_RELEASE);
FACTORY_TRAMPOLINE.store(0, Ordering::Release);
DESERIALIZER_TRAMPOLINE.store(0, Ordering::Release);
return InstallOutcome::PrePatchCleanFailure;
};
// Quiesced phase: fixed-capacity bookkeeping and Win32 code-page operations
// only. No Rust allocation, freeing, formatting, or file logging is permitted.
let mut peers = match suspend_peers(factory, deserializer) {
Ok(peers) => peers,
Err(failure) => {
return if failure == QuiesceFailure::Resume {
InstallOutcome::DegradedProcessState
} else {
VirtualFree(factory_trampoline as _, 0, MEM_RELEASE);
VirtualFree(deserializer_trampoline as _, 0, MEM_RELEASE);
FACTORY_TRAMPOLINE.store(0, Ordering::Release);
DESERIALIZER_TRAMPOLINE.store(0, Ordering::Release);
InstallOutcome::PrePatchCleanFailure
};
}
};
// Final no-allocation TOCTOU gate, immediately before the first write.
// The image was pinned by address before quiescence. Avoid loader APIs here:
// a suspended peer may own the loader lock. Page and byte identity checks are
// sufficient to reject any target change immediately before publication.
let final_valid = valid_cards_image(base)
&& executable_range(factory, FACTORY_SIGNATURE.len())
&& executable_range(deserializer, DESERIALIZER_SIGNATURE.len())
&& signature_matches(factory, &FACTORY_SIGNATURE)
&& signature_matches(deserializer, &DESERIALIZER_SIGNATURE);
let mut published = false;
let transaction = if !final_valid {
InstallOutcome::PrePatchCleanFailure
} else if let Err(clean) = write_entry(
factory,
factory_wrapper as *const () as usize,
&factory_original,
&mut published,
) {
if clean {
if published {
InstallOutcome::PostPatchCleanFailure
} else {
InstallOutcome::PrePatchCleanFailure
}
} else {
InstallOutcome::DegradedHookActive
}
} else if let Err(second_clean) = write_entry(
deserializer,
deserializer_wrapper as *const () as usize,
&deserializer_original,
&mut published,
) {
let first_clean = restore_entry(factory, &factory_original);
if first_clean && second_clean {
InstallOutcome::PostPatchCleanFailure
} else {
InstallOutcome::DegradedHookActive
}
} else {
InstallOutcome::Installed
};
let resumed = peers.resume_all();
let outcome = if resumed {
transaction
} else if transaction == InstallOutcome::DegradedHookActive
|| transaction == InstallOutcome::Installed
{
InstallOutcome::DegradedHookAndProcess
} else {
InstallOutcome::DegradedProcessState
};
// Post-resume phase: freeing is safe only when no hook can reference backing.
if may_free_trampolines(outcome) {
VirtualFree(factory_trampoline as _, 0, MEM_RELEASE);
VirtualFree(deserializer_trampoline as _, 0, MEM_RELEASE);
FACTORY_TRAMPOLINE.store(0, Ordering::Release);
DESERIALIZER_TRAMPOLINE.store(0, Ordering::Release);
}
outcome
}
unsafe fn worker() {
let _pending = CodeInstallerPending;
let mut base = 0usize;
for _ in 0..600u32 {
base = GetModuleHandleA(c"CardsDLL_Win64_retail.dll".as_ptr().cast()) as usize;
if base != 0 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
if base == 0 {
STATE.store(TraceState::Failed as usize, Ordering::Release);
crate::write_log("SBC_TRACE: CardsDLL unavailable; tracing inactive\n");
return;
}
let outcome = install_pair(base);
drop(_pending);
match outcome {
InstallOutcome::Installed => {
STATE.store(TraceState::Installed as usize, Ordering::Release);
crate::write_log("SBC_TRACE: factory+deserializer passive hooks installed\n");
}
InstallOutcome::PrePatchCleanFailure => {
STATE.store(TraceState::Failed as usize, Ordering::Release);
crate::write_log("SBC_TRACE: pre-patch clean failure; tracing inactive\n");
return;
}
InstallOutcome::PostPatchCleanFailure => {
STATE.store(TraceState::Failed as usize, Ordering::Release);
crate::write_log("SBC_TRACE: post-publication rollback completed; trampolines retained for process lifetime; tracing inactive\n");
return;
}
InstallOutcome::DegradedHookActive => {
STATE.store(TraceState::DegradedHookActive as usize, Ordering::Release);
crate::write_log("SBC_TRACE: DEGRADED HOOK MAY BE ACTIVE; trampolines retained; terminate game now\n");
return;
}
InstallOutcome::DegradedProcessState => {
STATE.store(TraceState::DegradedProcessState as usize, Ordering::Release);
crate::write_log("SBC_TRACE: DEGRADED THREAD RESUME FAILURE; terminate game now\n");
return;
}
InstallOutcome::DegradedHookAndProcess => {
STATE.store(
TraceState::DegradedHookAndProcess as usize,
Ordering::Release,
);
crate::write_log(
"SBC_TRACE: DEGRADED HOOK ACTIVE AND THREAD RESUME FAILURE; terminate game now\n",
);
return;
}
}
let mut seen_factory = 0u64;
let mut seen_deserializer = 0u64;
let mut reports = 0u8;
while reports < 32 {
std::thread::sleep(std::time::Duration::from_millis(250));
let factory_count = FACTORY_ENTRIES.load(Ordering::Acquire);
let deserializer_count = DESERIALIZER_ENTRIES.load(Ordering::Acquire);
if factory_count != seen_factory || deserializer_count != seen_deserializer {
crate::write_log(&format!(
"SBC_TRACE: factory entry={} exit={} tid={} this={:#x} result={:#x}; deser entry={} exit={} tid={} this={:#x} reader={:#x} result={} M={:#x} count={} Bready={}\n",
factory_count,
FACTORY_EXITS.load(Ordering::Acquire),
FACTORY_LAST_THREAD.load(Ordering::Relaxed),
FACTORY_LAST_THIS.load(Ordering::Relaxed),
FACTORY_LAST_RESULT.load(Ordering::Acquire),
deserializer_count,
DESERIALIZER_EXITS.load(Ordering::Acquire),
DESERIALIZER_LAST_THREAD.load(Ordering::Relaxed),
DESERIALIZER_LAST_THIS.load(Ordering::Relaxed),
DESERIALIZER_LAST_READER.load(Ordering::Relaxed),
DESERIALIZER_LAST_RESULT.load(Ordering::Acquire),
DESERIALIZER_EXIT_M.load(Ordering::Relaxed),
DESERIALIZER_EXIT_COUNT.load(Ordering::Relaxed),
DESERIALIZER_EXIT_B_READY.load(Ordering::Relaxed),
));
seen_factory = factory_count;
seen_deserializer = deserializer_count;
reports += 1;
}
}
crate::write_log("SBC_TRACE: report cap reached; hooks remain passive\n");
}
unsafe fn notifier_worker() {
let _pending = CodeInstallerPending;
let mut base = 0usize;
for _ in 0..600u32 {
base = GetModuleHandleA(c"CardsDLL_Win64_retail.dll".as_ptr().cast()) as usize;
if base != 0 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
let Some(target) = target_va(base, NOTIFIER_RVA) else {
crate::write_log("SBC_NOTIFIER_TRACE: target resolution failed; inactive\n");
return;
};
if base == 0
|| !valid_cards_image(base)
|| !executable_range_in_image(base, target, NOTIFIER_SIGNATURE.len())
|| !signature_matches(target, &NOTIFIER_SIGNATURE)
{
crate::write_log("SBC_NOTIFIER_TRACE: PE/signature validation failed; inactive\n");
return;
}
let mut pinned = core::ptr::null_mut();
if GetModuleHandleExA(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
target as *const u8,
&mut pinned,
) == 0
|| pinned as usize != base
{
crate::write_log("SBC_NOTIFIER_TRACE: module pin failed; inactive\n");
return;
}
let Some(trampoline) = allocate_trampoline(target, NOTIFIER_COPY_LEN) else {
crate::write_log("SBC_NOTIFIER_TRACE: trampoline allocation failed; inactive\n");
return;
};
NOTIFIER_TRAMPOLINE.store(trampoline, Ordering::Release);
let Some(_installer_gate) = acquire_patch_installer_gate() else {
VirtualFree(trampoline as _, 0, MEM_RELEASE);
NOTIFIER_TRAMPOLINE.store(0, Ordering::Release);
crate::write_log("SBC_NOTIFIER_TRACE: installer gate timeout; inactive\n");
return;
};
let mut peers = match suspend_peers(target, target) {
Ok(peers) => peers,
Err(_) => {
// A failed resume can leave peers suspended, so conservatively retain
// executable backing and require termination.
crate::write_log("SBC_NOTIFIER_TRACE: quiescence failed; trampoline retained; terminate game if unresponsive\n");
return;
}
};
let final_valid = pinned as usize == base
&& valid_cards_image(base)
&& executable_range_in_image(base, target, NOTIFIER_SIGNATURE.len())
&& signature_matches(target, &NOTIFIER_SIGNATURE);
let mut published = false;
let installed = final_valid
&& write_notifier_entry(
target,
notifier_wrapper as *const () as usize,
&mut published,
)
.is_ok();
let resumed = peers.resume_all();
drop(_installer_gate);
drop(_pending);
if !installed || !resumed {
if !published && resumed {
VirtualFree(trampoline as _, 0, MEM_RELEASE);
NOTIFIER_TRAMPOLINE.store(0, Ordering::Release);
crate::write_log("SBC_NOTIFIER_TRACE: clean install failure; inactive\n");
} else {
crate::write_log("SBC_NOTIFIER_TRACE: DEGRADED hook/thread state; backing retained; terminate game now\n");
}
return;
}
crate::write_log("SBC_NOTIFIER_TRACE: category success notifier hook installed\n");
let mut seen = 0u64;
let mut reports = 0u8;
while reports < 32 {
std::thread::sleep(std::time::Duration::from_millis(250));
let entries = NOTIFIER_ENTRIES.load(Ordering::Acquire);
if entries != seen {
crate::write_log(&format!(
"SBC_NOTIFIER_TRACE: entry={} exit={} ctx={:#x} byte88={}->{} handlers={:#x}..{:#x} count={}\n",
entries,
NOTIFIER_EXITS.load(Ordering::Acquire),
NOTIFIER_CTX.load(Ordering::Relaxed),
NOTIFIER_BYTE_BEFORE.load(Ordering::Relaxed),
NOTIFIER_BYTE_AFTER.load(Ordering::Relaxed),
NOTIFIER_BEGIN.load(Ordering::Relaxed),
NOTIFIER_END.load(Ordering::Relaxed),
NOTIFIER_COUNT.load(Ordering::Relaxed),
));
seen = entries;
reports += 1;
}
}
crate::write_log("SBC_NOTIFIER_TRACE: report cap reached; hook remains passive\n");
}
unsafe fn controller_register_worker() {
let _pending = CodeInstallerPending;
let mut base = 0usize;
for _ in 0..600u32 {
base = GetModuleHandleA(c"CardsDLL_Win64_retail.dll".as_ptr().cast()) as usize;
if base != 0 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
let Some(target) = target_va(base, CONTROLLER_REGISTER_RVA) else {
crate::write_log("SBC_CONTROLLER_TRACE: target resolution failed; inactive\n");
return;
};
if base == 0
|| !valid_cards_image(base)
|| !executable_range_in_image(base, target, CONTROLLER_REGISTER_SIGNATURE.len())
|| core::slice::from_raw_parts(target as *const u8, CONTROLLER_REGISTER_SIGNATURE.len())
!= CONTROLLER_REGISTER_SIGNATURE
{
crate::write_log("SBC_CONTROLLER_TRACE: PE/signature validation failed; inactive\n");
return;
}
let Some(trampoline) = allocate_trampoline(target, CONTROLLER_REGISTER_COPY_LEN) else {
crate::write_log("SBC_CONTROLLER_TRACE: trampoline allocation failed; inactive\n");
return;
};
CONTROLLER_REGISTER_TRAMPOLINE.store(trampoline, Ordering::Release);
let Some(_installer_gate) = acquire_patch_installer_gate() else {
VirtualFree(trampoline as _, 0, MEM_RELEASE);
CONTROLLER_REGISTER_TRAMPOLINE.store(0, Ordering::Release);
crate::write_log("SBC_CONTROLLER_TRACE: installer gate timeout; inactive\n");
return;
};
let mut peers = match suspend_peers(target, target) {
Ok(peers) => peers,
Err(_) => {
crate::write_log(
"SBC_CONTROLLER_TRACE: quiescence failed; terminate game if unresponsive\n",
);
return;
}
};
let mut published = false;
let installed = write_controller_register_entry(
target,
controller_register_wrapper as *const () as usize,
&mut published,
)
.is_ok();
let resumed = peers.resume_all();
drop(_installer_gate);
drop(_pending);
if !installed || !resumed {
if !published && resumed {
VirtualFree(trampoline as _, 0, MEM_RELEASE);
CONTROLLER_REGISTER_TRAMPOLINE.store(0, Ordering::Release);
crate::write_log("SBC_CONTROLLER_TRACE: clean install failure; inactive\n");
} else {
crate::write_log("SBC_CONTROLLER_TRACE: DEGRADED state; terminate game now\n");
}
return;
}
crate::write_log("SBC_CONTROLLER_TRACE: category controller registration hook installed\n");
}
fn install_notifier(enabled: bool) {
if enabled {
crate::write_log("SBC_NOTIFIER_TRACE: requested; deferred install starting\n");
std::thread::spawn(|| unsafe { notifier_worker() });
crate::write_log("SBC_CONTROLLER_TRACE: requested; deferred install starting\n");
std::thread::spawn(|| unsafe { controller_register_worker() });
} else {
crate::write_log("SBC_NOTIFIER_TRACE: disabled\n");
}
}
pub(crate) fn install() {
// The repair's evidence traces (parser, notifier, controller registration) are
// its decision inputs, so they follow the promoted repair, not an env var.
let dispatch_repair = crate::sbc_dispatch::REPAIR_PROMOTED;
let dispatch_trace =
dispatch_repair || env_enabled(std::env::var("OPENFUT_SBC_DISPATCH_TRACE").ok().as_deref());
let enabled =
dispatch_repair || env_enabled(std::env::var("OPENFUT_SBC_TRACE").ok().as_deref());
let notifier_enabled =
dispatch_repair || env_enabled(std::env::var("OPENFUT_SBC_NOTIFIER_TRACE").ok().as_deref());
CODE_PATCH_PENDING.store(
enabled as usize + (notifier_enabled as usize * 2) + dispatch_trace as usize,
Ordering::Release,
);
install_notifier(notifier_enabled);
ENABLED.store(enabled, Ordering::Release);
if !enabled {
STATE.store(TraceState::Disabled as usize, Ordering::Release);
crate::write_log("SBC_TRACE: disabled (set OPENFUT_SBC_TRACE=1 to enable)\n");
return;
}
STATE.store(TraceState::Requested as usize, Ordering::Release);
crate::write_log("SBC_TRACE: requested; deferred signature validation starting\n");
std::thread::spawn(|| unsafe { worker() });
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trace_gate_is_exact_and_default_off() {
assert!(!env_enabled(None));
assert!(!env_enabled(Some("0")));
assert!(!env_enabled(Some("true")));
assert!(env_enabled(Some("1")));
}
#[test]
fn target_resolution_checks_overflow() {
assert_eq!(target_va(0x1000, CATEGORY_FACTORY_RVA), Some(0x17ba10));
assert_eq!(target_va(usize::MAX, CATEGORY_FACTORY_RVA), None);
}
#[test]
fn absolute_jump_has_indirect_rip_encoding_and_exact_destination() {
let jump = absolute_jump(0x1234_5678_9abc_def0);
assert_eq!(&jump[..6], &[0xff, 0x25, 0, 0, 0, 0]);
assert_eq!(
u64::from_le_bytes(jump[6..].try_into().unwrap()),
0x1234_5678_9abc_def0
);
}
#[test]
fn relocation_spans_end_on_proven_instruction_boundaries() {
assert_eq!(
&FACTORY_SIGNATURE[..COPY_LEN],
&[
0x48, 0x89, 0x4c, 0x24, 0x08, 0x53, 0x48, 0x83, 0xec, 0x30, 0x48, 0xc7, 0x44, 0x24,
0x20, 0xfe, 0xff, 0xff, 0xff,
]
);
assert_eq!(
&NOTIFIER_SIGNATURE[..NOTIFIER_COPY_LEN],
&[
0x48, 0x89, 0x5c, 0x24, 0x08, 0x48, 0x89, 0x74, 0x24, 0x10, 0x57, 0x48, 0x83, 0xec,
0x20
]
);
assert_eq!(
&DESERIALIZER_SIGNATURE[..COPY_LEN],
&[
0x48, 0x8b, 0xc4, 0x55, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x8d,
0xa8, 0xe8, 0xfd, 0xff, 0xff,
]
);
}
#[test]
fn instruction_span_is_half_open_and_overflow_fails_closed() {
assert!(instruction_pointer_in_span(0x1000, 0x1000));
assert!(instruction_pointer_in_span(0x1012, 0x1000));
assert!(!instruction_pointer_in_span(0x1013, 0x1000));
assert!(!instruction_pointer_in_span(0x0fff, 0x1000));
assert!(instruction_pointer_in_span(usize::MAX, usize::MAX));
}
#[test]
fn only_clean_failure_allows_trampoline_free() {
assert!(may_free_trampolines(InstallOutcome::PrePatchCleanFailure));
assert!(!may_free_trampolines(InstallOutcome::PostPatchCleanFailure));
assert!(!may_free_trampolines(InstallOutcome::Installed));
assert!(!may_free_trampolines(InstallOutcome::DegradedHookActive));
assert!(!may_free_trampolines(InstallOutcome::DegradedProcessState));
assert!(!may_free_trampolines(
InstallOutcome::DegradedHookAndProcess
));
}
}