Instrument FIFA17 SBC completion dispatch

This commit is contained in:
funman300
2026-08-18 20:20:00 +00:00
parent 1cd4f18e92
commit 6cdb45e482
5 changed files with 830 additions and 242 deletions
+2 -4
View File
@@ -79,12 +79,10 @@ unsafe extern "system" fn worker(_: *mut core::ffi::c_void) -> u32 {
));
dump_modules();
write_log("fifa17: worker complete (injection healthy)\n");
// SBC render intervention (inert unless OPENFUT_SBC_HOOK=1). Spawns its own deferred
// worker that waits for CardsDLL to load. See sbc_hook.rs / docs/sbc-hook-dll-spec.md.
// Every SBC detour is deferred and inert unless its exact environment gate is `1`.
crate::sbc_hook::install();
// Passive transaction tracing has a separate kill switch from cache resolution.
// It currently fails closed until safe relocating trampolines are proven.
crate::sbc_trace::install();
crate::sbc_dispatch::install();
crate::sbc_request_trace::install();
0
}
+2
View File
@@ -21,6 +21,8 @@ mod probe;
#[cfg(feature = "capture_baseline")]
mod recv_hook;
#[cfg(feature = "fifa17")]
mod sbc_dispatch;
#[cfg(feature = "fifa17")]
mod sbc_hook;
#[cfg(feature = "fifa17")]
mod sbc_request_trace;
+756
View File
@@ -0,0 +1,756 @@
//! Guarded FIFA 17 SBC completion dispatch and passive event tracing.
//!
//! `OPENFUT_SBC_DISPATCH=1` permits one narrowly-scoped repair per native
//! deserializer generation. The default and `OPENFUT_SBC_DISPATCH_TRACE=1` paths
//! are behavior-preserving.
use core::ffi::c_void;
use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use windows_sys::Win32::System::Diagnostics::Debug::FlushInstructionCache;
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, MEM_COMMIT, MEM_RELEASE, MEM_RESERVE,
PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_READWRITE,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetCurrentThreadId};
const COMPLETION_RVA: usize = 0x0b8950;
const EVENT_DISPATCH_RVA: usize = 0x1a4cd0;
const CATEGORY_RESPONSE_VTABLE_RVA: usize = 0x22e5b0;
const SBC_CONTROLLER_VTABLE_RVA: usize = 0x20a820;
const SBC_CONTROLLER_EVENT_VTABLE_RVA: usize = 0x20a888;
const SBC_CONTROLLER_EVENT_SUBOBJECT_OFF: usize = 0x138;
const SBC_CONTROLLER_MODEL_OFF: usize = 0x140;
const COMPLETION_COPY_LEN: usize = 14;
const EVENT_COPY_LEN: usize = 16;
const ABS_JUMP_LEN: usize = 14;
const COMPLETION_TRAMPOLINE_LEN: usize = 12 + 2 + ABS_JUMP_LEN * 2;
const UNKNOWN_TRANSPORT_STATUS: u32 = 999;
const FUT_SBS_CATEGORIES_EVENT: u32 = 0x756c;
const FUT_SBS_CATEGORIES_READY_EVENT: u32 = 0x756d;
const SBC_REFRESH_EVENT: u32 = 0x138c;
const COMPLETION_SIGNATURE: [u8; 32] = [
0x40, 0x53, 0x48, 0x83, 0xec, 0x20, 0x48, 0x8b, 0xd9, 0x48, 0x85, 0xd2, 0x74, 0x4e, 0x83, 0x7a,
0x1c, 0x00, 0x75, 0x48, 0xc6, 0x81, 0x1d, 0x02, 0x00, 0x00, 0x01, 0x48, 0x8b, 0x89, 0x40, 0x01,
];
const EVENT_SIGNATURE: [u8; EVENT_COPY_LEN] = [
0x48, 0x8b, 0xc4, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0xc7, 0x40, 0xb8, 0xfe, 0xff, 0xff, 0xff,
];
type CompletionFn = unsafe extern "system" fn(*mut c_void, *mut c_void) -> usize;
type EventDispatchFn = unsafe extern "system" fn(*mut c_void, u32, *mut c_void) -> usize;
static REPAIR_ENABLED: AtomicBool = AtomicBool::new(false);
static COMPLETION_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
static EVENT_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
static SBC_CONTROLLER: AtomicUsize = AtomicUsize::new(0);
static LAST_REPAIRED_GENERATION: AtomicU64 = AtomicU64::new(0);
static COMPLETION_ENTRIES: AtomicU64 = AtomicU64::new(0);
static COMPLETION_EXITS: AtomicU64 = AtomicU64::new(0);
static COMPLETION_THREAD: AtomicUsize = AtomicUsize::new(0);
static COMPLETION_CONTROLLER: AtomicUsize = AtomicUsize::new(0);
static COMPLETION_STATUS_OBJECT: AtomicUsize = AtomicUsize::new(0);
static COMPLETION_STATUS: AtomicUsize = AtomicUsize::new(usize::MAX);
static COMPLETION_GENERATION: AtomicU64 = AtomicU64::new(0);
static COMPLETION_DECISION: AtomicUsize = AtomicUsize::new(Decision::NativeSuccess as usize);
static COMPLETION_REJECTION: AtomicUsize = AtomicUsize::new(Rejection::None as usize);
static EVENT_ENTRIES: AtomicU64 = AtomicU64::new(0);
static EVENT_EXITS: AtomicU64 = AtomicU64::new(0);
static EVENT_THREAD: AtomicUsize = AtomicUsize::new(0);
static EVENT_CONTROLLER: AtomicUsize = AtomicUsize::new(0);
static EVENT_ID: AtomicUsize = AtomicUsize::new(0);
static EVENT_PAYLOAD: AtomicUsize = AtomicUsize::new(0);
static EVENT_CATEGORIES: AtomicU64 = AtomicU64::new(0);
static EVENT_REFRESH: AtomicU64 = AtomicU64::new(0);
static EVENT_READY: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(usize)]
enum Decision {
NativeSuccess,
Repair,
}
const REJECTED_DECISION: usize = 2;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(usize)]
enum Rejection {
None,
RepairDisabled,
NullStatus,
StatusUnreadable,
UnsupportedStatus,
CardsBuildMismatch,
ParserUnbalanced,
FactoryMismatch,
ParserThreadMismatch,
ReaderMissing,
ParseFailed,
ResponseClassMismatch,
ModelEmpty,
NotifierNotCurrent,
ControllerMismatch,
ControllerModelMismatch,
DuplicateGeneration,
}
#[derive(Clone, Copy)]
struct DecisionInput {
repair_enabled: bool,
status: Option<u32>,
status_present: bool,
status_copyable: bool,
cards_build_matches: bool,
factory_entries: u64,
factory_exits: u64,
factory_result: usize,
factory_thread: usize,
deserializer_entries: u64,
deserializer_exits: u64,
deserializer_this: usize,
deserializer_reader: usize,
deserializer_result: bool,
deserializer_thread: usize,
response_class_matches: bool,
model: usize,
category_count: usize,
notifier_entries: u64,
notifier_exits: u64,
controller_matches: bool,
controller_model_matches: bool,
last_repaired_generation: u64,
}
fn decide(input: DecisionInput) -> Result<Decision, Rejection> {
let Some(status) = input.status else {
return Err(if input.status_present {
Rejection::StatusUnreadable
} else {
Rejection::NullStatus
});
};
if status == 0 {
return Ok(Decision::NativeSuccess);
}
if !input.repair_enabled {
return Err(Rejection::RepairDisabled);
}
if status != UNKNOWN_TRANSPORT_STATUS {
return Err(Rejection::UnsupportedStatus);
}
if !input.status_copyable {
return Err(Rejection::StatusUnreadable);
}
if !input.cards_build_matches {
return Err(Rejection::CardsBuildMismatch);
}
let generation = input.deserializer_exits;
if generation == 0
|| input.factory_entries != input.factory_exits
|| input.deserializer_entries != generation
|| input.factory_exits != generation
{
return Err(Rejection::ParserUnbalanced);
}
if input.factory_result == 0 || input.factory_result != input.deserializer_this {
return Err(Rejection::FactoryMismatch);
}
if input.factory_thread == 0 || input.factory_thread != input.deserializer_thread {
return Err(Rejection::ParserThreadMismatch);
}
if input.deserializer_reader == 0 {
return Err(Rejection::ReaderMissing);
}
if !input.deserializer_result {
return Err(Rejection::ParseFailed);
}
if !input.response_class_matches {
return Err(Rejection::ResponseClassMismatch);
}
if input.model == 0 || input.category_count == 0 || input.category_count == usize::MAX {
return Err(Rejection::ModelEmpty);
}
if input.notifier_entries != generation
|| input.notifier_entries == 0
|| input.notifier_exits.checked_add(1) != Some(input.notifier_entries)
{
return Err(Rejection::NotifierNotCurrent);
}
if !input.controller_matches {
return Err(Rejection::ControllerMismatch);
}
if !input.controller_model_matches {
return Err(Rejection::ControllerModelMismatch);
}
if input.last_repaired_generation >= generation {
return Err(Rejection::DuplicateGeneration);
}
Ok(Decision::Repair)
}
unsafe fn guarded_u32(address: usize) -> Option<u32> {
crate::sbc_trace::readable_range(address, 4)
.then(|| core::ptr::read_volatile(address as *const u32))
}
unsafe fn status_code(status: usize) -> Option<u32> {
status
.checked_add(0x1c)
.and_then(|address| guarded_u32(address))
}
unsafe fn controller_identity(base: usize, controller: usize, model: usize) -> (bool, bool) {
if base == 0 || controller == 0 {
return (false, false);
}
let main_vtable = crate::sbc_trace::guarded_usize(controller);
let event_vtable = controller
.checked_add(SBC_CONTROLLER_EVENT_SUBOBJECT_OFF)
.and_then(|address| crate::sbc_trace::guarded_usize(address));
let controller_model = controller
.checked_add(SBC_CONTROLLER_MODEL_OFF)
.and_then(|address| crate::sbc_trace::guarded_usize(address));
(
main_vtable == base.checked_add(SBC_CONTROLLER_VTABLE_RVA)
&& event_vtable == base.checked_add(SBC_CONTROLLER_EVENT_VTABLE_RVA),
controller_model == Some(model),
)
}
pub(crate) unsafe fn note_sbc_controller(controller: usize, base: usize) {
let (identity_matches, _) = controller_identity(base, controller, 0);
if identity_matches {
SBC_CONTROLLER.store(controller, Ordering::Release);
crate::write_log(&format!(
"SBC_DISPATCH: captured category controller={controller:#x}\n"
));
} else {
crate::write_log(&format!(
"SBC_DISPATCH: rejected category controller={controller:#x} (class mismatch)\n"
));
}
}
#[repr(C, align(16))]
struct CompletionStatusShadow([u8; 0x20]);
unsafe extern "system" fn completion_wrapper(
controller: *mut c_void,
status: *mut c_void,
) -> usize {
COMPLETION_ENTRIES.fetch_add(1, Ordering::Relaxed);
COMPLETION_THREAD.store(GetCurrentThreadId() as usize, Ordering::Relaxed);
COMPLETION_CONTROLLER.store(controller as usize, Ordering::Relaxed);
COMPLETION_STATUS_OBJECT.store(status as usize, Ordering::Relaxed);
let evidence = crate::sbc_trace::dispatch_evidence();
let status_address = status as usize;
let observed_status = if status_address == 0 {
None
} else {
status_code(status_address)
};
COMPLETION_STATUS.store(
observed_status
.map(|value| value as usize)
.unwrap_or(usize::MAX),
Ordering::Relaxed,
);
COMPLETION_GENERATION.store(evidence.deserializer_exits, Ordering::Relaxed);
let response_vtable = crate::sbc_trace::guarded_usize(evidence.deserializer_this);
let captured_controller = SBC_CONTROLLER.load(Ordering::Acquire);
let (controller_matches, controller_model_matches) =
controller_identity(evidence.base, captured_controller, evidence.model);
let input = DecisionInput {
repair_enabled: REPAIR_ENABLED.load(Ordering::Acquire),
status: observed_status,
status_present: status_address != 0,
status_copyable: status_address != 0
&& crate::sbc_trace::readable_range(status_address, 0x20),
cards_build_matches: crate::sbc_trace::valid_cards_image(evidence.base),
factory_entries: evidence.factory_entries,
factory_exits: evidence.factory_exits,
factory_result: evidence.factory_result,
factory_thread: evidence.factory_thread,
deserializer_entries: evidence.deserializer_entries,
deserializer_exits: evidence.deserializer_exits,
deserializer_this: evidence.deserializer_this,
deserializer_reader: evidence.deserializer_reader,
deserializer_result: evidence.deserializer_result,
deserializer_thread: evidence.deserializer_thread,
response_class_matches: response_vtable
== evidence.base.checked_add(CATEGORY_RESPONSE_VTABLE_RVA),
model: evidence.model,
category_count: evidence.category_count,
notifier_entries: evidence.notifier_entries,
notifier_exits: evidence.notifier_exits,
controller_matches: controller_matches && captured_controller == controller as usize,
controller_model_matches,
last_repaired_generation: LAST_REPAIRED_GENERATION.load(Ordering::Acquire),
};
let original: CompletionFn =
core::mem::transmute(COMPLETION_TRAMPOLINE.load(Ordering::Acquire));
let result = match decide(input) {
Ok(Decision::Repair) => {
if LAST_REPAIRED_GENERATION
.compare_exchange(
input.last_repaired_generation,
evidence.deserializer_exits,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
{
let mut shadow = CompletionStatusShadow([0; 0x20]);
core::ptr::copy_nonoverlapping(
status_address as *const u8,
shadow.0.as_mut_ptr(),
shadow.0.len(),
);
shadow.0[0x1c..0x20].copy_from_slice(&0u32.to_le_bytes());
COMPLETION_DECISION.store(Decision::Repair as usize, Ordering::Relaxed);
COMPLETION_REJECTION.store(Rejection::None as usize, Ordering::Relaxed);
original(controller, shadow.0.as_mut_ptr().cast())
} else {
COMPLETION_DECISION.store(REJECTED_DECISION, Ordering::Relaxed);
COMPLETION_REJECTION
.store(Rejection::DuplicateGeneration as usize, Ordering::Relaxed);
original(controller, status)
}
}
Ok(Decision::NativeSuccess) => {
COMPLETION_DECISION.store(Decision::NativeSuccess as usize, Ordering::Relaxed);
COMPLETION_REJECTION.store(Rejection::None as usize, Ordering::Relaxed);
original(controller, status)
}
Err(rejection) => {
COMPLETION_DECISION.store(REJECTED_DECISION, Ordering::Relaxed);
COMPLETION_REJECTION.store(rejection as usize, Ordering::Relaxed);
original(controller, status)
}
};
COMPLETION_EXITS.fetch_add(1, Ordering::Release);
result
}
unsafe extern "system" fn event_wrapper(
controller: *mut c_void,
event: u32,
payload: *mut c_void,
) -> usize {
EVENT_ENTRIES.fetch_add(1, Ordering::Relaxed);
EVENT_THREAD.store(GetCurrentThreadId() as usize, Ordering::Relaxed);
EVENT_CONTROLLER.store(controller as usize, Ordering::Relaxed);
EVENT_ID.store(event as usize, Ordering::Relaxed);
EVENT_PAYLOAD.store(payload as usize, Ordering::Relaxed);
match event {
FUT_SBS_CATEGORIES_EVENT => {
EVENT_CATEGORIES.fetch_add(1, Ordering::Relaxed);
}
SBC_REFRESH_EVENT => {
EVENT_REFRESH.fetch_add(1, Ordering::Relaxed);
}
FUT_SBS_CATEGORIES_READY_EVENT => {
EVENT_READY.fetch_add(1, Ordering::Relaxed);
}
_ => {}
}
let original: EventDispatchFn = core::mem::transmute(EVENT_TRAMPOLINE.load(Ordering::Acquire));
let result = original(controller, event, payload);
EVENT_EXITS.fetch_add(1, Ordering::Release);
result
}
unsafe fn allocate_completion_trampoline(target: usize) -> Option<usize> {
let failure_target = target.checked_add(0x5c)?;
let success_target = target.checked_add(COMPLETION_COPY_LEN)?;
let memory = VirtualAlloc(
core::ptr::null(),
COMPLETION_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, 12);
// The relocated branch preserves the original null-status failure edge.
core::ptr::copy_nonoverlapping([0x75, 0x0e].as_ptr(), (memory + 12) as *mut u8, 2);
let failure = crate::sbc_trace::absolute_jump(failure_target);
core::ptr::copy_nonoverlapping(failure.as_ptr(), (memory + 14) as *mut u8, ABS_JUMP_LEN);
let success = crate::sbc_trace::absolute_jump(success_target);
core::ptr::copy_nonoverlapping(success.as_ptr(), (memory + 28) as *mut u8, ABS_JUMP_LEN);
let mut old = 0u32;
if VirtualProtect(
memory as _,
COMPLETION_TRAMPOLINE_LEN,
PAGE_EXECUTE_READ,
&mut old,
) == 0
|| FlushInstructionCache(GetCurrentProcess(), memory as _, COMPLETION_TRAMPOLINE_LEN) == 0
{
VirtualFree(memory as _, 0, MEM_RELEASE);
return None;
}
Some(memory)
}
unsafe fn restore_entry<const N: usize>(target: usize, original: &[u8; N]) -> bool {
let mut old = 0u32;
if VirtualProtect(target as _, N, PAGE_EXECUTE_READWRITE, &mut old) == 0 {
return false;
}
core::ptr::copy_nonoverlapping(original.as_ptr(), target as *mut u8, N);
let flushed = FlushInstructionCache(GetCurrentProcess(), target as _, N) != 0;
let mut ignored = 0u32;
flushed && VirtualProtect(target as _, N, old, &mut ignored) != 0
}
unsafe fn write_entry<const N: usize>(
target: usize,
destination: usize,
original: &[u8; N],
) -> Result<(), bool> {
let mut patch = [0x90u8; N];
patch[..ABS_JUMP_LEN].copy_from_slice(&crate::sbc_trace::absolute_jump(destination));
let mut old = 0u32;
if VirtualProtect(target as _, N, PAGE_EXECUTE_READWRITE, &mut old) == 0 {
return Err(true);
}
core::ptr::copy_nonoverlapping(patch.as_ptr(), target as *mut u8, N);
let flushed = FlushInstructionCache(GetCurrentProcess(), target as _, N) != 0;
let mut ignored = 0u32;
if flushed && VirtualProtect(target as _, N, old, &mut ignored) != 0 {
Ok(())
} else {
Err(restore_entry(target, original))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum InstallOutcome {
Installed,
CleanFailure,
DegradedHookActive,
DegradedProcessState,
DegradedHookAndProcess,
}
unsafe fn install_pair(base: usize) -> InstallOutcome {
let Some(completion) = crate::sbc_trace::target_va(base, COMPLETION_RVA) else {
return InstallOutcome::CleanFailure;
};
let Some(event) = crate::sbc_trace::target_va(base, EVENT_DISPATCH_RVA) else {
return InstallOutcome::CleanFailure;
};
let completion_original: [u8; COMPLETION_COPY_LEN] = COMPLETION_SIGNATURE
[..COMPLETION_COPY_LEN]
.try_into()
.unwrap();
if !crate::sbc_trace::valid_cards_image(base)
|| !crate::sbc_trace::executable_range_in_image(
base,
completion,
COMPLETION_SIGNATURE.len(),
)
|| !crate::sbc_trace::executable_range_in_image(base, event, EVENT_SIGNATURE.len())
|| core::slice::from_raw_parts(completion as *const u8, COMPLETION_SIGNATURE.len())
!= COMPLETION_SIGNATURE
|| core::slice::from_raw_parts(event as *const u8, EVENT_SIGNATURE.len()) != EVENT_SIGNATURE
{
return InstallOutcome::CleanFailure;
}
let mut pinned = core::ptr::null_mut();
if GetModuleHandleExA(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
completion as *const u8,
&mut pinned,
) == 0
|| pinned as usize != base
{
return InstallOutcome::CleanFailure;
}
let Some(completion_trampoline) = allocate_completion_trampoline(completion) else {
return InstallOutcome::CleanFailure;
};
let Some(event_trampoline) = crate::sbc_trace::allocate_trampoline(event, EVENT_COPY_LEN)
else {
VirtualFree(completion_trampoline as _, 0, MEM_RELEASE);
return InstallOutcome::CleanFailure;
};
COMPLETION_TRAMPOLINE.store(completion_trampoline, Ordering::Release);
EVENT_TRAMPOLINE.store(event_trampoline, Ordering::Release);
let Some(_gate) = crate::sbc_trace::acquire_patch_installer_gate() else {
VirtualFree(completion_trampoline as _, 0, MEM_RELEASE);
VirtualFree(event_trampoline as _, 0, MEM_RELEASE);
COMPLETION_TRAMPOLINE.store(0, Ordering::Release);
EVENT_TRAMPOLINE.store(0, Ordering::Release);
return InstallOutcome::CleanFailure;
};
let mut peers = match crate::sbc_trace::suspend_peers(completion, event) {
Ok(peers) => peers,
Err(crate::sbc_trace::QuiesceFailure::Acquire) => {
VirtualFree(completion_trampoline as _, 0, MEM_RELEASE);
VirtualFree(event_trampoline as _, 0, MEM_RELEASE);
COMPLETION_TRAMPOLINE.store(0, Ordering::Release);
EVENT_TRAMPOLINE.store(0, Ordering::Release);
return InstallOutcome::CleanFailure;
}
Err(crate::sbc_trace::QuiesceFailure::Resume) => {
return InstallOutcome::DegradedProcessState;
}
};
let final_valid = crate::sbc_trace::valid_cards_image(base)
&& core::slice::from_raw_parts(completion as *const u8, COMPLETION_SIGNATURE.len())
== COMPLETION_SIGNATURE
&& core::slice::from_raw_parts(event as *const u8, EVENT_SIGNATURE.len())
== EVENT_SIGNATURE;
let transaction = if !final_valid {
InstallOutcome::CleanFailure
} else {
match write_entry(
completion,
completion_wrapper as *const () as usize,
&completion_original,
) {
Ok(()) => {
match write_entry(event, event_wrapper as *const () as usize, &EVENT_SIGNATURE) {
Ok(()) => InstallOutcome::Installed,
Err(event_clean) => {
let completion_clean = restore_entry(completion, &completion_original);
if event_clean && completion_clean {
InstallOutcome::CleanFailure
} else {
InstallOutcome::DegradedHookActive
}
}
}
}
Err(true) => InstallOutcome::CleanFailure,
Err(false) => InstallOutcome::DegradedHookActive,
}
};
let resumed = peers.resume_all();
let outcome = if resumed {
transaction
} else if matches!(
transaction,
InstallOutcome::Installed | InstallOutcome::DegradedHookActive
) {
InstallOutcome::DegradedHookAndProcess
} else {
InstallOutcome::DegradedProcessState
};
if outcome == InstallOutcome::CleanFailure {
VirtualFree(completion_trampoline as _, 0, MEM_RELEASE);
VirtualFree(event_trampoline as _, 0, MEM_RELEASE);
COMPLETION_TRAMPOLINE.store(0, Ordering::Release);
EVENT_TRAMPOLINE.store(0, Ordering::Release);
}
outcome
}
unsafe fn worker() {
let _pending = crate::sbc_trace::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 outcome = if base == 0 {
InstallOutcome::CleanFailure
} else {
install_pair(base)
};
drop(_pending);
match outcome {
InstallOutcome::Installed => crate::write_log(
"SBC_DISPATCH: completion+event hooks installed; repair remains gate-controlled\n",
),
InstallOutcome::CleanFailure => {
crate::write_log("SBC_DISPATCH: clean install failure; inactive\n");
return;
}
InstallOutcome::DegradedHookActive => {
crate::write_log("SBC_DISPATCH: DEGRADED hook may be active; terminate game now\n");
return;
}
InstallOutcome::DegradedProcessState => {
crate::write_log("SBC_DISPATCH: DEGRADED thread state; terminate game now\n");
return;
}
InstallOutcome::DegradedHookAndProcess => {
crate::write_log("SBC_DISPATCH: DEGRADED hook and thread state; terminate game now\n");
return;
}
}
let mut completion_seen = 0u64;
let mut event_seen = 0u64;
let mut reports = 0u8;
while reports < 64 {
std::thread::sleep(std::time::Duration::from_millis(250));
let completion_entries = COMPLETION_ENTRIES.load(Ordering::Acquire);
let event_entries = EVENT_ENTRIES.load(Ordering::Acquire);
if completion_entries != completion_seen || event_entries != event_seen {
crate::write_log(&format!(
"SBC_DISPATCH: completion entry={} exit={} tid={} controller={:#x} status_obj={:#x} status={} generation={} decision={} rejection={}; event entry={} exit={} tid={} controller={:#x} id={:#x} payload={:#x} categories={} refresh={} ready={}\n",
completion_entries,
COMPLETION_EXITS.load(Ordering::Acquire),
COMPLETION_THREAD.load(Ordering::Relaxed),
COMPLETION_CONTROLLER.load(Ordering::Relaxed),
COMPLETION_STATUS_OBJECT.load(Ordering::Relaxed),
COMPLETION_STATUS.load(Ordering::Relaxed),
COMPLETION_GENERATION.load(Ordering::Relaxed),
COMPLETION_DECISION.load(Ordering::Relaxed),
COMPLETION_REJECTION.load(Ordering::Relaxed),
event_entries,
EVENT_EXITS.load(Ordering::Acquire),
EVENT_THREAD.load(Ordering::Relaxed),
EVENT_CONTROLLER.load(Ordering::Relaxed),
EVENT_ID.load(Ordering::Relaxed),
EVENT_PAYLOAD.load(Ordering::Relaxed),
EVENT_CATEGORIES.load(Ordering::Relaxed),
EVENT_REFRESH.load(Ordering::Relaxed),
EVENT_READY.load(Ordering::Relaxed),
));
completion_seen = completion_entries;
event_seen = event_entries;
reports += 1;
}
}
crate::write_log("SBC_DISPATCH: report cap reached; hooks remain installed\n");
}
pub(crate) fn install() {
let repair =
crate::sbc_trace::env_enabled(std::env::var("OPENFUT_SBC_DISPATCH").ok().as_deref());
let trace = repair
|| crate::sbc_trace::env_enabled(
std::env::var("OPENFUT_SBC_DISPATCH_TRACE").ok().as_deref(),
);
REPAIR_ENABLED.store(repair, Ordering::Release);
if !trace {
crate::write_log("SBC_DISPATCH: disabled\n");
return;
}
crate::write_log(if repair {
"SBC_DISPATCH: repair ARMED; strict native evidence gate enabled\n"
} else {
"SBC_DISPATCH: passive trace requested; repair disabled\n"
});
std::thread::spawn(|| unsafe { worker() });
}
#[cfg(test)]
mod tests {
use super::*;
fn valid_input(generation: u64) -> DecisionInput {
DecisionInput {
repair_enabled: true,
status: Some(UNKNOWN_TRANSPORT_STATUS),
status_present: true,
status_copyable: true,
cards_build_matches: true,
factory_entries: generation,
factory_exits: generation,
factory_result: 0x2000,
factory_thread: 7,
deserializer_entries: generation,
deserializer_exits: generation,
deserializer_this: 0x2000,
deserializer_reader: 0x3000,
deserializer_result: true,
deserializer_thread: 7,
response_class_matches: true,
model: 0x4000,
category_count: 2,
notifier_entries: generation,
notifier_exits: generation - 1,
controller_matches: true,
controller_model_matches: true,
last_repaired_generation: generation - 1,
}
}
#[test]
fn native_success_is_never_rewritten() {
let mut input = valid_input(1);
input.status = Some(0);
assert_eq!(decide(input), Ok(Decision::NativeSuccess));
}
#[test]
fn exact_unknown_status_and_full_evidence_allow_repair() {
assert_eq!(decide(valid_input(1)), Ok(Decision::Repair));
}
#[test]
fn repair_is_exactly_gated_and_fail_closed() {
let mut input = valid_input(1);
input.repair_enabled = false;
assert_eq!(decide(input), Err(Rejection::RepairDisabled));
let mut input = valid_input(1);
input.status = Some(500);
assert_eq!(decide(input), Err(Rejection::UnsupportedStatus));
let mut input = valid_input(1);
input.category_count = 0;
assert_eq!(decide(input), Err(Rejection::ModelEmpty));
let mut input = valid_input(1);
input.controller_matches = false;
assert_eq!(decide(input), Err(Rejection::ControllerMismatch));
let mut input = valid_input(1);
input.status = None;
input.status_present = false;
assert_eq!(decide(input), Err(Rejection::NullStatus));
let mut input = valid_input(1);
input.status = None;
assert_eq!(decide(input), Err(Rejection::StatusUnreadable));
let mut input = valid_input(1);
input.notifier_exits = 1;
assert_eq!(decide(input), Err(Rejection::NotifierNotCurrent));
let mut input = valid_input(1);
input.controller_model_matches = false;
assert_eq!(decide(input), Err(Rejection::ControllerModelMismatch));
}
#[test]
fn each_generation_is_one_shot_but_next_lifecycle_is_allowed() {
let mut duplicate = valid_input(1);
duplicate.last_repaired_generation = 1;
assert_eq!(decide(duplicate), Err(Rejection::DuplicateGeneration));
let next = valid_input(2);
assert_eq!(decide(next), Ok(Decision::Repair));
}
#[test]
fn relocated_completion_branch_has_proven_layout() {
assert_eq!(COMPLETION_COPY_LEN, 14);
assert_eq!(
&COMPLETION_SIGNATURE[..12],
&[0x40, 0x53, 0x48, 0x83, 0xec, 0x20, 0x48, 0x8b, 0xd9, 0x48, 0x85, 0xd2]
);
assert_eq!(&COMPLETION_SIGNATURE[12..14], &[0x74, 0x4e]);
assert_eq!(COMPLETION_TRAMPOLINE_LEN, 42);
}
}
+3 -216
View File
@@ -4,11 +4,9 @@
//! (all addresses, RVA math, call order, crash risks, staged test plan):
//! fifa17-recon/docs/sbc-hook-dll-spec.md
//!
//! Everything here is **inert by default** and gated by env vars, so shipping the DLL
//! with this module compiled in changes nothing unless a var is set:
//! Everything here is **inert by default** and gated by env vars:
//! OPENFUT_SBC_HOOK=1 -> arm the deferred worker (resolve + log; READ-ONLY)
//! OPENFUT_SBC_ARM_ONLY=1 -> Tier-0 negative control: write BYTE[B+0x28]=1 (renders EMPTY)
//! OPENFUT_SBC_COMMIT=1 -> after proven native parse success, arm populated M
//! OPENFUT_SBC_POPULATE=1 -> legacy Tier-1 gate: BLOCKED (logs corrected trace gap, returns)
//!
//! CardsDLL_Win64_retail.dll is loaded lazily (only on entering Ultimate Team), so we
@@ -20,14 +18,11 @@
//! See the spec for the verified disassembly behind each one.
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use windows_sys::Win32::System::Diagnostics::Debug::FlushInstructionCache;
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
use windows_sys::Win32::System::Memory::{
VirtualProtect, VirtualQuery, MEMORY_BASIC_INFORMATION, MEM_COMMIT, PAGE_EXECUTE_READ,
PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_GUARD, PAGE_NOACCESS, PAGE_READWRITE,
PAGE_WRITECOPY,
VirtualQuery, MEMORY_BASIC_INFORMATION, MEM_COMMIT, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE,
PAGE_EXECUTE_WRITECOPY, PAGE_GUARD, PAGE_NOACCESS, PAGE_READWRITE, PAGE_WRITECOPY,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetCurrentThreadId};
// ── RVAs (verified byte-exact against /tmp/fut/cardsdll.dll this pass) ────────────
const IMAGE_BASE: usize = 0x180000000;
@@ -46,13 +41,6 @@ const B_READY_OFF: usize = 0x28; // B+0x28 ready byte (the isValid gate)
const B_COLL_OFF: usize = 0x08; // B+0x08 collection ptr (MUST stay 0 — see spec §4/C5)
const M_CACHE_OFF: usize = 0x20a68; // M = *(A + 0x20a68) (render source; per-session heap)
const M_COUNT_OFF: usize = 0x50; // WORD[M+0x50] category count
const SBC_CONTROLLER_VTABLE_RVA: usize = 0x20a820;
const SBC_CONTROLLER_EVENT_VTABLE_RVA: usize = 0x20a888;
const SBC_CONTROLLER_EVENT_SUBOBJECT_OFF: usize = 0x138;
const SBC_CONTROLLER_MODEL_OFF: usize = 0x140;
const SBC_COMPLETION_STATUS_JNE_RVA: usize = 0x0b8962;
const SBC_COMPLETION_STATUS_JNE: [u8; 2] = [0x75, 0x48];
const SBC_COMPLETION_STATUS_FALLTHROUGH: [u8; 2] = [0x90, 0x90];
const B_DTOR_RVA: usize = 0x63040;
const B_ISVALID_RVA: usize = 0x65d40;
const B_CLEAR_RVA: usize = 0x65d20;
@@ -87,11 +75,9 @@ mod rva {
static ARMED: AtomicBool = AtomicBool::new(false);
static ARM_ONLY: AtomicBool = AtomicBool::new(false);
static COMMIT: AtomicBool = AtomicBool::new(false);
static POPULATE: AtomicBool = AtomicBool::new(false);
static DONE: AtomicBool = AtomicBool::new(false);
static CARDS_BASE: AtomicUsize = AtomicUsize::new(0);
static SBC_CONTROLLER: AtomicUsize = AtomicUsize::new(0);
static STATE: AtomicUsize = AtomicUsize::new(RuntimeState::Disabled as usize);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -147,13 +133,6 @@ enum ValidationError {
CollectionUnreadable,
CollectionNotNull,
ReadyByteNotWritable,
ModelEmpty,
ControllerMissing,
ControllerVtableMismatch,
ControllerModelMismatch,
CompletionBranchMismatch,
CompletionBranchProtectFailed,
CompletionBranchFlushFailed,
}
#[derive(Clone, Copy, Debug)]
@@ -428,12 +407,6 @@ pub fn install() {
.unwrap_or(false),
Ordering::Relaxed,
);
COMMIT.store(
std::env::var("OPENFUT_SBC_COMMIT")
.map(|v| v == "1")
.unwrap_or(false),
Ordering::Relaxed,
);
POPULATE.store(
std::env::var("OPENFUT_SBC_POPULATE")
.map(|v| v == "1")
@@ -444,192 +417,6 @@ pub fn install() {
std::thread::spawn(|| unsafe { worker() });
}
/// Records the concrete SBC controller observed registering FUT_SBS_CATEGORIES.
/// The registration hook is observational; all structural checks happen again on
/// the notifier thread before this address is trusted.
pub(crate) unsafe fn note_sbc_controller(controller: usize) {
let base = CARDS_BASE.load(Ordering::Acquire);
let valid = base != 0
&& read_ptr(controller) == base.checked_add(SBC_CONTROLLER_VTABLE_RVA)
&& controller
.checked_add(SBC_CONTROLLER_EVENT_SUBOBJECT_OFF)
.and_then(|p| read_ptr(p))
== base.checked_add(SBC_CONTROLLER_EVENT_VTABLE_RVA);
if valid {
SBC_CONTROLLER.store(controller, Ordering::Release);
crate::write_log(&format!(
"SBC_CONTROLLER_TRACE: captured controller={controller:#x}\n"
));
} else {
crate::write_log(&format!(
"SBC_CONTROLLER_TRACE: rejected controller={controller:#x} (vtable mismatch)\n"
));
}
}
unsafe fn log_controller_model(native_model: usize) {
let controller = SBC_CONTROLLER.load(Ordering::Acquire);
let controller_model = controller
.checked_add(SBC_CONTROLLER_MODEL_OFF)
.and_then(|p| read_ptr(p))
.unwrap_or(0);
let main_vtable = read_ptr(controller).unwrap_or(0);
let event_vtable = controller
.checked_add(SBC_CONTROLLER_EVENT_SUBOBJECT_OFF)
.and_then(|p| read_ptr(p))
.unwrap_or(0);
crate::write_log(&format!(
"SBC_CONTROLLER_TRACE: notifier controller={controller:#x} main_vt={main_vtable:#x} event_vt={event_vtable:#x} controller_M={controller_model:#x} parsed_M={native_model:#x} match={}\n",
controller != 0 && controller_model == native_model,
));
}
unsafe fn validated_sbc_controller(
base: usize,
native_model: usize,
) -> Result<usize, ValidationError> {
let controller = SBC_CONTROLLER.load(Ordering::Acquire);
if controller == 0 {
return Err(ValidationError::ControllerMissing);
}
if read_ptr(controller) != base.checked_add(SBC_CONTROLLER_VTABLE_RVA)
|| controller
.checked_add(SBC_CONTROLLER_EVENT_SUBOBJECT_OFF)
.and_then(|p| read_ptr(p))
!= base.checked_add(SBC_CONTROLLER_EVENT_VTABLE_RVA)
{
return Err(ValidationError::ControllerVtableMismatch);
}
if controller
.checked_add(SBC_CONTROLLER_MODEL_OFF)
.and_then(|p| read_ptr(p))
!= Some(native_model)
{
return Err(ValidationError::ControllerModelMismatch);
}
Ok(controller)
}
/// Route the already-scheduled category completion through CardsDLL's own success
/// branch. The original function first rejects a non-zero status with a two-byte
/// `jne ServerErrSets`; after a separately proven native parse, that status belongs
/// to the stale scheduler completion rather than the category HTTP transaction.
unsafe fn arm_native_completion_success(base: usize) -> Result<(), ValidationError> {
let target = base
.checked_add(SBC_COMPLETION_STATUS_JNE_RVA)
.ok_or(ValidationError::AddressOverflow)?;
if !executable_range(target, SBC_COMPLETION_STATUS_JNE.len())
|| core::slice::from_raw_parts(target as *const u8, SBC_COMPLETION_STATUS_JNE.len())
!= SBC_COMPLETION_STATUS_JNE
{
return Err(ValidationError::CompletionBranchMismatch);
}
let mut old = 0u32;
if VirtualProtect(
target as _,
SBC_COMPLETION_STATUS_FALLTHROUGH.len(),
PAGE_EXECUTE_READWRITE,
&mut old,
) == 0
{
return Err(ValidationError::CompletionBranchProtectFailed);
}
core::ptr::copy_nonoverlapping(
SBC_COMPLETION_STATUS_FALLTHROUGH.as_ptr(),
target as *mut u8,
SBC_COMPLETION_STATUS_FALLTHROUGH.len(),
);
let flushed = FlushInstructionCache(
GetCurrentProcess(),
target as _,
SBC_COMPLETION_STATUS_FALLTHROUGH.len(),
) != 0;
let mut ignored = 0u32;
let protected = VirtualProtect(
target as _,
SBC_COMPLETION_STATUS_FALLTHROUGH.len(),
old,
&mut ignored,
) != 0;
if !flushed || !protected {
return Err(ValidationError::CompletionBranchFlushFailed);
}
crate::write_log(&format!(
"SBC_HOOK: armed native completion success branch at {target:#x} tid={}\n",
GetCurrentThreadId(),
));
Ok(())
}
/// Commit the already-populated native SBC model after the category success notifier.
///
/// This is called synchronously by the passive notifier wrapper *after* the original
/// notifier returns. It never invokes a parser or constructs game objects. The only
/// mutation is the established cache-ready byte, and only when the normal parser has
/// produced at least one category and every pointer/vtable invariant still matches.
pub(crate) unsafe fn commit_after_native_parse() {
if !COMMIT.load(Ordering::Acquire) {
return;
}
let base = CARDS_BASE.load(Ordering::Acquire);
if base == 0 || !control_matches(base) {
set_failed(ValidationError::AUnreadable);
return;
}
let snapshot = match runtime_snapshot(base).and_then(|snapshot| {
validate_snapshot(base, &snapshot)?;
if snapshot.m == 0
|| read_u16(snapshot.m + M_COUNT_OFF)
.filter(|&count| count > 0)
.is_none()
{
return Err(ValidationError::ModelEmpty);
}
if !writable_u8(snapshot.b + B_READY_OFF) {
return Err(ValidationError::ReadyByteNotWritable);
}
Ok(snapshot)
}) {
Ok(snapshot) => snapshot,
Err(error) => {
set_failed(error);
return;
}
};
let count = read_u16(snapshot.m + M_COUNT_OFF).unwrap_or(0);
log_controller_model(snapshot.m);
if DONE.swap(true, Ordering::AcqRel) {
return;
}
crate::write_log(&format!(
"SBC_HOOK: post-parse commit -> M={:#x} categories={} BYTE[{:#x}]=1\n",
snapshot.m,
count,
snapshot.b + B_READY_OFF,
));
core::ptr::write_volatile((snapshot.b + B_READY_OFF) as *mut u8, 1);
if read_u8(snapshot.b + B_READY_OFF) != Some(1)
|| !transition(RuntimeState::Validated, RuntimeState::Committed)
{
set_failed(ValidationError::ReadyByteUnexpected);
return;
}
let _controller = match validated_sbc_controller(base, snapshot.m) {
Ok(controller) => controller,
Err(error) => {
set_failed(error);
return;
}
};
if let Err(error) = arm_native_completion_success(base) {
set_failed(error);
return;
}
crate::write_log(
"SBC_HOOK: post-parse commit DONE; awaiting CardsDLL native completion events\n",
);
}
/// Deferred worker: waits (up to ~5 min) for CardsDLL to load — it only appears when
/// the user enters Ultimate Team — then runs the resolve/log (+ optional Tier-0 arm)
/// exactly once.
+67 -22
View File
@@ -93,7 +93,7 @@ static NOTIFIER_COUNT: AtomicUsize = AtomicUsize::new(usize::MAX);
static PATCH_INSTALLER_BUSY: AtomicBool = AtomicBool::new(false);
static CODE_PATCH_PENDING: AtomicUsize = AtomicUsize::new(0);
struct PatchInstallerGate;
pub(crate) struct PatchInstallerGate;
impl Drop for PatchInstallerGate {
fn drop(&mut self) {
@@ -101,7 +101,7 @@ impl Drop for PatchInstallerGate {
}
}
fn acquire_patch_installer_gate() -> Option<PatchInstallerGate> {
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)
@@ -114,7 +114,7 @@ fn acquire_patch_installer_gate() -> Option<PatchInstallerGate> {
None
}
struct CodeInstallerPending;
pub(crate) struct CodeInstallerPending;
impl Drop for CodeInstallerPending {
fn drop(&mut self) {
@@ -138,15 +138,15 @@ enum TraceState {
DegradedHookAndProcess,
}
fn env_enabled(value: Option<&str>) -> bool {
pub(crate) fn env_enabled(value: Option<&str>) -> bool {
matches!(value, Some("1"))
}
fn target_va(base: usize, rva: usize) -> Option<usize> {
pub(crate) fn target_va(base: usize, rva: usize) -> Option<usize> {
base.checked_add(rva)
}
fn absolute_jump(destination: usize) -> [u8; ABS_JUMP_LEN] {
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());
@@ -160,7 +160,7 @@ fn instruction_pointer_in_span(rip: usize, target: usize) -> bool {
.unwrap_or(true)
}
struct SuspendedPeers {
pub(crate) struct SuspendedPeers {
handles: [HANDLE; MAX_PEERS],
tids: [u32; MAX_PEERS],
count: usize,
@@ -179,7 +179,7 @@ impl SuspendedPeers {
self.tids[..self.count].contains(&tid)
}
unsafe fn resume_all(&mut self) -> bool {
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];
@@ -208,14 +208,14 @@ impl Drop for SuspendedPeers {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum QuiesceFailure {
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.
unsafe fn suspend_peers(
pub(crate) unsafe fn suspend_peers(
factory: usize,
deserializer: usize,
) -> Result<SuspendedPeers, QuiesceFailure> {
@@ -329,7 +329,7 @@ unsafe fn executable_range(address: usize, length: usize) -> bool {
) && end <= mbi.BaseAddress as usize + mbi.RegionSize
}
unsafe fn executable_range_in_image(base: usize, address: usize, length: usize) -> bool {
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;
};
@@ -347,7 +347,7 @@ unsafe fn executable_range_in_image(base: usize, address: usize, length: usize)
&& end <= mbi.BaseAddress as usize + mbi.RegionSize
}
unsafe fn readable_range(address: usize, length: usize) -> bool {
pub(crate) unsafe fn readable_range(address: usize, length: usize) -> bool {
let Some(end) = address.checked_add(length) else {
return false;
};
@@ -362,20 +362,20 @@ unsafe fn readable_range(address: usize, length: usize) -> bool {
&& end <= mbi.BaseAddress as usize + mbi.RegionSize
}
unsafe fn guarded_usize(address: usize) -> Option<usize> {
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))
}
unsafe fn guarded_u16(address: usize) -> Option<u16> {
pub(crate) unsafe fn guarded_u16(address: usize) -> Option<u16> {
readable_range(address, 2).then(|| core::ptr::read_volatile(address as *const u16))
}
unsafe fn guarded_u8(address: usize) -> Option<u8> {
pub(crate) unsafe fn guarded_u8(address: usize) -> Option<u8> {
readable_range(address, 1).then(|| core::ptr::read_volatile(address as *const u8))
}
unsafe fn valid_cards_image(base: usize) -> bool {
pub(crate) unsafe fn valid_cards_image(base: usize) -> bool {
let Some(control) = base.checked_add(CONTROL_RVA) else {
return false;
};
@@ -413,7 +413,7 @@ unsafe fn signature_matches(target: usize, signature: &[u8; 32]) -> bool {
core::slice::from_raw_parts(target as *const u8, signature.len()) == signature
}
unsafe fn allocate_trampoline(target: usize, copy_len: usize) -> Option<usize> {
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(),
@@ -595,7 +595,10 @@ unsafe extern "system" fn controller_register_wrapper(controller: *mut c_void, e
core::mem::transmute(CONTROLLER_REGISTER_TRAMPOLINE.load(Ordering::Acquire));
original(controller, event);
if event == FUT_SBS_CATEGORIES_EVENT {
crate::sbc_hook::note_sbc_controller(controller as usize);
crate::sbc_dispatch::note_sbc_controller(
controller as usize,
TRACE_BASE.load(Ordering::Acquire),
);
}
}
@@ -630,7 +633,6 @@ unsafe extern "system" fn notifier_wrapper(ctx: *mut c_void) {
let original: unsafe extern "system" fn(*mut c_void) =
core::mem::transmute(NOTIFIER_TRAMPOLINE.load(Ordering::Acquire));
original(ctx);
crate::sbc_hook::commit_after_native_parse();
NOTIFIER_BYTE_AFTER.store(
address
.checked_add(0x88)
@@ -688,6 +690,44 @@ unsafe extern "system" fn deserializer_wrapper(this: *mut c_void, reader: *mut c
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) 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),
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 {
@@ -1103,10 +1143,15 @@ fn install_notifier(enabled: bool) {
}
pub(crate) fn install() {
let enabled = env_enabled(std::env::var("OPENFUT_SBC_TRACE").ok().as_deref());
let notifier_enabled = env_enabled(std::env::var("OPENFUT_SBC_NOTIFIER_TRACE").ok().as_deref());
let dispatch_repair = env_enabled(std::env::var("OPENFUT_SBC_DISPATCH").ok().as_deref());
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),
enabled as usize + (notifier_enabled as usize * 2) + dispatch_trace as usize,
Ordering::Release,
);
install_notifier(notifier_enabled);