What Stern checks when it says GAME VALIDATION ERROR ... UPDATE SD CARD: two independent layers. (1) the sidx per-file HMAC-SHA1+MD5 manifest over the 1306 /games files, verified by spk/spike_menu; (2) the game's own boot check (SD card geometry via hdparm, node firmware, SHA1) that produces the permanent #1..#6 service-menu message — the #6 12:1:0 we got after the 2026-10-22 card reflash. Includes the game ELF's layout gotchas (two VA mappings, all-indirect PLT calls, unreferenced localized catalog) that stalled the static trace, and the open questions with the on-machine diagnostic path. Co-Authored-By: Claude Code <noreply@anthropic.com>
7.5 KiB
Stern Spike 2 SD / game validation — what the machine checks
Findings from the 2026-10-23 investigation of the "GAME VALIDATION ERROR —
#6 12:1:0 UPDATE SD CARD" message that appeared on King Kong LE 0.97 after
the 2026-10-22 card reflash. King Kong LE 0.97 binary analyzed (ELF32 ARM,
stripped, BuildID b7e3cc96…8a8a, the game on /games).
Short version
There are two independent validation layers, and they fail with different messages:
| Layer | Who runs it | What it checks | Failure looks like |
|---|---|---|---|
| sidx manifest | /usr/local/bin/spk (and the spike_menu installer) |
every file the manifest lists, on /games |
update/install refuses; spk prints FILE NOT FOUND / FILE SIZE MISMATCH / wrong checksum / FAILED TO VERIFY |
| game's own boot check | the game binary itself, at boot |
SD card geometry + game files (details below) | the permanent service-menu message GAME VALIDATION ERROR #N … UPDATE SD CARD — this is the one that printed #6 12:1:0 |
The game binary does not parse .sidx files and does not link HMAC
(its OpenSSL imports are SHA1_*, EVP_* only), so the service-menu error is
produced by the game's own check, not by the sidx layer. The two can fail
independently.
Layer 1 — the sidx manifest (the known one)
Format fully documented; tooling lives in
pinball-asset-decryptor/pinball_decryptor/plugins/stern/sidx.py (plus
sidx_append.py, sidx_deliver.py, tools/spike2_emu/sidx_invariants.py).
Restated here because it is what "UPDATE SD CARD" usually means:
- Manifest:
/spk/index/<title>.sidxon the/gamespartition (same partition asimage.bin). - Layout:
SIDXLU+ version 3 + title (32 bytes), anSKKblock,0x30= record count, thenSTRS(NUL-separated relative paths), then one record per path in the same order, then aFENDtrailer. 0x34is the sum of all record sizes, not a checksum (FINF cards; FI64 cards store0xffffffffthere and the sum in anSZ64block). King Kong is FINF (60-byte payloads: size u32 @4, keyed HMAC-SHA1 @21, plain MD5 @41).- HMAC key (global across titles and cards, recovered by RE):
8e1f5543c2f54a11673a282a2f87c006. - King Kong LE 0.97 manifest: 1306 records, all under
king_kong_le/: 17 node.hexfirmwares,game,conagent,image.bin, 2 files underdata/, 1126assets/lcd+ 158assets/nukassets. Nothing on the rootfs (p2) is indexed — rootfs mods (dropbear, PAD mode, log shipper) cannot trip this layer. - Verification semantics (from
spk's strings): it walks the manifest records and checks each file's presence, size and digests. A file added to/gamesthat the manifest does not list is not itself the failure; a manifest file that is missing, resized or modified is. - After any size-neutral write to an indexed file, the record's HMAC + MD5
must be rewritten with the key (that's what
sidx.py'srecord_field_writesdoes).
Layer 2 — the game's own validation (the #N messages)
Everything below is from the game binary.
The message set
Six permanent (FG) message templates, stored in a 5-language localized catalog (each entry = 5 identical pointer slots + NUL; all languages English on this build):
#6 %d:%d:%d UPDATE SD CARD (3 runtime numbers)
#5 %d:%d UPDATE SD CARD (2)
#4 %d:%d UPDATE SD CARD (2)
#3 UPDATE SD CARD (0)
#2 UPDATE SD CARD (0)
#1 UPDATE SD CARD (0)
The older spike_menu installer carries the same six with #6 %d:%d (two
numbers) — the game build added the third field. The FG fatal-state enum
includes FG_GAME_VALIDATION_ERROR_PERM_MESSAGE (enum value 5, between
FG_LOCKOUT_START_AND_TEST and FG_FATAL_NON_FATAL_ERROR_DISPLAY); the
service-menu strings around it are the SD-diagnostics block:
GameSD card: %s %dGB
GameSD card required sector %lld:
%lld
MicroSD card: …
What the game checks (observed strings/imports, exact logic not yet decoded)
- SD card geometry: it runs
/sbin/hdparm -g /dev/mmcblk0(GameSD) and/sbin/hdparm -g /dev/mmcblk2(connectivity microSD), parsessectors; failure strings arehdparm failedandunknown card size. The service menu's "required sector" line suggests the game expects a specific sector count for the GameSD card. - Connectivity card setup:
/sbin/fdisk /dev/mmcblk2 < /tmp/fdskXXXXXX,/sbin/mkfs.ext3 -m0 /dev/mmcblk2p1,/bin/mount /dev/mmcblk2p1 /connectivity— it (re)partitions and formats the microSD itself. - Node firmware: glob patterns
./%s-%s-%d_%d_[0-9]*.hex/./%s-%s-[0-9]*_[0-9]*_[0-9]*.hexfor the node.hexfiles, and the installer-sidechecksum error node %d: is 0x%04x, should be 0x%04x(spike_menu) show node firmware is checksummed against the card's files. - Crypto available in-process:
SHA1_Init/Update/Final,EVP_*(the EVP set is the asset-decryption path),popen,system,memcmp,fopen. No HMAC — so no sidx recomputation in-process.
Not yet decoded: which check maps to #6 and what 12:1:0 means
(check id : sub-code : result? record index? sector index?). The static
trace stopped at the message-catalog indirection — see "Binary layout gotchas"
below for why this binary resists naive cross-reference hunting.
Diagnostic path for the live error
The machine logs its trace to /dump (rw partition):
ssh root@<machine>
grep -iE 'valid|sector|sd card|mmcblk|checksum|node' /dump/debug_log.txt | tail -50
tail -50 /dump/log/connectivity/game.log
hdparm -g /dev/mmcblk0
The line(s) immediately before the permanent message should name the failing
check and the values that became 12:1:0.
Binary layout gotchas (why the trace stalled)
For anyone re-attacking the game ELF:
- Two LOAD segments, two VA mappings: RX segment
fo+0x8000(covers file offsets< 0x7f9cb4, i.e..text/.rodata); RW segmentfo+0x80a000(covers.data,.got, and the non-standard.flash(fo0x80ee60, ~470 KB) and.nonvolsections). Usingfo+0x8000for data-segment addresses silently finds no references. - All external calls are indirect: a full scan of
.textfound zero directblto any PLT slot; every libc call goesldr rN, [pc,#lit]; blx rN. PLT-slot hunting viabltargets comes up empty by design. - The localized catalog is referenced only via data: 5-slot entries in
.flash, a 3940-entry pointer array (.flashfo0x846c6c, message id → catalog slot, descending), and(ptr, count, 4)descriptor triples in.data(the pointer-array entry is at fo0x7fff10:0x856c6c, 0xf64, 4). No code constant (literal pool, ADR/ADRL or MOVW/MOVT) references any of these — the head of the descriptor chain is reached some other way (still unknown). - Hopper's function boundaries in this binary were unreliable (disassembly misaligned vs. raw bytes in at least one traced function); verify any decompilation against raw words before trusting it.
Open questions
- Meaning of
#6 12:1:0— needs the pre-error lines in/dump/debug_log.txt(machine was unreachable at192.168.1.167on 2026-10-23; retry when it's back on the network). - Whether the game's check includes a whole-card sector-count comparison (the "required sector" string suggests yes) — a different physical SD card with different geometry would trip it without touching a single file.
- Where the validation result is cached (
.nonvol?/data?) — a stale cached result could keep the message up after the underlying problem is fixed.