Skip to content

The WTF, the recovery stage

When a host pushes an image to the boot ROM's USB DFU loop to bring a device back, the first image it sends is the WTF. This page is everything the WTF does, read out of its disassembly function by function. All of it is MEASURED from the image unless it says otherwise. Every function has also been reproduced in Zig and tested against the image, in chikuma/wtf/ (see Reproduced).

The short version: the WTF is not a loader of its own. It sets the device up, publishes who the device is, and then runs the boot ROM's own DFU code a second time with its own parameters, to receive, verify and jump to the next image. It never draws on the screen. It carries a complete NOR write driver, but nothing inside the WTF calls it.

graph TD
    ROM["Boot ROM DFU loop<br/>(no bootable NOR)"] -->|host sends the WTF| R["wtf_reset_entry<br/>0x22001420"]
    R --> M["wtf_main<br/>0x22002f20"]
    M --> C["boot cookie, nvram from the NOR's syscfg,<br/>device info, hwid check"]
    C --> P["platform_init_for_dfu<br/>clocks, timer, SDRAM, heap"]
    P --> S["wtf_dfu_session<br/>0x22001698"]
    S -->|"blx through the ROM's table"| RD["the ROM's dfu_session_init<br/>+ dfu_wait_for_done"]
    RD -->|image received| V["dfu_header_verify<br/>(the ROM's, mode 2)"]
    V -->|ok| J["jump to 0x08000800"]
    V -->|fail| X["return 0x57"]

The image

container wtf.dfu, an im3 container with an 0x800-byte header, enc_type 3
body 0x7830 bytes, firmware/extracted/wtf-decrypted.bin
load and run address 0x22000000, internal SRAM. 113 of the 135 words that look like SRAM pointers fall inside [0x22000000, 0x22007830)
version header 'vers', 0x00010000 at +0x40
.data 0x22006cc8 .. 0x22007810, run where it is loaded
.bss 0x22007810 .. 0x220085c4
stacks IRQ 0x2203fbfc, SVC 0x2203f7fc, below the handoff block
functions 274

It carries the same runtime library as RetailOS and the shipped click-wheel games: the signal strings (Divide By Zero, : Heap memory corrupted, ...) and Apple's btMm heap. The heap's source path is in the image: c:\BWA\N25CFirmwareWin-247\srcroot\Firmware\BootLoader\btMm.c.

Reset and vectors

The vector table at 0x22000000:

offset vector goes to
0x00 reset wtf_reset_entry
0x04 undefined b .
0x08 SWI mov r0,#0 ; movs pc,lr: every SWI returns 0
0x0c prefetch abort if the faulting word is swi 0x123456, return 0; otherwise fall into the IRQ stub
0x10 data abort b .
0x18 IRQ push r0-r12/lr, pop them, subs pc,lr,#4: nothing
0x1c FIQ b .

Semihosting (swi 0x123456) is how the WTF prints. With no debugger attached, the SWI and prefetch-abort vectors make every call answer 0. The IRQ vector does nothing because interrupts are served by the boot ROM's handlers, which the WTF installs through the ROM (see the DFU session).

wtf_reset_entry is the crt0, in this order:

  1. Invalidate the I-cache, disable the I-cache and MMU, and turn on the alignment check.
  2. Set up the IRQ and SVC stacks.
  3. Relocate itself if it is not running where it was linked.
  4. Copy .data and zero .bss.
  5. Call wtf_main.
  6. Semihosting SYS_EXIT, in case wtf_main returns.

wtf_main

boot_cookie_read                      decode the SRAM cookie (below), cache it
boot_status_word_set(0)
nvram_init                            fail -> reboot reason "softupdt", reset
device_info_build                     syscfg tags -> the device-info block
sysinfo_get_version                   HwVr
hwid_is_supported                     only 0x13 (N25) and 0x14 (N46); else "softupdt", reset
flash_state_init
chipid_read                           halts if the chip-id flag is set and flags & 8 is clear
sysinfo_handoff_publish               'SysI' + the device-info address into SRAM
platform_init_for_dfu                 then dram_size_detect_store if it returned 0
mm_heap_init                          512 KiB at 0x08f80000
boot_cookie_write(2)                  "booting!"
-> wtf_dfu_session(flags, 0x08000000, 0xf80000, version)      a tail call

