System Internals

The iOS Chain of Trust

iOS runs nothing it hasn't verified. This is the boot chain walked in reverse, from the first immutable byte in the Boot ROM, through iBoot and the Image4 container format, up to the kernelcache, with a stop at the one ROM bug (checkm8) that rewrote the rules for an entire era of devices.

Turn on an iPhone and within a few milliseconds it is running an operating-system kernel signed by Apple. The flash storage that holds that kernel is writable, and the USB port is right there, so in principle an attacker who can change the bytes on disk should be able to boot their own code. In practice they cannot, because the device runs a sequence of signature checks that begins in the SoC and continues until the kernel is loaded. Each stage checks the signature of the next before running it.

This post follows that sequence in reverse, from the part an attacker cannot modify up to the kernel, looking at what each stage checks before it trusts the next.

Everything in this article is public: Apple’s Platform Security documentation, the Image4 format as implemented by open-source tooling, and the checkm8 research from 2019. It contains no exploit, private detail, or 0day.

The problem: a root of trust

Verification like this is circular unless it ends somewhere. The kernel is trusted because iBoot checked its signature, iBoot because an earlier stage checked its signature, and so on down. Either that regress continues forever or it stops at something the device trusts without checking. That something is the root of trust. For it to be useful it has to meet two conditions: an attacker must not be able to modify it, and it must hold the reference value that every later check compares against.

The chain provides two separate guarantees, and they defend against different things. Integrity means that at each stage, only code signed by Apple runs. Anti-downgrade means the device will not accept an older signed version whose bugs are already public. Integrity prevents booting an unsigned kernel. Anti-downgrade prevents reinstalling an old iBoot to reuse a vulnerability Apple has since fixed.

The anchor: Boot ROM

The root of trust is the first code the Application Processor runs when it leaves reset. Apple calls it the Boot ROM. Most people who study it call it SecureROM, the name that appears as a string inside the code itself. It is read-only memory written when the chip is fabricated, and it contains a small amount of code plus the Apple Root CA public key.

The Boot ROM uses that key to verify that the next stage was signed by Apple before running it. Because the memory is read-only, neither the key nor the code can be changed after fabrication. That is what lets the Boot ROM serve as the anchor: if it could be modified, an attacker could modify it, and it would then verify the attacker’s code as readily as Apple’s.

The same property has a cost. A bug in later code can be patched; a bug in the Boot ROM cannot, because read-only silicon cannot be updated in the field. checkm8, later in this post, is an example.

Trust is passed up from the anchor one stage at a time, following the same rule throughout: a stage verifies the signature on the next stage before transferring control to it. A valid signature at the bottom therefore vouches for the entire sequence above it.

The exact stages depend on the age of the device. On an A9 or earlier SoC there is an extra one, the Low-Level Bootloader (LLB): the Boot ROM verifies and runs LLB, and LLB verifies and runs iBoot. On A10 and later the Boot ROM loads iBoot directly. LLB still exists as an image but is now identical to iBoot, tagged so the Boot ROM jumps to it, and iBoot runs in two internal phases, the first performing LLB’s old role. On a modern device the chain is three stages:

   Application Processor, out of reset
             │
             ▼
        Boot ROM        immutable, holds the Apple Root CA public key
             │          verify signature, then jump
             ▼
         iBoot          (A10+; older SoCs run LLB before this stage)
             │          verify signature, then jump
             ▼
       kernelcache      XNU + kexts, wrapped in an Image4 container

iBoot is a full bootloader. It has its own USB stack, a command interpreter in development builds, and the code that loads the kernel. When it finishes its setup it verifies and runs the kernelcache.

A kernelcache is not a bare kernel. It is XNU prelinked with the kernel extensions the device needs, compressed, and, on every 64-bit device, wrapped in the Image4 container described in the next section. That container is what lets iBoot confirm the image it read from disk is the exact kernel Apple signed.

Boot modes: normal, recovery, and DFU

The same chain runs differently depending on how the device was started. The three modes below stop at different stages and expose different things over USB.

Normal boot runs the entire chain and starts the OS.

Normal boot
    Boot ROM ──▶ iBoot ──▶ kernelcache ──▶ iOS
    the whole chain runs and the OS starts

Recovery mode runs the chain up to iBoot, then iBoot stops short of loading the kernel and waits for a host over USB. This is the “connect to computer” screen, and it is what an ordinary restore or update talks to. The code handling the host is iBoot, one stage above the Boot ROM.

Recovery mode
    Boot ROM ──▶ iBoot ──▶ ✗  kernel is not loaded
                    │
                    └── iBoot waits on USB ("connect to computer" screen)
                        used for normal restores and updates

