usbliter8: A BootROM USB Heap Underflow on Apple A12/A13 SecureROM

Original text: “Introducing usbliter8”PS Team, Paradigm Shift Technology blog (June 18, 2026). Code, ASCII diagrams and figures below are reproduced verbatim with attribution captions.

Executive Summary

usbliter8 is a new BootROM-level exploit chain targeting Apple A12, S4/S5 and A13 SoCs — the silicon under iPhone XS/XR/11, Apple Watch Series 4 and Series 5, and the iPads built around those parts. At its core sits an inherent hardware bug in the Synopsys DesignWare DWC2 USB controller: when the controller resets its DMA write pointer after the third Setup packet, it always decrements DOEPDMA by 24 bytes, but accepts smaller (12-byte) Setup payloads. The mismatch yields a 12-byte-step buffer underflow primitive over USB, with no authentication and no interaction beyond plugging the device into a host.

The Paradigm Shift team turned that primitive into full SecureROM code execution on both A12 (no PAC) and A13 (PAC-protected) by chaining heap corruption with task-structure tampering, IRQ-handler hijacking, and a controlled-X22 BLRAAZ gadget on A13. After exploitation they relocate and patch SecureROM in SRAM, force the device back into DFU with a PWND-tagged serial number, and inject a custom DFU request handler that bypasses iBoot signature checks. PoC source is on GitHub. The findings were coordinated with Apple Product Security prior to publication. Because the bug lives in immutable BootROM, no patch is possible for affected silicon; A14 and later configure DART correctly and are not exploitable. This post walks the bug, the PC-control techniques on A12 and A13, post-exploitation, and detection/hardening implications.

usbliter8 logo banner
The usbliter8 logo — an A12/A13 SecureROM USB exploit. Source: original article.

TLDR — PoC is at github.com/prdgmshift/usbliter8.

This write-up details a novel iPhone BootROM vulnerability discovered and exploited by the Paradigm Shift team. It walks the underlying bug, the exploitation techniques, and the post-exploitation steps required to fully compromise the application processor’s boot chain. The exploit chains a hardware bug in the USB controller with a configuration flaw present in the device firmware.

Currently supported targets are Apple A12, S4/S5, and A13. A12X/Z support is technically possible but not implemented — the existing set was already enough to validate both the vulnerability and the exploitation strategy.

The motivation for publishing the research and the accompanying PoC is to document the real-world impact of this class of hardware vulnerabilities, contribute to the broader understanding of modern BootROM security, and demonstrate that even recent SecureROM generations remain susceptible to subtle hardware flaws. Because these vulnerabilities reside in immutable code, affected users should be aware that migrating to newer hardware remains the most effective mitigation.

USB Setup Packets Anatomy

In the USB specification, every control transfer must initiate with a Setup transaction — the mechanism the host uses to issue any kind of request to the attached device. A proper Setup transaction is two packets sent by the host:

(1) TOKEN PACKET                                             host → device
┌───────┬─────────┬────────┬────────┬───────┬──────┐
│ SYNC  │   PID    │  ADDR   │  ENDP   │  CRC5  │ EOP  │
│ 8 bit │  8 bit   │  7 bit  │  4 bit  │  5 bit │      │
│       │ (SETUP)  │ device  │endpoint │        │      │
└───────┴─────────┴────────┴────────┴───────┴──────┘

(2) DATA PACKET                                              host → device
                   ┌────────────────────────┐
┌───────┬─────────┤   DATA  (8 bytes)      ├────────┬──────┐
│ SYNC  │   PID    │  = USB Device Request  │ CRC16  │ EOP  │
│ 8 bit │  8 bit   │                        │ 16 bit │      │
│       │ (DATA0)  │ This is what the USB   │        │      │
└───────┴─────────┤    driver receives     ├────────┴──────┘
                   └────────────────────────┘

Per the spec, the data payload of a Setup transaction must be exactly 8 bytes and adhere to a strict format. The device-request structure contained inside is passed verbatim to the software driver for handling. For clarity in the rest of this article we refer to that 8-byte payload as the “Setup packet.”

DWC2 Direct Memory Access

The USB controller in Apple’s SoCs is the Synopsys DesignWare DWC2. Information about how it behaves can be inferred from existing driver implementations such as the one in the Linux kernel.

