Files
OpenFUT/fifa17-recon/docs/plan-2026-08-07-sbc-predicate.md
T
2026-08-07 11:44:05 -07:00

13 KiB

SBC "problem communicating with the FIFA Ultimate Team servers" — definitive analysis

Date: 2026-08-07 Binary under study: /tmp/fut/cardsdll.dll (on-disk PE, image base 0x180000000; copy of /mnt/games/FIFA 17/CardsDLL_Win64_retail.dll) Method: clean-room, read-only. On-disk objdump re-verified in this pass; live values quoted from prior read-only /proc/<pid>/mem reads (pid 12201, slide 0x6ffe7c140000, FNV control MATCH). No memory was written; FIFA was not touched.


VERDICT (one line)

The SBC modal is a CLIENT-SIDE, per-feature completion-path defect — the FUT client never re-arms a fetch/re-render for sbs/sets the way it does for the hub — so NO server response can cure it; the only offline lever is a client-memory patch, and the clean single-byte patch (model+0x1fa00 = 1) only SUPPRESSES the modal by forcing the completion predicate true, rendering from an empty, never-populated cache. It is NOT the go-online wall.


1. What the SBC completion predicate actually checks (CONFIRMED on-disk)

The SBC menu entry runs a completion continuation whose gate is the shared predicate 0x180065d40, called as [cache_vtable+0x08]. Re-disassembled this pass, byte-for-byte:

180065d40  call 0x1801642c0        ; online/liveness sub-check
180065d4e  test al,al
180065d50  je   fail
180065d52  cmp  byte [rbx+0x28],0   ; <-- THE GATE: "value ready" flag
180065d56  je   fail
180065d58  cmp  qword [rbx+0x8],0   ; pending-op ptr
180065d5d  je   pass (mov al,1)     ; empty-collection shortcut -> success
180065d5f  lea  rcx,[rsp+0x38]
180065d64  call QueryPerformanceCounter  ; [rip]->0x1801e50c0
180065d6a  mov  rax,[rbx+0x20]      ; QPC deadline
180065d6e  sub  rax,[rsp+0x38]
180065d73  js   fail                ; deadline passed -> fail
180065d75  mov  al,1                ; pass
...
180065d7d  xor  al,al               ; fail

Reduces to: subcheck() && byte[cache+0x28]!=0 && (qword[cache+0x08]==0 || deadline[cache+0x20] not yet past).

  • The online/liveness sub-check 0x1801642c0 is stubbed OUT. On-disk bytes are b0 01 c3 = mov al,1; ret — always true, in the shipped file (not a live loader patch). This is the reason SBC is NOT the go-online wall (see §5).
  • cache (rbx) is an embedded sub-object of the FUT root singleton A = *[0x1802e6398], selected by a vtable thunk (see §2). Its +0x28 byte is a "value-ready" flag (init 0 by ctor 0x180062460); +0x08 is a pending-op pointer; +0x20 is a QPC deadline. This is a copyable future/async-result value type. The predicate never reads the parsed SBC categories, HTTP status, session, or any live-connection boolean.

AUTHORITY BOUNDARY: everything the predicate reads lives inside client process memory (A+…). Nothing in the sbs/sets HTTP response is an input to it. This is a client-authority decision end to end.


2. Why hub passes but sbs/sets fails (CORRECTED after adversarial verification)

Both features run the same predicate function 0x180065d40, but on different embedded caches, reached through different per-response-class continuations. That structural divergence is real and confirmed. The originally-stated reason ("hub passes because its cache +0x28 is set") is WRONG and is corrected here — corroborated by a live measurement (HUB cache +0x28 = 0 while the hub is displayed with no modal) and by the on-disk FALSE-branch disassembly gathered this pass.

The two continuations, side by side (on-disk, this pass)

SBC (FutLoadSetTypesServerResponse) HUB (FutGetHubDataServerResponse)
continuation 0x180154860 0x180173770
get singleton A call 0x18011a830 (mov rax,[0x1802e6398]) same
select cache call [rdx+0x4e8] → thunk 0x18011c1f0 = lea rax,[rcx+0x1f9d8]SBC cache A+0x1f9d8 call [rdx+0x1f8] → thunk 0x18011a810 = lea rax,[rcx+0x1fd70]HUB cache A+0x1fd70
predicate call [rdx+0x08] = 0x180065d40 same 0x180065d40
on TRUE (jne) render 0x18015491a → 0x180154600 render 0x18017383d → 0x1801735e0
on FALSE lea rdx,[rbp-0x9] (descriptor 0x18020a8b8); call 0x18016c330; jmp return call 0x1801213b0 (state reset); lea 0x1801736f0 (continuation fn); call 0x18011f8e0 (register completion closure); lea 0x18022cd30 (descriptor); call 0x18016c330; call 0x18011f900 (cleanup)

