diff --git a/docs/sd-validation.md b/docs/sd-validation.md
new file mode 100644
index 0000000..c31d520
--- /dev/null
+++ b/docs/sd-validation.md
@@ -0,0 +1,156 @@
+# 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/
.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@
+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.