diag(fifa17): capture WEBFILE_DL url + guarded CACHE_PACKNAMES bypass

- Passive: log FUN_18017ff90 param_1 = the pack-names/cards-tournament-list
  WEBFILE_DL url (via relocating installer; rip-relative MOV R8,[DAT_1802e6580]).
- Guarded one-shot (staging client only): in the final completion FUN_1800ffe90,
  when the delivered result string is CACHE_PACKNAMES_FAILED, rewrite result byte0
  so it delivers SUCCESS -> LoadSeasons_Complete advances to LoadCurrentOfflineSeason.
  String-verified, once per process.
This commit is contained in:
openfut
2026-08-20 00:18:51 +00:00
parent 9c4db41289
commit 8d5bb6202a
+59 -15
View File
@@ -12,7 +12,7 @@
//! flow. Targets are chosen so their copied prologues are position-independent
//! (no rip-relative / rel32 in the copied bytes).
use core::sync::atomic::{AtomicUsize, Ordering};
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use windows_sys::Win32::System::Diagnostics::Debug::FlushInstructionCache;
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
@@ -28,6 +28,8 @@ use crate::sbc_trace::{
use crate::write_log;
static REPORTS: AtomicUsize = AtomicUsize::new(0);
/// One-shot guard for the staging-only CACHE_PACKNAMES_FAILED -> SUCCESS bypass.
static BYPASS_DONE: AtomicBool = AtomicBool::new(false);
unsafe fn rd_i32(addr: usize) -> Option<i32> {
readable_range(addr, 4).then(|| core::ptr::read_volatile(addr as *const i32))
@@ -315,30 +317,47 @@ unsafe fn install_detour_reloc(
// string ptr when byte0==0). Logs the EXACT status string delivered. Passive.
static FINAL_COMPLETION_TRAMP: AtomicUsize = AtomicUsize::new(0);
unsafe extern "system" fn final_completion_wrapper(ctx: usize, result: usize, r8: usize, r9: usize) -> usize {
// Read the delivered status: byte0==0 => failure with an error string at +8.
let flag = rd_u8(result);
let errstr = if flag == Some(0) {
let p = if readable_range(result + 8, 8) {
core::ptr::read_volatile((result + 8) as *const usize)
} else {
0
};
rd_cstr(p, 96)
} else {
String::new()
};
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
if n < 64 {
let flag = rd_u8(result);
let (kind, s) = match flag {
Some(0) => {
let p = if readable_range(result + 8, 8) {
core::ptr::read_volatile((result + 8) as *const usize)
} else {
0
};
("ERROR", rd_cstr(p, 96))
}
Some(_) => ("SUCCESS", String::from("SUCCESS")),
None => ("??", String::from("<result unreadable>")),
};
let cbref = if readable_range(ctx + 0x18, 8) {
core::ptr::read_volatile((ctx + 0x18) as *const usize)
} else {
0
};
let kind = match flag {
Some(0) => "ERROR",
Some(_) => "SUCCESS",
None => "??",
};
let shown = if flag == Some(0) { errstr.as_str() } else { "SUCCESS" };
write_log(&format!(
"SEASONS_LOAD_CALLBACK: final kind={kind} result={s:?} flag={flag:?} ctx={ctx:#x} cbref={cbref:#x}\n"
"SEASONS_LOAD_CALLBACK: final kind={kind} result={shown:?} flag={flag:?} ctx={ctx:#x} cbref={cbref:#x}\n"
));
}
// Guarded one-shot bypass (staging diagnostic only): rewrite the pack-names
// failure to SUCCESS so the offline-season load advances to
// LoadCurrentOfflineSeason. Fires only for the exact CACHE_PACKNAMES failure,
// once per process; verified by the error string before touching memory.
if flag == Some(0)
&& errstr.contains("CACHE_PACKNAMES")
&& readable_range(result, 1)
&& !BYPASS_DONE.swap(true, Ordering::AcqRel)
{
core::ptr::write_volatile(result as *mut u8, 1u8); // take the SUCCESS branch
write_log("SEASONS_BYPASS: forced CACHE_PACKNAMES_FAILED -> SUCCESS (one-shot, staging)\n");
}
let t = FINAL_COMPLETION_TRAMP.load(Ordering::Acquire);
if t == 0 {
return 0;
@@ -375,6 +394,25 @@ unsafe extern "system" fn stage1_completion_wrapper(param1: usize, result: usize
original(param1, result, r8, r9)
}
// WEBFILE_DL download start FUN_18017ff90(url, ctx): param_1 (rcx) is the C-string
// URL of the pack-names/cards-tournament-list web file. Passive capture. Its
// prologue has a rip-relative `MOV R8,[DAT_1802e6580]`, so it uses the relocating
// installer (disp32 at copied offset 7, instruction end 11).
static URL_CAPTURE_TRAMP: AtomicUsize = AtomicUsize::new(0);
unsafe extern "system" fn url_capture_wrapper(rcx: usize, rdx: usize, r8: usize, r9: usize) -> usize {
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
if n < 64 {
write_log(&format!("SEASONS_WEBFILE_URL: url={:?}\n", rd_cstr(rcx, 256)));
}
let t = URL_CAPTURE_TRAMP.load(Ordering::Acquire);
if t == 0 {
return 0;
}
let original: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
core::mem::transmute(t);
original(rcx, rdx, r8, r9)
}
unsafe fn worker() {
let mut base = 0usize;
for _ in 0..600u32 {
@@ -440,6 +478,12 @@ unsafe fn worker() {
&[0x48, 0x8b, 0xc4, 0x55, 0x48, 0x8d, 0x68, 0xa1, 0x48, 0x81, 0xec, 0xc0, 0x00, 0x00, 0x00],
stage1_completion_wrapper as *const () as usize, &STAGE1_COMPLETION_TRAMP,
);
install_detour_reloc(
base, 0x17ff90, "start_webfile_dl_url", 14,
&[0x48, 0x83, 0xec, 0x38, 0x4c, 0x8b, 0x05, 0xe5, 0x65, 0x16, 0x00, 0x4c, 0x8b, 0xd1],
7, 11,
url_capture_wrapper as *const () as usize, &URL_CAPTURE_TRAMP,
);
write_log("SEASON_TRACE: all season-native traces armed\n");
}