What this proves

  1. 0x18016c330 is NOT an SBC-only "modal" function. The HUB continuation calls the very same 0x18016c330 (at 0x18017382c) on its own not-ready branch. It is a shared, descriptor-parameterized async dispatcher; SBC passes descriptor 0x18020a8b8, hub passes 0x18022cd30.

  2. At idle both predicates return FALSE. Live: HUB cache A+0x1fd70+0x28 = 0 and SBC cache A+0x1f9d8+0x28 = 0, both +0x08 = 0. The hub is on screen with no modal while its own predicate would return FALSE. So "hub +0x28 is set" is false; a set flag is not what makes the hub pass.

  3. The real asymmetry is the FALSE-branch work. On not-ready the HUB continuation resets its request-state region (0x1801213b0), registers a completion closure (0x18011f8e0, continuation 0x1801736f0) so the arriving response re-runs the continuation and re-renders, then cleans up (0x18011f900). It is a proper get-or-fetch: cache-miss → (re)issue request → render on completion. The SBC continuation does NONE of that — it fires the dispatcher once with delegate 0x180154590/descriptor 0x18020a8b8 and returns. It never re-arms a fetch and never wires the sbs/sets response back into a re-render.

Conclusion: hub and SBC diverge at the cache-selection call site ([rdx+0x1f8] vs [rdx+0x4e8], one instruction apart), and — decisively — in the not-ready handling. The modal is produced downstream in the SBC dispatched path (dispatcher 0x18016c330 + delegate 0x180154590), because the SBC feature is wired as a one-shot with no re-fetch/re-render, whereas the hub is wired as a self-rearming get-or-fetch. It is not decided by cache selection alone, not by the shared predicate, and not by the +0x28 byte value at idle.


3. VERDICT by route — is SBC beatable, and how?

Route Outcome Why
A. Server response field / header / status RULED OUT — no offline fix here No field in the sbs/sets body reaches the predicate (client-authority §1). Deeper: the SBC continuation never registers a completion closure to consume the response and re-render, so even a perfect response is dropped on the floor. The deserializer 0x18017b2b0 returning TRUE is genuinely irrelevant.
B. Client memory byte patch model+0x1fa00 = 1 Suppresses the modal, but empty menu — cosmetic Forces predicate TRUE → routes to the SBC render branch 0x18015491a → 0x180154600, which reads the embedded SBC cache. That cache was never populated (+0x08 == 0, empty collection), so the likely result is an empty / non-functional SBC screen, not populated SBCs. Untested under the read-only rule.
C. Config FUT/SBC_USE_STUBS (rdata 0x1802270f8) Not the gate Read at the deser top only; the normal (off) path already runs. Flipping it does not touch +0x28 or the continuation wiring.
D. "Needs the go-online wall solved" REFUTED The only connection-like sub-check on this path (0x1801642c0) is stubbed to always-true on-disk. SBC is blocked by local per-feature completion wiring, not by the reconnect gate. See §5.
E. Client CODE patch of the SBC FALSE-branch The only route to a functional SBC menu Make 0x180154860's not-ready branch replicate the hub's sequence: state reset 0x1801213b0 + register completion closure 0x18011f8e0/0x1801736f0 + dispatch + cleanup 0x18011f900, so the sbs/sets response is fetched and rendered. This is a code patch, not a byte flip and not a server change. Out of scope for a server-side preservation fix; a client-side authority modification.