DFU mode is lower. The device halts in the Boot ROM, before iBoot runs, with the screen black, and the Boot ROM waits for a host to send the next image over USB. It verifies and runs whatever it receives, the same as during a normal boot. DFU is used for the lowest-level restores, and it is the mode a Boot ROM exploit needs, because the USB code listening in DFU belongs to the Boot ROM itself. checkm8 is triggered here.

DFU mode
    Boot ROM ──▶ ✗  iBoot has not started
        │
        └── the Boot ROM waits on USB (screen stays black)
            used for low-level restores; checkm8 attacks the USB code at this stage

Image4: the container everything is signed in

Every signed object in the chain uses the same container: Image4, written IMG4. iBoot, the kernelcache, the device tree, the SEP firmware, and the restore ramdisk are all IMG4 files. It is an ASN.1 structure, DER-encoded, with three parts that matter here.

The payload is the IM4P. It holds a four-character type tag identifying the contents (krnl for the kernelcache, ibot for iBoot, sepi for the SEP firmware, and so on), a description string such as a build version, the payload bytes, and, if the payload is compressed, the compression scheme. Two schemes appear in practice: LZSS, which Apple wraps with a complzss header, and LZFSE, whose stream starts with a bvx block magic such as bvx2. An encrypted payload also carries a KBAG, the wrapped key material.

The manifest is the IM4M, also called the APTicket. It contains the expected digests of every image in the boot chain (SHA-384 on modern devices), a set of manifest properties, an X.509 certificate chain, and an RSA signature over all of it. One point is often stated incorrectly: this is not a CMS or PKCS#7 signature. It is an Apple-specific ASN.1 structure with an RSA signature over a SHA-384 digest, and its certificate chain terminates at an Apple-controlled secure-boot root whose public key is the one held in the Boot ROM.

The restore info is the IM4R. Its main content is the boot nonce, tagged BNCN, which the next section depends on.

Verifying an image against a manifest is two steps: confirm that the manifest’s signature is valid and chains to Apple, then confirm that the image about to run hashes to the digest the manifest lists for it.

Personalization and the signing window

The checks so far verify a signature. They do not yet explain why a valid, Apple-signed iBoot from an old release cannot be installed today. That is enforced by a separate mechanism.

Apple signs each build per device and per install rather than once for all devices. During a restore or update, the device contacts Apple’s signing service (named Tatsu, referred to as TSS) and sends the list of images it wants to install along with two device-specific values. The first is the ECID, the Exclusive Chip Identification, a value unique to that SoC. The second is the ApNonce, a fresh anti-replay nonce derived from a random generator value and hashed (SHA-384 on current chips) into the manifest field BNCH. TSS returns a manifest bound to that ECID and that nonce. This personalized signed manifest is the APTicket; a saved copy of it is an SHSH blob, or on modern devices a .shsh2 file.

This binding has two effects. Because the manifest is tied to the ECID, a ticket signed for one device does not validate on another, so signed firmware cannot be copied between devices. Because it is tied to a fresh nonce, a saved ticket cannot be replayed later unless the matching nonce generator can be reproduced, which is not possible on a stock device.

The signing window is a time limit on top of this. Apple issues fresh signatures only for the build it currently ships. Some days or weeks after a new release, it stops signing the previous one, after which TSS will not personalize that build for any device. Without a previously saved ticket and a reproducible nonce, there is then no way to restore or downgrade to it.

This is what made saving SHSH blobs worthwhile. While Apple still signs a build, you can request and store its personalized ticket with a tool such as tsschecker. Once the signing window closes, that saved ticket lets you restore or downgrade to that build later without a fresh signature from Apple. Early on, a saved blob could be replayed as is. Apple then bound each ticket to the ApNonce, so a stock device generates a new nonce at restore and the saved ticket stops matching. Reusing blobs after that also requires fixing the boot-nonce generator to reproduce the original nonce, which needs a jailbreak; futurerestore does both, taking a saved blob plus a forced generator. On A12 and later this is largely closed off.

The Secure Enclave boots alongside

While the Application Processor works through that sequence, the Secure Enclave performs an equivalent one in parallel, isolated in hardware. The enclave has its own dedicated Boot ROM, a separate root of trust on the same die. At startup iBoot reserves a region of memory for it, and the Application Processor passes the enclave its operating system, sepOS. The enclave’s own Boot ROM verifies that image’s hash and signature before running it. iBoot and the Application Processor deliver the image but do not perform this check. If the check fails, the enclave stops operating until the next full chip reset.

On A13 and later, an additional Boot Monitor measures the sepOS that booted, and a hardware mechanism, System Coprocessor Integrity Protection (SCIP), restricts the enclave to executing only its own Boot ROM code. The enclave is covered in its own article. Secure boot is really two chains, rooted in two separate ROMs, that meet at a shared memory region.

