wip(seasons): offline-Seasons base-supply URL rewriter + VEH crash logger
RETAINED DIAGNOSTIC WIP (pre-existing, brought forward, NOT production-ready). Rewrites bare WEBFILE relpaths to a season content base and adds a vectored crash logger used during offline-Seasons RE. Contains a HARDCODED staging base (http://10.10.0.120:8110/fut/) \u2014 must be env-parameterized before any promotion to main. Kept on this wip branch so main stays clean.
This commit is contained in:
@@ -21,6 +21,9 @@ use windows_sys::Win32::System::Memory::{
|
|||||||
PAGE_EXECUTE_READWRITE, PAGE_READWRITE,
|
PAGE_EXECUTE_READWRITE, PAGE_READWRITE,
|
||||||
};
|
};
|
||||||
use windows_sys::Win32::System::Threading::GetCurrentProcess;
|
use windows_sys::Win32::System::Threading::GetCurrentProcess;
|
||||||
|
use windows_sys::Win32::System::Diagnostics::Debug::{
|
||||||
|
AddVectoredExceptionHandler, EXCEPTION_POINTERS,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::sbc_trace::{
|
use crate::sbc_trace::{
|
||||||
absolute_jump, allocate_trampoline, readable_range, target_va, validate_cards_build,
|
absolute_jump, allocate_trampoline, readable_range, target_va, validate_cards_build,
|
||||||
@@ -346,17 +349,12 @@ unsafe extern "system" fn final_completion_wrapper(ctx: usize, result: usize, r8
|
|||||||
"SEASONS_LOAD_CALLBACK: final kind={kind} result={shown:?} 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
|
// Base-supply experiment: the CACHE_PACKNAMES failure is expected to be fixed
|
||||||
// failure to SUCCESS so the offline-season load advances to
|
// by the WEBFILE base-supply (the real file now downloads), so the guarded
|
||||||
// LoadCurrentOfflineSeason. Fires only for the exact CACHE_PACKNAMES failure,
|
// success-forcing bypass is DISABLED — a recurring CACHE_PACKNAMES here means
|
||||||
// once per process; verified by the error string before touching memory.
|
// the base-supply did not take effect and MUST NOT be masked.
|
||||||
if flag == Some(0)
|
if flag == Some(0) && errstr.contains("CACHE_PACKNAMES") && !BYPASS_DONE.swap(true, Ordering::AcqRel) {
|
||||||
&& errstr.contains("CACHE_PACKNAMES")
|
write_log("SEASONS_BYPASS: DISABLED (base-supply active); CACHE_PACKNAMES not masked\n");
|
||||||
&& 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);
|
let t = FINAL_COMPLETION_TRAMP.load(Ordering::Acquire);
|
||||||
if t == 0 {
|
if t == 0 {
|
||||||
@@ -395,14 +393,44 @@ unsafe extern "system" fn stage1_completion_wrapper(param1: usize, result: usize
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WEBFILE_DL download start FUN_18017ff90(url, ctx): param_1 (rcx) is the C-string
|
// 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
|
// URL of the pack-names / cards-tournament-list web file. Its prologue has a
|
||||||
// prologue has a rip-relative `MOV R8,[DAT_1802e6580]`, so it uses the relocating
|
// rip-relative `MOV R8,[DAT_1802e6580]`, so it uses the relocating installer
|
||||||
// installer (disp32 at copied offset 7, instruction end 11).
|
// (disp32 at copied offset 7, instruction end 11).
|
||||||
|
//
|
||||||
|
// BASE-SUPPLY (staging experiment): the client's RS4::ServerSettings CDN base
|
||||||
|
// (DAT_1802e6408+0x30) is EMPTY in the emulator — FUN_180124270 only sets it when
|
||||||
|
// the OSDK getter slot0x3f8 is non-empty, and it has no default (unlike the API
|
||||||
|
// base). So every FUT WEBFILE url arrives here as a BARE relative path and 999s
|
||||||
|
// (client sentinel). We supply the missing intended `<CDN>/fut/` prefix pointing
|
||||||
|
// at the staging content server so the REAL file downloads and parses. This is a
|
||||||
|
// data-supply, NOT a success-forcing bypass; absolute urls (containing "://",
|
||||||
|
// e.g. the "http://sbc/..." tile route) pass through untouched.
|
||||||
|
const STAGING_FUT_BASE: &str = "http://10.10.0.120:8110/fut/";
|
||||||
static URL_CAPTURE_TRAMP: AtomicUsize = AtomicUsize::new(0);
|
static URL_CAPTURE_TRAMP: AtomicUsize = AtomicUsize::new(0);
|
||||||
unsafe extern "system" fn url_capture_wrapper(rcx: usize, rdx: usize, r8: usize, r9: usize) -> usize {
|
unsafe extern "system" fn url_capture_wrapper(rcx: usize, rdx: usize, r8: usize, r9: usize) -> usize {
|
||||||
|
let orig = rd_cstr(rcx, 256);
|
||||||
|
let mut arg_rcx = rcx;
|
||||||
|
// Owned buffer that stays alive across the original() call below. The caller
|
||||||
|
// frees its own url buffer immediately after FUN_18017ff90 returns, so the
|
||||||
|
// client copies the url synchronously during the call — a local buffer is
|
||||||
|
// sufficient and nothing is leaked.
|
||||||
|
let mut full: Vec<u8> = Vec::new();
|
||||||
|
if !orig.is_empty() && !orig.contains("://") {
|
||||||
|
full.extend_from_slice(STAGING_FUT_BASE.as_bytes());
|
||||||
|
full.extend_from_slice(orig.trim_start_matches('/').as_bytes());
|
||||||
|
full.push(0); // NUL terminator for the C-string
|
||||||
|
arg_rcx = full.as_ptr() as usize;
|
||||||
|
}
|
||||||
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
|
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
|
||||||
if n < 64 {
|
if n < 64 {
|
||||||
write_log(&format!("SEASONS_WEBFILE_URL: url={:?}\n", rd_cstr(rcx, 256)));
|
if arg_rcx != rcx {
|
||||||
|
write_log(&format!(
|
||||||
|
"SEASONS_WEBFILE_URL: orig={orig:?} rewritten={:?}\n",
|
||||||
|
rd_cstr(arg_rcx, 256)
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
write_log(&format!("SEASONS_WEBFILE_URL: url={orig:?} (unchanged)\n"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let t = URL_CAPTURE_TRAMP.load(Ordering::Acquire);
|
let t = URL_CAPTURE_TRAMP.load(Ordering::Acquire);
|
||||||
if t == 0 {
|
if t == 0 {
|
||||||
@@ -410,7 +438,80 @@ unsafe extern "system" fn url_capture_wrapper(rcx: usize, rdx: usize, r8: usize,
|
|||||||
}
|
}
|
||||||
let original: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
|
let original: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
|
||||||
core::mem::transmute(t);
|
core::mem::transmute(t);
|
||||||
original(rcx, rdx, r8, r9)
|
let ret = original(arg_rcx, rdx, r8, r9);
|
||||||
|
drop(full); // ensure the url buffer outlives the download-start call
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────── crash locator (VEH) ──────────────────────────────
|
||||||
|
// A vectored exception handler that logs the faulting code/address/module for
|
||||||
|
// fatal exceptions, then lets the crash proceed (EXCEPTION_CONTINUE_SEARCH). It
|
||||||
|
// pinpoints the StartSeason crash: whether it is a CardsDLL season-data
|
||||||
|
// null-deref (fixable by supplying matches/opponents) or an engine/other fault.
|
||||||
|
static CARDS_BASE: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
static CARDS_SIZE: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
static CRASH_LOGS: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
const EXCEPTION_CONTINUE_SEARCH: i32 = 0;
|
||||||
|
|
||||||
|
/// OptionalHeader.SizeOfImage from the module's PE headers (fallback 64 MiB).
|
||||||
|
unsafe fn cards_image_size(base: usize) -> usize {
|
||||||
|
if !readable_range(base + 0x3c, 4) {
|
||||||
|
return 0x0400_0000;
|
||||||
|
}
|
||||||
|
let e_lfanew = core::ptr::read_volatile((base + 0x3c) as *const u32) as usize;
|
||||||
|
let so_off = base + e_lfanew + 0x50; // NT header + OptionalHeader.SizeOfImage
|
||||||
|
if !readable_range(so_off, 4) {
|
||||||
|
return 0x0400_0000;
|
||||||
|
}
|
||||||
|
core::ptr::read_volatile(so_off as *const u32) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "system" fn crash_logger(info: *mut EXCEPTION_POINTERS) -> i32 {
|
||||||
|
if info.is_null() {
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
let rec = (*info).ExceptionRecord;
|
||||||
|
if rec.is_null() {
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
let code = (*rec).ExceptionCode as u32;
|
||||||
|
// Only fatal codes; skip the many benign first-chance SEH exceptions.
|
||||||
|
let interesting = matches!(
|
||||||
|
code,
|
||||||
|
0xC000_0005 // access violation
|
||||||
|
| 0xC000_001D // illegal instruction
|
||||||
|
| 0xC000_0094 // integer divide by zero
|
||||||
|
| 0xC000_00FD // stack overflow
|
||||||
|
| 0xC000_0025 // noncontinuable exception
|
||||||
|
);
|
||||||
|
if !interesting || CRASH_LOGS.fetch_add(1, Ordering::Relaxed) >= 8 {
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
let addr = (*rec).ExceptionAddress as usize;
|
||||||
|
let base = CARDS_BASE.load(Ordering::Acquire);
|
||||||
|
let size = CARDS_SIZE.load(Ordering::Acquire);
|
||||||
|
let module = if base != 0 && addr >= base && addr < base + size {
|
||||||
|
format!("CardsDLL+{:#x}", addr - base)
|
||||||
|
} else {
|
||||||
|
"other".to_string()
|
||||||
|
};
|
||||||
|
let (kind, fault) = if code == 0xC000_0005 && (*rec).NumberParameters >= 2 {
|
||||||
|
let op = (*rec).ExceptionInformation[0];
|
||||||
|
let fa = (*rec).ExceptionInformation[1];
|
||||||
|
let k = match op {
|
||||||
|
0 => "read",
|
||||||
|
1 => "write",
|
||||||
|
8 => "exec",
|
||||||
|
_ => "?",
|
||||||
|
};
|
||||||
|
(k, fa)
|
||||||
|
} else {
|
||||||
|
("", 0usize)
|
||||||
|
};
|
||||||
|
write_log(&format!(
|
||||||
|
"SEASON_CRASH: code={code:#010x} at={addr:#x} module={module} access={kind} fault_addr={fault:#x}\n"
|
||||||
|
));
|
||||||
|
EXCEPTION_CONTINUE_SEARCH
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn worker() {
|
unsafe fn worker() {
|
||||||
@@ -426,6 +527,10 @@ unsafe fn worker() {
|
|||||||
write_log("SEASON_TRACE: CardsDLL unavailable/invalid; season trace inactive\n");
|
write_log("SEASON_TRACE: CardsDLL unavailable/invalid; season trace inactive\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
CARDS_BASE.store(base, Ordering::Release);
|
||||||
|
CARDS_SIZE.store(cards_image_size(base), Ordering::Release);
|
||||||
|
AddVectoredExceptionHandler(1, Some(crash_logger));
|
||||||
|
write_log("SEASON_TRACE: crash logger (VEH) armed\n");
|
||||||
// (rva, name, copy_len, signature, wrapper, trampoline slot)
|
// (rva, name, copy_len, signature, wrapper, trampoline slot)
|
||||||
install_detour(
|
install_detour(
|
||||||
base, 0x4eb70, "LoadCurrentOfflineSeason_native", 15,
|
base, 0x4eb70, "LoadCurrentOfflineSeason_native", 15,
|
||||||
|
|||||||
Reference in New Issue
Block a user