What matters for this exploit is how the controller writes data to main memory. The AP configures DMA by allocating a memory region and writing its physical address into a specific MMIO register, DOEPDMA. The controller then uses that buffer to store data received in SETUP and OUT packets, which is processed by the device.

An important observation: when the USB controller writes a chunk of data, it directly increments the address stored in DOEPDMA. That register value acts as the source of truth for the next DMA destination, not merely a configuration value.

The Bug

The DesignWare USB controller stores up to three consecutive Setup packets in memory. Upon receiving a fourth Setup transaction, the DMA base address is reset to its starting position before writing — behaving like a ring buffer. After writing each received packet the controller increments DOEPDMA by the size of data written, and the reset operation is implemented by decrementing DOEPDMA by 24.

DWC2 DMA ring buffer animation
Normal DWC2 ring-buffer behaviour: three 8-byte Setup packets, then a 24-byte rewind on the fourth. Source: original article (animated SVG captured as PNG).

The core issue is that the controller also accepts smaller packets (though it always stores them in 4-byte chunks). Because the pointer increment does not match the fixed decrement amount, a buffer-underflow primitive in 12-byte steps falls out.

DWC2 ring buffer underflow bug animation
The bug: short Setup packets advance DOEPDMA by 12 instead of 24, while the rewind still subtracts 24 — each iteration walks the destination pointer 12 bytes backwards. Source: original article (animated SVG captured as PNG).

This appears to be an inherent bug in the USB controller itself. While it could potentially affect many devices, the vulnerability is exploitable only under specific circumstances. As of today, A12 and A13 SecureROMs are confirmed vulnerable; A11 is not, because the A11 USB driver manually resets the DMA address to its initial value after every received packet. On A12 and A13, USB DART is configured in bypass mode — SRAM data can be overwritten freely. A14 and later generations appear to configure DART correctly inside SecureROM, making the vulnerability unexploitable on those parts.

PC control on A12

Achieving PC control on A12 is straightforward, because the USB controller’s DMA buffer is allocated on the heap shortly after the USB task’s stack. The simplest approach: overwrite a saved LR on the stack and obtain direct PC control when the scheduler performs a context switch back to the USB task.

PC control on A13

Things are harder on A13 SecureROM thanks to Pointer Authentication. From observation, PAC appears to be applied only to stack-stored LRs — which is enough to prevent directly targeting the USB task’s saved LR the way it works on A12.

Several mitigations had to be bypassed along the way, including heap-metadata checksums verified during heap operations, and LR signing during context switches (which run whenever the USB task is woken up to process USB packets). After a few iterations the team arrived at the following multi-step technique for PC control.

The first step is to overwrite some DART-related data on the heap that sits immediately before the USB controller’s DMA buffer. That gives a very limited set of write primitives, triggered once on the path that exits the DFU loop. The exploit leverages the cleanup routines — which operate on controlled data — to zero out the global pointer to the DART allocation. This is necessary to keep the corrupted allocation from being freed, which would otherwise trigger a panic when the heap checksum is checked.

void dart_stop(unsigned int dart_id)
{
  // [1] we can fully overwrite the heap memory for this object
  dart = darts[dart_id];
  mmio_base = dart->info->mmio_base;
  v4 = 16 * dart->ctx->field_11 + 0x200;
  for ( int i = 0; i < 16; i += 4 ) {
    // [2] this gives us a 16-byte zero write primitive
    *(_DWORD *)(mmio_base + v4 + i) = 0;
  }
  dart_flush_maybe(mmio_base);
}

void dart_free(__int64 dart_id)
{
  // [...]
  dart_stop(dart_id);
  enter_critical_section();
  ref_count = info->ref_count - 1;
  info->ref_count = ref_count;
  if ( !ref_count )
    irq_mask(info->int_irq_num);
  exit_critical_section();
  // [3] we need to use the zero write primitive for these second deref to return 0 and make the free a no-op
  dart = darts[dart_id];
  free(dart);
  darts[dart_id] = 0;
}

Next, on the same cleanup path, a 0xF write primitive overwrites a global panic counter. From that point on, the next panic will spin the CPU in an infinite loop instead of triggering a reboot.

