The boot chain¶
From reset to RetailOS, four stages run before a single line of the OS itself executes: the boot ROM, the ONB (Apple's own NOR-resident secondary loader), and the point where the ONB hands control to RetailOS on disk.
This page is the reverse engineering: what each stage does, read out of the image. What Chikuma built from it - a boot ROM and an ONB that are entirely C, and a chain that boots Apple's own RetailOS - is The boot chain, in C.
graph TD
R["Reset<br/>0x20000000"] --> BM["bootrom_main<br/>0x20003458"]
BM --> BC["boot_context_init<br/>reads GPIO group 3, pins 6/7"]
BC -->|mode 1/2| NOR["boot_from_spi_nor<br/>0x20002fa8"]
BC -->|mode 3/4| NAND["boot_from_nand"]
BC -->|no image found| DFU["USB DFU loop<br/>dfu_command_dispatch"]
NOR --> V1["dfu_verify_signature<br/>+ dfu_header_verify"]
V1 -->|ok| JMP["jump_to_image<br/>0x200034a0"]
V1 -->|fail| DFU
JMP --> ONB["ONB: SEC → PEI → DXE_CORE → drivers → BDS"]
ONB --> BDS["onb_bds_boot_candidate_policy<br/>tries osos, osr3, diag, wtf, aupd…"]
BDS --> OS["RetailOS, on the disk's FAT16 firmware partition"]
Stage 1: the boot ROM picks a device from GPIO¶
bootrom_main zeroes its own stack frame - that frame is BootContext, the one structure the
whole ROM passes around - then hands off to boot_context_init, which reads two GPIO pin pairs and
picks a mode:
| Mode | Pins | Path |
|---|---|---|
| 1 | group 3, pins 6/7 = 00 |
boot_from_spi_nor(0) |
| 2 | boot_from_spi_nor(2) |
|
| 3 | boot_from_nand(0) |
|
| 4 | boot_from_nand(1) |
On the iPod Classic 6G, both pins float low, which selects mode 1 - the only path taken on this device.
Stage 2: SPI-NOR read, verify, decrypt, jump¶
spi_flash_read_image(bus, 0x800, 0x22020000, 0) the container header
dfu_verify_signature(header, mode=2)
if header.enc_type in {1, 2}:
spi_flash_read_image(bus, header.data_sz, 0x22000000, 0x8800) the body
dfu_header_verify(header, 0x22000000, 1) verifies AND decrypts in place
jump_to_image(BootContext[0x30] + 0x22000000)
The digest the leaf signature actually covers, read directly out of dfu_header_verify:
digest = SHA1(header[0:0x800] || body[0:round16(data_sz)])
signature = header.data_sz rounded up to 16, immediately after the body (0x80 bytes, RSA-1024)
The certificate chain itself is standard DER, not a custom struct - dfu_x509_chain_and_sig_verify
looks for 30 82 <len> (an ASN.1 SEQUENCE, long-form length) at the front of the chain blob and
slices up to three back-to-back certificates by their own encoded length. The root anchor is a
literal 20-byte SHA-1 digest baked into the ROM (root_public_key_blob+1088) - a cert is only
accepted if it hashes to that exact value, which is mask ROM: no fuse, no field, nothing that
changes it. Subject-name strings (/CN=Apple Secure Boot Certification Authority and similar) pick
which chain slot is root/intermediate/leaf; they do not replace the RSA check on any of them.
Source offset 0x8800 and header at 0x8000 are exactly where the ONB lives in NOR - so the ROM's
normal path loads the ONB to 0x22000000 and jumps to it.
Stage 3: the ONB is a real, small UEFI environment¶
This is the part that looks nothing like a typical embedded bootloader. The ONB's body is an EFI
Firmware Volume - _FVH signature, a real block map, a real header checksum - carrying 42 FFS
files: a SEC/SECURITY_CORE, a PEI_CORE, a DXE_CORE (its GUID matches Intel/EDK2's own
DxeMainDxe exactly, byte for byte - this really is Apple's build of Tiano's reference DXE core for
ARM), and 38 drivers, some stock EDK2 (DiskIoDxe, PartitionDxe), most Apple-authored.
graph LR
SEC["SECURITY_CORE<br/>TE image"] --> PEI["PEI_CORE"]
PEI --> DXE["DXE_CORE<br/>= DxeMainDxe"]
DXE -->|dispatches| DRV["38 drivers:<br/>ATA, LCD, DiskIo, PartitionDxe,<br/>IpodFirmwareFS, the ROM/BDS policy…"]
DRV --> BDSMOD["Bds.efi"]
BDSMOD --> POLICY["onb_bds_boot_candidate_policy"]
Stage 4: BDS picks a boot candidate¶
onb_bds_boot_candidate_policy checks named variables in order - osos (RetailOS), osr3,
diag, dfup, rsrc, bdhw among them - each resolved through the plain, standard EFI file API:
Find(name):
LocateHandleBuffer(ByProtocol, EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID, &count, &buffer)
for each handle in buffer:
HandleProtocol(handle, &SFS_GUID, &sfs)
sfs->OpenVolume(sfs, &root)
root->Open(root, &file, name, EFI_FILE_MODE_READ, 0)
if success: break
return file (or NULL)
There is no drive-letter router and no custom index lookup at this layer - it is tried, unmodified,
against every filesystem-capable volume in whatever order LocateHandleBuffer returns them.
The one filesystem BDS can actually see¶
Exactly one driver in the ONB produces EFI_SIMPLE_FILE_SYSTEM_PROTOCOL: IpodFirmwareFS.efi,
over the Apple_MDFW/iPod_Firmware partition (not the FAT16 resources volume, and not the HFS+
data partition - neither has a driver present at this boot stage). Its on-disk format, traced end
to end:
| Offset | Size | Field |
|---|---|---|
0x00 |
u32 |
per-entry magic, "flsh" |
0x04 |
u32 |
4-byte ASCII name, big-endian packed ("osos" = 0x6f736f73) |
0x08 |
u16 |
type/flags; 0xFFFF (erased NOR) = slot unused |
0x10 |
u32 |
raw value; real byte offset = round_up(raw + 0x200, 64) |
40 bytes per entry, a flat array at a fixed partition offset, gated by a "STOP screen" copyright
banner at partition offset 0 that every reader checks before trusting the table. Open() here is a
plain UCS-2 string compare against the decoded 4-character name - which is also why it can only ever
resolve a 4-character request.
The genuine Apple image directory, and the read path¶
The flsh table above is the structure this project's patch_apm_osos.py writes at partition
offset 0xffe00, and Open()'s name match resolves against it. But the candidate loader's actual
file read walks a different, genuine Apple structure: an "!ATA" image directory at partition
offset 0x5000, byte-identical between the built disk and the pristine ipsw bundle, so it is Apple's
own firmware-partition layout, not anything this project synthesises. Measured 2026-09-08 from the
parse code at IpodFirmwareFS's live base +0x714:
| Offset | Size | Field |
|---|---|---|
0x00 |
u32 |
per-entry magic, "!ATA" |
0x04 |
u32 |
4-byte name, 16-bit-half-swapped ("osos" stored 73 6f 73 6f) |
0x0c |
u32 |
start offset, partition-relative (INFERRED: offset-shaped, matches the real container, not watched being read) |
0x10 |
u32 |
length in bytes (MEASURED: used in the read's covers check, and size-shaped) |
40-byte stride, indexed with a bounds check against a live entry count. Apple's own entries:
| Name | start +0x0c |
length +0x10 |
|---|---|---|
rsrc |
does not fit start/length (its +0x0c is size-shaped, +0x10 offset-shaped) - unresolved |
|
osos |
0x4e07000 |
0xa06aa3 |
aupd |
0x580f000 |
0x11c8d3 |
hash |
0x592d000 |
0x1000 |
So Apple's directory already names osos and points it at a real enc_type 3 container in the
partition. Nothing about the directory or the loader needs changing to boot osos.
Why the built disk lands on the restore screen¶
Not a missing image, not crypto, not the firmware. Our build damages the real partition. Comparing
the built ipod_apm.img against the pristine bundle, the firmware partition diverges in two 10.5 MB
regions, both ours:
- around
0x100000:patch_apm_osos.py'sflshdirectory and its symmetricososcontainer - a scheme the read path never consults - overwriting 10 MB of real Apple content. 0x4e07000to0x580f000:osos's own native slot rewritten, the write running past its end and zeroingaupd's container at0x580f000. That zeroed region is exactly where the loader'sososread landed, so it read all zeros,ososnever became a live file object, the"8702"magic compare failed, and BDS fell through to the restore screen (Stage 5).
The honest fix is build-side only: leave Apple's !ATA directory intact, stop writing the ignored
flsh scheme, and replace osos in place at partition offset 0x4e07000 with a Chikuma-signed
enc_type 3 container no longer than the 0xa06aa3 Apple's own entry declares, without overrunning
into aupd. Faking a directory to make Find() succeed would only stage a boot that is not real.
Open, and the peer session's to close: whether Open()'s name match genuinely uses the flsh table
while the read uses !ATA, or the earlier flsh trace and this !ATA one are the same lookup seen
at two layers. The +0x0c start field has not been watched being consumed by the LBA computation,
only matched by its value.
The unsigned debug path, and why it can never fire¶
Before any of the named candidates, onb_bds_boot_candidate_policy unconditionally runs a debug
hook that tries to load C:/MicroShell.fv - an EFI-shell-style engineering tool, Apple's own
internal name for it. Traced end to end:
graph TD
A["LocateProtocol(MicroShell resolver GUID)"] -->|NULL| X["return, clean"]
A --> B["resolver(this, L#quot;C:/MicroShell.fv#quot;)"]
B -->|NULL| X
B --> C["bounds check: file size vs FV window"]
C -->|fail| DL["CpuDeadLoop(), hard hang"]
C -->|ok| D["HandleProtocol"]
D -->|status<0| DL
D --> E["build MEDIA_PIWG_FW_FILE_DP path node"]
E --> F["LoadImage(BootPolicy=TRUE, …)"]
F --> G["StartImage(handle, NULL, NULL)"]
G --> H["return, unconditionally, status never checked"]
No EFI_SECURITY_ARCH_PROTOCOL/EFI_SECURITY2_ARCH_PROTOCOL call exists anywhere in the 42-file
volume, so this path has no authentication to fail even if reached. It is unreachable for a
different reason: IpodFirmwareFS.efi - the only filesystem the ONB can see at this point in
boot - matches names with a plain string compare against a 4-character decoded field.
"C:/MicroShell.fv" is 17 characters. It cannot match any entry, on any partition, under any
circumstance. Not a security gate - a structural mismatch left over from an engineering build that
never had a way to fire once shipped.
Reassembling, and what replaced it¶
Chikuma never hex-patched Apple's compiled images. The first pass at changing the chain reassembled
it: firmware/reasm re-emitted each module's own Ghidra listing, assembled it, and cmp'd the
result against the real .efi, byte for byte. That gate did exactly one job, and did it well - a
reassembly that matches to the byte proves the disassembly was read correctly - and it is history
now. Both stages are C:
C is not byte-identical to Apple's compiler output, by construction, and the gates that replaced the byte compare test behaviour instead: per-module host suites against the image's own bytes, QEMU integration payloads against the real device models, and the whole chain booting the original firmware. See The boot chain, in C for how it builds and what each gate covers.
The Firmware Volume is assembled by this tree's own parser/serialiser (chikuma/onb/tools/fv_bundler.py),
which substitutes each module's C-built PE image into the volume by GUID and reserialises it:
cd chikuma
make onb-full-c-fv # every non-SecCore module as an independently emitted C PE image
make secureboot # ... plus the bootrom, signed with the Chikuma CA
make secureboot-disk # ... planted on a fresh copy of the disk image
Signing a Chikuma build¶
Because the reassembled boot ROM swaps the root-of-trust hash for Chikuma's own, the whole X.509
chain can be Chikuma's rather than Apple's. firmware/chikuma_ca is a self-contained CA (its own git
repo, kept out of the main tree): a root that mirrors Apple's RSA-2048, an RSA-1024 intermediate and
leaf, all with SHA-1 signatures and the ROM-required 9-byte 01 FB 01 FB serials. bootstrap.sh
regenerates the chain from the committed keys without ever replacing a key (the leaf key signs the
image, so a new key would invalidate an already-signed build).
Two build steps turn that chain into a bootable image:
cd firmware/chikuma_ca
# sign an arbitrary OSOS body (e.g. Apple's own decrypted osos-dec.dfu[0x800:])
make chikuma-osos-x509 OSOS_BODY=../extracted/osos-dec.dfu.body
# or pack and sign OUR OWN build in one step: payload.elf -> OSOS two-view body -> signed container
make chikuma-osos-container # CHIKUMA_ELF defaults to ../../chikuma/payload.elf
make_chikuma_osos.py packs the payload ELF into the OSOS two-view body the loader expects
(body[0:0xAE5C] at 0x22000000, body[0xAE5C:] at 0x08000000) and wraps it in the signed
enc_type 3 container, leaf certificate first. The result is planted in place at the !ATA
directory's osos slot (partition offset 0x4e07000), no longer than the 0xa06aa3 Apple's own
entry declares, without overrunning aupd.
Stage 5: what BDS does when nothing boots¶
Measured live on 2026-09-08 (Stall hooks over a whole boot, a call ladder in Bds, and QEMU's own MMIO trace), so this is the ONB's behaviour, not the emulator's:
- Every candidate has failed. BDS writes the
--------reboot reason. - It reads the PMU status through
DxePcf50635: bit 2 is PMU register0x12bit 2, the data-host detect (reports/usb_power_sources.md, source 3). - No computer attached: it draws the
bdswpicture from theflshdirectory and polls that bit every 100 ms for 10 s, exiting early if a host appears and holds for two reads. ThenDisplay(1),Power(0). - Computer attached: it skips the wait, tries the
diskcandidate, drawsbdhw, stalls 30 s, then the sameDisplay(1),Power(0).
Power(0) is DxePcf50635's 0xbeec5aa: write PMU register 0x0c := 1 (PCF5063x
OOCSHDWN.GOSTDBY, power off) and spin forever. The "terminal halt" at 0xbeec5d8 that every
session ended on is the firmware waiting for the PMU to cut its power.
bdsw is the restore prompt:

The seven flsh pictures are ciphertext in the NOR, so this is the first time one has been seen:
the ONB decrypts and decodes it itself (DrawJpeg.efi, JpegDecoder.efi) and writes it to the
panel through the LCD controller's WDATA port, one pixel per write - a whole-panel white fill by
DMA, then a 176x116 window for the text - after CASET/RASET set the window. RetailOS never does
this; it streams frames through the display pipe, and the QEMU display used to show only the pipe,
so this screen was drawn and invisible until devices/lcd.c learned the write window and
devices/pipe.c learned to show the panel's memory when the pipe is idle.
Two consequences for the emulator: PMU register 0x0c bit 0 should end the machine
(qemu_system_shutdown_request) instead of spinning, and the 10 s and 30 s waits are Stall()
busy-loops on a timer read, which run at a fifth of real time under -icount shift=3; use
--shift 7 --warp on this chain (qemu-ipod/USING.md).
Where this stands under QEMU¶
The chain is complete. From reset, on the emulated machine, with nothing of Apple's binary left in it:
- boot ROM → SPI-NOR read → RSA/X.509 verify → AES decrypt → jump into the ONB: works, from the C boot ROM.
- SEC → PEI (
PreEfi) → DXE core → drivers → BDS: works, from the C volume, all 40 modules. - BDS finds
osos, verifies it and jumps: works, and what it starts can be either Chikuma or Apple's own unmodified RetailOS, signed with the Chikuma CA. The second is the control: a bootloader that starts the original firmware is a bootloader that behaves.
Getting there needed real emulator work, and the distinction matters - none of these were firmware
bugs, they were gaps in what the machine had to supply: SPI-NOR framing, SHA-1 padding (a missing
start-of-hash reset on the DATAIN-fed path), an unmodelled NVRAM read, and the 0x3d000000 modexp
engine below.
The modexp engine, and the gates that are not crypto¶
The last real gap was that BDS's candidate loader calls back into the boot ROM's own
dfu_header_verify with a hardcoded mode=2 - the X.509/RSA branch - regardless of the container's
own enc_type, so osos is routed through X.509 verification unconditionally.
0x3d000000is modelled (qemu-ipod/devices/eccrsa.c). It is not "ECC" and not NAND: it is a hardware modular-multiply accelerator the RSA verify drives throughecc_decode_page's square-and-multiply ladder (the ROM's own softwarempi_exptmod/mpi_modinvhave zero callers). A per-command multiplych[dest] = ch[a] * ch[b] mod Nwith the destination in the descriptor's low byte; a config-bit-1 command that loads the identity to seed the accumulator; andecc_wait_channel's own inline trigger copying the result into the channel the read returns. The model reproduces a real intermediate-by-root RSA-1024 verify against Apple's own 2048-bit root modulus.- The chain's non-crypto gates are recovered. Beyond the per-cert RSA-1024 PKCS#1v1.5 signature
and the root-hash anchor,
bootrom_verify_cert_chainrequires every certificate's serial number to be exactly 9 bytes with the first four bytes01 FB 01 FB- an Apple prefix the ROM hard-compares (an alternate01 FB 00 FBis accepted only when a chip-ID fuse bit is clear). Certificate signatures must be SHA-1: the ROM's DigestInfo templates are MD5/SHA-1 only, so a SHA-256-signed cert fails the parse before verification.
Timing, and one open emulator question¶
The ONB's waits are Stall() busy-loops on a timer read, which run at a fifth of real time under
-icount shift=3; use --shift 7 --warp on this chain (qemu-ipod/USING.md). PMU register 0x0c
bit 0 ends the machine rather than spinning, so the ONB's own power-off path terminates QEMU.
Still open: the machine merges the two timer interrupt lines. The hardware splits them - IRQ 8 for
the 16-bit blocks, IRQ 7 for the 32-bit ones, and ONB module 36 routes its own Timer F to 7 -
while devices/timer.c ORs both pending sets onto line 8. Splitting them was tried and broke the
ONB's timer path on the model's unverified TSTAT write-1-to-clear semantics, so it stands as
written with the divergence recorded.
The full detail lives in reports/nor_contents.md and reports/bootrom_crypto.md.