b0d5e04bb9
Port the PROVEN empty-"My Packs" resolver crash-guard into the canonical autopatch.py /proc-mem patcher. When no `mypacks` purchase group exists, a fresh FIFA 17 client resolves category id -1; CardsDLL FUN_1800147f0 at RVA 0x14858 (`JNZ 0x14869`, bytes 75 0f) treats every non-zero category as resolvable, calls FUN_180014420, gets NULL, and dereferences [NULL+0x48] at 0x180014882 (0xC0000005). Rewriting JNZ->JG (7f 0f) preserves positive-category resolution (EDI>0) while routing zero/negative categories to the existing Browse/list-all path -> no NULL lookup, no crash, Store opens on Browse Packs. - STORE_PATCHES_GUARDED table pins RVA 0x180014858 orig 75 0f -> patch 7f 0f. - Applied every tick, fail-closed via guarded_action(): apply only when the live bytes are the known original; no-op when already patched; SKIP+log an unrecognised CardsDLL build (never blindly overwritten). - Runtime watch loop moved under `if __name__ == "__main__"` so the module imports cleanly for unit testing; script behavior is unchanged. Existing ProtoSSL cert-gate and STORE_PATCHES enforcement are byte-identical (indent only). - test_autopatch_guard.py: pure test covering PATCH/NOOP/SKIP and pinning the exact guarded RVA/bytes. Proven on the tested build (CardsDLL 4706a881...) by a clean fresh-process no-sentinel A/B (R1). Dormant while the backend active-sentinel is present. See docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md PART IV.
39 lines
1.4 KiB
Python
Executable File
39 lines
1.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Pure unit test for the empty-My-Packs store resolver guard in autopatch.py.
|
|
|
|
Covers the fail-closed guard decision (original -> PATCH, already-patched -> NOOP,
|
|
unknown -> SKIP) and pins the guarded patch table to the exact RVA/bytes proven on
|
|
the tested FIFA 17 build (JNZ 0x14869 -> JG 0x14869 at CardsDLL RVA 0x14858).
|
|
|
|
Run: python3 test_autopatch_guard.py
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import autopatch # importable: runtime loop is guarded by `if __name__ == "__main__"`
|
|
|
|
GUARD_VA = 0x180014858
|
|
ORIG = bytes.fromhex("750f") # JNZ 0x14869
|
|
PATCH = bytes.fromhex("7f0f") # JG 0x14869
|
|
|
|
|
|
def test_table_exact():
|
|
assert autopatch.STORE_PATCHES_GUARDED == {GUARD_VA: (ORIG, PATCH)}, \
|
|
autopatch.STORE_PATCHES_GUARDED
|
|
# Byte-level pin so a bad hex literal cannot slip through.
|
|
assert ORIG == b"\x75\x0f" and PATCH == b"\x7f\x0f"
|
|
|
|
|
|
def test_decision():
|
|
assert autopatch.guarded_action(ORIG, ORIG, PATCH) == "patch" # apply
|
|
assert autopatch.guarded_action(PATCH, ORIG, PATCH) == "noop" # already patched
|
|
assert autopatch.guarded_action(b"\x00\x00", ORIG, PATCH) == "skip" # build mismatch
|
|
assert autopatch.guarded_action(b"\x90", ORIG, PATCH) == "skip" # wrong length
|
|
|
|
|
|
if __name__ == "__main__":
|
|
test_table_exact()
|
|
test_decision()
|
|
print("OK: autopatch guard table + fail-closed decision (PATCH/NOOP/SKIP)")
|