The flags start as 0x1d, become 0x3d once nvram_init succeeds, and gain 0x40 once the hwid is supported. Both other outcomes reset the device, so the session always sees 0x7d:

bit meaning in the session 0x7d
0x01 bootrom_init_params+0x34 set
0x02 +0x37, stop polling when idle clear
0x04 +0x35, unmask IRQ 0x28 (the SHA-1 interrupt) set
0x08 the WTF verifies the image itself set
0x10 jump to the image when all is well set
0x20, 0x40 which USB product string "iPod Recovery"

platform_init_for_dfu runs, in order:

  1. power_state_transition(6) - state 6 is the one case of that switch that does nothing (see What it carries and never runs).
  2. clock_tree_init.
  3. With clock gate 0x10000 on: clear the low three bits of 0x3930003c.
  4. Write 0x1002 to the EIC at 0x39a00000.
  5. timer_init.
  6. sdram_set_power_mode(2).
  7. Describe the heap region: 0x08f80000, 0x80000 bytes, immediately after the 0xf80000-byte load window at 0x08000000.

The SRAM handoff block

The top of internal SRAM survives the reset that halt_and_request_reboot asks for. The WTF reads and writes it as one block:

address size what written by
0x2203ff00 8 reboot reason: -------- diskmode diskscan softupdt retailOS (0-4) reboot_reason_set
0x2203ff08 8 boot cookie: booting! 2, hotstuff 3, battislo 4, anything else 0 boot_cookie_write
0x2203ff10 4 status word, set to 0 first thing boot_status_word_set
0x2203ff18 4 'SysI' sysinfo_handoff_publish
0x2203ff1c 4 address of the device-info block sysinfo_handoff_publish

The reboot-reason block is the same one aupd and RetailOS use. The cookie beside it belongs to the WTF. sysinfo_handoff_publish (Ghidra once called it object_create_18) looks like an allocation but is not one: mm_alloc_static(n) returns 0x2203ff00 + n. Its job is to leave the next stage a tagged pointer to the device-info block.

Resetting is two stores and a spin: 0 to the watchdog at 0x3c800000, then 0xaa5 to 0x3c500050.

Who the device is

Device info. device_info_build fills a 0x120-byte block from the syscfg tags stored in NOR. The field offsets were checked by running a real device's NOR through the port:

offset from offset from
+0x08 HwNm +0x98 Mod#
+0x18 SrNm +0xa8 HwO1
+0x38 FwId +0xb8 Cont
+0x48 HwId - but see below +0xba BkLt
+0x4c 0x4044 if 0x4040 holds 'vers' +0xbc UntC
+0x5c Btry, three words +0xce DrmV
+0x80 RtcA +0xe0 DRAM size
+0x84 HwVr +0xe8/+0xec SRAM size and base
+0x88 "NA" +0xf0/+0xf4 NOR size and base
+0x92 Regn +0x104/+0x108 Codc, SwVr

HwId is never stored. The code tests the tag's first word for zero the same way it does for HwVr, but then stores the word it just tested, so +0x48 is always 0.

Hardware id to codename (hwid_to_codename):

id >> 16 id >> 16 id >> 16
01 P68 07 Q22B 10 N36
02 P97 08 Q21B 11 M68
03 Q14 09 P98B 12 N35
04 Q22, or Q22 Dev if the low half < 0x10 0a Q22C 13 N25, this device (HwVr 0x00130100)
05 Q21, or Q21 Dev if the low half < 0x10 0b M25 14 N46
06 P98 0c M26 15 N45

Ids 0x00010001 and 0x00010002 both answer iPodP68C (with no space) before the switch is reached. Everything else answers ???.

Chip id (chipid_read, block 0x3d100000, gated by clock 0x4000 for the one read):

  • +8 is 0x8900: the revision is +4[27:24].
  • +8 is 0x8702: the revision is +4[27:24] bit-reversed, a second nibble is +4[31:28] bit-reversed, and a flag is +4 bit 4.
  • Anything else, or bit 0 of +0 clear: id 1.