When the chain breaks: checkm8

The chain’s security depends entirely on the anchor, and on a large range of devices the anchor is exploitable.

In September 2019, axi0mX published checkm8 (CVE-2019-8900), a use-after-free in the Boot ROM’s USB code. It is reachable only in DFU mode, Apple’s low-level Device Firmware Update state, and only over a physical USB connection. When DFU initializes USB it allocates a global buffer for control transfers and keeps a pointer to it. If a control transfer is started but its data phase is left unfinished, and DFU is then aborted, the teardown frees the buffer without clearing the pointer. Re-entering DFU reuses the freed pointer. With control over what gets allocated into the freed memory, that use-after-free becomes code execution in the earliest and most privileged code on the device.

Reduced to its core, the bug is a freed pointer that is never cleared. The code below captures the shape of the flaw in simplified pseudocode. Apple’s Boot ROM is closed source, so this is reconstructed from public analysis, not copied from it:

static void *io_buffer;              /* global buffer for USB control transfers */

/* A control transfer with a data stage sets the buffer up. */
static void usb_setup_transfer(size_t len)
{
    io_buffer = alloc(len);
}

/* Aborting DFU tears the transfer down. */
static void usb_quiesce(void)
{
    free(io_buffer);
    /* freed, but io_buffer is never set back to NULL */
}

/* Re-entering DFU drives another transfer through the same global. */
static void usb_handle_transfer(const void *data, size_t len)
{
    memcpy(io_buffer, data, len);    /* io_buffer now points at freed memory */
}

The free runs in the abort path and leaves io_buffer dangling, and the next transfer writes through it. The rest of checkm8 is timing and heap control: the attacker reallocates the freed region with its own data before the stale pointer is used, so the write lands on something it controls.

checkm8 affects every SoC from A5 to A11, meaning iPhones from the 4S through the iPhone 8 and iPhone X, along with many iPads and iPods and the A10-derived T2 chip in Intel Macs. Because the vulnerable code is in read-only ROM, none of these devices can be patched.

Two distinctions matter. First, the checkm8 bug is not the checkra1n jailbreak built on it: the bug covers A5 to A11, while checkra1n supported only A7 through A11, and A5 and A6 required other tools. Second, checkm8 does not by itself provide access to user data. It requires physical possession of the device and a cable, it is non-persistent (it does not survive a reboot and must be re-run over USB), and it does not defeat the Secure Enclave, the passcode, or the data-at-rest encryption they protect. What checkm8 gives is control of the boot chain itself.

That control is still significant, because every guarantee above the Boot ROM (the signature checks, the signing window, the trust caches and code-integrity mechanisms covered in later articles) is enforced by code that checkm8 can replace. This is why a Boot ROM bug is worth more than most bugs above it, and why later mitigations are designed on the assumption that the layer below them may be compromised.

From a Boot ROM bug to a jailbreak

The Boot ROM is the first code that runs, and its job is to decide what runs next by checking Apple’s signature on it. Every stage after it does the same for the stage above, so the rule that the device runs only Apple-signed code is enforced from the bottom up. checkm8 lets an attacker run their own code inside that first stage. Once you are running inside the component that makes the decision, you can change the decision: your code can skip the signature check instead of performing it.

From there a jailbreak is a climb. You control the bottom stage, so when it loads the next one, iBoot, you give it a modified version and your code lets it through without verifying it. Now you control iBoot. iBoot loads the kernel the same way, so you pass it a patched kernel and it loads without objection. Each stage you own is used to switch off the check on the stage above it, and you move up the chain one step at a time, every gate open because the stage that enforces it already belongs to you.

At the top you have a kernel running your changes. That is what a jailbreak is. In practice it means the device will run software Apple never signed, you have full control of the system including the parts normally locked down, and you can load your own kernel code. For a researcher it is a complete environment for inspecting, patching, instrumenting, and debugging the device from the inside.

One practical consequence follows from how this works. Because the Boot ROM is read-only, nothing you do to the chain sticks: reboot the device without a computer attached and it comes back up as stock, so the exploit has to be re-run over USB each time. That is why checkm8 jailbreaks are described as semi-tethered.

Apple Silicon Macs: LocalPolicy and security levels

Everything above is an iPhone description, but since Apple Silicon it applies to Macs as well, on the same primitives with one addition.

The Mac boot chain is Boot ROM, then LLB, then iBoot, then the kernel. On the Mac, LLB is a distinct stage, unlike the modern iPhone where it is merged into iBoot. The addition is that a Mac owner can choose how strict the chain is, and that choice is itself signed.

