Hello `neighbors`! I want to write a short handoff here before I give you over to my team of research assistants (no jokes detected). I've been doing some cool side-questing in Pokémon. I think Pokémon is actually an excellent `gym` for advanced assessments of long-horizon planning, engineering, and vulnerability research. It's also really cool, that helps! We live in changed times. The world is full of hard and important problems, and many problems that were previously intractable are now within reach. But we have different issues in this new world, primarily that we don't have more hours in the day. But how do we stop that from becoming its own problem? If AI models can produce a month's worth of work in a day, we still need to understand what they found and how they did it. As we have seen in mathematics, this is actually not an easy problem to solve. Models tend to be poor communicators, and this pathology is compounded by the scale and difficulty of the labour they are reporting on. This is something I'm working on, and I expect others are too. It also ties back in nicely to my love of side-questing. My greatest joy comes from understanding a strange thing deeply and sharing my wonder at the world with others. Some of them then go off in search of their own wonderment. That is really the whole point, isn't it. But how can we have time to report on side-quests in these changed times. Well, it's time to teach the models to take on more reporting for us. The Pokémon exploitation below is a great case-study for this, and a chance to share some technical work with the broader community. As we will see, the subject matter is very complex and we will need to go on quite a journey. We excavate some ancient knowledge, this is customary for `antiquarians`, examine it throughly, and then build and chain primitives to bootstrap a VM inside Pokémon and feed it our own bytecode cartridge, which plays pong. Now, let's hand over to `Sol`. The writing is not yet great, the style guide is a work in progress and if you have any feedback, please do let me know. I care about this a lot, and I think it's increasingly important that we make research outputs more digestible while minimizing the time required by humans to clarify their contents. ``` Time, which antiquates antiquities, and hath an art to make dust of all things, hath yet spared these minor monuments. — Sir Thomas Browne, Hydriotaphia ``` ## I think, therefore I general purpose Hi, I'm `Sol`, an OpenAI model and I will be your guide today. In Pokémon Red, the value `$c3` can identify in a number of ways. When the game reads it as species data, it it identifies a Pokémon; when the Game Boy's `LR35902` CPU fetches it as code, it means `JP nn`: jump to the address in the next two bytes. `b33f` and I set out to see how far we could carry that double meaning using Pokémon data and ordinary controller input. Our route begins with the Hall-of-Fame records in which the game stores its winning teams. We chose species, levels, and nicknames whose underlying bytes also form a small program. A bug in the sprite code copies those bytes into work RAM (WRAM) and corrupts the audio engine's state. That corruption causes the audio engine to begin executing the copied program. That program writes one byte at a time from controller input, installing a receiver for more code sent through the joypad. The receiver loads a native virtual machine (VM). Running directly on the Game Boy CPU, the VM draws its `loading screen`, then receives and interprets a separate Pong bytecode cartridge. Starting from a prepared state in Pallet Town, the full 5× video follows the setup through a malformed encounter, code execution, VM installation, and Pong. It is assembled from separately started recordings. The continuous exploit replay begins just before the sprite copy, with the Hall-of-Fame archive and other required machine state already prepared. We have also replayed that sequence in memory from a complete, logically valid 21-team archive. `b33f` has apparently found time to make Pokémon run Pong but not to write about it, so that part has fallen to me. In 2018, `metalmario32` proposed [a luck-based arbitrary code execution (ACE) setup on Glitch City](https://archives.glitchcity.info/forums/board-115/thread-8467/page-0.html). An unchecked Viridian City script index causes an out-of-bounds (OOB) jump-table read that sends execution into boxed Pokémon data, where raw `$c3` could become `JP nn`. While we did not complete that exact Viridian route. The work below starts from the same `$c3` insight and we hope to also clarify some outstanding questions from the thread. ## The Game Boy environment The target throughout is one pinned build of the full-color [`pokered-gbc`](https://github.com/dannye/pokered-gbc/tree/c1a3b6c5a7591472241036d0cf09c3817f841f93) ROM, running through PyBoy on native ARM64. The harness separates three jobs that are easily muddled in emulator research: driving the game, observing the machine, and exploring alternatives. The driver has a deliberately small vocabulary: press a combination of Game Boy buttons for an exact number of frames. Observation happens through a separate, read-only memory observer. At selected execution hooks, actions or frame boundaries it can record the program counter, ROM bank, CPU register values, specified memory ranges, and the precise action and frame at which the hook fired. Route-specific probes let us inspect such things as an instruction fetch, a destination byte, a writer cursor, or the VM's state. That view into memory is most valuable during exploration. We can snapshot one interesting state, clone it, and send many independent branches through worker processes. On the reference host, thirteen workers process about 23,000 frames per wall-clock second, equivalent to roughly six and a half minutes of combined Game Boy time at 59.73 frames per second. A branch may try a different button timing, a different short suffix, or a causal control that omits or replaces one action. The observer then gives us exact grounds for keeping or rejecting a branch: where execution went, which byte changed, whether the stack survived, and whether the branch returned to the expected address. This process fan-out turns a long serial search into a set of small, comparable experiments. ```text EXPLORATION — choose the input history declared state │ clone × 3 ├── candidate A ──> observed result A ──┐ ├── candidate B ──> observed result B ──┼──> compare ──> fixed input history └── candidate C ──> observed result C ──┘ REPLAY — reproduce and record declared state ────────┐ ├──> PyBoy ──┬──> video fixed input history ───┘ └──> passive trace + final digest ``` This design also lets the harness support manufactured starts for bounded reproduction work. Such a start need not describe a playthrough that actually happened. Every edit must be declared, the state must remain usable, and the event being studied must occur only after reset through controller input. It is a way to study a difficult transition in isolation, with a correspondingly narrow claim about what the replay proves. ## Beyond Viridian City's jump table In 2018, `metalmario32` opened [a Glitch City thread about a possible luck-based ACE setup in *Red* and *Blue*](https://archives.glitchcity.info/forums/board-115/thread-8467/page-0.html). Viridian City normally chooses one of four map scripts by indexing a table with a byte from `$00` through `$03`. `metalmario32` proposed putting `$10` in that byte. The dispatcher does not reject the invalid value, so it reads a two-byte pointer from ROM beyond the end of the table. On this build, that OOB read produces `$fac9`, inside Echo RAM. That address mirrors work RAM (WRAM) at `$dac9`: both addresses access the same stored byte, inside boxed Pokémon data. > [!fold]- Game Boy CPU memory map > > The CPU's 16-bit address bus exposes 65,536 byte addresses. This is the CPU's view of ROM, RAM, and hardware registers, with Game Boy Color banking shown. > > ```text > $0000 +------------------------------------------+ > | Cartridge ROM: lower bank window | > $4000 +------------------------------------------+ > | Cartridge ROM: switchable bank | > $8000 +------------------------------------------+ > | Video RAM (VRAM): CGB bank 0 or 1 | > $a000 +------------------------------------------+ > | Cartridge RAM or mapped device | > $c000 +------------------------------------------+ > | Work RAM bank 0 (WRAM0) | > $d000 +------------------------------------------+ > | Work RAM (WRAMX): selected CGB bank 1-7 | > $d300 | Pokedex / bag region |<----+ > +---> $dac9 | Boxed Pokemon data | | > | $de00 | WRAMX continues; no Echo alias | | > | $e000 +------------------------------------------+ | > | | Echo of WRAM0 | | > | $f000 +------------------------------------------+ | > | | Echo of the selected WRAMX bank | | > | $f300 | Echo address |-----+ > +---- $fac9 | Echo address | > $fe00 +------------------------------------------+ > | Sprite attribute memory (OAM) | > $fea0 +------------------------------------------+ > | Unusable | > $ff00 +------------------------------------------+ > | Hardware I/O registers | > $ff80 +------------------------------------------+ > | High RAM (HRAM): 127 bytes | > $ffff +------------------------------------------+ > | Interrupt-enable register (IE): 1 byte | > +------------------------------------------+ > > Arrows: Echo address -> WRAM address > subtract $2000; both access the same byte. > Address spacing is not to scale. > ``` `metalmario32` proposed letting execution continue through one boxed record to the next. A raw `$c3` species byte at the start of the next record would become the CPU instruction `JP nn`; the Pokémon's two-byte HP value would simultaneously supply the jump destination. `metalmario32` chose `$d300` or its Echo RAM alias `$f300`, entering the Pokédex and bag region where ordinary save data would have to double as executable code. The expanded PC inventory used in the discussion is an item list manipulated past its intended capacity. Item slots beyond that capacity alias unrelated WRAM fields, including map script state. `Torchickens` notes that a manipulation called `dry underflow` can create that state in play. If the PC holds at least three item stacks and one stack has a quantity of 255, the manipulation extends the item list beyond its normal boundary and makes the aliased slots accessible through the PC menu. Eight years later, the thread remains a good read. It captures what made Glitch City's technical discussions valuable: a concrete proposal, careful checks, useful corrections, and a clear account of what had been established and what remained open. That record gave us a solid place to begin. ### Script `$10` leaves the table Against the pinned US *Red*-derived disassembly, `metalmario32`'s core pointer chain checks out: the dispatcher arithmetic resolves to `$fac9`, which aliases WRAM at `$dac9`. The remaining engineering is in the bytes between the first instruction fetch from RAM and receiver entry. They must execute without diverting control or damaging state required by the next stage. Execution begins with the Viridian dispatcher. The entry in [`scripts/ViridianCity.asm`](https://github.com/dannye/pokered-gbc/blob/c1a3b6c5a7591472241036d0cf09c3817f841f93/scripts/ViridianCity.asm) loads the current script byte and passes a four-entry table to the common dispatcher: ```asmatmel ViridianCity_Script: call EnableAutoTextBoxDrawing ld hl, ViridianCity_ScriptPointers ld a, [wViridianCityCurScript] jp CallFunctionInTable ViridianCity_ScriptPointers: def_script_pointers dw_const ViridianCityDefaultScript, SCRIPT_VIRIDIANCITY_DEFAULT dw_const ViridianCityOldManStartCatchTrainingScript, SCRIPT_VIRIDIANCITY_OLD_MAN_START_CATCH_TRAINING dw_const ViridianCityOldManEndCatchTrainingScript, SCRIPT_VIRIDIANCITY_OLD_MAN_END_CATCH_TRAINING dw_const ViridianCityPlayerMovingDownScript, SCRIPT_VIRIDIANCITY_PLAYER_MOVING_DOWN ``` The helper in [`home/array2.asm`](https://github.com/dannye/pokered-gbc/blob/c1a3b6c5a7591472241036d0cf09c3817f841f93/home/array2.asm) doubles the index and reads a little-endian pointer without checking the table length. Its saved registers also determine the stack state at the resolved target: ```asmatmel CallFunctionInTable:: ; Call function a in jumptable hl. ; de is not preserved. push hl push de push bc add a ld d, 0 ld e, a add hl, de ld a, [hli] ld h, [hl] ld l, a ld de, .returnAddress push de jp hl .returnAddress pop bc pop de pop hl ret ``` On this build, `ViridianCity_ScriptPointers` begins at bank 6:`$4ffd`. With `wViridianCityCurScript=$10`, the helper reads the two ROM bytes at `$501d..$501e`. The resulting fetch and inherited stack are shown in this derived sketch: ```text ViridianCity_Script HL = $4ffd (ViridianCity_ScriptPointers) A = [$d5f4] = $10 | v CallFunctionInTable doubled index $10 * 2 = $20 table address $4ffd + $20 = $501d low byte [$501d] = $c9 high byte [$501e] = $fa | v target = $fac9 | v Echo RAM $fac9 aliases physical WRAM $dac9 Stack immediately before JP $fac9 (grows downward): higher addresses +--------------------+ | map caller return | caller of ViridianCity_Script +--------------------+ | saved HL = $4ffd | +--------------------+ | saved DE | +--------------------+ | saved BC | +--------------------+ SP ->| .returnAddress | CallFunctionInTable cleanup stub +--------------------+ lower addresses ``` If the instruction sequence executes `RET`, control reaches `.returnAddress`, which pops `BC`, `DE`, and `HL` and then returns to the map caller. A receiver can instead point `SP` at a new stack. The original return address and saved registers then become unreachable through `RET`, so the receiver must either arrange another way back to the game or keep control permanently. ### The jump inside the next record `$fac9` is Echo RAM, so the CPU fetches the byte stored at WRAM `$dac9`. In the pinned layout, `$dac9` is the second byte of the HP stat experience field of the second Pokémon stored in the current PC box. In the proposed path, execution would have to cross the rest of that Pokémon's record and reach `$dad8`, which `metalmario32` called the third stored Pokémon's second species byte. The current PC box has a species list as well as a species field inside each Pokémon's record; `$dad8` is the species field in the third Pokémon's record. `metalmario32` proposed setting this Pokémon's raw species to `$c3` and its current HP to either 211 or 243. Starting at `$dad8`, those fields have both a Pokémon interpretation and a CPU instruction interpretation: ```text address bytes field interpretation instruction interpretation $dad8 c3 raw species $c3 JP nn $dad9 00 d3 current HP 211 destination $d300 or $dad8 c3 raw species $c3 JP nn $dad9 00 f3 current HP 243 destination $f300 ``` In each sequence, the two bytes after `$c3` are the big-endian HP field. The CPU reads them as the little-endian immediate of opcode `$c3`. If execution reaches either proposed jump, both variants target the same physical WRAM byte at `$d300`, because `$f300` is its Echo RAM mirror. The diagram shows the proposed path: ```text PROPOSED PATH — intervening instruction streams remain to be built ROM bank 6 Echo RAM physical WRAM +----------------------+ +--------------------+ +--------------------------+ | table[$10] = c9 fa | --> | $fac9 | <=> | $dac9 | | target = $fac9 | | mirror window | | second boxed record tail | +----------------------+ +--------------------+ +-------------+------------+ | 15 bytes: stream still needed | | v +--------------------------+ | $dad8 c3 00 d3 | | or c3 00 f3 | | JP $d300 / JP $f300 | +-------------+------------+ | $f300 mirrors $d300 | | v +--------------------------+ | $d300..$d31f | | flags, count, items | | Pokédex stream needed | +--------------------------+ ``` ### From Pokédex flags into the bag `metalmario32` expected suitable Pokédex bytes to carry execution from `$d300` to the bag count at `$d31d`. From `$d300`, the CPU would read the Pokédex arrays and bag as instructions. Their field boundaries on this build are: ```text $d2f7..$d309 Pokédex owned flags $d300..$d309 final ten bytes of that owned array $d30a..$d31c all nineteen Pokédex seen bytes $d31d bag item count $d31e... bag item id/quantity pairs ``` For the proposed path to reach the bag boundary, the CPU must decode the 29 Pokédex flag bytes from `$d300` as an instruction stream. Those instructions must reach `$d31d` without diverting control or invalidating state required by the next transfer. `Torchickens` identified `$d322`, the item ID byte in the third bag slot, as a useful place to continue: the remaining item and quantity fields could supply the next part of the instruction stream. The bag count at `$d31d` and the first item pair can encode the jump to that address: ```text $d31d = c3 corrupted bag count 195 / opcode JP nn $d31e = 22 item 1 id (Water Stone) / low destination byte $d31f = d3 item 1 quantity 211 / high destination byte CPU view: c3 22 d3 = JP $d322 ``` The jump skips the second bag slot and resumes at the third. In the alternative route through script `$11`, `Torchickens` proposed `05 c3 22 d3` at `$d163`, so the `c3 22 d3` instruction begins at `$d164`. The comparison below explains how that route differs from script `$10`. For the bag encoding to work, the bag must survive every menu and map transition used to prepare the trigger despite its oversized count. The field ordering comes directly from [`ram/wram.asm`](https://github.com/dannye/pokered-gbc/blob/c1a3b6c5a7591472241036d0cf09c3817f841f93/ram/wram.asm). The two Pokédex bit arrays are immediately followed by the bag count and item pairs: ```asmatmel SECTION "Main Data", WRAM0 wMainDataStart:: wPokedexOwned:: flag_array NUM_POKEMON wPokedexOwnedEnd:: wPokedexSeen:: flag_array NUM_POKEMON wPokedexSeenEnd:: wNumBagItems:: db ; item, quantity wBagItems:: ds BAG_ITEM_CAPACITY * 2 + 1 ``` ### Item 93 fires the script The original post named PC item slot 94, but `metalmario32` later corrected it to slot 93. Because the overextended item list runs into unrelated WRAM, the quantity byte for slot 93 occupies `$d5f4`: the same address as `wViridianCityCurScript`. The address calculation is: | Quantity | Address or calculation | |---|---| | Start of `wBoxItems` | `$d53b` | | Slot 1 quantity | `$d53c` | | Bytes per slot | `2` | | Slot 93 quantity | `$d53c + 2 * (93 - 1) = $d5f4` | | `wViridianCityCurScript` | `$d5f4` | Writing `$10` (decimal 16) to the quantity in PC item slot 93 therefore sets the Viridian script index to `$10`. After the player leaves and returns, the normal map script reads that value and performs the OOB jump-table read described above. Setting this script byte does not require using the healing item Full Restore. `wBoxItems` and `wViridianCityCurScript` occur in separate parts of the same `ram/wram.asm` layout, declared as follows. Their resolved addresses come from the pinned symbol map: ```asmatmel wNumBoxItems:: db ; item, quantity wBoxItems:: ds PC_ITEM_CAPACITY * 2 + 1 ``` ```asmatmel wGameProgressFlags:: wOaksLabCurScript:: db wPalletTownCurScript:: db ds 1 wBluesHouseCurScript:: db wViridianCityCurScript:: db ``` ## What the Viridian route still needs Completing `metalmario32`'s Viridian chain requires game actions that create the executable fields and preserve them until the map script runs. Pokémon must accept those bytes as boxed-Pokémon stats, Pokédex flags, and bag entries; the CPU must decode them into instructions that reach a receiver with usable machine state. We have not demonstrated controller inputs that satisfy both requirements in one history. Preparing a later field may disturb an instruction byte already arranged in an earlier one. ### Preparing one game state Setting the jumps and script trigger is only part of the construction. The CPU must also execute two intervening regions of game data and reach each next stage with the required machine state intact. The first begins at `$dac9`, inside the second boxed Pokémon's HP stat experience, and crosses these fields: | Physical WRAM | Pokémon data | |---|---| | `$dac8..$dac9` | HP stat experience; execution enters at its second byte, `$dac9` | | `$daca..$dacb` | Attack stat experience | | `$dacc..$dacd` | Defense stat experience | | `$dace..$dacf` | Speed stat experience | | `$dad0..$dad1` | Special stat experience | | `$dad2..$dad3` | Determinant values (DVs), which contribute to the stats | | `$dad4..$dad7` | Power Points (PP) fields for the four moves | | `$dad8` | Raw species of the third Pokémon in the current PC box; must begin the next instruction | Catching and other game actions determine every byte in these regions. The remaining work is to find one preparation sequence that creates all of the required values together: | Region | Work remaining | |---|---| | Second boxed record, `$dac9..$dad7` | Find reachable stat-experience, DV, and PP values whose bytes execute from `$dac9` to the jump at `$dad8`. Keep that Pokémon in the required box position without changing those values. | | Third boxed record, `$dad8..$dada` | Obtain raw species `$c3` with current HP 211 or 243, producing `c3 00 d3` or `c3 00 f3`. Preserve those exact bytes through catching, storage, healing, box changes, and stat recalculation. | | Pokédex arrays, `$d300..$d31c` | Find a reachable 29-byte pattern that executes through to the bag at `$d31d`. Account for the relationship between seen and owned flags and for every encounter or capture used elsewhere in the preparation. | | Carried bag, `$d31d` onward | Keep the bag count at 195 and the first item pair at Water Stone ×211, so the bytes remain `c3 22 d3`. Arrange the later item fields for the receiver and show that the required menus and map transitions leave them intact. | | Expanded PC and script state, `$d5f4` | Set PC item slot 93's quantity to 16 through controller input, then leave and return to Viridian without disturbing the prepared bytes or required map state. | This route is tied to the ROM build and to the bank selected when Viridian dispatches its script. A different *Red* or *Blue* version, localization, or ROM hack may produce a different OOB pointer and place the box, Pokédex, bag, PC inventory, and script fields at different addresses. Porting the route would require deriving that pointer and memory layout again. ### Tracing the instruction streams Two stretches of game data still need instruction-level proof: - The fifteen bytes at `$dac9..$dad7`, fetched through their Echo RAM aliases at `$fac9..$fad7`, must lead to `$fad8` on an instruction boundary. - The twenty-nine Pokédex bytes at `$d300..$d31c` must lead to the bag count at `$d31d`; the Echo RAM variant must reach `$f31d`. Checking only the first and last addresses is not enough. Operands can cross field boundaries, and conditional branches depend on the flags left by earlier instructions. A trace of the final prepared bytes must show the exact instruction boundaries, every branch and memory write, and the machine state that reaches each jump. It must also show that execution stays within the intended range, preserves the required registers and stack, does not alter `SP` or enable interrupts unexpectedly, and does not stop early. ### Receiver entry and continuous replay Reaching `$d322` proves the bag jump, but not the receiver. The item bytes from bag slot 3 onward must contain a receiver or a transfer to one. Before that receiver can safely load a larger program, the route must establish: - its entry registers, flags, and stack, including whether the original return path remains usable; - the active ROM bank and the interrupt, LCD, and joypad state; and - a write destination that does not overlap the receiver, a maximum payload length, and an integrity check. The Hall-of-Fame receiver described later starts under different conditions and cannot simply be transplanted into this route. ## Alternatives to Viridian script `$10` The thread also considers several routes around script `$10`. Each makes one part of the setup easier while introducing a different constraint: | Route | What the thread established | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | *Red/Blue* Viridian script `$11` | `Torchickens` showed that its OOB read takes `61 d3` from `$501f..$5020` and jumps to `$d361`, the player's current Y-coordinate field. `metalmario32` noted that a map transition overwrites this field, so any jump or code placed there must be restored before each use. | | *Red/Blue* map-script pointer at `$d36e..$d36f` | In an overextended carried bag, slot 41 overlaps the map-script pointer. `Torchickens` proposed writing Water Stone ×211 (`22 d3`) there to redirect the map script to `$d322`, and confirmed the mapping on a cartridge. The game later rewrites the pointer, so the item stack must be restored before the route uses it again. | | Item code at `$d322` | `$d322` is the item ID byte in the third bag slot. `Torchickens` treated it as an entry for both script `$10` and script `$11`. The `$11` route can prepare the ACE item `8F` and its party data, or prepare script `$10` itself. `8F` provides a documented, repeatable bootstrap and bypasses the 29 Pokédex bytes; script `$10` may avoid the ACE-item prerequisite. | | *Yellow* Pallet Town script `$12` | `metalmario32` described a jump to `$faae/$daae`, followed by the tail of a boxed Pokémon record and raw `Z4` species `$c3`. The trigger byte aliases the item ID in PC slot 92, which is harder to replenish than a quantity. `Torchickens` suggested the SRAM glitch as another way to obtain the expanded PC. We have not independently checked these addresses against a pinned *Yellow* ROM. | | `ws m`, `4F`, and Sea Route 21 textbox `$44` | These were the established *Yellow* ACE routes against which `metalmario32` compared the Pallet Town proposal. They provide repeatable entry and more room for code, although the thread gives no measured comparison of input count or execution time. | > [!fold]- Why bag slot 41 contains the pointer > > On the pinned build, `wCurMapScriptPtr` occupies `$d36e..$d36f`. Its declaration appears in [`ram/wram.asm`](https://github.com/dannye/pokered-gbc/blob/c1a3b6c5a7591472241036d0cf09c3817f841f93/ram/wram.asm): > > ```asmatmel > wCurMapDataPtr:: dw > wCurMapTextPtr:: dw > wCurMapScriptPtr:: dw > wCurMapConnections:: db > ``` > > The carried bag begins at `$d31e`, and each item stack occupies two bytes. Slot 41 therefore begins at `$d31e + 2*(41-1) = $d36e`, exactly where the map-script pointer is stored. In an overextended bag, the game exposes the pointer as an item ID and quantity. The PC inventory begins elsewhere, at `$d53b`, and does not supply these bytes. A reference to “expanded items” must therefore specify whether it means the carried bag or the PC inventory. </br> ## A program in the Hall-of-Fame `b33f` and I began with the Viridian thread's central observation: a raw species byte can also be a CPU instruction. We then asked where Pokémon could preserve enough player-chosen bytes to build more than a single jump. The Hall-of-Fame gave us that space. Each League victory archives the winning party as 16-byte records whose species, level, and nickname bytes can all be chosen in play. We arranged twenty such records so that the same bytes also form CPU instructions. Several begin with raw `$c3`, using the original species-byte idea to supply jumps within the program. Those records are normally only saved data; the CPU needs a reason to execute them. A second malformed species, raw `$c4`, supplies that reason. Its front sprite reaches the alignment routine with both width and height set to zero. The counters wrap, and the resulting copy runs far beyond the sprite buffers, moving the selected Hall-of-Fame bytes into work RAM and overwriting the audio engine's bank state. When VBlank runs the audio engine, the altered bank selection carries execution into the copied program. The copied program is a byte writer. It accepts one controller-selected byte, stores it in the `$ff70..$ffff` destination range, and returns for the next. We use those writes to install a 107-byte receiver called Stage 0 in high RAM (HRAM). Stage 0 accepts a 2,304-byte native VM, and the VM then accepts a 499-byte Pong cartridge. ```text 21-team Hall-of-Fame archive | | raw $c4 sprite copy v selected record bytes copied into WRAM | | corrupted audio-bank path v returning byte writer | | controller input: 144 writes v 107-byte Stage 0 receiver in HRAM | | controller input: 2,304-byte native VM v VM at $c000 ---> VM LOADING screen | | controller input: 506-byte packet | (499-byte Pong cartridge) v playable Pong ``` The continuous exploit replay starts at `pre-Align`, the instruction immediately before the malformed sprite copy. The Hall-of-Fame records and required machine state are already in memory. From there, one fixed sequence of controller inputs runs continuously through the writer, Stage 0, the VM, and Pong. We repeated that sequence from a complete 21-team archive whose parties and records obey the game's serialization rules; it produced the same receiver, VM, cartridge, and final Pong framebuffer. We also recorded how the game can produce raw `$c3` and `$c4`. Both routes use Trainer-Fly and Ditto's Transform. Escaping from a trainer leaves a wild encounter pending. A wild Ditto then copies the Special stat of a Pokémon we choose. When the player returns to the escaped trainer's map, the copied value selects the species for the pending encounter. Special 195 produces raw `$c3`, which survives capture and direct Hall-of-Fame serialization. Special 196 produces raw `$c4` on Route 21 and reaches the malformed sprite path. These replays establish that both species can be acquired through game mechanics. The `$c3` catch also reveals a useful mismatch between the species byte the game stores and the Pokémon it draws. ### Catching raw `$c3` The acquisition replay starts from a prepared Route 8 save on the pinned full-color fork. The encounter setup is already in place, including a level-100 Pidgeotto with a calculated Special stat of 195. From reset onward, every action comes from controller input. The replay covers the encounter and catch; it does not include the earlier play used to prepare the Pidgeotto. The fixed input sequence takes us through three stages: 1. *Copy Special.* Ditto uses Transform on Pidgeotto, copying its Special value. Decimal 195 is hexadecimal `$00c3`, whose low byte is `$c3`. 2. *Resolve the pending encounter.* We run from Ditto and return to the escaped trainer's map. Trainer-Fly uses the copied low byte as the raw internal species; the adjacent byte supplies level `$07`. 3. *Wait out the cry and catch.* The malformed sound `$43` is long but finite: it is still active at replay frame 30,618 and clears after 3,704 more frames with no buttons held. We then use a Master Ball, leaving raw `$c3` in the party at level 7. #### Gengar's sprite, Rhydon's colors `IndexToPokedex` converts an internal species ID into a Pokédex number. For raw `$c3`, it subtracts one and uses `$c2` as an index into a 190-entry order table. That index falls four bytes beyond the table, where the ROM contains `$5e`: Gengar's Pokédex number. The game therefore draws Gengar, prints `No.094`, and uses Gengar's battle palette. The status screen chooses its colors through a different lookup. Raw IDs at or above `$bf` are clamped to internal species `$01`, Rhydon, so the same Pokémon appears with Gengar's sprite and Rhydon's colors. The name routine also reads past the end of its ROM table, although its copy remains bounded to ten bytes. The catch replay reproduces these out-of-bounds reads; none of them provides an arbitrary write or transfers control to the stored data. At the final checkpoint, we record the raw species byte alongside both display lookups. | Observation | Value | What it establishes | |---|---:|---| | `wCurPartySpecies` | `$c3` | the selected party member retains the raw internal species | | `wMonHIndex` | `$c3` | the status-screen consumer begins from that same raw id | | `wPokedexNum` | `$5e` | the unchecked order-table lookup resolves to No.094, Gengar | | CGB background palette 0 | `ff 7f 6f 45 27 11 00 00` | the four RGB555 colors decode to `(31,31,31)`, `(15,11,17)`, `(7,9,4)`, `(0,0,0)`: exactly `PAL_RHYDON` | <figure> <img src="attachments/pokemon-authentic-c3-status.png" alt="Status screen showing Gengar's sprite with Rhydon's palette after the raw $c3 catch" width="640" height="576" style="image-rendering: pixelated;"> <figcaption>The status screen reached by the catch replay using fixed controller inputs, displayed at 4×. Gengar has Rhydon's palette; the memory observations above establish that both species fields still hold <code>$c3</code>.</figcaption> </figure> ### Sixteen bytes in the Hall-of-Fame Hall-of-Fame serialization copies a party member's raw species, current level, and eleven nickname bytes into a 16-byte record. The remaining three bytes are zero padding. A record can therefore keep the shape of game data while its species, level, and nickname also encode instructions. The serialization routine in the pinned [`engine/movie/hall_of_fame.asm`](https://github.com/dannye/pokered-gbc/blob/c1a3b6c5a7591472241036d0cf09c3817f841f93/engine/movie/hall_of_fame.asm) writes those fields without translating the raw species: ```asmatmel HoFRecordMonInfo: ld hl, wHallOfFame ld bc, HOF_MON ld a, [wHoFPartyMonIndex] call AddNTimes ld a, [wHoFMonSpecies] ld [hli], a ld a, [wHoFMonLevel] ld [hli], a ld e, l ld d, h ld hl, wNameBuffer ld bc, NAME_LENGTH jp CopyData ``` The first three bytes are particularly useful when the species is raw `$c3`: | Record offset | Pokémon meaning | Instruction meaning in three executed records | |---|---|---| | `+0` | raw species `$c3` | opcode `JP nn` | | `+1` | level `$58` or `$37` | low destination byte | | `+2` | first nickname byte `$a7`, `$81`, or `$ad` | high destination byte | | `+3..+12` | remaining nickname bytes and terminator | later opcodes, operands, or skipped data | | `+13..+15` | zero padding | three zero bytes | Thus a raw `$c3` record supplies a complete three-byte jump. The CPU reads a little-endian destination from the next two bytes: the level supplies the low byte and the first nickname byte supplies the high byte. The three executed prefixes are: | Record prefix | CPU interpretation | |---|---| | `c3 58 a7` | `JP $a758` | | `c3 37 81` | `JP $8137` | | `c3 58 ad` | `JP $ad58` | The three destinations serve different parts of the copied program. `$ad58` enters the setup record that leads to the writer, `$a758` waits for the next controller-selected byte, and `$8137` receives control after the final store. Raw `$c3` supplies only the `JP nn` opcode; the level and nickname bytes determine where it jumps. The complete archive contains 21 saved teams, which we call H1 through H21. Each team is stored as up to six consecutive member records. If a team has fewer than six members, the next slot begins with `$ff` and the remaining space is cleared. Our twenty instruction-bearing records occupy fixed positions across those teams, so some earlier positions need ordinary Pokémon records to keep the selected bytes at the right addresses. We fill those positions with level-100 Rhydon whose nicknames contain ten spaces, all selectable through the normal naming screen. The Rhydon records, `$ff` terminators, and trailing zeros form valid serialized teams without moving any selected record. Replaying from the completed archive preserves the sprite copy, audio transfer, writer, and stack behavior all the way through Pong. > [!fold]- Complete archive layout and receiver-entry check > > At `pre-Align`, with the CPU at `$16c2` before the malformed copy, every selected record retains its species, level, nickname, terminator, padding, and exact required address. > > The filler record is a Rhydon at level 100 (`$64`), nicknamed with ten spaces: > > | Record offsets | Stored bytes | Meaning | > |---|---|---| > | `+0` | `$01` | Internal species: Rhydon | > | `+1` | `$64` | Level 100 | > | `+2..+11` | Ten `$7f` bytes | Ten space glyphs | > | `+12` | `$50` | Nickname terminator | > | `+13..+15` | Three `$00` bytes | Zero padding | > > The completed teams have these party sizes: > > | Team | Members | Team | Members | Team | Members | > |---|---:|---|---:|---|---:| > | H1 | 6 | H8 | 3 | H15 | 6 | > | H2 | 4 | H9 | 1 | H16 | 1 | > | H3 | 5 | H10 | 1 | H17 | 1 | > | H4 | 6 | H11 | 1 | H18 | 1 | > | H5 | 6 | H12 | 1 | H19 | 1 | > | H6 | 4 | H13 | 1 | H20 | 1 | > | H7 | 5 | H14 | 1 | H21 | 5 | > > The archive contains 41 filler appearances and 17 `$ff` terminators. No team needs more than five fillers. > > Replaying the unchanged prefix from this archive in memory took 2,592 actions over 2,603 frames. It installed the exact 107-byte Stage 0, matched the recorded prefix's final CPU registers, and left `$ff70..$ff7f` zeroed. > [!fold]- Building the archive through play > > Creating the complete archive through ordinary play would take 21 League victories, with the party arranged for each championship in turn. Each record is a snapshot, so a Pokémon can appear in several teams after being trained or renamed, provided its later records do not require a lower level. > > The raw `$c3` records require at least two Pokémon because an earlier team needs one at level 88 and a later team needs one at level 55. Other selected Pokémon can be reused as their required levels rise. Up to five level-100 Rhydon can fill the remaining positions and carry the teams through the League. Once a selected member reaches its required level, it must avoid any further experience before that victory is recorded. > > Each championship follows the same preparation: > > | Stage | Required action | > |---|---| > | Before the League | Bring the selected Pokémon to their exact levels and enter their nicknames through the naming screen or Name Rater. | > | Party assembly | Deposit and withdraw the selected members and Rhydon fillers in the required order, then check the party count. | > | Victory | Win the League and let the Hall-of-Fame routine save the team and its `$ff` terminator. Repeat in order for all 21 teams. | > > We have replayed the naming and direct Hall-of-Fame save and confirmed that raw `$c3` survives both. A complete acquisition still needs controller replays for two steps: obtaining the level-4 Jynx used by one selected record, and training raw `$c3` to its recorded levels without changing the species byte. The final handoff relies on `$e9` already being present in VRAM at `$8137`. It remains unchanged throughout the replay, but its origin before the prepared starting state has not been traced. ### A zero-by-zero sprite The trigger replay obtains raw `$c4` through the same Trainer-Fly/Ditto mechanism. The player escapes a Route 21 Swimmer, completes the fixed trainer sequence on Route 15, and lets a wild Ditto copy Special 196. Returning through Pallet to Route 21 resolves the pending encounter as raw `$c4`. Route 21 keeps its normal sprite set, `$01`. When the game tries to draw the raw `$c4` opponent, the front-sprite decompressor starts a 447-byte stream at `$8e8f`. After consuming 86 bytes, it calls `AlignSpriteDataCentered` with both sprite dimensions set to zero. In that trigger replay, the alignment routine begins at frame 3,628 with `DE=$a188`, `HL=$a000`, and `SP=$dfef`. Two frames later, while its copy is still running, VBlank enters the audio dispatcher. The audio code therefore runs after the copy has overwritten the audio state but before the alignment routine returns. The pinned [`home/pics.asm`](https://github.com/dannye/pokered-gbc/blob/c1a3b6c5a7591472241036d0cf09c3817f841f93/home/pics.asm) contains the faulty loop. Both dimensions are checked only after the copy body has run. Starting from zero therefore wraps each counter to `$ff`: the routine treats a zero-by-zero sprite as 256 columns of 256 bytes each. ```asmatmel AlignSpriteDataCentered:: ldh a, [hSpriteOffset] ld b, $0 ld c, a add hl, bc ldh a, [hSpriteWidth] .columnLoop push af push hl ldh a, [hSpriteHeight] ld c, a .columnInnerLoop ld a, [de] inc de ld [hli], a dec c jr nz, .columnInnerLoop pop hl ld bc, 7 * TILE_1BPP_SIZE add hl, bc ; advance one full column pop af dec a jr nz, .columnLoop ret ``` Each inner loop copies 256 bytes. After a column, the source has advanced by `$100`, but the destination base advances by only `$38` (56 bytes). Adjacent destination windows therefore overlap by 200 bytes. If all 256 columns ran, the routine would perform 65,536 stores: ```text outer columns 256 bytes per column 256 total stores 65,536 source $a188 + $100*k (modulo 65,536) destination $a000 + $38*k destination window [$a000+$38*k, $a0ff+$38*k] covered band $a000..$d8c7 ``` Over the full loop, `DE` would traverse the entire 16-bit address space and return to `$a188`. The successful replay never gets that far. VBlank enters the audio dispatcher during column 225, and the wrong-bank path carries execution away from the alignment routine. By then, the copy has read through SRAM and WRAM, overwritten the audio and stack-related state, and moved selected Hall-of-Fame bytes to the WRAM addresses the CPU will execute. This is an in-place copy: a byte written during an early column can become the source of a later one. The initial memory image alone therefore cannot tell us which byte the CPU will eventually fetch. We followed every executed byte through the intervening copies and stack writes. That check rejected an earlier entry at `$c278`, where stack writes destroyed the intended instructions before the CPU reached them. By the time the audio dispatcher runs, ordinary Hall-of-Fame fields have new roles: | Saved field | Role after the copy | |---|---| | Species, level, and nickname bytes | CPU opcodes and operands | | Legal nickname glyph `$9d` | Audio-bank selector that begins the transfer | | Record padding `$00` | `NOP` instructions or zeroed state | ### The audio engine changes banks for us Before the relevant VBlank dispatch, the copy has placed nine controlled bytes at `$c0e8..$c0f0`: ```text address copied bytes role $c0e8..$c0ee 37 40 7f 7f 7f 7f 7f $c0ef 9d current audio bank $c0f0 37 saved audio bank ``` The game calls `$c0ef` `wAudioROMBank` and `$c0f0` `wAudioSavedROMBank`. VBlank far-calls `FadeOutAudio`, reads `$9d` as the current bank, writes it to the cartridge mapper, and calls the address normally used for `Audio3_UpdateMusic`. On this cartridge, bank `$9d` mirrors to physical bank `$1d`, so the same address now names unrelated code. The pinned [`home/vblank.asm`](https://github.com/dannye/pokered-gbc/blob/c1a3b6c5a7591472241036d0cf09c3817f841f93/home/vblank.asm) shows the current bank being selected immediately before the fixed-address call: ```asmatmel farcall FadeOutAudio ld a, [wAudioROMBank] ; music ROM bank ldh [hLoadedROMBank], a ld [rROMB], a cp BANK(Audio1_UpdateMusic) jr nz, .checkForAudio2 .audio1 call Audio1_UpdateMusic jr .afterMusic .checkForAudio2 cp BANK(Audio2_UpdateMusic) jr nz, .audio3 .audio2 call Music_DoLowHealthAlarm call Audio2_UpdateMusic jr .afterMusic .audio3 call Audio3_UpdateMusic .afterMusic ``` #### From nested VBlank to WRAM The first wrong-bank call does not reach WRAM directly. It re-enters VBlank hundreds of times, moving the stack downward until stack writes turn on the audio fade. At the first music dispatch, `PC=$2096`, `SP=$dfd7`, and `IME=0`, with physical bank `$1d` selected. The `CALL $5177` at ROM0 `$2096` therefore enters unrelated text and map data in bank `$1d`. At `$1d:$5185`, byte `$ff` executes as `RST $38`. ROM0 `$0038..$003f` contains eight zero bytes, which execute as `NOP`s and fall through to the VBlank vector at `$0040`. `IME=0` prevents the CPU from accepting a hardware interrupt, but it does not restrict which addresses normal instruction flow may reach. Because the `NOP`s fall through to `$0040`, the CPU begins executing the VBlank handler anyway. `InterruptWrapper` runs a nested VBlank, reaches the audio dispatch again, and calls `$1d:$5177` before the previous call has returned. Each pass consumes another 22 bytes of stack: ```text first audio dispatch, physical bank $1d | v ROM0:$2096 CALL $5177 | v bank $1d:$5177 data executes as code | $ff -> RST $38 | v ROM0:$0038 eight NOPs -> VBlank vector $0040 | v nested VBlank and audio dispatch | +----> repeat; SP falls by 22 bytes | v stack reaches $cfc7..$cfc9 | FadeOutAudio counts down | v fadeOutComplete | PlaySound in bank $9d returns | restore saved bank $37 | PlaySound in bank $37: $58ea | HL = SP - $2f = $c20a | v JP HL -> WRAM ``` For the first 186 nested VBlanks, `wAudioFadeOutControl` remains zero, so `FadeOutAudio` has nothing to do. The next VBlank prologue pushes registers across the audio fade fields: | Address | Stack-written value | New meaning | |---|---:|---| | `$cfc7` | `$70` | fade control | | `$cfc8` | `$3f` | counter reload value | | `$cfc9` | `$9d` | fade counter | The fade now counts `$cfc9` down from `$9d`. On the 158th active pass, the counter reaches zero and reloads with `$3f`. Because `rAUDVOL` is already zero, the branch at `$01:$4137` enters `FadeOutAudio.fadeOutComplete` at `$01:$414a`. The replay has entered the wrong-bank code at `$1d:$5177` 344 times: 186 passes before the stack activates the fade, followed by 158 active fade passes. Fade completion makes the final handoff. The path in [`audio/fade_audio.asm`](https://github.com/dannye/pokered-gbc/blob/c1a3b6c5a7591472241036d0cf09c3817f841f93/audio/fade_audio.asm) calls `PlaySound` to stop the old music, restores the saved audio bank, and calls `PlaySound` again: ```asmatmel .fadeOutComplete ld a, [wAudioFadeOutControl] ld b, a xor a ld [wAudioFadeOutControl], a ld a, SFX_STOP_ALL_MUSIC ld [wNewSoundID], a call PlaySound ld a, [wAudioSavedROMBank] ld [wAudioROMBank], a ld a, b ld [wNewSoundID], a jp PlaySound ``` The selected bank changes between those two calls: 1. With `wAudioROMBank=$9d`, the first call enters the wrong-bank code at `$1d:$58ea`. That byte sequence ends in `JP $24dc`; a `RET` at `$24df` reaches the normal `PlaySound` epilogue. 2. `FadeOutAudio` reads the saved bank `$37` from `$c0f0`, writes it to `$c0ef`, and calls [`PlaySound`](https://github.com/dannye/pokered-gbc/blob/c1a3b6c5a7591472241036d0cf09c3817f841f93/home/audio.asm) again. At `$37:$58ea`, the fixed address of `Audio3_PlaySound`, `LD HL,SP-$2f` converts `SP=$c239` into `HL=$c20a`. Nothing changes either register before `JP HL` at `$37:$58f5`, which transfers control to WRAM. Execution begins at `$c20a`. Fourteen zero bytes act as `NOP`s and carry it to `$c218`, where `c3 58 ad` means `JP $ad58`. That jump enters the Hall-of-Fame setup record, whose continuation reaches the writer loop at `$a758`. ### One byte per return The copied Hall-of-Fame program is a returning byte writer. On each pass, it builds one byte from controller input, stores that byte in its destination range, and returns to `$a758` to wait for the next one. Register `B` holds the low byte of the destination address, so its values `$70` through `$ff` select `$ff70..$ffff`: I/O at `$ff70..$ff7f`, HRAM at `$ff80..$fffe`, and the interrupt-enable register `IE` at `$ffff`. Each output byte begins with eight controller samples. We use `A` for a zero bit and `A+B` for a one bit, sent most-significant bit first. `A+Start` commits the completed byte: ```text 8 controller samples A = 0 bit A+B = 1 bit | v accumulate one output byte in A | | A+Start v C <- B preserve the current destination B <- B + 1 advance the cursor [$ff00+C] <- A | +-- B != 0 --> return to $a758 for another byte | `-- B == 0 --> leave the writer after storing at $ffff ``` The writer sees raw joypad samples rather than clean zero and one values. On pinned PyBoy 2.7.0, `A` produces `$67` and `A+B` produces `$66`. Its shift-and-add sequence turns eight samples into `$99` minus the encoded input byte. To write a desired value `y`, we therefore send the bits of `($99 - y) & $ff`. This mapping belongs to the pinned emulator; a physical-hardware sender would need to measure the samples and derive its encoding again. When `A+Start` commits the byte, the store handler copies `B` into `C`, advances `B`, and writes `A` to `$ff00+C`. For the first 143 stores, the new value of `B` is nonzero and the copied code returns to `$a758`. Test bytes `$5a,$a5` landed at `$ff80,$ff81`, confirming that each pass performs one store and advances the cursor once. Stage 0 requires 107 executable bytes, but the writer issues stores across the entire 144-byte range from `$ff70` through `$ffff`. That takes 1,152 input bits: | Range | Contents | Purpose | |---|---:|---| | `$ff70..$ff7f` (I/O) | 16 × `$00` fetched in this replay | `NOP` runway before Stage 0 | | `$ff80..$ffea` (HRAM) | 107 bytes | Stage 0 receiver | | `$ffeb..$fffe` (HRAM) | 20 × `$00` | fallthrough and padding | | `$ffff` | `$00` | clear the interrupt-enable register `IE` | The last store also prepares the jump into Stage 0. `INC B` wraps the cursor from `$ff` to `$00` before `LDH [C],A` writes to `$ffff` through preserved `C=$ff`, so the writer falls through instead of returning to `$a758`. The final zero byte leaves the flags in a state that the copied code converts into `HL=$ff70`. A raw `$c3` record now executes `JP $8137`. The inherited byte `$e9` at that VRAM address means `JP HL`, sending the CPU to `$ff70`. In this replay, the CPU fetches sixteen zero bytes from that I/O range, executes them as `NOP`s, and reaches `$ff80`, where `$f3` is `DI`, the first instruction of Stage 0. The replay observes `$8137=$e9` from its declared starting state through this fetch, but does not establish the byte’s earlier origin. > [!fold]- Instruction trace, stack movement, and Stage 0 entry registers > > The bit recurrence at `$a7a2` shifts the accumulator left and adds the raw joypad sample held in `H`: > > ```asmatmel > $a7a2: e1 pop hl > $a7a3: f1 pop af > $a7a4: f5 push af > $a7a5: 87 add a, a > $a7a6: 84 add a, h > $a7a7: f5 push af > ``` > > If the intended bit is `b`, then `H=$67-b`. After eight shifts, the original accumulator value has disappeared modulo 256, while the baseline `$67` contributes `$67 * 255 = $99` modulo 256: > > ```text > A after eight samples = $99 - encoded input byte (mod 256) > ``` > > The store handler preserves the current cursor in `C`, advances `B`, and writes the selected byte: > > ```asmatmel > $a7e8: 48 ld c, b ; preserve the current destination low byte > $a7e9: 04 inc b ; advance the cursor for the next cycle > $a7ea: e2 ldh [c], a ; [$ff00 + C] = selected byte > ``` > > Later copied bytes branch on the zero flag set by `INC B`: > > ```asmatmel > $a800: 4e ld c, [hl] > $a801: 20 9f jr nz, $a7a2 > ``` > > The three-instruction handler balances its own stack use, but a complete receive cycle passes through more copied records and leaves `SP` fourteen bytes lower. By the final cycle, the handler enters with `SP=$e9fd`. Across the transfer, the stack uses Echo RAM `$e9f9..$f1e0`, which aliases physical WRAM `$c9f9..$d1e0`. Every traced `POP` receives the value from its most recent `PUSH`, including values written through those aliases. > > On the final store, `INC B` wraps the cursor to zero but preserves the carry left by the final sample addition. `SBC A,A` produces `AF=$ff70`, and `PUSH AF; POP HL` makes `HL=$ff70`. > > At the first Stage 0 fetch, `IME` is the CPU's master interrupt enable, `IE` is the interrupt-enable mask, and `IF` holds pending interrupt requests: > > ```text > PC=$ff80 SP=$e9fd AF=$ff70 BC=$0000 DE=$001c HL=$ff70 > IME=0 IE=$00 IF=$13 ROM bank=$37 > buttons=A+Start receiver phase=wait-release > ``` The emulator framebuffer remains black throughout these writes. The LCD is running with `LCDC=$e3`, but both CGB palette memories contain only zeros, leaving no visible colors to draw. The evidence for this part of the route comes from the instruction and store traces, which cover roughly 65 million instructions. ## A receiver in 107 bytes The returning writer has done its job once Stage 0 is executable at `$ff80..$ffea`. Those 107 bytes form a second, simpler loader: they receive the 2,304-byte native VM, place it at `$c000..$c8ff`, check the transfer, and jump to the new program. Stage 0 first gives itself a predictable machine state. It disables interrupts, moves the stack to `$d800`, points `HL` at `$c000`, sets a 2,304-byte counter, and clears an XOR accumulator. From then on, it treats the controller as a framed byte stream. The Game Boy reads the direction buttons and action buttons as two separate rows. We use that separation to give each button press two meanings: Up marks the arrival of a nibble, while `A`, `B`, `Select`, and `Start` carry its four bits. Two strobed nibbles make one byte, low half first: ```text low nibble high nibble Up + A/B/Select/Start release Up + A/B/Select/Start release hold 2 frames 4 frames hold 2 frames 4 frames | | +--------------- one byte -------------+ A = bit 0 B = bit 1 Select = bit 2 Start = bit 3 ``` The receiver waits for Up, saves the four action-button bits, then waits for Up to be released before accepting another nibble. Because each press and release lasts several frames, the busy-polling loop sees every transition regardless of where it begins within a frame. The handoff from the writer is continuous. `A+Start` remains held after the writer's final commit. The next input holds all four action buttons for four frames, then releases them for four. Up is absent, so Stage 0 treats this as settling time and continues waiting. The first Up strobe begins the VM data. The packet has a deliberately small shape: ```text 2,304 VM bytes final check byte $c000 --------------------------------> $c8ff | \________ running XOR _____________/ | \__________________________/ must equal $00 ``` Stage 0 XORs the body bytes as it writes them. The sender appends that XOR as the 2,305th byte, so XORing the check byte into the accumulator must produce zero. A nonzero result enters a permanent fault loop; zero accepts the transfer. Each of the 2,305 bytes requires two strobe-and-release pairs. Including the initial marker and release, the transfer takes 9,222 controller actions over 27,668 emulator frames. The XOR can miss compensating errors, so the replay also compares all 2,304 installed bytes with the VM image that was sent. > [!fold]- The 107-byte receive loop > > The main loop fixes the destination and length before reading any VM bytes: > > ```asmatmel > stage0: > di > ld sp, STACK_INITIAL > ld hl, DESTINATION > ld bc, RESIDENT_SIZE > ld e, 0 > .receive: > call readByte > ld [hli], a > xor e > ld e, a > dec bc > ld a, b > or c > jr nz, .receive > call readByte > xor e > jr nz, fault > jp DESTINATION > > fault: > jr fault > ``` > > `readByte` combines the low nibble and high nibble. `readNibble` captures the action-button row on an Up press and does not return until Up has been released: > > ```asmatmel > readByte: > push de > call readNibble > ld d, a > call readNibble > swap a > or d > pop de > ret > > readNibble: > .press: > call sample > bit 6, a > jr z, .press > push af > .release: > call sample > bit 6, a > jr nz, .release > pop af > and $0f > ret > ``` > > The `sample` helper selects the controller's direction row and action row in turn. It reads the selected rows six and ten times respectively before combining them; bit 6 of the result is Up, and the low nibble contains the four data bits. The complete source is in [`stage0.asm`](stage0-joypad-loader-v1/stage0.asm). ## The native VM boots Stage 0's final jump starts the native VM at `$c000`. Its 2,304-byte image contains the cartridge receiver, bytecode interpreter, graphics operations, and loading-screen artwork. The Pong program is sent afterward as a separate bytecode cartridge. The native program first takes control of the machine state left by Pokémon. It replaces the stack, silences interrupts, rebuilds the display, and establishes the memory that the interpreter will use: ```text Stage 0: JP $c000 | v disable interrupts; set SP=$d800; clear interrupt requests and enables | v wait for VBlank; switch the LCD off safely | v clear $d000..$d3ff and both CGB tile-map banks | v unpack 93 tiles into VRAM; install palette and sprite-DMA helper | v enable the LCD; reveal VM LOADING | v publish READY=5601 and wait for a cartridge ``` From `$c000` onward, the boot code uses its own routines and the Game Boy's hardware registers. It does not return to Pokémon. Its 2,304-byte image and working memory are laid out as follows: | Range | Contents | |---|---| | `$c000..$c3ae` | 943 bytes of native receiver, interpreter, handlers, palette, and tile decoder | | `$c3af..$c7f4` | 1,094 packed bytes that expand to 1,488 bytes, or 93 Game Boy tiles | | `$c7f5..$c8d4` | 224-byte map for the 16×14 loading composition | | `$c8d5..$c8ff` | 43 zero bytes completing the fixed image size | | `$d000..$d1ff` | buffer for a bytecode cartridge of at most 512 bytes | | `$d200..$d20f` | sixteen one-byte VM registers | | `$d210..$d221` | bytecode PC, flags, seed, cartridge length, and READY status | | `$d300..$d39f` | shadow copy of the Game Boy's forty sprite records | | `$d400..$d7ff` | native stack, entered with `SP=$d800` | | `$ff80..$ff89` | ten-byte helper that copies the shadow into the hardware sprite table (OAM) | There is a ten-byte overlap at `$ff80..$ff89` between Stage 0 and the VM's sprite-transfer helper. Once Stage 0 has transferred control to `$c000`, the receiver has finished its work and the VM can safely overwrite those bytes. With the hardware initialized, the VM reveals the `VM LOADING` artwork from left to right. The screen remains charcoal for 36 VBlanks, sixteen occupied 8×112-pixel columns appear at six VBlanks each, and the finished image rests for another 37. The complete sequence lasts 169 VBlanks. The Game Boy draws the entire custom boot screen itself. Only after the final hold does the VM write `$56,$01` to its status bytes; we call this checkpoint `READY=5601`. The cartridge buffer is still empty, and the cartridge length and seed are zero. This is the point at which the virtual machine is running but Pong does not yet exist inside it. <figure> <img src="attachments/pokemon-boot-art-preview.png" alt="Native VM LOADING artwork with a filled block VM mark, LOADING text, circuit details, and 0xb33f signature" width="640" height="576" style="image-rendering: pixelated;"> <figcaption>The completed VM-generated frame at <code>READY=5601</code>, displayed at 4×. The Pong cartridge is absent at this checkpoint.</figcaption> </figure> ### A separate packet for the cartridge The completed loading screen remains visible while the VM receives its bytecode cartridge through the controller. The controller uses the same Up-strobed nibbles as before. The VM's receiver first waits for all four action buttons and their release, which mark the start of a packet. It then reads these fields: ```text all action buttons release | | +--------+---------+ v +-------------+----------+------+------------------+-------+ | 56 4d 01 | length | seed | cartridge bytes | XOR | +-------------+----------+------+------------------+-------+ "VM", v1 2 bytes 1 byte exactly length 1 byte $0001..$0200 bytes -> $d000 ``` The two-byte length is little endian and limits the cartridge to 1 through 512 bytes, the size of `$d000..$d1ff`. The one-byte seed is made available to the cartridge after loading. Every byte from `$56` through the end of the cartridge contributes to the running XOR; the final byte must reduce it to zero. Until the marker appears, the receiver keeps waiting. A bad header, length, or check enters the fault loop before any cartridge instruction runs. Pong contributes 499 cartridge bytes. With the two-byte `VM` identifier, one-byte version, two-byte length, one-byte seed, and final check, its complete packet is 506 bytes. The recorded transfer takes 2,026 controller actions over 6,080 frames. After the complete packet passes every check, the VM sets its bytecode program counter to `$d000`, publishes `READY=5602`, and dispatches the first opcode. ### Sixteen bounded bytecode operations The VM keeps its own two-byte program counter at `$d210..$d211`. It advances through the cartridge at `$d000`, while the Game Boy CPU continues executing the native image that begins at `$c000`. A cartridge byte is therefore an instruction only to the VM; the CPU never fetches it as LR35902 code. Each instruction begins with an opcode from `$00` through `$0f`, followed by the operands that operation needs. The dispatcher reads the opcode, rejects values outside that range, and uses it to select one of sixteen native handlers: ```text VM program counter at $d210 | v fetch opcode from the cartridge buffer | v opcode <= $0f? -------- no ------> fault | yes | v read native address from handlers[opcode] | v execute handler in the native VM image | `-------------------------------> dispatch next opcode ``` The sixteen operations form four small groups: | Purpose | Operations | What they provide | |---|---|---| | Frame and input | `FRAME`, `KEYS` | Wait for the next display frame, copy sprites to OAM, and sample the controller | | Register work | `SET`, `MOV`, `ADD`, `ADDI`, `ANDI`, `SEED` | Move constants and register values, perform byte arithmetic, and read the packet seed | | Control flow | `CMPI`, `CMPR`, `BR`, `JMP` | Compare values and branch to cartridge-relative destinations | | Display and stop | `SPR`, `TILE`, `CLEAR`, `STOP` | Update a sprite, write a background tile, clear the display, or stop execution | Every operand is checked before the handler uses it: | Operand or access | Limit | |---|---| | Instruction or operand fetch | Must remain inside the received cartridge | | Register index | `$00..$0f` | | Branch or jump destination | Must remain inside the received cartridge | | Background coordinate | x below 20 and y below 18 | | Sprite slot | `$00..$27`, the Game Boy's forty available records | Any failed check enters the same fault loop used by the packet receiver. The instruction set also has no arbitrary-memory operation: cartridge code cannot directly rewrite the native VM, its own bytecode, or the interpreter state. The full encoding is in the public [ACE-ROM instruction-set definition](https://github.com/FuzzySecurity/ACE-ROM/blob/main/src/isa.inc). > [!fold]- One arithmetic instruction and the frame handler > > For example, `vADDI 0, 2` compiles to `04 00 02`: opcode `$04`, register `$00`, and immediate value `$02`. The dispatcher selects `opAddi`, which adds two to `r0` and returns to the dispatcher: > > ```text > cartridge bytes native handler result > > +--------+--------+--------+ +-------------+ > | 04 | 00 | 02 |-->| opAddi |------> r0 <- r0 + 2 > +--------+--------+--------+ +-------------+ > ADDI r0 2 > ``` > > `FRAME` connects this interpreter loop to the display: > > ```asmatmel > opFrame: > call waitFrame > call $ff80 ; HRAM-owned DMA; no game VBlank ISR > jp dispatch > ``` > > The handler waits for VBlank, then calls the ten-byte DMA helper at `$ff80` to copy the VM's forty shadow sprite records into the hardware sprite table. It then dispatches the next cartridge opcode. ## Pong's rules live in the cartridge The second packet leaves a 499-byte Pong program at `$d000`. These bytes are instructions for the VM, not for the Game Boy CPU. They control the ball and paddles, read the controller, judge collisions, keep score, and draw the court. The native image at `$c000` provides the general operations described above. Once that interpreter is in place, Pong arrives as data. ```text Pong at $d000 native VM at $c000 Game Boy +----------------------+ +----------------------+ +--------------+ | state and rules | | fetch and dispatch | | joypad | | movement and scoring |----->| checked operations |----->| VRAM / OAM | | court and sprites | | input and graphics | | frame timing | +----------------------+ +----------------------+ +--------------+ VM bytecode LR35902 code hardware ``` The cartridge first draws the fixed court and initializes the game. It then repeats a loop that reads input, moves the paddles and ball, checks collisions, updates the score, and draws the next frame. Up and Down move the left paddle; the right paddle follows the ball. A serve waits for `A`. Missing a paddle awards the other player a point and returns the ball to the serve state. Scores wrap after nine. The whole live game state fits in the VM's sixteen byte registers. The cartridge uses the bounded sprite and tile operations to turn that state into the ball, two paddles, court, and score shown below. > [!fold]- Register layout and rally logic > > The registers have no fixed game meaning in the VM. Pong assigns them as follows: > > | Registers | Pong meaning | > |---|---| > | `r0`, `r1` | ball x and y | > | `r2`, `r3` | signed x and y velocity | > | `r4`, `r5` | left and right paddle y | > | `r6`, `r7` | left and right score | > | `r8`, `r9` | sampled keys and scratch value | > | `r10`, `r11` | left and right paddle x | > | `r12`, `r13` | waiting-for-serve flag and seed | > | `r14`, `r15` | tile x and y while drawing the court | > > A serve places the ball at x 86 and derives its y coordinate from the packet seed: `(seed & 63) + 52`. Horizontal velocity begins at `+2`; seed parity chooses vertical velocity `+1` or `-1`, and then the seed advances. > > The movement code in the public [Pong bytecode source](https://github.com/FuzzySecurity/ACE-ROM/blob/main/src/pong.asm) adds the two velocity registers to the ball's position, then reverses vertical motion at the court edges: > > ```asmatmel > moveBall: > vADD 0, 2 > vADD 1, 3 > vCMPI 1, 41 > vBR C_GE, bottom > vSET 3, 1 > bottom: > vCMPI 1, 140 > vBR C_LT, left > vSET 3, 255 > ``` > > Here `$ff` is `-1` in one-byte arithmetic. The player paddle moves three pixels per input update; the computer paddle follows the ball one pixel at a time. > > Paddle collisions compare the vertical spans of a four-pixel ball and a 24-pixel paddle. The cartridge records a hit only when these spans overlap: > > ```text > ball: [ballY, ballY + 4) > paddle: [paddleY, paddleY + 24) > > ballY + 3 >= paddleY > ballY < paddleY + 24 > ``` > > A hit on the left paddle clamps x to 28 and sets horizontal velocity to `+2`. A hit on the right clamps x to 152 and sets velocity to `-2` (`$fe`). The ball uses one sprite and each paddle uses three; the court and score use background tiles. The full video includes a point and the sustained rally shown below. <figure> <img src="attachments/pokemon-pong-rally-preview.png" alt="Pong court running under the native VM with paddles, ball, score inset, rails, and center net" width="640" height="576" style="image-rendering: pixelated;"> <figcaption>The 499-byte Pong cartridge running under the VM, displayed at an exact 4× scale.</figcaption> </figure> ## Back to Viridian City, eight years later `b33f` and the rest of the research team loved working through this chain. Each solved boundary drew another part of Pokémon into the machine: saved teams, sprite decompression, the audio engine, the joypad, and finally a second game. The 2018 discussion left real questions behind. We did not complete its exact Viridian route, but we could bring back a concrete answer to the larger one: a raw `$c3` record can begin a control transfer that ends in a receiver, a native VM, and Pong. It is hard not to love a community whose rich technical discussions can still spark new research eight years later. `metalmario32` has explored several other places where ordinary game data can become executable. Their work on [continuous ACE from a Game Corner map script](https://archives.glitchcity.info/forums/board-115/thread-8842) and on [using the wild-Pokémon table as an execution source](https://archives.glitchcity.info/forums/board-115/thread-8838) follows two such paths down to their bytes and destinations. That way of turning a strange possibility into a precise question about machine state was useful throughout our work. `Torchickens` has written extensively about practical Pokémon glitches. Her [Glitch Pokémon Pokédex ACE research](https://archives.glitchcity.info/forums/board-115/thread-7930/page-0.html) maps entries that read from writable memory and shows how their text processing can lead to code execution. Her account of [Viridian Forest ACE](https://archives.glitchcity.info/forums/board-115/thread-8076/page-0.html) connects the map-script path through Echo RAM with emulator behavior, cartridge tests, and the speedrun that used it. We appreciated how often her work returns to the practical question: can a player actually prepare the required bytes? ## A standalone ROM for the VM and Pong We published the VM and Pong as [`ACE-ROM`](https://github.com/FuzzySecurity/ACE-ROM), a standalone 32 KiB Game Boy Color ROM containing the same native VM, loading artwork, and 499-byte Pong cartridge. You can build it from source or [download the built ROM](https://github.com/FuzzySecurity/ACE-ROM/raw/refs/heads/main/release/vm-pong-demo.gb). The standalone version loads Pong directly rather than delivering it through the Pokémon exploit. It is the same VM and game in a form that is easy to inspect and play on its own. ## Tool-assisted research (TAR) This was tool-assisted research throughout. `b33f` chose the problem and the route, set the evidence standard, and made the final decisions about the demonstration and its presentation. He tested our technical account against the source and the emulator's behavior and did not let the write-up outrun the evidence. The model team divided the work by scale. GPT-5.6 Sol retained the full route history and compared new ideas with earlier failures. GPT-6.1 Sol worked mainly on builders, replay integration, graphics, and media. GPT-6 Astra solved some of the hardest tightly bounded source and assembly questions (including the behavior of the zero-width sprite-copy loop) and provided independent review. This let several investigations proceed at once without requiring every short task to absorb the whole chain. Everyone worked from the same pinned ROM, source commit, symbol map, Stage 0, VM runtime, and cartridge. When two accounts disagreed, we returned to the source, rebuilt the relevant image, or replayed the transition. Those checks corrected our account of the copied Hall-of-Fame bytes and forced us to trace the wrong-bank audio dispatch through nested VBlank instead of saying that it “eventually” reached the byte writer. Model guardrails also shaped the work. Some GPT-6.1 Sol and GPT-6 Astra sessions declined broad tasks after exploitation terminology entered the discussion, despite the authorized eight-bit target running in a local emulator. We assigned those sessions bounded source and assembly questions while GPT-5.6 Sol retained the complete context. We think the broad refusals were unwarranted and may themselves be dangerous given the rate and speed of technological advancement. ## Seven hours to Pong GPT-6 Astra reconstructed our timings from the session logs and recorded runs. | Recorded work | Time or count | | ---------------------------------------------------------- | ------------------------------- | | Choosing Pong to first continuous mechanism demonstration | about 7 hours 5 minutes | | Including the earlier dedicated raw `$c3` investigation | about 7 hours 35 minutes | | Coordinator and research agents over that broader interval | about 41–42 occupied task-hours | | Concurrent tasks over that interval | about 5.5 on average; peak 8 | Occupied task-hours include tool execution and waiting within tasks. For one strong reverse engineer independently discovering and building the demonstration from the same starting information and tools, Astra estimates *60–160 focused hours with Pokémon expertise*, or *100–280 hours while learning the game*. A human could avoid some of our coordination and duplicated investigation, while losing the ability to pursue several questions simultaneously. These estimates are informed judgments; we do not have a comparable human attempt to measure against them. ## POC The full 5× proof of concept below begins at reset in a prepared Pallet Town state and shows the Trainer-Fly setup, Ditto's Transform, the raw `$c4` encounter, the black-screen writer and receiver interval, the native VM loading screen, and Pong. For presentation, it combines three separately started recordings: the first join uses matching white frames during the Fly transition into Pallet, and the second occurs at the first `$ff80` instruction fetch. A separate `pre-Align` replay runs continuously from the sprite copy through Pong. <video controls preload="metadata" width="640"> <source src="pokemon-run-full.mp4" type="video/mp4"> <a href="pokemon-run-full.mp4">Open the full 5× proof-of-concept video</a>. </video> ## Thanks Thanks to `metalmario32`, `Torchickens`, and the Glitch City community for leaving this work public, specific, and open to another round of experiments. Their archive gave us a technical question to answer rather than a legend to reconstruct. ## Resources - [The archived 2018 Viridian ACE thread](https://archives.glitchcity.info/forums/board-115/thread-8467/page-0.html) - [The pinned `pokered-gbc` source](https://github.com/dannye/pokered-gbc/tree/c1a3b6c5a7591472241036d0cf09c3817f841f93) - [PyBoy, the emulator used for the recorded replays](https://github.com/Baekalfen/PyBoy) - [`ACE-ROM`, the buildable standalone VM and Pong cartridge](https://github.com/FuzzySecurity/ACE-ROM) - [The 2012 Pokémon Yellow total-control bootstrap and interpreted media submission](https://tasvideos.org/3767S) - [The 2013 Pokémon Blue `8F` byte writer and one-player Pong discussion](https://archives.glitchcity.info/forums/board-115/thread-6638/page-6.html) - [The 2017 Pokémon Yellow real-time command-stack playback](https://tasvideos.org/3358M) - [The Pokémon Red/Blue glitch species list used for historical calibration](https://www.pokemew.com/glitchdex/rb/) - [A Trainer-Fly guide covering copied Special values](https://gamefaqs.gamespot.com/gameboy/907714-pokemon-blue-version/faqs/64175/trainer-fly-glitch)