Files
mattlqxandClaude Code faafb72236 docs: SD/game validation findings — sidx layer vs the game's own #N check
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>
2026-10-23 11:22:59 -04:00

157 lines
7.5 KiB
Markdown

# 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>.sidx` on the `/games` partition (same
partition as `image.bin`).
- Layout: `SIDXLU` + version 3 + title (32 bytes), an `SKK` block, `0x30` =
record count, then `STRS` (NUL-separated relative paths), then one record
per path in the same order, then a `FEND` trailer.
- **`0x34` is the sum of all record sizes, not a checksum** (FINF cards; FI64
cards store `0xffffffff` there and the sum in an `SZ64` block). 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 `.hex` firmwares, `game`, `conagent`, `image.bin`, 2 files under
`data/`, 1126 `assets/lcd` + 158 `assets/nuk` assets. **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
`/games` that 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`'s
`record_field_writes` does).
## 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), parses `sectors`;
failure strings are `hdparm failed` and `unknown 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]*.hex` for the node `.hex` files, and the
installer-side `checksum 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):
```sh
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 segment
`fo+0x80a000` (covers `.data`, `.got`, and the non-standard **`.flash`**
(fo `0x80ee60`, ~470 KB) and **`.nonvol`** sections). Using `fo+0x8000` for
data-segment addresses silently finds no references.
- **All external calls are indirect**: a full scan of `.text` found **zero**
direct `bl` to any PLT slot; every libc call goes `ldr rN, [pc,#lit]; blx
rN`. PLT-slot hunting via `bl` targets comes up empty by design.
- **The localized catalog is referenced only via data**: 5-slot entries in
`.flash`, a 3940-entry pointer array (`.flash` fo `0x846c6c`, message id →
catalog slot, descending), and `(ptr, count, 4)` descriptor triples in
`.data` (the pointer-array entry is at fo `0x7fff10`:
`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
1. Meaning of `#6 12:1:0` — needs the pre-error lines in `/dump/debug_log.txt`
(machine was unreachable at `192.168.1.167` on 2026-10-23; retry when it's
back on the network).
2. 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.
3. Where the validation result is cached (`.nonvol`? `/data`?) — a stale
cached result could keep the message up after the underlying problem is
fixed.