The choice is stored in a file called LocalPolicy. It is an Image4 object, but unlike the OS images it is not signed by Apple. It is signed by the machine’s own Secure Enclave, using a key generated on that specific Mac that never leaves it. Apple signs the operating system. The Secure Enclave signs the LocalPolicy, the record of how strictly the owner wants that operating system checked. The enclave’s attached secure storage blocks rollback of the policy itself, so it cannot be silently downgraded to a weaker setting.

There are three settings. Full Security is the default and matches iOS: the OS is personalized to the machine with its ECID, giving the same anti-rollback guarantee. Reduced Security uses Apple’s global, non-personalized signatures, which allows booting older signed versions of macOS and is also required to load third-party kernel extensions. Permissive Security accepts boot objects signed locally by the enclave rather than by Apple, including a custom XNU kernel, and requires System Integrity Protection to be off; it is intended for developers and researchers. The local signing chain still runs at this setting: the boot objects are checked against a key held on the machine.

Changing any of these settings requires physical access. The user boots into recoveryOS through One True Recovery, entered by pressing and holding the power button (a signal software running in macOS cannot generate), and authenticates as an administrator.

Hands-on: from IPSW to kernelcache

An IPSW is a zip archive of Image4 objects, and two open tools, blacktop’s ipsw and pyimg4, are enough to go from the download to a kernel you can disassemble.

The commands below show the sequence. Exact flags change between tool versions, so check each tool’s --help, and read the output as representative of the format rather than one device’s actual signed values.

# 1. Pull an IPSW for a device and build you want to look at.
ipsw download ipsw --device iPhone10,3 --build <build>

# 2. An IPSW is a zip. Look at what is inside.
unzip -l iPhone10,3_*.ipsw
#   kernelcache.release.iphone10b
#   Firmware/dfu/iBoot.d22.RELEASE.im4p
#   Firmware/all_flash/DeviceTree.d22ap.im4p
#   ...
# 3. Read the payload header: what is this, and how is it packed?
pyimg4 im4p info -i kernelcache.release.iphone10b
#   FourCC:      krnl
#   Description: KernelCache
#   Data compression type: LZSS  (Apple 'complzss' wrapper)
# 4. Pull the raw payload out of the IM4P and decompress it.
pyimg4 im4p extract -i kernelcache.release.iphone10b -o kernelcache.raw
file kernelcache.raw
#   kernelcache.raw: Mach-O 64-bit executable arm64

The kernelcache is now a Mach-O. Before disassembling it, look at the manifest that vouches for it:

# 5. The manifest: the object that says 'Apple signed this'.
# The personalized manifest is not in the IPSW; it comes from your own
# device or a saved SHSH blob (for example via tsschecker).
pyimg4 im4m info -i APTicket.der
#   Device Processor:    T8015           (A11, the iPhone10,3 SoC)
#   ECID (hex):          0x<your device's ECID>
#   ApNonce (hex):       <nonce hash, the BNCH field>
#   SepNonce (hex):      <SEP nonce>
#   Manifest images (N): krnl, ibot, sepi, rdsk, dtre, ...
#   (add -v for each image's DGST digest and the rest of the properties)

The Manifest images list is the set of components this ticket vouches for; with -v, each is shown alongside the digest (DGST) the loader will require the real image to match. The ECID is the personalization described earlier, which ties the ticket to one device, so running pyimg4 im4m info on a manifest from your own device is a useful check: the ECID it prints is that device’s. The manifest also carries an X.509 certificate chain that terminates at the key held in the Boot ROM. Open kernelcache.raw from step four in a disassembler and you have the starting point for the next article, on XNU.

State in 2026

The main change is convergence: the iOS boot chain and the Apple Silicon Mac boot chain now use the same primitives (Image4, TSS, per-device personalization), differing mainly in the LocalPolicy layer that Macs add. The coprocessors boot the same way, each verifying its own signed image against its own root.

The mitigations above the boot chain are covered in later articles rather than here. Once the kernel is running, pointer authentication changes what an attacker can do with a bug, which is the subject of the arm64e post and continues when this series reaches the kernel.

The anchor itself is also still active research. In mid-2026, researchers reported a checkm8-style Boot ROM bug said to reach the A12 and A13 generations. If it holds up, it extends the unpatchable-ROM problem two generations forward.

Where this leaves us

This is the full chain, from the Boot ROM to the kernel the rest of the system runs inside. Little of what follows in the series introduces a new kind of trust. The sandbox, code signing, the trust caches, and the entitlements that decide what a process can do are all enforced by a kernel that runs only because this sequence of signatures verified in order. That is also why a Boot ROM compromise like checkm8 undermines all of them at once. The next article moves up one level, into the kernel itself: XNU, its Mach and BSD halves, and the capability model the rest of the security stack is built on.

Notes and sources

Everything here is drawn from public documentation, open tooling, and published research.

Work like this is what we do under engagement.

contact@sigreturn.com Vulnerability Research