The result is cached as 8 bytes.

The boot ROM's table

The WTF owns no USB code, no crypto and no DFU state machine. The word at 0x22007784 is 0x20000020, the boot ROM's exported table, and the WTF blxes through its slots. The slot values below are read from bootrom.bin:

slot value what
+0x44 0x2203fff8 the cell holding the ROM's bootrom_ctx pointer (data, not a function)
+0x68 0x200035a8 dfu_session_init
+0x6c 0x200036c8 dfu_wait_for_done
+0x74 0x200006dc dfu_header_verify
+0x78 0x20009eb0 usb_irq_handler, passed as a handler
+0x7c 0x2000a068 usb_poll_otg_events, passed as a handler
+0x80 0x200031dc usb_set_descriptor_pointers
+0x8c 0x2000106c irq_set_handler
+0x90 0x2000132c irq_get_handler
+0x94 0x20001198 eic_set_pin_handler
+0x9c 0x20001154 eic_dispatch_group0, passed as a handler

Signature checking therefore stays in the ROM, and the WTF inherits whatever the ROM's USB handler does.

The DFU session

wtf_dfu_session(flags, base = 0x08000000, max = 0xf80000, version), at 0x22001698, is a separate function that Ghidra had merged into wtf_main through the tail call. It borrows the ROM's machinery and gives every piece back:

  1. mm_alloc a 0x2764-byte scratch block (tag DUFD) and check it is in DRAM.
  2. Mask interrupts. Clear the low three bits of the SDRAM control word 0x38100000 (saved).
  3. Save the ROM's context pointer from 0x2203fff8 and put the WTF's own block (0x22007e88) there.
  4. usb_set_descriptor_pointers, then patch the device descriptor (USB identity).
  5. Open UART 0 and write DFU Start\r\n.
  6. Fill bootrom_init_params:

    field value
    ctx 0x22007e88
    +4 the scratch block
    +0x28 size 0xf80000
    +0x2c base 0x08000000
    +0x30 clock 0 × 1,000,000
    +0x34 flags & 1
    +0x35 flags & 4
    +0x37 flags & 2
    +0x36 (auto-jump), +0x38 (verify) 0: the ROM neither jumps nor verifies; the WTF does both
  7. Pulse the OTG core's soft disconnect: set bit 1 of 0x38400804, wait 500 ms, clear it, wait 10 µs. The host sees a fresh device.

  8. Save the ROM's handlers for IRQ 0x13 and 0x21. Install usb_irq_handler on 0x13 and eic_dispatch_group0 on 0x21, and set usb_poll_otg_events on EIC port 0 pin 2.
  9. Read clock-gate words 0x3c500048/4c, then gate on 0x400, 0x1 and 0x2000.
  10. Save both VIC enable words and mask everything (0xffffffff to +0x14 on each VIC).
  11. dfu_session_init. Enable IRQs 0x27, 0x28, 0x13, 0x21. Set GPIO group 0 bit 2. irq_restore(0x40) - a constant, not the saved mask, so IRQs come on and FIQs stay masked.
  12. dfu_wait_for_done. If it returns 0, the status is 0x17.
  13. If flags & 8: dfu_header_verify(base, base + 0x800, 2). A result of 0 gives status 0x57. Otherwise, if the AES block is busy, wait for its done bit.
  14. Put everything back in reverse:
    1. Clock-gate words.
    2. SDRAM control.
    3. The ROM's two handlers.
    4. Both VIC enable words.
    5. The ROM's context pointer.
    6. The saved interrupt mask.
  15. Free the scratch block and write DFU End\r\n. If the status is 0 and flags & 0x10, jump to base + 0x800, or to base when no verify was asked. Close the UART.

Jumping writes cookie 0 first. If the image ever returns, the WTF writes booting! again.

The two early failures (dram_pointer_validate, usb_build_device_descriptor) return before the restore in step 14. The ROM's context cell is then left pointing at the WTF's block. That is the image's behaviour, and the port keeps it.

USB identity

