From 5aec83ce971a70489d0f28fd659aad7dd6db2423 Mon Sep 17 00:00:00 2001 From: funman300 Date: Mon, 29 Jun 2026 20:21:42 -0700 Subject: [PATCH] M2: confirm worker-thread + event-driven gate (architectural wall) Add a bounded manual stack-scan in the GoOnline detour (FIFA-frame finder). Result: zero FIFA23.exe frames, sp ~2.4KB below stack top => GoOnline runs on an anadius worker thread (queued), not FIFA's thread. Three-way corroboration that the gate is an async "online established" event anadius (offline-only) never pushes; FIFA waits/retries. No handler-response flip can cross it. Documented in connection-gate-findings.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/connection-gate-findings.md | 24 ++++++++++++ openfut-hook/src/lib.rs | 67 +++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/docs/connection-gate-findings.md b/docs/connection-gate-findings.md index bbcd1f5..ca1d73b 100644 --- a/docs/connection-gate-findings.md +++ b/docs/connection-gate-findings.md @@ -207,6 +207,30 @@ Next: trace the FIFA-side FUT online-flow (what the game does after `GoOnline` and exactly which event/condition it waits on) — Ghidra on FIFA23.exe (import saved at `C:\openfut\gh-proj`), or RE anadius's LSX event-send path. `TODO/CONFIRM`. +### M2 deeper finding — worker-thread + event architecture (confirmed) +A live call-stack capture from inside the GoOnline detour (manual stack scan, +bounded by `GetCurrentThreadStackLimits`) found **zero FIFA23.exe frames** and +showed `sp` sitting ~2.4 KB below the thread's stack top. So the handler runs at +the top of a short stack — i.e. on an **anadius worker thread** (IOCP/threadpool), +not FIFA's calling thread. anadius **queues** the GoOnline command and a worker +services it. + +Combined with the retry behaviour, the architecture is now clear and three-way +corroborated: **FIFA calls `EbisuSDK::GoOnline` → anadius queues it → returns → +FIFA waits for an async "online established" event → anadius (offline-only, no +online-event code) never pushes it → timeout/retry.** No handler-response flip +can unblock this; the game waits on a *push* anadius never produces. + +**Conclusion:** crossing this gate requires emulating the EA-app online-event +sequence (synthesize + inject the online-status event on the worker→game callback +/ LSX path, in EbisuSDK's expected format) — a research-grade emulation effort, +preceding the Blaze backend. The cheap in-process flips are exhausted. + +Reaching the FIFA-side flow would need either: (a) locate `EbisuSDK::GoOnline` in +FIFA23.exe via anadius's detour table, then `callers` to the online-flow; or +(b) find anadius's LSX event-send path and reverse the online-event format. Both +are deep. `TODO/CONFIRM`. + ### M2 directions (superseded — see above) - Check whether the flags at `+0xCAB1A` / `+0xCAB1B` are settable via anadius config / a hidden option (cheapest flip). diff --git a/openfut-hook/src/lib.rs b/openfut-hook/src/lib.rs index fc8789b..0ff5b40 100644 --- a/openfut-hook/src/lib.rs +++ b/openfut-hook/src/lib.rs @@ -31,7 +31,7 @@ use windows::Win32::System::ProcessStatus::{GetModuleInformation, MODULEINFO}; use windows::Win32::System::SystemInformation::GetLocalTime; use windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH; use windows::Win32::System::Threading::{ - CreateThread, GetCurrentProcess, Sleep, THREAD_CREATION_FLAGS, + CreateThread, GetCurrentProcess, GetCurrentThreadStackLimits, Sleep, THREAD_CREATION_FLAGS, }; // --------------------------------------------------------------------------- @@ -185,6 +185,7 @@ unsafe extern "system" fn hooked( unsafe extern "system" fn init_thread(_: *mut c_void) -> u32 { // Connection capture first. Listen on the LSX port (gate 1, so the launcher // bootstrap succeeds) and on the redirect port (for external TLS/Blaze). + store_exe_range(); start_listener(LSX_PORT, "LSX"); start_listener(LOCAL_PORT, "BLZ"); hook_dns(); @@ -418,6 +419,69 @@ unsafe fn install_detour_at(addr: usize, detour: *const (), slot: &AtomicUsize, // --- M1 read-only probe: anadius GoOnline handler ------------------------- static ORIG_GOONLINE: AtomicUsize = AtomicUsize::new(0); +static EXE_BASE: AtomicUsize = AtomicUsize::new(0); +static EXE_SIZE: AtomicUsize = AtomicUsize::new(0); +static STACK_LOGGED: AtomicUsize = AtomicUsize::new(0); + +/// Record FIFA23.exe's base + size so we can recognise its frames in a backtrace. +unsafe fn store_exe_range() { + if let Ok(h) = GetModuleHandleW(PCWSTR::null()) { + let mut mi = MODULEINFO::default(); + if GetModuleInformation( + GetCurrentProcess(), + h, + &mut mi, + core::mem::size_of::() as u32, + ) + .is_ok() + { + EXE_BASE.store(h.0 as usize, Ordering::SeqCst); + EXE_SIZE.store(mi.SizeOfImage as usize, Ordering::SeqCst); + } + } +} + +/// Scan the raw stack for values that land in FIFA23.exe (the game-side +/// online-flow return addresses that called into GoOnline). Unwind-free, so it +/// survives the detour trampolines that break RtlCaptureStackBackTrace. +/// One-shot to avoid log spam. +unsafe fn log_fifa_callstack(tag: &str) { + if STACK_LOGGED.swap(1, Ordering::SeqCst) != 0 { + return; + } + let base = EXE_BASE.load(Ordering::SeqCst); + let size = EXE_SIZE.load(Ordering::SeqCst); + if base == 0 || size == 0 { + log(&format!("{tag} stack scan skipped (exe range unknown)")); + return; + } + // Address of a local ~= current rsp; the stack grows down, so callers' + // return addresses sit at HIGHER addresses. Scan upward, but NEVER past the + // committed stack top (reading beyond it faults — that crashed the game). + let mut low: usize = 0; + let mut high: usize = 0; + GetCurrentThreadStackLimits(&mut low, &mut high); + let probe: usize = 0; + let sp = &probe as *const usize as usize; + let end = high; // scan the whole rest of the stack (committed, safe) + let mut line = format!( + "{tag} stack[low=0x{low:X} high=0x{high:X} sp=0x{sp:X}] FIFA23.exe refs:" + ); + let mut count = 0; + let mut p = sp; + while p + 8 <= end { + let val = *(p as *const usize); + if val >= base && val < base + size { + line.push_str(&format!(" +0x{:X}", val - base)); + count += 1; + if count >= 40 { + break; + } + } + p += 8; + } + log(&line); +} /// M2 flip on anadius's GoOnline handler (anadius64.dll+0x2BB90). The original /// handler is `mov rcx,rdx; lea r8,[+0xADD73]; lea rdx,[+0xADE64 = "0"]; call @@ -425,6 +489,7 @@ static ORIG_GOONLINE: AtomicUsize = AtomicUsize::new(0); /// "0" (offline). We replicate it but pass "1" (+0xAF530 = the connected value), /// so GoOnline reports online, then return success (al=1). unsafe extern "system" fn hooked_goonline(_a: usize, b: usize, _c: usize, _d: usize) -> usize { + log_fifa_callstack("GoOnline"); let base = ANADIUS_BASE.load(Ordering::SeqCst); if base != 0 { log("FLIP GoOnline -> reporting online (\"1\")");