From 3d5892c00f72002326264a1e4661125a16f5faf6 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sat, 4 Jul 2026 21:14:07 -0500 Subject: [PATCH] RIO firmware: build + verify patched image (reply-wedge fix) make_patch.py applies the two-site fix to RIOv4_2.bin (asserting original bytes first) -> RIOv4_2_patched.bin (23 bytes changed). Re-disassembling and diffing confirms the change is confined to $D9DD, $DA21-$DA2E, and the $DFF0 stub with no downstream desync. Clears the reply-in-progress latch $2521 on every teardown so a stress collision no longer leaves the board mute to analog. Flash directly to the DIP-28 W27C512; static verification only -- dynamic RIO_TAP mash test pending the burned chip. Co-Authored-By: Claude Fable 5 --- emulator/rio-firmware/RIOv4_2-ANALYSIS.md | 15 + emulator/rio-firmware/RIOv4_2_patched.bin | 38 + .../rio-firmware/RIOv4_2_patched.disasm.asm | 13133 ++++++++++++++++ emulator/rio-firmware/make_patch.py | 64 + 4 files changed, 13250 insertions(+) create mode 100644 emulator/rio-firmware/RIOv4_2_patched.bin create mode 100644 emulator/rio-firmware/RIOv4_2_patched.disasm.asm create mode 100644 emulator/rio-firmware/make_patch.py diff --git a/emulator/rio-firmware/RIOv4_2-ANALYSIS.md b/emulator/rio-firmware/RIOv4_2-ANALYSIS.md index e609bb2..8115c2f 100644 --- a/emulator/rio-firmware/RIOv4_2-ANALYSIS.md +++ b/emulator/rio-firmware/RIOv4_2-ANALYSIS.md @@ -133,6 +133,21 @@ safe. This is the board-side analogue of the game-side "make collisions harmless" patches (BTL4OPT v2-v4) — instead of widening a timing window it removes the latch leak entirely. +### Patched binary — built & statically verified (2026-07-04) + +`make_patch.py` applies both edits to `RIOv4_2.bin` (asserting the exact +original bytes at each site first) → **`RIOv4_2_patched.bin`** +(sha256 `3fc8170caf60e2580641724ff995176c93c4f2e706f31487beded8233142493f`, +23 bytes changed). Re-disassembling it (`RIOv4_2_patched.disasm.asm`) and +diffing against the original confirms the change is confined to exactly +three regions with no downstream desync: +- `$D9DD` `JMP $DA2F` → `JMP $DFF0` +- `$DFF0` new stub: `CLR $2521 ; CLR $2522 ; JMP $DA2F` +- `$DA21` `CLR $2521 ; CLR $2522 ; NOP×7` (RTS at `$DA2E` intact) + +Flash `RIOv4_2_patched.bin` directly to the W27C512 (DIP-28). This is +static verification only; dynamic proof still needs the burned chip. + ### Validation plan (when a chip is available) Burn the two edits to a W27C512, socket it (preserve the original AMD chip), then run the `RIO_TAP` two-handed 8-button mash test. Expect: no diff --git a/emulator/rio-firmware/RIOv4_2_patched.bin b/emulator/rio-firmware/RIOv4_2_patched.bin new file mode 100644 index 0000000..eaec905 --- /dev/null +++ b/emulator/rio-firmware/RIOv4_2_patched.bin @@ -0,0 +1,38 @@ +$"f qFLiÐÙƽôýâë,ʽ5ӽ$$ J K M 4 7  N$ɽ|$!$B$C$G$E$I$F$J$$K$$H$1111% %!%"Ty1y~y˽y~<<67̽4hɜ$F$E32889}$I'|$J$J&$!'м$J$I9̭#""9    =  =   @#""9 #""9^ q9oZ&J&9í$E#; @&    @ @#; $I#;%;<<76 %;% +&~8 & +&G ~8& +&6 &0~$') +&  &&~8$!˧A%523889~;&F(;'~O& ̽;'~^& ;'u~o& 4;'d~€ & h;'S~‘& ɜ;'B~¢&%ұB;'\~¹&ҽJ;'H~Ё&*p;'4~˧; '~A|,$!9&ν~;'~~%~ÐÙi M9 K~S J91Ԣ%9 P۰d< 1 18Z&9" + 32< 1 18Z&z 3&9`!b>9H#ν>9H!½>9$>9H$T>9H$>9$Ļ3U9%08 (9 >9 >9oZ&9   Π)Π!Π#Π%Π' 9 Π) 9 Π! 9 Π# 9 Π% 9 Π' 9 + '>9$"91D1X 9i&$"1''X' +''9~~ƿâë$$!J1Ѱ$ɽ1S~bNJǵ$!99˽ 9~ƾi&1'~ƏLiÐÙƽôýâë,$$ J K M 4 7  N$ɽ|$!$B$C$G$E$I$F$J$$K$$H$1111% %!%"~ƾ&z~ƾ&Ċ~ƾ&Ě~ƾ&Ī~ƾ&ĺ9i&1$Gi&1$K$GH$?967<<-A-B;8832967<<-A-B$!;8832967<<-A-B%%;8832967<<-A-B%%;8832967<<-A-B1;8832967<<-A-B1;8832967<<-A-B1;8832967<<$!'0 '% +%-B -A<%%8z%&;88329$!'-A$ -B 7;9$!'#%-A$ -B% J;9$!'#%-A$ -B% K;99 LΠ( Π8 !ʗ #ʰ &   + 9 LΠ Π0 !ʗ #ʰ &    9 LΠ" Π2 !ʗ  #ʰ &     9 LΠ$ Π4 !ʗ  #ʰ &     9 LΠ& Π6 !ʗ  #ʰ &     9 ' L&#  H$ H~ʖ H~ʖ&#  H$ H~ʖ H~ʖ&#  H$ H~ʖ H~ʖ &#  H$ H~ʖ H~ʖ&  H$ H ~ʖ H 9  & ! %9< 89O_ % & #'|   ҽ9<6$!& 8  H  H 6I289<6$!& 8 % H & H 6I289 HDDDD9O L H 9 8962962962962962967<<  N P !̲8832967<<  N : P2: :!̲88329  4 4&~&O~&̀~ͱ "Z& 5 +< 1 18Z&z 5&z 4&9  + + 9 + +  9 + +   9 + + 9  6 5 NZ'D~&r~| z 5&z 6&967<< !:&7< 1 1 : N& J8~d K8$ Ȝ~l$ o8832967<< !:'@< 1 1 : N& J8~κ K8$ Ȝ~Ȇ$ J%o88329<6$!& 8 K H O L6I289 M& +$ ~$ # M& +$ +~0$ +967<< 7$B N$!P϶| NV!P϶| N܈!P϶ܺ!P϶| N!P϶!P϶P!P϶| N| N| N݂!P϶| Nݴ!P϶883299D967<<!R!PZ& 6 +< 1 18Z&z 6&8832967< RIOv4_2_patched.bin. + +Fix (see RIOv4_2-ANALYSIS.md): clear the reply-in-progress latch $2521 on +EVERY reply teardown, not just the $2522-gated success path. + + 1. Give-up path: redirect $D9DD `JMP $DA2F` to a stub at free ROM $DFF0 + that clears $2521/$2522 then continues to $DA2F. + 2. Success path: make $DA00's clear of $2521/$2522 unconditional. + +Each edit asserts the exact original bytes first, so a wrong assumption +aborts instead of corrupting the image. Address == file offset. +""" +import sys, hashlib + +SRC = sys.argv[1] if len(sys.argv) > 1 else "RIOv4_2.bin" +DST = sys.argv[2] if len(sys.argv) > 2 else "RIOv4_2_patched.bin" + +d = bytearray(open(SRC, "rb").read()) +assert len(d) == 0x10000, f"expected 64KB image, got {len(d)}" +orig_sha = hashlib.sha256(d).hexdigest() +assert orig_sha == "60a88718835c654b6135dbec7721c40ef99dca07df2ad4b57eedeb24037a5f73", \ + f"unexpected source image {orig_sha}" + +def patch(addr, expect, new): + got = bytes(d[addr:addr+len(expect)]) + assert got == bytes(expect), ( + f"@${addr:04X}: expected {got.hex()} to be {bytes(expect).hex()}") + assert len(new) == len(expect), "length mismatch" + d[addr:addr+len(new)] = bytes(new) + +# --- edit 1: give-up path redirect --------------------------------------- +# $D9DD 7E DA 2F JMP $DA2F -> 7E DF F0 JMP $DFF0 +patch(0xD9DD, [0x7E, 0xDA, 0x2F], [0x7E, 0xDF, 0xF0]) + +# stub at $DFF0 (was erased $FF): CLR $2521; CLR $2522; JMP $DA2F +patch(0xDFF0, [0xFF]*8, + [0x7F, 0x25, 0x21, # CLR $2521 + 0x7F, 0x25, 0x22, # CLR $2522 + 0x7E, 0xDA]) # JMP $DA2F (hi + first target byte) +patch(0xDFF8, [0xFF], [0x2F]) # JMP low byte + +# --- edit 2: success teardown, unconditional clear ----------------------- +# $DA21 B6 25 22 LDAA $2522 +# $DA24 81 01 CMPA #$01 +# $DA26 26 06 BNE $DA2E +# $DA28 7F 25 21 CLR $2521 +# $DA2B 7F 25 22 CLR $2522 (13 bytes $DA21-$DA2D; $DA2E RTS untouched) +# -> CLR $2521 ; CLR $2522 ; NOP x7 +patch(0xDA21, + [0xB6,0x25,0x22, 0x81,0x01, 0x26,0x06, 0x7F,0x25,0x21, 0x7F,0x25,0x22], + [0x7F,0x25,0x21, 0x7F,0x25,0x22, 0x01,0x01,0x01,0x01,0x01,0x01,0x01]) +assert d[0xDA2E] == 0x39, "RTS at $DA2E must be intact" + +open(DST, "wb").write(d) +new_sha = hashlib.sha256(d).hexdigest() +# byte-diff report +diffs = [(a, orig, d[a]) for a, orig in + enumerate(open(SRC,"rb").read()) if d[a] != orig] +print(f"source : {SRC} sha256 {orig_sha}") +print(f"patched: {DST} sha256 {new_sha}") +print(f"{len(diffs)} bytes changed:") +for a, o, n in diffs: + print(f" ${a:04X}: {o:02X} -> {n:02X}")