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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
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`.
|
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)
|
### M2 directions (superseded — see above)
|
||||||
- Check whether the flags at `+0xCAB1A` / `+0xCAB1B` are settable via anadius
|
- Check whether the flags at `+0xCAB1A` / `+0xCAB1B` are settable via anadius
|
||||||
config / a hidden option (cheapest flip).
|
config / a hidden option (cheapest flip).
|
||||||
|
|||||||
+66
-1
@@ -31,7 +31,7 @@ use windows::Win32::System::ProcessStatus::{GetModuleInformation, MODULEINFO};
|
|||||||
use windows::Win32::System::SystemInformation::GetLocalTime;
|
use windows::Win32::System::SystemInformation::GetLocalTime;
|
||||||
use windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH;
|
use windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH;
|
||||||
use windows::Win32::System::Threading::{
|
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 {
|
unsafe extern "system" fn init_thread(_: *mut c_void) -> u32 {
|
||||||
// Connection capture first. Listen on the LSX port (gate 1, so the launcher
|
// Connection capture first. Listen on the LSX port (gate 1, so the launcher
|
||||||
// bootstrap succeeds) and on the redirect port (for external TLS/Blaze).
|
// bootstrap succeeds) and on the redirect port (for external TLS/Blaze).
|
||||||
|
store_exe_range();
|
||||||
start_listener(LSX_PORT, "LSX");
|
start_listener(LSX_PORT, "LSX");
|
||||||
start_listener(LOCAL_PORT, "BLZ");
|
start_listener(LOCAL_PORT, "BLZ");
|
||||||
hook_dns();
|
hook_dns();
|
||||||
@@ -418,6 +419,69 @@ unsafe fn install_detour_at(addr: usize, detour: *const (), slot: &AtomicUsize,
|
|||||||
// --- M1 read-only probe: anadius GoOnline handler -------------------------
|
// --- M1 read-only probe: anadius GoOnline handler -------------------------
|
||||||
|
|
||||||
static ORIG_GOONLINE: AtomicUsize = AtomicUsize::new(0);
|
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::<MODULEINFO>() 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
|
/// 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
|
/// 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),
|
/// "0" (offline). We replicate it but pass "1" (+0xAF530 = the connected value),
|
||||||
/// so GoOnline reports online, then return success (al=1).
|
/// so GoOnline reports online, then return success (al=1).
|
||||||
unsafe extern "system" fn hooked_goonline(_a: usize, b: usize, _c: usize, _d: usize) -> usize {
|
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);
|
let base = ANADIUS_BASE.load(Ordering::SeqCst);
|
||||||
if base != 0 {
|
if base != 0 {
|
||||||
log("FLIP GoOnline -> reporting online (\"1\")");
|
log("FLIP GoOnline -> reporting online (\"1\")");
|
||||||
|
|||||||
Reference in New Issue
Block a user