//! 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 { 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 { 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 { 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::() 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::() 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::(), ) == 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::(), ) == 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::(), ) != 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 { (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 { readable_range(address, 2).then(|| core::ptr::read_volatile(address as *const u16)) } pub(crate) unsafe fn guarded_u8(address: usize) -> Option { 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 { 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 )); } }