docs(fifa17): record no-sentinel client resolver proof
Land the client-side empty-My-Packs resolver evidence and the session-stability invariant established by the F3/R1 experiments. - PART III (F3, CONFOUNDED CRASH): a mid-process sentinel -> no-sentinel flip left a stale POSITIVE My-Packs ordinal that still took the resolve branch and crashed at 0x180014882. Preserved verbatim (not a guard failure). - PART IV (R1, SUCCESS): backend set no-sentinel first, then a FRESH FIFA process; genuine purchasegroup response ids [1,5,6,7] is byte-identical to the F3 capture, so client process lifetime is the only changed variable. Store opens on Browse Packs, no crash, no dialog. Guard PROVEN on the tested build. - New INVARIANT: empty-My-Packs capability MUST be session-stable -- the server must not switch a running client between sentinel-present and sentinel-absent for the My Packs group within one FIFA process, because the client caches the group ordinal and a stale positive ordinal still crashes the resolver. - Both no-sentinel captures kept: client_guard (F3) and freshretest (R1). Backend P2 active-sentinel (65534) remains production default; no capability handshake is implemented yet.
This commit is contained in:
@@ -411,3 +411,33 @@ Scaleform/movie-driven). Recommend: adopt the active placeholder as an optional
|
||||
backend compatibility mode (safe, strictly better than baseline) AND pursue a
|
||||
client-side fix (hide the fake tile / stop the forced My-Packs resolution) for the
|
||||
fully clean result. **Not implemented.** Do NOT grant a real pack.
|
||||
|
||||
---
|
||||
|
||||
## Client resolver-guard experiment (2026-08-13) — RESULT F3 (crash, confounded)
|
||||
|
||||
A client-side `autopatch.py` memory guard (CardsDLL `0x180014858` `JNZ`→`JG`, routing
|
||||
category `<0` to list-all/Browse) was tested against the exact no-sentinel server condition
|
||||
(sentinel 65534 suppressed; `GET /store/purchasegroup` ids `[1,5,6,7]`, no mypacks group).
|
||||
The client **crashed at the identical resolver site `0x180014882`** (`[NULL+0x48]`), because
|
||||
it presented a **positive** My-Packs ordinal (crash is in the `>0` resolve branch), not the
|
||||
`-1` the guard diverts. **Confound:** FIFA was not relaunched after the backend flip, so it
|
||||
reused stale (sentinel-present) tab state. So the negative-only guard is **insufficient for a
|
||||
positive stale/invalid ordinal**, and the fresh-client case is **not yet decided** (needs a
|
||||
clean re-test: fresh launch with backend already no-sentinel). Backend P2 sentinel was
|
||||
restored immediately (mandatory rollback). Full record + candidate stronger guard:
|
||||
`docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md` PART III.
|
||||
|
||||
## Fresh-process no-sentinel retest (2026-08-13) — RESULT R1 (SUCCESS)
|
||||
|
||||
Re-ran the above cleanly: backend entered no-sentinel mode **while FIFA was closed**, then a
|
||||
**fresh** FIFA (pid 553220, new autopatch 552999, guard `85 ff 7f 0f` enforced) launched and
|
||||
opened the Store. The genuine no-sentinel `/store/purchasegroup` (ids `[1,5,6,7]`, no 65534/
|
||||
mypacks) is **byte-identical** to the F3 capture, so the only changed variable is client
|
||||
process lifetime. Outcome: **no crash, no dialog, Store opens on Browse Packs, packs
|
||||
navigable** (cosmetics only: no tabs / no cover art / "0 items" — pre-existing). A fresh
|
||||
client publishes category `-1` for the absent group, which `JNZ→JG` routes to Browse/list-all
|
||||
with no NULL deref. **This confirms F3 was stale-positive-ordinal contamination, and proves
|
||||
Strategy A (resolver guard) on the tested build.** Backend P2 sentinel restored immediately
|
||||
(`f416e71e…`, `state=active`) and remains production default. Full record:
|
||||
`docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md` PART IV.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"purchase": [{"assetId": 1, "id": 1, "packType": "BRONZE", "description": "Bronze Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 1, "currencies": [{"name": "coins", "funds": 400, "finalFunds": 400}], "extPrice": {"finalPrice": {"amount": 4, "currency": "mtx"}, "originalPrice": {"amount": 4, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 5, "silverQuantity": 0, "goldQuantity": 0, "rareQuantity": 0, "itemQuantity": 5}, "unopened": false, "displayGroup": {"value": "bronze"}}, {"assetId": 5, "id": 5, "packType": "GOLD", "description": "Gold Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 2, "currencies": [{"name": "coins", "funds": 5000, "finalFunds": 5000}], "extPrice": {"finalPrice": {"amount": 50, "currency": "mtx"}, "originalPrice": {"amount": 50, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 7, "rareQuantity": 7, "itemQuantity": 7}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 6, "id": 6, "packType": "GOLD", "description": "Premium Gold", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 3, "currencies": [{"name": "coins", "funds": 15000, "finalFunds": 15000}], "extPrice": {"finalPrice": {"amount": 150, "currency": "mtx"}, "originalPrice": {"amount": 150, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 7, "id": 7, "packType": "GOLD", "description": "Special Players Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 4, "currencies": [{"name": "coins", "funds": 25000, "finalFunds": 25000}], "extPrice": {"finalPrice": {"amount": 250, "currency": "mtx"}, "originalPrice": {"amount": 250, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "special"}}], "timestamp": 1596326400}
|
||||
@@ -0,0 +1 @@
|
||||
{"purchase": [{"assetId": 1, "id": 1, "packType": "BRONZE", "description": "Bronze Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 1, "currencies": [{"name": "coins", "funds": 400, "finalFunds": 400}], "extPrice": {"finalPrice": {"amount": 4, "currency": "mtx"}, "originalPrice": {"amount": 4, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 5, "silverQuantity": 0, "goldQuantity": 0, "rareQuantity": 0, "itemQuantity": 5}, "unopened": false, "displayGroup": {"value": "bronze"}}, {"assetId": 5, "id": 5, "packType": "GOLD", "description": "Gold Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 2, "currencies": [{"name": "coins", "funds": 5000, "finalFunds": 5000}], "extPrice": {"finalPrice": {"amount": 50, "currency": "mtx"}, "originalPrice": {"amount": 50, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 7, "rareQuantity": 7, "itemQuantity": 7}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 6, "id": 6, "packType": "GOLD", "description": "Premium Gold", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 3, "currencies": [{"name": "coins", "funds": 15000, "finalFunds": 15000}], "extPrice": {"finalPrice": {"amount": 150, "currency": "mtx"}, "originalPrice": {"amount": 150, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 7, "id": 7, "packType": "GOLD", "description": "Special Players Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 4, "currencies": [{"name": "coins", "funds": 25000, "finalFunds": 25000}], "extPrice": {"finalPrice": {"amount": 250, "currency": "mtx"}, "originalPrice": {"amount": 250, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "special"}}], "timestamp": 1596326400}
|
||||
@@ -380,3 +380,144 @@ active placeholder.
|
||||
configuration (to be observed in the experiment); whether any non-store path also
|
||||
drives `screen+0x290` to a My-Packs ordinal (none found; `FUN_18007e7f0` case `0x7551`
|
||||
and the ctor are the only writers).
|
||||
|
||||
---
|
||||
|
||||
# PART III — Final no-sentinel resolver experiment (2026-08-13) — RESULT F3 (CRASH), CONFOUNDED
|
||||
|
||||
Vehicle change: the resolver guard was implemented as an **`autopatch.py` memory patch**
|
||||
(the live FIFA-17 client-patch mechanism), NOT the `version.dll` proxy — Proton loads its
|
||||
builtin `version.dll`, so the earlier `store_hook`/`version.dll` prototype was inert and
|
||||
has been rolled back. Guard: at CardsDLL `0x180014858`, `JNZ 0x14869` (`75 0f`) →
|
||||
`JG 0x14869` (`7f 0f`), orig-verified; routes category `< 0` (and `== 0`) to the safe
|
||||
list-all/Browse path (`FUN_180014610`), category `> 0` to the existing resolver.
|
||||
`TEST EDI,EDI` at `0x180014856` is the flag source (OF cleared ⇒ `JG` = signed `> 0`).
|
||||
|
||||
## Setup (verified)
|
||||
- CLIENT: guard active/enforced — live bytes `85 ff 7f 0f` at `0x180014856` (FIFA pid 547843,
|
||||
autopatch pid 547621; log `ENFORCED guarded store patch @ … (JNZ->JG)`, orig `75 0f` matched).
|
||||
- BACKEND: sentinel 65534 suppressed by a one-line `if not owned_ids:` → `if False:` in the
|
||||
container copy only (committed source `f42279f` untouched; backup `/tmp/utas_server.EXP_ORIG.py`).
|
||||
Genuine `GET /store/purchasegroup` (02:44:39Z) → ids `[1,5,6,7]`, **no 65534, no mypacks group**,
|
||||
normal packs unchanged (evidence: `docs/evidence/store_purchasegroup_capture_client_guard_no_sentinel_2026-08-13.json`).
|
||||
- PROFILE: `unopenedPackIds=[]`, coins 29,876,776, sha `39bb3e83…` — unchanged throughout.
|
||||
|
||||
## Result — F3 (CRASH)
|
||||
Minidump `CrashDump_2026.08.12_20.44.40.302.dmp` (preserved `/tmp/expF_crash.dmp`, sha `4dcb0cb7…`):
|
||||
`0xC0000005` READ of VA `0x48` at `ExceptionAddress 0x6ffffc224882` → **RE `0x180014882`** —
|
||||
the **identical** resolver crash instruction as Experiment B (`FUN_1800147f0`,
|
||||
`MOV R8,[RDX+0x8]` with the group ptr NULL).
|
||||
|
||||
**Mechanism (decisive):** `0x14882` lives in the *resolve* branch, which the guard's `JG`
|
||||
reaches **only when category `> 0`**. Since the guard was verified in place, the client
|
||||
presented a **positive** My-Packs ordinal that no longer resolves (no mypacks group) →
|
||||
`FUN_180014420` returned NULL → crash. The guard's design assumption — *absent mypacks ⇒
|
||||
category `-1`* — did NOT hold on this path.
|
||||
|
||||
## Confound (uncontrolled variable)
|
||||
FIFA was **not relaunched** after the backend flipped to no-sentinel; the client carried
|
||||
**stale store/tab state** from the sentinel-present safe stage, where the mypacks group
|
||||
existed at a *positive* ordinal `N` (`MYPACK_CATEGORY_ID = N`). Reopening the Store reused
|
||||
that stale positive ordinal rather than the `-1` a **fresh** launch publishes
|
||||
(`FUN_18007df60 → FUN_180014580(store,0) = -1` when absent). So the intended clean A/B (client
|
||||
only ever sees the no-sentinel response) was not achieved — the category that reached the
|
||||
resolver was a stale `>0`, exactly the case the negative-only guard does not divert.
|
||||
|
||||
## Conclusion / strategy status
|
||||
- **The guard as-written does NOT handle a positive, now-invalid My-Packs ordinal** — proven
|
||||
by this crash. Diverting only `category < 0` is insufficient when the client presents a
|
||||
stale/positive ordinal for an absent group.
|
||||
- **Not falsified for the fresh-client case.** Whether a fresh no-sentinel launch presents
|
||||
`-1` (guard diverts → Browse, no crash) or still a positive ordinal is **UNKNOWN** and needs
|
||||
a **clean re-test**: launch FIFA fresh with the backend already in no-sentinel mode so the
|
||||
client never sees a mypacks group. That is the proper equivalent of Experiment B.
|
||||
- **Candidate stronger guard** (design only, not implemented): divert to list-all when the
|
||||
resolved group is NULL for *any* category (guard `FUN_180014420`'s NULL return at the
|
||||
`0x14870`/`0x14882` site), not merely when `category < 0`. This covers the positive-invalid
|
||||
ordinal too, at the cost of being a generic miss-fallback (the higher-risk Rank-2 behavior).
|
||||
Do NOT implement without authorization and a clean re-test first.
|
||||
|
||||
**Strategy A / resolver guard status: NOT PROVEN.** Crash-guard installs and is build-validated
|
||||
and dormant-safe with the sentinel present, but the first no-sentinel test CRASHED at the
|
||||
resolver via a positive stale ordinal (confounded by no relaunch). Backend P2 active-sentinel
|
||||
was restored immediately (mandatory rollback; source `f416e71e…`, sentinel `state=active`),
|
||||
and remains the production safety net. Guard left in `autopatch.py` (dormant) pending the
|
||||
clean re-test decision; `autopatch.py.pre-storeguard.bak` available to remove it.
|
||||
|
||||
---
|
||||
|
||||
# PART IV — Fresh-process no-sentinel retest (2026-08-13) — RESULT R1 (SUCCESS)
|
||||
|
||||
Corrects PART III's confound. This time the mandatory ordering was enforced: the backend
|
||||
entered no-sentinel mode **while FIFA was closed**, then FIFA launched **fresh** (new pid,
|
||||
new autopatch) so the process never saw a sentinel-present Store response.
|
||||
|
||||
## Setup (verified, clean A/B)
|
||||
- BACKEND set no-sentinel at 02:55:09Z with FIFA down; genuine `GET /store/purchasegroup`
|
||||
(02:58:45Z) served to the fresh client = ids `[1,5,6,7]`, **no 65534, no mypacks group**,
|
||||
packs 1/5/6/7 present. This body is **byte-identical** to the PART III (F3) no-sentinel
|
||||
capture — the ONLY changed variable vs F3 is the client process lifetime.
|
||||
Evidence: `docs/evidence/store_purchasegroup_capture_freshretest_no_sentinel_2026-08-13.json`.
|
||||
- CLIENT: NEW FIFA pid 553220, NEW autopatch pid 552999; guard ENFORCED (orig `75 0f`
|
||||
matched → `85 ff 7f 0f` = `TEST EDI,EDI; JG`). Process never saw a sentinel response
|
||||
(0 purchasegroup responses containing 65534 after the no-sentinel restart).
|
||||
- PROFILE unchanged throughout (`39bb3e83…`, `[]`, coins 29,876,776).
|
||||
|
||||
## Result — R1 (operator-observed)
|
||||
- **No crash** (FIFA 553220 alive after the test; no new minidump), **no dialog**, **Store
|
||||
stays open**, opens on **Browse Packs**, Bronze/Gold/Special packs visible and navigable.
|
||||
- Cosmetic-only imperfections (pre-existing, NOT caused by the guard): the six-tab bar is
|
||||
unbound (no tabs), packs render without cover art, and tiles show "0 items". These match
|
||||
the known store tab-bind / list-all rendering quirks (`plan-2026-08-05-store-subsystem.md`
|
||||
§2.1) and are independent of the resolver guard.
|
||||
|
||||
## Causal conclusion (decisive A/B)
|
||||
```
|
||||
server response (no sentinel, no mypacks group) == byte-identical across F3 and R1
|
||||
client original JNZ + this response -> CRASH 0x180014882 (Experiment B)
|
||||
client JG (stale positive ordinal) -> CRASH 0x180014882 (PART III F3, contaminated)
|
||||
client JG (FRESH, category = -1) -> NO CRASH, Browse Packs (PART IV R1) ✅
|
||||
```
|
||||
A **fresh** client publishes `MYPACK_CATEGORY_ID = FUN_180014580(store,0) = -1` for the absent
|
||||
group; the movie echoes `-1`; `TEST EDI,EDI; JG` does **not** take the resolve branch, so the
|
||||
client runs the list-all/Browse path (`FUN_180014610`) — no `FUN_180014420(NULL)` deref, no
|
||||
crash. **PART III's F3 is confirmed as stale-positive-ordinal contamination** (FIFA not
|
||||
relaunched across the sentinel→no-sentinel flip), not a guard failure.
|
||||
|
||||
## Strategy status
|
||||
**Strategy A / resolver guard: PROVEN ON THE TESTED FIFA 17 BUILD** (CardsDLL
|
||||
`4706a881…`) for the clean process-lifetime case — it safely routes the absent My-Packs
|
||||
category to Browse Packs with no crash and no dialog, needing **no** backend sentinel. Scope
|
||||
caveats: (1) tested build only; (2) the negative-only guard does NOT cover a stale/positive
|
||||
invalid ordinal (PART III) — only arises if the client's Store state predates a sentinel→
|
||||
no-sentinel change within one process, which does not happen on a normal launch; a NULL-return
|
||||
guard at `FUN_180014420` would additionally cover that, deferred/not implemented; (3) UX still
|
||||
has the pre-existing no-tabs/no-art/"0 items" cosmetics.
|
||||
|
||||
Backend P2 active-sentinel was restored immediately after capture (mandatory rollback; source
|
||||
`f416e71e…`, sentinel `state=active`) and **remains production default**. The clean UX is only
|
||||
safe to serve when the server knows the client is patched — see PART II §12 rollout options
|
||||
(recommend B: suppress the sentinel only when client patch-capability is known; keep the
|
||||
sentinel universal by default). Guard retained in `autopatch.py` (dormant with the sentinel).
|
||||
|
||||
## INVARIANT — empty-My-Packs capability MUST be session-stable
|
||||
|
||||
F3 vs R1 establish a hard operational invariant for any deployment (sentinel or client
|
||||
guard): **the server MUST NOT switch a running FIFA client between sentinel-present and
|
||||
sentinel-absent for the My Packs group within a single FIFA process lifetime.**
|
||||
|
||||
Rationale: the client resolves and caches the My-Packs group **ordinal** (positive when a
|
||||
group — real or sentinel — is present; `-1` when absent) from the `purchasegroup` response
|
||||
seen at Store-subsystem init. The resolver guard only reclassifies the ordinal *sign*
|
||||
(`≤0` → Browse). If a client that already cached a **positive** ordinal later receives a
|
||||
no-sentinel topology, the stale positive ordinal still takes the resolve branch and
|
||||
`FUN_180014420` returns NULL → crash at `0x180014882` (exactly F3). A **fresh** process that
|
||||
only ever sees the no-sentinel topology caches `-1` and is routed to Browse safely (R1).
|
||||
|
||||
Practical rules:
|
||||
- Choose the My-Packs representation (sentinel-present vs sentinel-absent) **before** a client
|
||||
starts its session, and hold it for that session.
|
||||
- The future patch-capability handshake (PART II §12) MUST therefore be decided at
|
||||
login/session start, not toggled mid-session.
|
||||
- A NULL-return guard at `FUN_180014420` (deferred) is the only thing that would make a
|
||||
mid-session flip crash-safe; until then, session stability is mandatory.
|
||||
|
||||
Reference in New Issue
Block a user