Bottom line: there is no server-side fix. SBC is "beatable" only in the client-authority sense — either cosmetically (byte B, hides the modal over an empty menu) or functionally (route E, a code patch replicating the hub's re-arm). Neither is a change our offline server can make.


4. Memory patch details (if used) — flagged CLIENT-SIDE AUTHORITY

CLIENT-SIDE AUTHORITY — this is a modification of the FIFA client's own process memory, not an OpenFUT server response. It changes what the client decides, and it violates the current read-only rule; it is documented for completeness, not endorsed as the fix.

  • Cosmetic modal-suppression (route B):

    • Absolute displacement into FUT root singleton: A + 0x1f9d8 + 0x28 = model + 0x1fa00, where A = *[0x1802e6398].
    • Live absolute (pid 12201 snapshot): 0xb8402538 + 0x28 = 0xb8402560.
    • Value: write 0x01 (one byte).
    • Effect: predicate 0x180065d40 short-circuits at cmp byte[rbx+0x28],0 → with +0x08==0 the empty-collection shortcut returns TRUE → continuation jne 0x18015491a renders. Modal gone; SBC cache empty → expect an empty/possibly-broken menu. Not verified (read-only).
    • Persistence: the object is embedded in the singleton (singleton lifetime). The SBC path calls only [vt+0x08]; nothing on this path calls the invalidator [vt+0x10]=0x180065d20, so a write should persist across menu re-entry (inferred from structure, not demonstrated).
  • Functional fix (route E) requires a .text patch to the SBC continuation, not a data byte — see §3 row E. Do not confuse the two.


5. Relationship to the online-modes / go-online-wall finding

SBC is not the same wall as online Draft's "PRESS Q TO RECONNECT":

  • The single connection-like sub-check reachable from the SBC predicate, 0x1801642c0, is compiled out (mov al,1; ret) in the shipped binary. The SBC gate therefore encodes no unmet network condition — it is a purely local completion-wiring problem.
  • The online modes differ structurally: their gate keeps a real pending network op at +0x08 and/or a non-stubbed sub-check, so their predicate encodes a network state a local byte-flip cannot satisfy. That is why the online wall is not beatable by a byte and SBC's modal is (cosmetically).
  • This is consistent with the prior "refusing modes = no server fix" finding: no field, count, header, or status in any HTTP response flips the client-side completion state for these features. SBC extends that finding with the precise mechanism — the client never re-arms the sbs/sets fetch/re-render at all.

Appendix — confirmed addresses (image base 0x180000000)

Symbol Address Note
FUT root singleton getter 0x18011a830 mov rax,[0x1802e6398]; ret
FUT root singleton ptr [0x1802e6398] live A = 0xb83e2b60
FUT root vtable (static) 0x18021c2a0
SBC cache selector thunk 0x18011c1f0 lea rax,[rcx+0x1f9d8] (slot A.vt+0x4e8)
HUB cache selector thunk 0x18011a810 lea rax,[rcx+0x1fd70] (slot A.vt+0x1f8)
SBC cache A+0x1f9d8 vtable 0x1801fae70; live 0xb8402538
HUB cache A+0x1fd70 vtable 0x18021c1e0
shared predicate isValid 0x180065d40 cache.vt+0x08 for both
stubbed online sub-check 0x1801642c0 b0 01 c3 = mov al,1; ret
cache ctor / copy-ctor 0x180062460 / 0x1800c21f3 init byte[+0x28]=0
invalidator 0x180065d20 cache.vt+0x10; not called on SBC path
SBC continuation 0x180154860 class RS4:FutLoadSetTypesServerResponse (str 0x1802270b8, vt row 0x180227090)
HUB continuation 0x180173770 class RS4:FutGetHubDataServerResponse (str 0x18022ce40, vt row 0x18022ce18)
shared async dispatcher 0x18016c330 called by BOTH FALSE-branches (SBC 0x180154913, HUB 0x18017382c)
SBC delegate / descriptor invoke 0x180154590 / desc 0x18020a8b8
HUB re-arm: state reset 0x1801213b0 HUB-only, 0x1801737bd
HUB re-arm: register closure 0x18011f8e0 (cont. 0x1801736f0) HUB-only, 0x180173815
HUB re-arm: cleanup 0x18011f900 HUB-only, 0x180173836
SBC render branch (on TRUE) 0x18015491a → 0x180154600 reads empty SBC cache
sbs/sets deserializer 0x18017b2b0 returns TRUE unconditionally (mov al,1 @0x18017b751); irrelevant to predicate
QueryPerformanceCounter import 0x1801e50c0

Which prior conclusion won: the structural divergence (same predicate, different cache, different continuation; online sub-check stubbed; not server-fixable) is upheld. The specific pass/fail reason is corrected: it is the FALSE-branch re-arm asymmetry, not a set +0x28 byte and not an SBC-exclusive 0x18016c330.