Skip to content

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 table aupd_spi_port_base (0x08003694) has and that chikuma/aupd/zig/flash/spinor.zig ports as port_base[3];
  • the write opcodes, clustered in 0x5af8..0x5cfc: EWSR 0x50, 4K erase 0x20, 64K erase 0xd8, AAI 0xad, alongside WREN 0x06 and RDSR 0x05.

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:

  1. status A == 7 -> diag; status B: 5 -> diag, 0x11 (ATA present) -> disk, 9 -> scan
  2. aupd, if its info word is zero - prep, mark handled, load and start
  3. reason != "diskmode": if dfup absent and rsrc present -> osr3, else osos
  4. the disk path - if the ATA protocol locates, disk_prep then disk with mode 0x10
  5. nothing booted -> bdhw screen, 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:

  1. RetailOS is running and the user connects iTunes. RetailOS presents mass storage plus the vendor SCSI extension - the same 0xc0..0xff VPD range and vendor opcodes the restore path uses.
  2. iTunes writes the new osos, and an aupd, 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, not WRITE(10).
  3. The aupd index entry is left at type 0 - "update pending".
  4. Reboot. The ROM loads the ONB from NOR; BDS reaches step 2 of its candidate order, finds aupd with a zero info word, runs onb_bds_aupd_prep, marks the entry handled with SetInfo and starts it - all before it would ever look for osos.
  5. aupd updates the NOR - erase 0x08000..0x28000, AAI-program the ONB region - and reboots.
  6. 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_type is 3 or 4, boot.c falls through to dfu_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 signs enc_type 3 containers, which is why run_secure_boot.sh boots 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_digest over the 0x40-byte header, then aupd_aes_job_run over 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..0x5cfc would also show the erase extent, which should be the ONB's 0x08000..0x28000 if this reading is right.
  • §6 - watch iTunes drive a real restore and capture the vendor CDBs, or read disk.bin's handler for opcode 0xc6. The second is free and safe; disk.bin is already extracted at firmware/extracted/onb_flsh_images/disk_body.bin.