void dart_flush_maybe(__int64 mmio_base)
{
  __dmb();
  // [4] these gives us a 0xF write primitive targeting the panic depth counter
  *(_DWORD *)(mmio_base + 52) = 0xF;
  *(_DWORD *)(mmio_base + 32) = 0;
  ticks = get_ticks();
  while ( 1 ) {
    v3 = get_ticks();
    if ( (*(_DWORD *)(mmio_base + 32) & 4) == 0 )
      break;
    if ( v3 - ticks >= 1000000 )
      panic();
  }
}

void __noreturn panic()
{
  // [5] after overwritting this global we would enter an infinite loop on panic
  if ( ++panic_cnt >= 3 )
    spin();
  // [...]
}

The next step is to avoid breaking the USB task’s context — in particular the LR and SP registers that are saved to and restored from the task structure during context switches. To do that the exploit times DMA writes to land while the task is awake, so that when it later yields, the correct register values overwrite the ones that were corrupted.

            Task structure
 0x000  ┌─────────────────────┐
        │                     │
        │ Task state,         │
        │ scheduler data,     │
        │ crit. section depth │
        │                     │
 0x030  ├─────────────────────┤
        │                     │
        │   Other registers   │
        │                     │ <──┐
        ├──────────┬──────────┤    │ This area needs to
        │    LR    │    SP    │    │ be overwritten while
        ├──────────┴──────────┤    │ USB task is running
        │                     │ <──┘
        │  Safe to overwrite  │
        │                     │
 0x1b0  └─────────────────────┘

After that, the exploit targets a field inside the task structure itself that tracks the task’s critical-section depth. This allows it to trigger a panic with IRQs enabled, sending execution into the infinite loop set up in the first step while still letting ISRs run. The USB controller also stays in a state that allows further memory writes.

void enter_critical_section()
{
  current_task = current_task();
  critical_section_depth = current_task->critical_section_depth;
  if ( critical_section_depth < 0 || critical_section_depth >= 10000 )
    panic();
  // [1] on each call increments task's critical_section_depth
  current_task->critical_section_depth = critical_section_depth + 1;
  if ( !critical_section_depth ) {
    irq_disable();
  }
}

void exit_critical_section()
{
  current_task = current_task();
  // [2] should be equal to the amount of entries to `enter_critical_section`
  // but we can overwrite it with a smaller count
  critical_section_depth = current_task->critical_section_depth;
  // [3] on first entry it will enable interrupts and on second entry it will panic
  if ( critical_section_depth <= 0 )
    panic();
  critical_section_depth = critical_section_depth - 1;
  current_task->critical_section_depth = v2;
  if ( !critical_section_depth )
    irq_enable();
}

After all that setup, the exploit can freely overwrite memory until it reaches the global variable that holds the USB IRQ handler. Overwriting it with an arbitrary value yields PC control:

// [1] an array of `irq_handler_ctx` structs lives in the BSS section which we can reach with our bug
00000000 struct irq_handler_ctx // sizeof=0x18
00000000 {
00000000     void (*handler)(void *arg);
00000008     __int64 *arg;
00000010     _BYTE unk;
00000011     _BYTE shall_mask;
00000012     // padding byte
00000013     // padding byte
00000014     // padding byte
00000015     // padding byte
00000016     // padding byte
00000017     // padding byte
00000018 };

void handle_irq()
{
  irq_num = MEMORY[0x23B102004];
  if ( MEMORY[0x23B102004] ) {
    while ( irq_num ) {
      if ( (irq_num & 0x70000) != 0x10000 )
        panic();
      irq_num__ = irq_num & 0x1FF;
      // [2] after the Setup we can freely overwrite handler for USB interrupt with a controlled value
      handler = irq_list[irq_num__].handler;
      if ( handler )
        // [3] fully controlled handler gets called with fully controlled arg
        handler(irq_list[irq_num__].arg);
      _clr_mask_irq(irq_num__);
      irq_num = MEMORY[0x23B102004];
    }
  } else {
    ++stale;
  }
}

Post-Exploitation

Starting with A12, SecureROM runs mostly in EL0. That means no access to protected memory regions (MMU tables, ROM metadata) or special CPU registers (SCTLR, TTBR and friends).