The ROM's device descriptor is copied into the WTF's .bss at 0x22007e74 so its idProduct (+0x0a) can be patched for the model and revision:

HwVr idProduct
0x0013xxxx, xxxx < 0x100 0x1241
0x0013xxxx, 0x100-0x1ff 0x1245 - this device; matches what real hardware enumerates as
0x0013xxxx, 0x200-0x2ff 0x1247
0x0013xxxx, ≥ 0x300 0x1250
0x0014xxxx 0x1242
anything else no descriptor; the session returns 2

The code that looks like a fallback compares version >> 16 against 0x7fffffff. A 16-bit value can never match, so that branch is dead.

The string descriptors are the WTF's own:

descriptor slot string
4 iPod GRecovery (flags & 0x20 clear)
4 iPod URecovery (0x20 set, 0x40 clear)
4 iPod Recovery (both set; what 0x7d selects)
6 iPod Recovery, ASCII, length-prefixed
7 ROM BOOT, ASCII, length-prefixed

Drivers it runs

block address used for
clocks and PLLs 0x3c500000 the clock tree, gates, the CPU divider
SDRAM controller 0x38100000 power mode, refresh, the session's control word
timer 0x3c700000 the free-running counter behind every delay
GPIO / EIC 0x3cf00000, 0x39a00000 pin functions, the strap pins, the USB pin handler
SPI + NOR 0x3c300000 / 0x3ce00000 / 0x3d200000 reading syscfg out of NOR
UART 0x3cc00000 DFU Start / DFU End, by DMA, at no particular baud rate: uart_configure never reads the rate it is given
DMA 0x38200000, 0x39900000 the UART's transfers
VIC 0x38e00000, 0x38e01000 saved, masked, restored around the session
chip id 0x3d100000 chipid_read
AES 0x38c00000 only waited on, after the ROM's verify

nvram is a RAM copy of the NOR's syscfg store. nvram_init probes NOR offsets 0, 0x2000 and 0x4000 for the SCfg magic. If none has it, it returns 0x52, which makes wtf_main reset with "softupdt". Nothing on the path to the session writes NOR (see below).

What it carries and never runs

Statically, 146 of the 274 functions are reachable from wtf_reset_entry. Fewer run: everything below power_state_transition is reachable only through it, and its one caller passes 6, which returns without doing anything.

  • The panel, the PMU and the click wheel never run. The LCD controller, all seven panel command scripts, backlight, PMU (I2C 0x73) and wheel code are present but unused, so the WTF draws nothing. The screen stays as the ROM left it. Of the seven scripts, the one that powers a panel up is reachable only from state 4. Even with an image buffer the draw op could not draw: all three panel descriptors carry format 0x3d, which lcd_blit_rect does not handle.
  • The NOR write side has no caller inside the WTF. The driver is complete: nor_write_bytes (AAI word program), nor_program_page, nor_erase_sector (0x20), nor_erase_block (0xd8), and nor_program_range above them, which erases, merges a 4 KiB sector and rewrites it. The write functions are reached only through the NOR device table at 0x220073bc, through its pointer cell 0x220073ac. Inside the WTF the only code that reads that cell is the open, close, query and read wrappers. No bl targets nor_program_range or nor_op_range_with_progress, and no word points at them except the table's own slots. OPEN: whether a later stage uses them. The WTF stays resident in SRAM after its jump, so the image it loads could call through 0x220073bc, as the WTF calls through the ROM's table. None of the other images in this tree (RetailOS, aupd, disk, diag, the recovery image, the ONB) contains 0x220073bc, 0x220073ac or any of these function addresses.

Why an unused writer is linked at all (MEASURED). The table is a generic flash-driver interface, statically initialised in .data with every op:

slot op
open nor_init
close spi_clock_disable
erase a range nor_op_range_with_progress
erase a sector nor_erase_sector
read nor_read_data
write nor_write_bytes
program a range nor_program_range
query nor_query_geometry, nor_query_zero

An initialiser that names a function keeps it in the link, whether or not anything calls the slot. The image shows the same pattern throughout: 128 of its 274 functions are unreachable, including a whole protection-unit CP15 library and every panel script. The link did no dead-stripping, and this driver comes from the shared BootLoader tree, as btMm.c does.

