Implement the LSX (EA App bootstrap, TCP :3216→:3217) server in the bridge
and clear the loading-screen crash gate. FIFA 23 now completes the full LSX
handshake + session against OpenFUT and advances to the online/Blaze phase
("<persona> is connecting to the EA Servers…") — verified live, not a cached
session (the displayed persona "fun" comes from our GetProfile).
Root cause of the invariant loading-screen crash: get_config() omitted the
PROGRESSIVE_INSTALLATION and PROGRESSIVE_INSTALLATION_EVENT services (Name="PI").
FIFA builds its service registry from GetConfig and resolves that handle during
online-init; the missing service left a null handle it dereferenced at
FIFA23.exe+0x133dfc5 (mov rax,[r13+0x18], r13=NULL) ~2s after LSX — identical
across every prior run. Found via FIFA's own CrashReport_*.json (stable RIP) +
diffing anadius64.dll's service-registration table against ours.
Also in this change:
- process_frame(): handle FIFA's pipelined (batched) LSX messages per read,
eliminating the ~15s handshake hang.
- Real anadius values (RE'd from anadius64.dll + anadius.cfg): PersonaId/UserId/
persona, locale list, GetGameInfo fixed `GameInfo="<value>"` attribute,
GetAllGameInfo 14-attr schema, GetSetting `Setting=` attribute.
- IsProgressiveInstallationAvailable served from sender="PI".
- docs/connection-gate-findings.md: full RE trail; relocate stale openfut-hook
copy under docs/ (canonical hook lives in openfut-launcher).
Next gate: Blaze/online (M2), ridden over ProtoSSL in-process.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
34 KiB
Connection-gate findings (clean-room)
Status: observed behaviour only — derived from running the client and reading
the repack's own files. No EA source used. Unconfirmed values are marked
TODO/CONFIRM rather than guessed.
Components (observed)
| Component | Role |
|---|---|
FIFA23.exe |
Game. Statically links EA's Ebisu SDK (online) and DirtySDK/ProtoSSL (transport). Loads at fixed base 0x140000000 — no ASLR seen across launches. |
_ProtoSSLSendPacket @ FIFA23.exe+0xEFA530 |
DirtySDK TLS record assembler — plaintext in, encrypts in place. Already located + hooked. |
anadius64.dll |
anadius EA-app/Origin LSX server emulator (ASLR'd). Provides offline DRM / entitlements / persona so the game launches; deliberately keeps it offline. |
FakeEAACLauncher |
Anti-cheat bypass only. Irrelevant to the online gate. |
Observed online-startup flow
- Ebisu SDK opens LSX socket(s) to anadius; a challenge/response handshake
completes — captured XML:
<Challenge>/<ChallengeResponse>/<ChallengeAccepted>,sender="EALS",ContentId 16115019. - River/PIN telemetry
<session type=boot>is emitted. - No further socket traffic. Clicking Ultimate Team → "connecting to the EA
Servers…" → zero network attempts (no Blaze DNS, no
connect, nothing onsend/recv/WSASend/WSARecv) → falls back to offline.
Conclusion: the decision is upstream and in-process
- The online/offline verdict is delivered through anadius's in-process
detoured Ebisu calls, not socket messages. (No
GetAuthCode/GoOnline/ entitlement query appears on any socket, sync or async.) - The game therefore never reaches DirtySDK/ProtoSSL for Blaze — it aborts
before any
ProtoSSLConnect(gosredirector…). The gate is an Ebisu-SDK connection-state / online-session check upstream of the transport. ONLINE_ACCESSis aBlaze::Nucleus::EntitlementType(enum value1). anadius'sGoOnlineLSX handler (anadius64.dll+0x2BB90) is a success stub;anadius64.dll+0xB6B1is a large entitlement/profile builder that does referenceONLINE_ACCESS. Reverse-engineering that entitlement-status logic is the wrong fight (see strategy).
Strategy (decided)
Do not make anadius report "online." anadius exists to keep the game offline, and it can't be removed without breaking launch/DRM. Instead:
Let the game run its real online flow and answer that flow ourselves via the hook + brain. Target the Ebisu connection-state check the FUT-entry path polls; make the game pass it so it issues the real
ProtoSSLConnectto the Blaze redirector → our redirect + ProtoSSL hook capture the real frames.
This deletes the "reverse-engineer anadius's entitlement logic" problem.
Next RE step (converges with the ProtoSSL hook)
- Locate and read-only hook
ProtoSSLConnect(same DirtySDK module and technique that found_ProtoSSLSendPacket). Confirms the game never initiates the Blaze connection (pins the gate strictly upstream) and gives us the exact symbol that will flip from "never called" to "called" on success. - Locate the Ebisu connection-state getter the FUT-entry / "connecting" UI
polls. Candidate string anchors (observed):
ONLINE_STATUS_EVENT,GoOnlineresult handling, the "connecting to the EA Servers" UI trigger. - Determine the minimal intervention to make that getter report connected
(out-hook it in our
version.dll, winning over anadius's detour) so the game proceeds toProtoSSLConnect.TODO/CONFIRM: whether out-hooking the getter is sufficient, or secondary checks (auth-token presence) also gate the attempt.
M1 results (read-only investigation)
Method: protossl-scan (xref / disasm / callers, app-module-restricted) against
the live client, plus the existing connect/DNS/LSX hooks. Observed behaviour
only — no EA source.
Point 1 — is ProtoSSLConnect reached on the "connecting" abort? NO — gate is upstream. CONFIRMED.
- The existing
connect/GetAddrInfoW/getaddrinfohooks show the client makes zero network attempts during the "connecting to the EA Servers" abort: no DNS for anygosredirector.*host, noconnectto any external address. - The DirtySDK/ProtoSSL transport is therefore never reached — the abort is entirely upstream and in-process.
ProtoSSLConnecthas no debug string and sits behind the DirtySDK API, so an explicit hook on it isn't needed to prove this; the behavioural evidence is conclusive.TODO/CONFIRM: locateProtoSSLConnectby signature later, as a positive M2 trip-wire (it should fire once we flip the gate).
Surface mapped (clean-room)
- Redirector host table (
FIFA23.exe.rdata, pointer table @+0x83FC858):spring18.gosredirector.{sdev,stest,scert}.ea.com+ productionspring18.gosredirector.ea.com. - Redirector request/response config (same region):
X-BLAZE-ERRORCODE,Authorization:,<errorCode>— the redirector request carries an Authorization header (a Nucleus token). - Enum-name tables (serialization only, NOT decision code):
ONLINE_ACCESS(Blaze::Nucleus::EntitlementType= 1),ONLINE_STATUS_EVENT, … These are reflection tables keyed by enum value; string-xref of them is a dead end for the decision (the decision compares values, not strings).
Point 2 — name the connection-state function: PARTIAL.
- The exact connection-state decision is behind heavy C++ vtable indirection
(the redirector config's two code pointers resolved to a virtual-dispatch
thunk @
FIFA23.exe+0x27BD4C0and aret 0stub @+0x4F3CBC0). The memory-scan toolkit (string / pattern / xref / callers) cannot efficiently navigate this. - Pinning the exact function needs a static disassembler with decompilation
(Ghidra / IDA) on
FIFA23.exe.protossl-scanremains the bridge to the live process (mapping static addresses to the ASLR'd runtime, confirming hits, installing hooks). TODO/CONFIRM: name the connection-state getter by module+offset via Ghidra.
Point 3 — gate count: TWO gates (state + token). Evidence-backed.
- The online-connect path reads/requires an auth (Nucleus) token: the
redirector request carries an
Authorization:header, and the SDK surface hasGetAuthCode/FakeAuth/<GameToken>. So it is not a single connection-state boolean flip — even with the state forced "online", the client needs a valid token to build the redirector request. - Conclusion for M2: plan for two gates — (a) the connection-state decision, and (b) supplying an auth token the client accepts.
TODO/CONFIRMthe exact abort point (no-token vs state-says-offline vs both) — needs Ghidra-level control-flow tracing.
Dynamic probe result (read-only) — GoOnline is NOT the gate
A read-only detour on anadius's GoOnline handler (anadius64.dll+0x2BB90)
shows the game does call GoOnline during the "connecting" attempt (incl.
on the Ultimate Team click) and anadius returns success — yet no Blaze
connection follows (still zero external CONNECT/DNS).
Therefore the gate is downstream of GoOnline: the game decides to go
online and the request is accepted, then it aborts at the auth-token step
(GetAuthCode) and/or while waiting for the "online established" status
callback (ONLINE_STATUS_EVENT), and times out into offline.
This sharpens the two-gate picture: GoOnline passes; the real blocker is the
token / online-status step. TODO/CONFIRM which (token-missing vs
status-never-fires) — via a GetAuthCode probe and/or Ghidra.
Recommendation / next
Name the downstream gate. Two complementary routes:
- Dynamic: probe anadius's
GetAuthCodehandler (does it return a token or fail?) — distinguishes token-gate from status-callback. - Static (Ghidra): xref the
GoOnline/GetAuthCode/ONLINE_STATUS_EVENTstrings inFIFA23.exeto find the FUT online-flow code that issuesGoOnlinethen waits for the token/status, and decompile it. FIFA isn't ASLR'd, so Ghidra addresses (image base0x140000000) map 1:1 to our recorded offsets.
M1 COMPLETE — the gate is GetInternetConnectedState
Found without Ghidra, by reading anadius's LSX command-registration function
(anadius64.dll+0x14C0), which lists every command-name → handler inline. NB the
lea rax,[handler] is offset by one from the lea rdx,[name] in that listing
(verified empirically: +0x27060 decompiles to GetProfile — it builds a
GetProfileResponse for persona "fun"). Corrected handler map:
| anadius LSX command | handler (anadius64.dll + …) |
|---|---|
| GetProfile | 0x27060 |
| GetInternetConnectedState | 0x27790 |
| GoOnline | 0x2BB90 |
| GetAuthCode | 0x2BBC0 |
The gate, named: GetInternetConnectedState @ anadius64.dll+0x27790
It serializes an LSX InternetConnectedState response with a connected
attribute whose value is:
connected = (byte[+0xCAB1B] != 0 || byte[+0xCAB1A] != 0) ? str(+0xAF530)
: str(+0xADE64)
Both flag bytes default to 0, so it reports the offline value (+0xADE64).
That is precisely why the client sits at "connecting to the EA Servers" and falls
back offline.
Answers to the three M1 points
- ProtoSSLConnect reached on the abort? NO — gate upstream. Confirmed.
- The connection-state function:
GetInternetConnectedState@anadius64.dll+0x27790. Decision = the two-flag branch above. - Gate count: ONE actionable gate. The auth token is already satisfied —
GoOnline(+0x2BB90) is called during the attempt and returns success, and anadius provides a fake auth code; the blocker is purely the connection-state. So M2 = makeGetInternetConnectedStatereport connected (then the game proceeds with its existing token).
M2 attempt results — the gate is an async EVENT, not a poll
Tried (read/write, EAAC neutralized):
- Forced
GetInternetConnectedState→ connected (set flags +0xCAB1A/+0xCAB1B → value"1"; strings confirmed: +0xADE64 ="0"offline, +0xAF530 ="1"connected). - Flipped
GoOnlineto report"1"(replicated its builder+0x25BE0with the connected string instead of"0").
Neither made the game proceed. Both handlers fire, no crash — but the game
keeps retrying GoOnline every ~7s and never attempts the Blaze connect.
That retry-on-timeout pattern means the FUT-online flow is event-driven: the
game submits GoOnline, gets success, then waits for an async "online
established" event (ONLINE_STATUS_EVENT-class) that anadius — being offline-only
— never pushes (the only <Event sender="EALS"> it ever sends is the Challenge
handshake). So flipping poll/return values can't unblock it.
Implication: getting past "connecting" requires emulating the EA-app online event sequence the game waits for (inject the online-status event over LSX, in the format/order EbisuSDK expects), not a single function flip. This is a substantially deeper task (and precedes the Blaze backend emulation).
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.
Path A attempt: reach the FIFA-side online-flow (blocked with live toolkit)
Goal: find EbisuSDK::GoOnline in FIFA23.exe → callers → the game's online-flow
→ read what event it waits on. Every angle our live-memory toolkit offers is
blocked:
- String xref: FIFA23.exe contains no
"GoOnline"string (typed SDK call, not a string-built command). - Call-stack from the handler: GoOnline runs on an anadius worker thread; a bounded stack scan finds zero FIFA frames.
- Detour scan (
jmpscan): scanning FIFA23.exe for function-entryE9jumps leaving the module yields ~3875 hits — overwhelmingly false positives, because the 505 MB image is mostly embedded data (not code), and the real detours don't cleanly cluster. (A .text-section-only scan would help but the chain after — isolate GoOnline → callers → event format → emulate — remains long and each link is gated by SDK abstraction / anadius indirection / worker threads.)
Verdict: crossing this gate to playable FUT is research-grade. It needs an interactive disassembler (IDA/Ghidra GUI, human-driven) to trace the EbisuSDK online-flow, and then a full EA-online + Blaze emulator. The live-memory toolkit (string/xref/disasm/callers/read/jmpscan) has been exhausted for the FIFA side. The clean-room spec (this document) is the finished, valuable artifact.
- Check whether the flags at
+0xCAB1A/+0xCAB1Bare settable via anadius config / a hidden option (cheapest flip). - Else out-detour
GetInternetConnectedStatein ourversion.dllto force the+0xAF530("connected") path. - Then watch for the client to attempt the real Blaze connect (our
connectredirect + ProtoSSL hook capture the first plaintext — milestone M3). TODO/CONFIRM: exact text of the offline/connected value strings (+0xADE64/+0xAF530); whether any secondary check gates the attempt after the state flips.
LSX delivery channel — 2026-06-30
Question: does FIFA's port-3216 connection reach the bridge LSX server, or is it serviced inside anadius before the TCP stack (making the bridge irrelevant)?
Evidence from openfut_hook.log (6 consecutive sessions, no bridge running)
connect_hook: call 1.0.0.127:3216 sock_type=1
connect_hook: result=0 wsa_err=0
1.0.0.127:3216— FIFA's EbisuSDK connects to127.0.0.1:3216(address in the log is little-endian u32; port is big-endian u16 → both decode to127.0.0.1:3216).sock_type=1—SOCK_STREAM(TCP), not a pipe or named-object.result=0 wsa_err=0— the OSconnect()syscall returned success.- This line appears exactly once per game session, at early startup (DLL load phase),
before the game reaches any menu. No other external
connect()calls are logged.
Implication: anadius is NOT in-process-only for LSX
Our hook is an inline detour on the real ws2_32.dll!connect function (not an IAT
patch). It fires regardless of who calls connect(). Because it fires for the 3216
call and result=0, the connection goes through the real TCP stack — it is NOT
intercepted inside anadius before the socket layer.
In the sessions above, anadius was the answering server (bridge not running); it binds
127.0.0.1:3216 as a real Wine/Proton TCP socket. Anadius is a true TCP server on
port 3216, not an in-process detour.
Bridge delivery test (native Linux process)
Bridge binary confirmed bindable: ./target/release/openfut-bridge starts and logs
LSX server listening on 127.0.0.1:3216 immediately. Port 3216 is free before FIFA
launches (anadius only binds it when FIFA/anadius64.dll loads).
Pending live test: start the bridge (native Linux process → real Linux socket on
3216), THEN launch FIFA. When EbisuSDK calls connect(127.0.0.1:3216):
- If bridge receives it → bridge logs
DEBUG openfut_bridge::lsx: LSX: connection from 127.0.0.1:xxxxx - If anadius somehow wins (or game crashes because anadius can't bind) → bridge logs nothing
Run once to confirm:
cd /home/alex/Documents/OpenFUT/openfut-bridge
RUST_LOG=openfut_bridge=debug ./target/release/openfut-bridge 2>&1 | tee /tmp/bridge-lsx-test.log &
# then launch FIFA normally, reach any menu, check log:
grep "LSX:" /tmp/bridge-lsx-test.log
Pre-test concern: LSX challenge-response AES key
The bridge lsx.rs uses a placeholder AES_KEY = [0,1,2,...,15]. The real key was
RE'd from anadius but not yet substituted. If FIFA validates the ChallengeAccepted
response the bridge sends, the handshake fails and FIFA may not reach the main menu.
This does not affect the delivery classification — LSX: connection from ... fires
at TCP accept(), before any protocol. But it may cause the game to stall/crash after
the first connection, which would be a separate issue to fix.
Classification — CONFIRMED 2026-07-02 (supersedes the 2026-06-30 prediction)
CONFIRMED NEGATIVE — a host-side bridge on 3216 does NOT receive FIFA's LSX traffic. Cause: anadius handles LSX in-process, inside FIFA23.exe, before the connect reaches the host TCP stack. This is NOT a network/loopback problem — the network namespace is shared and the bridge is reachable.
The 2026-06-30 prediction that this subsection replaced was wrong on both counts
(it guessed "anadius intercepts in-process" = RULED OUT and "bridge receives LSX" = EXPECTED); the live test showed the exact opposite.
Evidence (live test, bridge holding host 127.0.0.1:3216 throughout):
- Namespace is shared, not isolated.
readlink /proc/<FIFA23.exe>/ns/netequals the host's/proc/self/ns/net(bothnet:[4026531833]). Pressure-vessel /umu does not unshare the network — Wine's127.0.0.1is the host's. - The bridge is reachable and logs correctly. A host-side
connect(127.0.0.1:3216)was accepted, loggedDEBUG openfut_bridge::lsx: LSX: connection from 127.0.0.1:34730, and returned the 157-byte LSX greeting. So "bridge logged nothing during FIFA" is a real absence, not a logging/filter artefact. - FIFA's connect succeeds but never reaches the bridge.
openfut_hook.logshows 8×connect_hook: call 1.0.0.127:3216 ... result=0 wsa_err=0(real, successful connects) while the bridge log stayed at its 8-line startup baseline (zeroLSX: connection from). FIFA then advanced to "connecting to FUT" — anadius satisfied LSX in-process; the hook log stopped growing once it did. - Our hook does not fake the 3216 result — it passes it through. In
openfut-launcher/openfut-hook/src/connect_hook.rs,redirect_if_earewrites only 443/10041/42127; port 3216 falls through to the realws2_32!connectand its genuine return is logged. The comment there ("3216 ... handled by the native openfut-bridge LSX server — just let connect() go through") encodes the disproven premise: "going through" reaches anadius in-process, not the host bridge.
Conclusion: the "native Linux bridge binds 3216 first and wins the race" strategy cannot work as designed — there is no host-stack connect to win; anadius answers inside the process. A host-side LSX server is architecturally off the path.
OVERTURNED 2026-07-02 — see the next section. This conclusion was correct only for port 3216. anadius keys its in-process interception on port 3216 specifically; a hook-side redirect of FIFA's connect to a different host port (3217) slips past it and lands on the host bridge over the shared loopback. A host-side LSX server does work once the redirect is in place. The "fix direction" below is now done.
Consequences / open items:
TODO/CONFIRM: the exact interception layer anadius uses (IAT hook on FIFA'sconnectimport / Winsock LSP / inline hook on a lower function) is undetermined. Our inline hook onws2_32!connectdoes fire, so FIFA reachesws2_32!connect; yet the real call returns 0 without touching the host socket — naming the layer that absorbs it requires inspecting anadius.- The placeholder
AES_KEYwas never exercised — delivery fails upstream of any handshake, so the challenge-response never ran. The earlier "supply the real AES key" follow-up is moot unless/until FIFA is actually routed to our LSX server. - Fix direction (not yet implemented): the viable lever is our own hook, not
anadius (which is closed, load-bearing, and binds no host port to free). Candidate:
have the hook redirect the 3216 connect to a distinct host port the bridge listens
on, to slip past anadius's port-specific in-process interception — routing LSX to
our bridge on the shared host stack (this revives the real-AES-key requirement
above). Scoped to
openfut-hook, not this repo.
LSX SOLVED via hook redirect + session-seed fix — 2026-07-02
RESULT: FIFA now completes the LSX handshake AND a sustained encrypted session against our own bridge, live. The "fix direction" from the previous section was implemented and works. This is the first EA-side gate serviced end-to-end by our code.
1. Hook redirect gets FIFA onto the bridge (delivery achieved)
Added one arm to redirect_if_ea in openfut-launcher/openfut-hook/src/connect_hook.rs:
FIFA's connect to 127.0.0.1:3216 is rewritten to 127.0.0.1:3217, where the bridge
runs its LSX server (LSX_ADDR=127.0.0.1:3217). anadius's in-process interception is
keyed on port 3216 specifically, so the redirected connect slips past it and lands
on the host bridge over the shared loopback.
- Hook log:
connect_hook: 1.0.0.127:3216 → 127.0.0.1:3217thenredirect result=0. - Bridge log:
LSX: connection from 127.0.0.1:…— FIFA reached the bridge. Overturns the "off the path" conclusion above.
2. AES key was never a placeholder — it is 000102…0f
The AES_KEY = [0,1,2,…,15] in src/lsx.rs (and the hook's lsx.rs) is the real,
correct key, confirmed two ways:
- Oracle: a captured FIFA
ChallengeResponsegives a known plaintext→ciphertext pair under the real key — our greetingcacf897a20b6d612ad0c05e011df52bb(ASCII) → FIFA's5c52e8ca….AES-ECB-PKCS7([0..15], greeting)reproduces it byte-for-byte. - Static: that same 16-byte value is embedded literally in
anadius64.dllat offsets0xa1710,0xa1890,0xa3c0f.
The earlier "supply the real AES key" TODO is therefore closed — no key extraction
was needed. The handshake auth was always correct; FIFA accepted our ChallengeAccepted.
3. The real bug: session-seed derivation (now fixed)
After a correct handshake, the first encrypted session message still decrypted to
garbage. Cause: compute_seed() derived the session-key seed by parsing the first
hex pair of our response ("0f" → 0x0F), when the client uses the raw ASCII byte
values of the first two characters ('0','f' → 0x30,0x66).
Confirmed by capturing the raw session ciphertext (bridge patched to log wire bytes)
and brute-forcing all 65,536 u16 seeds offline through get_lsx_key(seed):
- Exactly one seed,
0x3066(12390), decrypted the message to valid XML:<LSX><Request recipient="EbisuSDK" id="…"><GetConfig version="3"/></Request></LSX>. 0x3066= ASCII'0','f'= the first two characters of our response hex0f73…, not the parsed value0x0F73(3955) we were using.
Fix in src/lsx.rs compute_seed(): take hex.as_bytes()[0..2] directly instead of
u8::from_str_radix on the hex pairs. (Capture preserved at
captures/lsx-handshake-capture-2026-07-02.log; brute-forcer in scratchpad.)
4. Live confirmation
With the redirect deployed and the seed-fixed bridge on 3217, FIFA relaunched and:
- Held a single sustained LSX connection (
ssshowsESTAB FIFA23.exe ↔ openfut-bridge:3217) instead of the previous connect-once-then-crash. - Drove 20 real EbisuSDK requests in one conversation:
GetConfig,GetProfile(×5),GetSetting,GetGameInfo(×8),SetPresence,GetInternetConnectedState. - Did not crash at the LSX phase.
5. Remaining gap (next step, not a blocker to this result)
GetGameInfo GameInfoId=… had no handler in the bridge dispatcher → fell to the
generic <Ok/> and logged unknown request type. FIFA tolerated it (session stayed
up), but real GetGameInfo responses may be needed to progress toward the next gate
(Blaze/online).
Update 2026-07-02 (later): a get_game_info handler was added to dispatch() in
src/lsx.rs. It routes GetGameInfo GameInfoId=…, logs the requested GameInfoId
(so the next live run reveals which keys FIFA asks for — we still have no captured real
response to model), and replies with a well-formed <GetGameInfoResponse GameInfoId="…" Value="" /> echo. A dispatcher routing test (dispatch_routes_get_game_info_and_echoes_id)
pins the fragile positional split('"') parsing. TODO/CONFIRM remains: the real
response schema and whether FIFA needs non-empty values — resolve by launching FIFA
with this build, reading the logged GameInfoIds, and observing how far FIFA advances.
Provenance note
Clean-room intact: the AES key was recovered by validating candidate byte windows from
the shipping anadius64.dll against the client's own observed output, and the seed rule
was recovered by brute-forcing against a live capture — nothing derived from EA source.
LSX fully satisfied — FIFA reaches the Blaze/online gate — 2026-07-02 (later)
RESULT (unconfirmed — read the caveat): FIFA reached the "connecting to EA servers"
screen alive, but the launch that did so used a CACHED EA session and did NOT re-hit our
bridge (no new 3216 connect, empty bridge log). The last launch that actually ran a
fresh LSX session through our bridge still CRASHED. So it is NOT yet established that the
fixed bridge carries a fresh FIFA launch past the crash — the crash-clear is confounded
by session caching. The fixes below are believed-correct and unit-tested, but need the
clean re-test in the "Confirmation caveat" section before LSX can be called solved.
Earlier in the day FIFA crashed ~15–17s into LSX, "right before the loading screen."
Root-causing that crash took several live runs and disproved two wrong theories.
The real root cause: dropped pipelined LSX messages (session-loop framing bug)
FIFA batches multiple LSX messages into a single TCP segment — e.g. SetPresence
immediately followed by a GetGameInfo GameInfoId="LANGUAGES" query, two
null-terminated ciphertext-hex blocks in one read. The old session loop decrypted the
whole read as one blob and dispatched only the first message, silently dropping the
rest. The dropped request left FIFA waiting on a response that never came; it timed out
after ~15s (the mysterious silent gap in every crash log) and then crashed.
Fix: src/lsx.rs process_frame() splits each read on the null terminators and
answers every sub-message, returning the concatenated null-terminated responses.
Regression test: process_frame_answers_every_pipelined_message. After the fix, FIFA
flowed through the entire LSX sequence in ~1.3s instead of hanging 15s.
Two theories the live runs DISPROVED first (recorded so we don't repeat them)
- "Empty/incorrect INSTALLED_LANGUAGE value crashes it." Changing the value
(
""→en_US) changed nothing — same 3× re-query, same crash. Because… - "Wrong attribute name only." Switching
Value=""→InstalledLanguage="en_US"also changed nothing on its own — because the crash-relevant request (the pipelinedLANGUAGES) was never being dispatched at all (see root cause). The attribute name did matter, but only became observable once pipelining was fixed: FIFA reads a lowercasevalueattribute (case-sensitive), so the response now emits the value under BOTHvalueand the per-key CamelCase name (InstalledLanguage/Languages).
anadius.cfg is the response oracle
/mnt/games/FIFA 23/anadius.cfg is the config anadius's in-process LSX emu reads to
build its responses — the clean, correct values to mirror (same provenance basis as the
AES-key recovery; it is anadius's own config, not EA source):
| Field | Value |
|---|---|
Language (INSTALLED_LANGUAGE) |
en_US |
Languages (LANGUAGES) |
pt_PT,tr_TR,ko_KR,cs_CZ,zh_CN,zh_HK,da_DK,no_NO,sv_SE,en_US,pt_BR,de_DE,es_ES,fr_FR,it_IT,ja_JP,es_MX,nl_NL,pl_PL,ru_RU,ar_SA |
PersonaId |
1144668899 |
UserId |
1000200030000 |
Username (Persona) |
fun |
src/lsx.rs now returns these real values (GetProfile IDs, GetGameInfo language keys)
instead of fabricated ones; fabricated persona/user IDs flow into FIFA's online state
and are a likely downstream break. Also added a real IsProgressiveInstallationAvailable
(Available="false", attribute recovered from anadius64.dll).
Confirmation caveat + how to verify cleanly
The launch that first reached the online gate did not re-hit our bridge — FIFA
presented a cached EA session (no new 3216 connect; empty bridge log) and jumped
straight to "connecting to EA servers." So that specific run didn't exercise the final
value-attribute change. To confirm on a clean boot, clear FIFA's cached EA/Origin
session token in the Wine prefix so it re-runs LSX through the bridge, and verify: one
sustained LSX session, INSTALLED_LANGUAGE queried once (not 3×), no crash, and
FIFA arriving at "connecting to EA servers".
GetGameInfo response shape (RE'd from the anadius64.dll builder, 2026-07-02)
Populating GetAllGameInfo did not fix the crash — FIFA still ran the full LSX
session (~2.6s, 17 dispatches) then crashed at the same point, ending with the 3×
INSTALLED_LANGUAGE re-query. So GetAllGameInfo was never the trigger.
Disassembling the individual GetGameInfo response builder in anadius64.dll
(around 0x18002a076) settled the actual wire shape. It emits, in order:
< + GetGameInfoResponse + + GameInfo + =" + + " + />. The
attribute name is the fixed 8-byte literal GameInfo (string constant at
.rdata VA 0x1800af818), independent of the requested GameInfoId — the value
is looked up per key but the attribute is always GameInfo. So the correct reply to
GetGameInfo GameInfoId=INSTALLED_LANGUAGE is:
<GetGameInfoResponse GameInfo="en_US" />
Our bridge had been sending the key as the attribute name
(<GetGameInfoResponse InstalledLanguage="en_US" />), an attribute FIFA never reads.
That is why FIFA always saw a null installed-language, re-queried INSTALLED_LANGUAGE
three times, and crashed — and why every prior value change (empty → en_US,
per-key naming) had no effect: FIFA wasn't reading our attribute at all. src/lsx.rs
get_game_info() now emits the fixed GameInfo="<value>" attribute.
ROOT CAUSE of the loading-screen crash: GetConfig missing PROGRESSIVE_INSTALLATION (2026-07-02)
FIFA's own crash reports (FIFA 23/Logs/CrashReport_*.json) pinned it. Every run
through our bridge — all 11 today, both the pre-pipelining (Up Seconds: 17) and
post-pipelining (Up Seconds: 2) runs — crashed at the identical address:
EXCEPTION_ACCESS_VIOLATION, reading 0x18
RIP = FIFA23.exe+0x133dfc5 → mov rax, QWORD PTR [r13+0x18] with r13 = NULL
Is Online: False, Blaze State: {} (crash is pre-Blaze, ~2s after LSX)
So none of the earlier LSX-response fixes (pipelining, account IDs, GetGameInfo attribute, GetAllGameInfo population) ever touched the crash — it was invariant, i.e. caused by a response that had been wrong the whole time. Disassembling the faulting function showed r13 is its 3rd argument (a Frostbite ref-counted service handle) — a null handle deref, i.e. a failed service lookup.
That pointed straight at GetConfig (request #1, the fabricated service list, never
verified). Extracting anadius's real service-registration table from anadius64.dll
(the lea rdx=Name / lea r8=Facility sequence at 0x180026d40) and diffing it
against our get_config() showed our list was correct on all 32 entries except it
omitted two services:
Facility="PROGRESSIVE_INSTALLATION" Name="PI"
Facility="PROGRESSIVE_INSTALLATION_EVENT" Name="PI"
FIFA queries IsProgressiveInstallationAvailable in every LSX session; it builds its
service registry from GetConfig and resolves the PROGRESSIVE_INSTALLATION handle
from it during online-init. With the service absent, the handle was null → the
0x133dfc5 deref. Fix: get_config() now declares both services, and
is_progressive_installation_available() responds from sender="PI" (the service's
registry Name). This is the first change that targets the actual crash address, so
it is the real test of the loading-screen gate.
Next gate: Blaze/online (M2)
"connecting to EA servers" is the Blaze layer. The hook log shows FIFA has made no Blaze connect yet (nothing on 443/10041/42127) while spinning, so it is stalled at pre-auth. Per the architecture, Blaze rides ProtoSSL in-process (a different transport than LSX), so serving it is separate, larger work — the M2 effort, now actually reached.