The restore, end to end¶
What happens when iTunes restores an iPod Classic 6G whose disk is empty, whose NOR is blank, or both. Each step below says whether it is MEASURED on hardware or in the emulator, INFERRED from code that has been read, or OPEN - meaning nobody in this project has established it yet.
The short version:
ROM -> DFU -> WTF -> (NOR gets an ONB) -> ONB -> disk mode -> firmware partition filled -> osos
0. The two things a blank device is missing¶
There are two separate stores, and a restore fills both:
| store | holds | size |
|---|---|---|
| NOR flash, 1 MiB | SysCfg, the ONB, and the flsh image directory (disk, diag, appl, lbat, bdsw, bdhw, chrg) |
ONB at offset 0x8000, 128 KiB |
| disk, firmware partition | osos (RetailOS), aupd, rsrc, and friends, in an image index |
100,663,296 bytes |
That firmware-partition size is MEASURED twice over and the two agree: the device's own info plist
says FWPartSize = 196608 (512-byte units = 100,663,296 bytes), and this project's built image uses
24576 blocks of 4096 = the same number.
The disk's data partition is the HFS+ volume the user sees. It is not where the OS lives.
1. Blank NOR: the ROM's DFU loop (MEASURED)¶
With nothing bootable in NOR the mask ROM falls into its USB DFU loop. reports/boot_chain.md:
cold boot ROM -> (GPIO picks the device) -> ONB from NOR at 0x22000000 -> RetailOS from disk
recovery ROM -> USB DFU -> WTF at 0x22000000 -> USB DFU -> whatever the host sends
The device enumerates as Apple 0x05ac / 0x1245, product string "iPod Recovery" - MEASURED
against real hardware, and worth stating because two written sources (wInd3x's Nano3 entry and
Rockbox's mks5lboot/ipoddfu.c) both say 0x1223 for this family and were both wrong for this
device. See mac-installer/.../DeviceWatcher.swift, which carries the correction and its evidence.
2. The host sends the WTF (MEASURED)¶
The ROM's DFU loop takes one image and jumps to it exactly as it would to the ONB. That image is
the WTF. Once running it calls back into the ROM through the trampolines at 0x20000028,
reads its boot cookie, brings up the clocks and SDRAM, initialises the NOR-backed SysCfg, builds
the device-info block from the tags, and then runs its own DFU loop, advertising iPod
Recovery to receive the next image.
The ROM's DFU loop and the WTF's DFU loop are different loops that look identical from the host.
3. The WTF writes the ONB into NOR (MEASURED, statically)¶
The WTF carries a complete SPI-NOR driver, write side included. firmware/extracted/wtf-decrypted.bin
is 30,768 bytes and holds:
- the SPI port-base table, three words at file offset
0x1c3c:0x3c300000,0x3ce00000,0x3d200000- consecutive, and byte for byte the tableaupd_spi_port_base(0x08003694) has and thatchikuma/aupd/zig/flash/spinor.zigports asport_base[3]; - the write opcodes, clustered in
0x5af8..0x5cfc: EWSR0x50, 4K erase0x20, 64K erase0xd8, AAI0xad, alongside WREN0x06and RDSR0x05.
EWSR, 64K erase and AAI appearing together in one region is not a coincidence of small constants - they are a SPI-NOR programming sequence and nothing else. So the WTF can erase and program the NOR on its own, without any help from the disk.
That is what makes the blank-device case work at all: the ONB lives at NOR offset 0x8000 and the
disk is empty, so nothing on the disk could supply it. The host sends the WTF down the ROM's DFU
loop, the WTF runs its own DFU loop, and it has the driver needed to put an ONB into NOR.
What is still INFERRED is the last step of that sentence - that a real restore actually uses this driver to write the ONB, rather than, say, only touching SysCfg with it. The capability is measured; the use of it in a restore has not been traced over USB.
Note aupd is a NOR flasher too, and the two are not alternatives: aupd handles the in-field
update (§"The ordinary update"), where the disk already has content and BDS can load it. The WTF
handles the case where it cannot.
4. NOR now boots: the ONB, with nothing on disk (MEASURED, in C)¶
The ROM loads the ONB from NOR. Its end-of-boot decision is
onb_bds_boot_candidate_policy, ported branch-for-branch in
chikuma/onb/02_Bds/policy.c, and it tries candidates in this order:
- status A == 7 ->
diag; status B: 5 ->diag, 0x11 (ATA present) ->disk, 9 ->scan aupd, if its info word is zero - prep, mark handled, load and start- reason !=
"diskmode": ifdfupabsent andrsrcpresent ->osr3, elseosos - the disk path - if the ATA protocol locates,
disk_prepthendiskwith mode 0x10 - nothing booted ->
bdhwscreen,Stall(30 s), display off
On a blank disk there is no osos and no rsrc, so step 3 finds nothing and control reaches step
4. disk is a NOR flsh image, not a disk one - disk.bin, 213 KiB - so it runs even though
the disk is empty. That is the whole trick: disk mode lives in NOR precisely so it can run when the
disk cannot.
MEASURED in reports/nor_contents.md: with IPOD_PWR_SRC=3 (a data host present) the policy
"tried the disk candidate, DrawJpeg(bdhw), Stall(30 s)". With IPOD_PWR_SRC=2 (charger only)
it never gets there - it stops in the power-off loop at 3.9 s.
5. Disk mode, over USB (MEASURED on real hardware)¶
disk.bin brings up the DWC2 core and presents a USB Mass Storage device. Its SCSI responder is
the same code aupd carries dead: disk_body.bin at 0x13194 and aupd-decrypted.bin at
0xfb30c are the same 48 bytes, the INQUIRY identity table.
Confirmed against a real device with mac-companion/scsiprobe:
INQUIRY 00 80 00 00 33 00 00 00 "Apple " "iPod " "1.62"
CAPACITY last LBA 29255990, block size 4096, 119.83 GB
Every field aupd_scsi_inquiry_standard (0x080fb0cc) writes, matching: removable 0x80 at +1,
additional length 0x33 at +4, vendor at +8, product at +0x10.
The firmware partition is not in the exposed LBA space. Reading the APM off the device gives
exactly two entries - the partition map, and Apple_HFS starting at LBA 64 running to the end.
There is no Apple_MDFW. So disk mode shows the host the data partition and nothing else.
6. The host fills the firmware partition (INFERRED)¶
Since the firmware partition is not addressable by READ(10)/WRITE(10), it cannot be written as
LBAs. The device's own info plist - itself read over the vendor VPD range - says how instead:
UpdateMethod 3
MaxFWBlocks 16
FWPartSize 196608
AutoRebootAfterFirmwareUpdate true
VolumeFormat HFSPLUS
CorruptDataPartition false
So firmware goes in through an update protocol with a 16-block ceiling per operation, and the
device reboots itself when the update finishes. The vendor SCSI opcode the recovered dispatcher
lists for this is 0xc6 (chikuma/aupd/zig/usb/scsi.zig), but its direction and payload have
not been established and no one has issued it - an unknown vendor opcode on real hardware is
how this project would lose a device.
Whether the same pass also formats the data partition is OPEN. VolumeFormat = HFSPLUS and
CorruptDataPartition exist, which says the device has an opinion about the data volume's state,
but that is not evidence that the restore formats it here rather than the OS doing it on first
boot.
aupd's own header is X.509-gated, not GID-MAC (MEASURED, in C)¶
onb_bds_load_and_start_image_candidate runs a header pre-check (rom->Validate, kind 1) then, on
success, rom->Finalize. chikuma/onb/26_ROM/rom.c:319:
onb26_rom->dfu_header_verify(img, addr, 2)
Hardcoded 2 - the X.509 chain + RSA signature branch (dfu_header_verify in
chikuma/bootrom/dfu.c), never the GID/UID-MAC branch (mode 1, no certificate) the ROM's own
cold-boot loader tries first for the ONB itself. Module 26's other entry point, its own RunDfu,
hardcodes the same 2. So aupd - and every other disk candidate - is accepted only through the
certificate path; this is why chikuma_secure_boot.py only ever emits enc_type 3 containers.
The vendor VPD range, which is real and readable¶
aupd_scsi_vpd_supported_pages (0x080fb57c) says the device advertises 0x00, 0x80, 0x83, 0xb0
then all of 0xc0..0xff. The device answers exactly that - 68 pages, page length 0x44. The
0xc2.. pages carry the device-info plist in 0xf8-byte chunks, which is what
aupd_scsi_vpd_vendor / aupd_scsi_vpd_log_chunk were paging. Page 0x83's identifier is the
FireWire GUID; page 0x80 is the serial.
These are read-only and safe, which is why they could be checked. They are information, not a path to the firmware partition.
7. Reboot into the OS (MEASURED, in the emulator)¶
With the firmware partition populated, the ROM loads the ONB from NOR, BDS's step 3 now finds
osos, and RetailOS starts. This project runs that exact chain - a signed osos planted in the
firmware partition at iPod_Firmware+0x4e07000 - with qemu-ipod/run_secure_boot.sh.
aupd fits here too: when the firmware partition carries an aupd whose index entry is still type
0 ("update pending"), BDS runs it before osos, it updates the NOR, marks the entry handled and
reboots. That is the in-field ONB update path, and it is reproduced in
chikuma/aupd/zig/main.zig.
The ordinary update, which is a different path (MEASURED / INFERRED)¶
Everything above is the restore - the blank-device case. A normal firmware update never goes near DFU, because RetailOS exposes the same SCSI vendor interface itself.
The responder is in all three images. The 48-byte INQUIRY identity table appears at:
osos-dec.dfu 0x0e6bd0 RetailOS
disk_body.bin 0x013194 the NOR disk-mode image
aupd-decrypted.bin 0x0fb30c aupd
byte for byte the same, "Apple " / "iPod ", differing only in the pointer to the
revision string. RetailOS drives it from usb_diskmode_task (0x221320d8), with
usb_diskmode_lun_construct, diskmode_lun_block_size, diskmode_lun_last_lba and the
usb_diskmode_session_* group beneath it - all named in this project's Ghidra database.
So the ordinary update runs:
- RetailOS is running and the user connects iTunes. RetailOS presents mass storage plus the vendor
SCSI extension - the same
0xc0..0xffVPD range and vendor opcodes the restore path uses. - iTunes writes the new
osos, and anaupd, into the firmware partition over that vendor path. As in §6 the firmware partition is not in the exposed LBA space, so this is the protocol, notWRITE(10). - The
aupdindex entry is left at type 0 - "update pending". - Reboot. The ROM loads the ONB from NOR; BDS reaches step 2 of its candidate order, finds
aupdwith a zero info word, runsonb_bds_aupd_prep, marks the entry handled withSetInfoand starts it - all before it would ever look forosos. aupdupdates the NOR - erase0x08000..0x28000, AAI-program the ONB region - and reboots.- The ROM loads the new ONB, BDS falls through to
osos, and the new RetailOS starts.
Steps 4 to 6 are MEASURED: this project reproduces them with
qemu-ipod/run_secure_boot.sh --apple-aupd (Apple's own body) and --aupd (ours), and
prepare_sb_disk.py plants the candidate exactly as step 3 describes - its own output line is
"aupd left type 0 (update pending) - BDS will load it before osos". Our own aupd completes the
cycle: it draws, programs the NOR, reboots, and the machine comes back up into the OS.
Steps 1 to 3 are INFERRED. The responder is demonstrably present in RetailOS and the firmware partition demonstrably is not LBA-addressable, but nobody here has watched iTunes actually perform the write.
This is why aupd carries a full USB and SCSI stack it never runs. Its MSC code is the same
shared library RetailOS and disk.bin link against - hooking a live --apple-aupd update showed
aupd_usb_port_sense firing twice (the battery gate asking about power) and aupd_msc_register
never firing at all. aupd is the NOR flasher; exposing the device to the host is somebody else's
job.
What is actually checked, and by whom (MEASURED)¶
Both verifiers are the same shape, and neither is a public-key signature on the fast path.
The ROM, which the WTF calls back into¶
dfu_verify_signature (0x200005dc), ported byte for byte in chikuma/bootrom/dfu.c:
SHA-1 over the first 0x40 bytes of the header
AES "job mode 7" over the 16-byte digest, key source = mode (1 or 2), zero IV
compare those 16 bytes against header + 0x40
then require the magic "8702"
That is a device-keyed MAC, not a signature: the AES key is the hardware GID/UID, so only this
family of SoC can produce a valid header and there is no public key involved. mode selects which
of the two key sources.
Two details worth knowing:
- it refuses outright when
boot_config_pins_read() == 1- a hardware strap makes this path fail, which pushes the caller to the other one; - when it fails and
enc_typeis 3 or 4,boot.cfalls through todfu_header_verify(0x200006dc), which is the X.509 certificate path. So the ROM supports both: a GID-MAC fast path and a certificate chain. This project's own CA signsenc_type 3containers, which is whyrun_secure_boot.shboots images Apple never signed.
So: not signed-only. Two verification families, chosen by enc_type and a strap.
aupd¶
aupd_verify_digest_mode2 (0x08009d0c) tail-calls aupd_verify_digest_dispatch (0x08009d60) with
selector 2, and the dispatch is the same two families:
- modes 1 and 2 -
aupd_digestover the 0x40-byte header, thenaupd_aes_job_runover the 16-byte result: the identical GID-MAC the ROM does; - modes 3 and 4 - the certificate path, laying out signature and certificate offsets at +0x10, +0x14 and +0x18 before checking;
- anything else answers 9.
The selector indexes aupd_verify_mode_table at 0x08011ee0, 16 bytes per row, one row per
SoC:
| index | header size | magic | version |
|---|---|---|---|
| 0 | 0x800 | 8441 |
1.0 |
| 1 | 0x800 | 8701 |
1.0 |
| 2 | 0x800 | 8702 |
1.0 |
| 3 | 0x800 | 8442 |
1.0 |
| 4 | 0x600 | 8720 |
2.0 |
Five rows. What looks like a sixth is the adjacent reboot-reason table bleeding in -
0x08011ee0 + 5*16 is 0x08011f30, which is "--------" / "diskmode" and not a verify entry.
Index 2 is this device: magic 8702, header 0x800, version 1.0 - the im3 container shape the
rest of this project already works with. aupd is built for the whole ARM iPod range and picks its
row at runtime, which is the same reason it carries a USB stack it never runs.
What would close the open steps¶
- §3 - the WTF's NOR write driver is measured; what remains is to watch a real restore and
confirm it is used to place the ONB. Disassembling the WTF around
0x5af8..0x5cfcwould also show the erase extent, which should be the ONB's0x08000..0x28000if this reading is right. - §6 - watch iTunes drive a real restore and capture the vendor CDBs, or read
disk.bin's handler for opcode0xc6. The second is free and safe;disk.binis already extracted atfirmware/extracted/onb_flsh_images/disk_body.bin.