Who writes NOR in a restore (INFERRED). What the WTF loads next is FIRMWARE.x1245.RELEASE.dfu (reports/recovery_firmware.md), an aupd-family image. aupd carries its own SPI-NOR writer (chikuma/aupd/flash/spinor.zig), so that stage most likely writes the ONB with its own driver. That stays open until its body is decrypted. * The "optional hooks" are audio-driver ops. They call through 0x22007398, the WTF's copy of RetailOS's audio-codec driver entry (identified through byte-identical twin code in RetailOS). Nothing in the WTF fills it, so all four answer 0x11. * Most of the CP15 layer is dead. Only the five maintenance calls in the crt0 run. The protection-region and "lockdown" accessors follow the ARM940T/946E-S protection-unit register layout, not the ARM926's, which suggests a shared library carried along.

Apple's own bugs, kept

These are reproduced as they are, and each has a test that pins it:

  • debug_vformat: %x and %X both print uppercase, widths are ignored for %d, %u and %s, and a trailing % reads past the NUL.
  • aeabi_idivmod by zero writes to semihosting and returns quotient 0, remainder 0x20026.
  • strlcpy_n leaves the destination unterminated when it truncates.
  • nor_program_page sends min(total, 0x100) bytes per page instead of the remainder.
  • nvram_set_tag counts an append before checking it can write it.
  • clock_set_source_and_divider programs domain 11's divider into domain 6's field.

Reproduced

chikuma/wtf/ is the WTF in Zig, all 274 functions, one folder per subsystem. PORT.md maps every Ghidra function to its file and Zig name. The tests:

  • 242 host tests (make wtf-zig-test), checked against the image and bootrom.bin and never against a fixture typed by hand;
  • register-level functions assert the exact store sequence the disassembly performs;
  • wtf_dfu_session runs end to end against a fake boot ROM;
  • the syscfg path runs over a real device's NOR.

make wtf-zig links it for the device at 0x22000000. Data the WTF addresses absolutely, such as its context block, descriptor copy and DMA list items, is the WTF's address under test and the build's own object on target.

On the emulator (MEASURED, qemu-ipod/gates/wtf_dfu_gate.py). Apple's WTF and ours go through the same chain on the same machine: boot ROM, USB DFU, WTF, USB DFU, the next image. Both produce the same observations:

  • the same UART bytes (DFU Start, then DFU End);
  • the same handoff block, apart from each build's own device-info address;
  • a second session that enumerates as 0x1245;
  • a jump into the uploaded image, which then runs.

The comparison found one port bug the host tests had encoded. Flags bit 0x04 goes to +0x35, not +0x36: the WTF never asks the ROM to jump, it prints DFU End and jumps itself.

The firmware-partition aupd cannot follow the WTF. It is linked at 0x08000000, and the WTF enters every image at 0x08000800. aupd's crt0 relocates itself by copying down to its link address, but with the 0x800 overlap the copy overwrites the loop running it. On the emulator it ends spinning in aupd's own code at 0x08000848. With the I-cache just turned off, hardware would fetch the same overwritten words. The stage iTunes really sends after the WTF, FIRMWARE.x1245, is linked at 0x08000800 (MEASURED from its crt0, reports/recovery_x1245.md). It is aupd's recovery build:

  • 97% of its functions are instruction-identical to aupd's;
  • the UI is removed;
  • the WTF's UART driver is added;
  • aupd's whole SPI-NOR write driver is kept.

OPEN:

  • Not run on a device.
  • Not pinned in the port:
  • the UART handle on the two early exits, which is stack garbage in the image;
  • clock_set_source_and_divider's unbounded table stores;
  • pll_set_frequency outside 1..3.
  • Who writes the ONB into NOR during a restore. It is not the WTF, which has the driver but never calls it. FIRMWARE.x1245 carries aupd's complete NOR writer and is the likely writer. Its main has not been traced to the write yet.
  • Who reads 'SysI'. The RetailOS image carries the string SysI at 0x42310, which makes it the likely consumer of the handoff at 0x2203ff18. That is INFERRED, not traced.