SecureROM can switch execution to the privileged EL1 mode by executing an SVC 0. This isn’t a syscall-style interface — the handler simply returns to the instruction after the SVC, but now in EL1; shortly afterwards the ROM ERETs back to EL0. There are very few sites in the ROM where this transition happens. The chosen one sits inside the function responsible for transferring execution to the next-stage iBoot:

# A12 SecureROM
...
1000088B0:        SVC             0                              # switching to EL1, we want to land here
1000088B4:        LDR             W8, [X22]
1000088B8:        CBNZ            W8, loc_10000891C
1000088BC:        MOV             W8, #1
1000088C0:        STR             W8, [X22]
1000088C4:        STRB            W8, [X23]
1000088C8:        ADRP            X9, #0x19C014033@PAGE
1000088CC:        LDRB            W10, [X9,#0x19C014033@PAGEOFF]
1000088D0:        CBNZ            W10, loc_10000891C
1000088D4:        STRB            W8, [X9,#0x19C014033@PAGEOFF]
1000088D8:        LDRB            W8, [X24,#0x19C014032@PAGEOFF] # unintended compiler behavior, described later
1000088DC:        CBZ             W8, loc_10000891C
1000088E0:        BL              sub_100000430                  # clearing some SCTLR bits
1000088E4:        BL              sub_100006798                  # clearing something in scratch regs?
1000088E8:        BL              sub_10000739C                  # returns boot trampoline address
1000088EC:        MOV             X8, X0
1000088F0:        CBZ             X8, loc_100008904
1000088F4:        MOV             X0, X20
1000088F8:        MOV             X1, X19
1000088FC:        BLR             X8                             # jump to the boot trampoline!
...

A12 & S4/S5

These SoCs do not use PAC inside SecureROM. Code execution is obtained by corrupting LR on the stack — time for some ROP. The chain is small, just a few frames:

  1. Perform a 32-bit write to the USB MMIO to change the DMA destination address.
  2. Sleep a bit (~400 ms).
  3. After sleeping, jump to 0x1000088B0 (the assembly above).

The DMA destination is set to a location inside the boot trampoline. That memory is obviously not writable from EL0 (or even EL1 — it is intended to be execute-only), but since the write goes through DMA the restriction does not apply. The boot trampoline is also always stored at the same address.

The delay gives the exploit enough time to write the attacker’s shellcode into the boot trampoline. After the delay, the actual jump is performed. The firmware contains checks designed to prevent jumping into the middle of the function — a series of state flags is maintained and updated after each step. If the previous-stage flag is not set, the firmware triggers a panic.

Interestingly, the compiler generated these instructions for the checks:

...
1000088C8:        ADRP            X9, #0x19C014033@PAGE           # get page addr for flags' area into X9
1000088CC:        LDRB            W10, [X9,#0x19C014033@PAGEOFF]  # fetching something via X9
1000088D0:        CBNZ            W10, loc_10000891C              # this check doesn't fail, luckily
1000088D4:        STRB            W8, [X9,#0x19C014033@PAGEOFF]   # storing some other flag, also via X9
1000088D8:        LDRB            W8, [X24,#0x19C014032@PAGEOFF]  # doing a dangerous check... via X24!
1000088DC:        CBZ             W8, loc_10000891C
...

X24 is a callee-saved register, which means its value lives on the stack — the very stack the exploit fully controls. At this point the attacker executes their own code with EL1 privileges. All that’s left is to clean up, apply the persistent changes, and return to DFU like nothing happened.

Long story short:

  1. The original boot trampoline needs to be copied back where it belongs.
  2. All the heap allocations that were corrupted need to be restored; luckily, there are only a few and they hold static data.
  3. A custom USB control-request handler is injected; it lives in unused space inside the boot trampoline area, and the corresponding callback pointer in BSS is overwritten to point at it.
  4. The PWND string is injected into the USB serial-number string.
  5. The usb_task() stack frame is carefully repaired and execution returns to it.

Done — DFU mode is now running with a custom request handler. Many important implementation details were skipped here; the full exploit is in the PoC repository.

The handler

The handler itself is very simple. All standard USB DFU requests are forwarded straight to the original handler. Two custom requests are added:

  1. Demotion: lowers the SoC’s production mode (temporarily, of course — reset clears it).
  2. Booting iBoot: allows raw iBoot images to be booted without any signature checks.

There was a funny issue with booting iBoot at first. If the jump function is called directly from the handler, it doesn’t work. The jump function quiesces various hardware components, including USB — which terminates the USB task. Yes, that very USB task the code is executing from.

So, instead of calling the jump function directly, the exploit overwrites the main task’s return address on the stack and then closes the DFU session. The USB task shuts down, the main task is woken up and resumes execution; here is the location it is rerouted to:

# A12 SecureROM
...
100001C8C:        BL              sub_100008F60           # something we cannot really skip
100001C90:        BL              sub_100008FE4           # same here
100001C94:        TBNZ            W28, #2, loc_100001C9C  # check if it's local boot
100001C98:        BL              sub_100006850           # set scratch reg to signify remote booting for next stage,
                                                          # we do it inside of the handler code unconditionally
100001C9C:        MOV             X1, #0x19C030000        # load address, our raw iBoot will end up there
100001CA4:        MOV             W0, #0
100001CA8:        MOV             X2, #0
100001CAC:        BL              jump_to_next_stage
...

A13

A13 is a whole different beast thanks to PAC. The post-exploitation strategy is largely the same, however.

Plain ROP is no longer an option. Luckily, the boot trampoline area sits between BSS and heap, which means it can be overwritten with controlled data midway through the exploit. The jump function suffers from a similar problem to the A12 case: the address used for the steps validation is saved on the stack in X22, and unfortunately that is not easy to control here.

Instead, the team found a very nice gadget:

# A13 SecureROM
...
10000A5E8:        MOV             X22, X0                           # land here, immediately get controlled X22
10000A5EC:        ADRP            X10, #__stack_cookie@PAGE
10000A5F0:        NOP
10000A5F4:        LDR             X10, [X10,#__stack_cookie@PAGEOFF]
10000A5F8:        STUR            X10, [X29,#var_38]
10000A5FC:        LDR             X10, [X22]
10000A600:        ADD             X10, X10, #0xF
10000A604:        AND             X10, X10, #0xFFFFFFFFFFFFFFF0
10000A608:        MOV             X11, SP
10000A60C:        SUB             X23, X11, X10
10000A610:        MOV             SP, X23                           # all this logic doesn't break anything, luckily
10000A614:        LDR             X10, [X22,#0x10]                  # oh look, function pointer from a controlled place!
10000A618:        MOV             X1, X23
10000A61C:        MOV             X2, X9
10000A620:        MOV             X3, X8
10000A624:        BLRAAZ          X10                               # jumping to the jump func, auth is fake
...

What’s notable is that the firmware makes heavy use of authenticated branch instructions, yet only the IB key is actually enabled. Convenient. At this point, code runs with full privileges. Cleaning up on A12/S4/S5 was already challenging; on A13 the situation is even worse because almost everything has been destroyed by the time control is gained.

ROM Restart

The way the team recovers on A13 is by restarting SecureROM and letting it reinitialize everything. The only complication: the modifications need to survive the restart. To make that work, the ROM is copied to the very end of SRAM, so it can be patched any way the attacker likes.

Unfortunately, the ROM expects to execute from its original address at 0x100000000, not from SRAM. It often performs PC-relative accesses into SRAM at 0x19C000000, which would go nowhere if the ROM ran from an unexpected location. To get around this, the MMU is configured so that the SRAM physical addresses where the code was copied are mapped to the original ROM virtual addresses.

The MMU is enabled from the very beginning of the in-SRAM ROM. At some point, the ROM creates its own translation tables and switches to them — at that exact moment, everything would fall apart. The exploit hooks ROM PTE generation and routes addresses the way it wants. There is quite a bit of dance in the code that performs the ROM restart; the source code has the full reference.

The list of patches applied to the relocated ROM:

  1. Lower load area size. The remapped ROM technically belongs to the load area (where raw data loaded from NAND or USB lands), so the ROM would otherwise try to clear it — a disaster. The ROM is patched to ignore the last few hundred kilobytes of SRAM.
  2. Force DFU. Return to DFU, do not boot whatever resides in NAND.
  3. Hook USB serial-number creation to add the PWND tag.
  4. Inject the custom USB request handler.

That’s it. SRAM is pretty big nowadays, and so is the load area — stealing a few hundred kilobytes should not cause problems.

We reported these findings to Apple Product Security prior to publication and coordinated disclosure together. We would like to thank the Apple Product Security team for their prompt response, constructive engagement, and cooperation throughout the disclosure process.

Paradigm Shift Technology team

Key Takeaways

  • Hardware bug, not firmware. The 12-byte buffer-underflow primitive is in the Synopsys DWC2 USB controller itself — immutable silicon on A12 and A13.
  • Software config is what makes it weaponizable. A11 sidesteps it by manually resetting DOEPDMA after every packet; A14+ correctly configures DART so DMA can’t reach SRAM. A12/A13 leave DART in bypass mode for SecureROM, which exposes the primitive.
  • No interaction required. A USB host that can issue Setup transactions is enough — no auth, no user prompt, works from DFU.
  • PAC slows but does not stop the attack. On A13 the team chains heap corruption, panic-counter overwrite, critical-section-depth tampering and an IRQ-handler hijack to land a controlled X22, then uses a BLRAAZ gadget where only the IB key is enabled.
  • Persistence via SRAM-relocated ROM. SecureROM is copied to the tail of SRAM, the MMU is reconfigured to keep PC-relative accesses valid, and patches force DFU plus a tagged serial number plus a custom DFU request handler that boots unsigned iBoot.
  • SEP boundary still matters. The exploit doesn’t break the Secure Enclave by itself, but it opens up much wider attack surface against it.
  • Apple was notified. Disclosure was coordinated with Apple Product Security; affected silicon cannot be patched, only retired.

Defensive Recommendations

  • Retire affected hardware where it carries sensitive data. A12 and A13 devices (iPhone XS/XR/11, iPad 8th gen, Apple Watch Series 4/5) cannot be patched against this; treat them as devices an attacker with brief physical access can compromise at the boot-chain level.
  • Physical-access control is the only real mitigation. Custody chain, tamper-evident enclosures and supervised handling matter more than software policies for these models.
  • Don’t trust attestations from affected silicon. If your threat model includes a remote attacker eventually getting physical access, treat any attestation rooted in A12/A13 BootROM as advisory, not authoritative.
  • For Apple’s customers: data at rest still matters. usbliter8 itself does not break the Secure Enclave or user data encryption — a strong passcode and Data Protection keep the file system out of reach even after BootROM compromise. Discourage four-digit passcodes on affected devices.
  • For enterprises: refresh the MDM-enrolled fleet. Move sensitive workloads to A14+ silicon (iPhone 12 and later, iPad Air 4 / iPad Pro 2020 and later). The DART configuration fix in those parts removes the SRAM-write primitive entirely.
  • Detection at the USB layer. If you control the USB host (e.g., a kiosk, a forensic workstation) and want to catch usbliter8-style activity, look for atypical Setup-transaction sequences: many short Setup payloads, repeated rounds of three+fourth Setups, and DFU mode entry followed by unusual control requests.
  • Forensics on suspect devices. Check the USB serial-number string for PWND, and look for unexpected DFU re-entry. Neither survives a clean restore from signed firmware, but they are useful tells while a device is still in the compromised state.
  • For your own firmware: never trust attacker-controlled DMA pointers as the source of truth. Reset DMA base addresses explicitly per transaction (the A11 approach), and never leave an IOMMU/DART in bypass mode for code paths that can be reached from external interfaces.

Conclusion

usbliter8 shows that even on more recent SecureROM generations — including those protected by Pointer Authentication — subtle hardware bugs can still be leveraged to achieve full code execution and break the chain of trust. The security of the BootROM is critical: vulnerabilities at this level compromise the integrity of the entire device. Apple’s SEP (Secure Enclave Processor) adds a separate security boundary between attacker and user data, and although usbliter8 doesn’t directly affect SEP itself, it opens up wider attack vectors against the Secure Enclave. Newer generations have addressed the underlying issue, but affected A12 and A13 devices will carry it for the remainder of their service life. For anyone who has followed iPhone exploitation and jailbreaking history, this research is a reminder that the BootROM occasionally still has a surprise left to give — and that defensive engineering and deep offensive research need each other to build more resilient systems.

Original text: “Introducing usbliter8” by PS Team at Paradigm Shift Technology. Full PoC is in the team’s GitHub repository.

Comments are closed.