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:
- Invalidate the I-cache, disable the I-cache and MMU, and turn on the alignment check.
- Set up the IRQ and SVC stacks.
- Relocate itself if it is not running where it was linked.
- Copy
.dataand zero.bss. - Call
wtf_main. - Semihosting
SYS_EXIT, in casewtf_mainreturns.
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:
power_state_transition(6)- state 6 is the one case of that switch that does nothing (see What it carries and never runs).clock_tree_init.- With clock gate
0x10000on: clear the low three bits of0x3930003c. - Write
0x1002to the EIC at0x39a00000. timer_init.sdram_set_power_mode(2).- Describe the heap region:
0x08f80000,0x80000bytes, immediately after the0xf80000-byte load window at0x08000000.
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):
+8is0x8900: the revision is+4[27:24].+8is0x8702: the revision is+4[27:24]bit-reversed, a second nibble is+4[31:28]bit-reversed, and a flag is+4bit 4.- Anything else, or bit 0 of
+0clear: 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:
mm_alloca0x2764-byte scratch block (tagDUFD) and check it is in DRAM.- Mask interrupts. Clear the low three bits of the SDRAM control word
0x38100000(saved). - Save the ROM's context pointer from
0x2203fff8and put the WTF's own block (0x22007e88) there. usb_set_descriptor_pointers, then patch the device descriptor (USB identity).- Open UART 0 and write
DFU Start\r\n. -
Fill
bootrom_init_params:field value ctx 0x22007e88+4the scratch block +0x28size 0xf80000+0x2cbase 0x08000000+0x30clock 0 × 1,000,000 +0x34flags & 1+0x35flags & 4+0x37flags & 2+0x36(auto-jump),+0x38(verify)0: the ROM neither jumps nor verifies; the WTF does both -
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. - Save the ROM's handlers for IRQ
0x13and0x21. Installusb_irq_handleron0x13andeic_dispatch_group0on0x21, and setusb_poll_otg_eventson EIC port 0 pin 2. - Read clock-gate words
0x3c500048/4c, then gate on0x400,0x1and0x2000. - Save both VIC enable words and mask everything (
0xffffffffto+0x14on each VIC). dfu_session_init. Enable IRQs0x27,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.dfu_wait_for_done. If it returns 0, the status is0x17.- If
flags & 8:dfu_header_verify(base, base + 0x800, 2). A result of 0 gives status0x57. Otherwise, if the AES block is busy, wait for its done bit. - Put everything back in reverse:
- Clock-gate words.
- SDRAM control.
- The ROM's two handlers.
- Both VIC enable words.
- The ROM's context pointer.
- The saved interrupt mask.
- Free the scratch block and write
DFU End\r\n. If the status is 0 andflags & 0x10, jump tobase + 0x800, or tobasewhen 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 format0x3d, whichlcd_blit_rectdoes 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), andnor_program_rangeabove them, which erases, merges a 4 KiB sector and rewrites it. The write functions are reached only through the NOR device table at0x220073bc, through its pointer cell0x220073ac. Inside the WTF the only code that reads that cell is the open, close, query and read wrappers. Nobltargetsnor_program_rangeornor_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 through0x220073bc, 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) contains0x220073bc,0x220073acor 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:%xand%Xboth print uppercase, widths are ignored for%d,%uand%s, and a trailing%reads past the NUL.aeabi_idivmodby zero writes to semihosting and returns quotient 0, remainder0x20026.strlcpy_nleaves the destination unterminated when it truncates.nor_program_pagesendsmin(total, 0x100)bytes per page instead of the remainder.nvram_set_tagcounts an append before checking it can write it.clock_set_source_and_dividerprograms 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 andbootrom.binand never against a fixture typed by hand; - register-level functions assert the exact store sequence the disassembly performs;
wtf_dfu_sessionruns 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, thenDFU 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_frequencyoutside 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.x1245carries 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 stringSysIat0x42310, which makes it the likely consumer of the handoff at0x2203ff18. That is INFERRED, not traced.