System Internals

The iOS Code-Signing Pipeline

The kernel is running, and it now decides what may execute. 'AMFI rejected your binary' is only the visible tip: underneath sits one MACF policy module and a verdict pipeline keyed on a single 20-byte value, the cdhash, running from the trust cache through CoreTrust to amfid and the entitlements that unlock capabilities.

The previous post ended on a single field. Inside every process’s credentials, p_ucred, sits cr_label, a slot the kernel reserves for the Mandatory Access Control Framework. We noted in passing that AMFI and the sandbox hang their per-process policy there, then moved on. This post is what hangs there.

Every time a process is created, through execve or posix_spawn, the kernel has to answer one question before it runs a single instruction of the new image: may these bytes execute? Anyone who has built for iOS has seen the visible answer, the line in the log that reads AMFI: code signature validation failed. AMFI gets the blame, so it is easy to think of it as the code-signing check. It is not. It is one policy module plugged into a generic kernel framework, and the real answer is produced by a pipeline behind it: a signature format, a fast-path allowlist, an in-kernel certificate check, a userland daemon, and a set of signed capabilities. This post takes that pipeline apart, in the order the kernel walks it.

Everything here is public: Apple’s open-source XNU, the Apple Platform Security documentation, and published research from Siguza, Project Zero, Linus Henze and others. It contains no exploit, private detail, or 0day.

AMFI is not special: the MACF reveal

Start with the framework, because it is the piece that reframes everything else.

MACF is the Mandatory Access Control Framework, inherited from TrustedBSD and wired into XNU. The important thing about it is what it is not: it is not a security policy. It enforces nothing on its own. It is a registration and dispatch layer, a mesh of hook points sprinkled through the kernel at every sensitive operation, into which separate policy modules plug themselves.

Now the reveal. AMFI is a MACF policy module. So is the sandbox. So is Quarantine. AppleMobileFileIntegrity.kext is nothing more than a set of mpo_* callbacks hung on MACF hook points. That is why the two acronyms always show up together in a stack trace: “AMFI” is the policy, “MACF” is the mechanism it runs on. The sandbox, covered in the next post, is the exact same shape of thing, a second set of callbacks on the same hooks. They are siblings.

Two properties of the framework matter for the rest of this series, and both are load-bearing for an attacker.

Checks are deny-wins. When several policies implement the same check, the kernel keeps the most restrictive answer, so AMFI and the sandbox each hold an independent veto over the same operation. A bug that makes AMFI return “allow” early does not disable the sandbox’s hook on that operation, and the reverse is also true. The policies compose; they do not share a fate.

Labels are how a policy remembers. Each policy gets one or more label slots on the objects the kernel tracks, and the one that matters here is cr_label, on a process’s credentials, the field we met at the end of the last post. AMFI’s per-process code-signing verdict and the sandbox’s compiled profile both live inside it. This is the direct offensive consequence: once you have kernel read/write, editing that label slot sheds the policy’s memory of you. Patch the AMFI slot and the kernel forgets your process failed a check; patch the sandbox slot and the process is no longer confined. The framework is also its own target, since corrupting the policy list turns enforcement off wholesale. The sanctioned form of exactly that switch is the boot argument amfi_get_out_of_my_way, though getting a machine to honor it takes more than setting it, as the hands-on ends by showing.

Hold on to the label idea. Everything AMFI decides below is, in the end, a value it writes into that slot.

What a signature is, ending at the cdhash

Before the kernel can decide whether an image may run, it needs something to decide about. That something is the code signature embedded in the Mach-O, and for our purposes it reduces to a single 20-byte number. Let us get to that number.

The invariant the whole component exists to enforce is W^X plus code integrity: no page of memory is ever both writable and executable, and the contents of every executable page hash to a value some trusted party blessed. Break that and a memory-corruption bug stops being a crash and becomes persistent native code, which is why every jailbreak eventually has to defeat this layer and not just the kernel’s memory safety.

A signed Mach-O carries an LC_CODE_SIGNATURE load command pointing at a blob in its __LINKEDIT segment. That blob is a SuperBlob (magic 0xFADE0CC0): a small header, a count, and an index of (type, offset) pairs, each pointing at a sub-blob. The sub-blobs are the parts of the signature:

Sub-blob Magic What it holds
CodeDirectory 0xFADE0C02 the page-hash tree, and the thing the cdhash is a hash of
Entitlements (XML / DER) 0xFADE7171 / 0xFADE7172 signed key/value capabilities
Requirements 0xFADE0C01 rules a valid signer must satisfy
CMS wrapper 0xFADE0B01 the actual cryptographic signature, a PKCS#7 structure

The CodeDirectory is the heart of it. It carries an array of hashes, one per 4 KiB page of the binary up to codeLimit: these are the code slots. When a page of the image is first faulted in, the virtual-memory system computes that page’s hash and compares it to the stored slot (cs_validate_page). On a process marked CS_HARD | CS_KILL, which is every normal process on iOS, a mismatch is a SIGKILL on the spot. That lazy, per-page check is the concrete mechanism behind “you cannot patch a signed page in memory”: the moment the modified page faults in, its hash no longer matches and the process dies, unless the page lives in a region that was never signed in the first place, which is the JIT hole we return to later.

The CodeDirectory also binds the other sub-blobs into itself through special slots, each holding the hash of one sub-blob. So you cannot alter the entitlements blob without changing the CodeDirectory, which is the whole reason the entitlement parser bugs later in this post are interesting.

And now the number. The cdhash is the hash of the CodeDirectory blob itself, truncated to CS_CDHASH_LEN, which is 20 bytes, whatever the underlying algorithm. It is the canonical identity of a binary. Every mechanism downstream keys on it: the trust cache is an allowlist of cdhashes, launch constraints are pinned to cdhashes, amfid’s reply is a cdhash, csops(CS_OPS_CDHASH) hands one back to userland. In the kernel it lives in a struct cs_blob attached to the file’s vnode.

So the rest of the article has a single subject. A binary is, for this purpose, its cdhash. The question “may these bytes run?” becomes “what does the kernel do with this 20-byte value at exec?”

The verdict pipeline

At exec, the kernel’s signature-check hook fans out to AMFI’s mpo_vnode_check_signature, the routine that produces the verdict. Sibling AMFI hooks around it do the smaller jobs: setting the CS_HARD | CS_KILL flags that make a hash mismatch fatal, enforcing library validation on loaded dylibs, and gating MAP_JIT and get-task-allow. But the signature check is the one that decides whether the process lives at all.

Inside that check, AMFI computes the binary’s cdhash and walks a pipeline. The order matters, because each stage is a different trust story with a different attack surface:

Step What AMFI checks On a match CMS validated?
1. Trust cache cdhash present in the static or a loadable trust cache runs as a platform binary No
2. CoreTrust CMS chain validates to a pinned Apple root; classify the signer App Store signer runs directly Yes, in the kernel
3. amfid + profile signer and entitlements checked against a provisioning profile developer / enterprise binary runs Yes (via CoreTrust) plus the profile
none of the above nothing vouches for the cdhash SIGKILL not reached

Read top to bottom, this is the entire answer to “may these bytes run?” The next four sections are just these rows in detail. Notice the first one already tells you something: for most of the code on the device, there is no cryptography at exec time at all.

Trust caches: the fast path

The base operating system is thousands of Mach-O files, and validating a CMS signature chain for each one on every launch would be slow. Apple’s answer is an allowlist. A trust cache is a sorted list of cdhashes that are trusted without any signature validation: if a binary’s cdhash is in the cache, it runs immediately as a platform binary, and the CMS blob is never even looked at. This is step 1 of the pipeline, and it is the path taken by essentially the whole OS.

The cache is carried in an Image4 container (an IM4P payload, the format from the boot-chain post), tagged trst for the static cache or ltrs for a loadable one, with restore and engineering variants besides. Inside is a short header (a version, a uuid, an entry count) followed by sorted { cdhash, hashType, flags } entries, so a lookup is a binary search on the 20-byte cdhash. Version 2 adds a byte tying each entry to a launch-constraint category.

There are two kinds:

This is the jailbreak’s oldest friend. Before the page-table monitors existed, a loadable trust cache lived in ordinary writable __DATA kernel memory. Once an exploit had kernel read/write, it appended its own binaries’ cdhashes to that list, and from that moment those binaries ran as platform code with no signature check. Electra’s inject_trusts is the canonical example, adding the cdhashes of amfid_payload.dylib and the rest of the jailbreak’s userland. Hold that thought until the 2026 section, because it is exactly the move that stopped working.

Aside: injecting a cdhash by hand in lldb

Attach a kernel debugger, lldb against a target matched to its Kernel Debug Kit, and you have kernel read/write for free. That is the same position a finished exploit is in when it reaches this step, which makes the debugger the cleanest way to prototype the injection an exploit would automate, the by-hand version of inject_trusts. On a build where the loadable trust cache still sits in writable kernel memory, the move is short: find the module, read its header, drop your binary’s cdhash into a fresh entry, and raise the count over it.

(lldb) # a loadable trust cache module in kernel memory
(lldb) #   (recover the list-head symbol during symbolication)
(lldb) p (struct trust_cache_module1 *)<trust cache module>
(struct trust_cache_module1 *) $0 = 0xffffff8000a1c000

(lldb) # header: version, a 16-byte uuid, then the entry count
(lldb) p $0->num_entries
(uint32_t) $1 = 41

(lldb) # each entry is { cdhash[20], hash_type, flags }, kept sorted
(lldb) # write your binary's cdhash (the 20 bytes from codesign -dvvv)
(lldb) # into the next slot, then raise the count over it
(lldb) memory write --infile cdhash.bin &$0->entries[41]
(lldb) expr -- $0->num_entries = 42

Two things make this less casual than the four lines suggest. The entries are sorted so the lookup can binary-search them, so a correct injection inserts in order, or, as real injectors do, splices in a fresh single-entry module rather than appending to a shared array. And it only works where that memory is writable at all. On a PPL device the loadable trust cache lives in pmap_cs pages the kernel is not allowed to write, and on an SPTM device it is a monitor-owned frame the kernel cannot write, so the identical write faults. That failure is the whole of the 2026 section compressed into one experiment: the exact write that made a cdhash trusted is the one Apple moved out of the kernel’s reach.

CoreTrust: the check that moved into the kernel

If the cdhash is not in a trust cache, the binary has to prove itself with its CMS signature, and here there is history worth telling, because it explains why this stage exists at all.

For years the real signature validation happened in userland, in the amfid daemon we meet next. The kernel’s AMFI would compute a cdhash, hand it to amfid, and trust amfid’s yes-or-no answer. That design has an obvious weakness once an attacker has kernel read/write: patch amfid. Every jailbreak of that era did. LiberiOS pointed amfid’s import of the validation function at a bad address and caught the resulting fault; Electra rebound it to a fake_MISValidateSignatureAndCopyInfo that simply returned success. The signature check was a userland function, and userland functions can be rewritten.

CoreTrust closed that door. It is an in-kernel validator (packaged as CoreTrust.kext on most builds) that parses the CMS SignedData structure, builds the X.509 certificate chain, verifies every signature in it, and confirms the chain terminates at an Apple root certificate pinned inside the kernel. Having done that, it classifies the leaf certificate by its extensions into a signer class: App Store, developer, enterprise, TestFlight. It hands those policy flags back to AMFI. It deliberately does not look at entitlements or provisioning profiles; its entire job is “is this a genuine Apple-rooted signature, and of what kind.”

The consequence is that a lying amfid buys you nothing anymore. The cryptographic decision now lives in the kernel, anchored to a key an attacker with read/write can read but cannot make the CMS math validate against. The loosely-signed path still runs amfid, but only after CoreTrust has bounded what a valid signer could be. That is why post-CoreTrust jailbreaks stopped shipping a patched amfid and went looking for logic bugs in CoreTrust itself.

Its entire input is attacker-controlled ASN.1, which makes the parser and the chain-validation logic a target in their own right. We come back to a real one, CVE-2022-26766, in the offensive section.

amfid and provisioning profiles

The third row of the pipeline is the one that carries third-party code: apps signed by a developer or an enterprise rather than baked into the OS or shipped through the App Store.

amfid (/usr/libexec/amfid) is the userland daemon that handles this path. The kernel’s AMFI reaches it over a dedicated Mach special port (port 18). Its job is to validate the binary against the provisioning profiles installed on the device, by calling MISValidateSignatureAndCopyInfo in libmis, and to return the cdhash and signer information.

A provisioning profile is a CMS-signed plist, stored under /var/MobileDeviceProvisioningProfiles, that binds four things together:

amfid cross-checks the binary’s actual signer and requested entitlements against this profile. This is the machinery behind a detail every iOS developer has hit: a free “personal team” profile expires in 7 days, so a sideloaded app signed that way stops launching a week later. Enterprise profiles last far longer, which is precisely why enterprise certificates are the perennial vehicle for sideloading and for iOS malware distribution.

Entitlements: signed capabilities

We have mentioned entitlements at every stage; now define them properly, because they are the bridge to the rest of the security model. An entitlement is a signed key/value pair bound into the CodeDirectory by a special slot, so it cannot be altered without breaking the cdhash. An entitlement is a capability the signer cryptographically granted.

They fall into three groups, and the distinction is the entire point:

Group Examples Who may carry it
Benign get-task-allow any developer-signed binary
Restricted platform-application, com.apple.private.*, apple-internal only Apple-signed or specially provisioned binaries
Sandbox exceptions file and mach-lookup exceptions granted here, enforced by the sandbox module

AMFI enforces that a third-party binary may carry only the entitlements its provisioning profile authorizes; a binary cannot simply ask for platform-application and receive it. The restricted group is what separates Apple’s own code from everyone else’s, and forging membership in it, by getting the kernel to believe a binary holds an entitlement it was never granted, is the exact prize the signature bugs below go after.

The last group is the handoff to the next post. A sandbox exception is an entitlement that AMFI validates here, at exec, and that the sandbox then consumes at runtime to widen what the process may touch. AMFI decides what a binary is allowed to be; the sandbox decides what a running process is allowed to do. They meet at the entitlement.

The offensive angle: logic beats corruption

Given all of the above, where are the bugs? The memory-safety surface is real (the CMS ASN.1 decoder and the CodeDirectory’s bounds arithmetic are reachable from anything that gets a Mach-O parsed, and worth fuzzing), but the defining bugs of this component are logic, and that is what makes them special. A logic bug in the code-signing policy needs no heap shaping, survives kalloc_type, is untouched by memory tagging, and does not care about the page-table monitor. It convinces the machine that a lie is a valid signature. Three cases show the pattern.

Psychic Paper (Siguza, 2020, CVE-2020-9842, fixed in iOS 13.5) is the purest of them. iOS parsed the entitlements blob with two different XML parsers, one in the kernel and one in userland, and Siguza found a comment construct they read differently: one saw a harmless plist, the other an entitlement that was not really there. The launch-time check validated the benign reading while the runtime granted the malicious one, so an unprivileged app could claim any entitlement it liked, up to platform-application. No memory was corrupted; two parsers simply disagreed, and the gap was a capability. The fix routed every consumer through one hardened parser, AMFIUnserializeXML.

The DER sequel (Ivan Fratric, Project Zero, CVE-2022-42855, fixed in iOS 15.7.2) is the same bug in binary clothes. Apple moved entitlements to DER partly to end these differentials, since DER is meant to have one canonical reading. But libCoreEntitlements had three traversals that disagreed on how far a sequence extended: the validation and query paths ran past its declared length while the profile-subset check respected it. An entitlement smuggled in as an extra element was honored at runtime but invisible to the check meant to reject it. Psychic Paper again, in the format introduced to prevent it.

The CoreTrust root bug (Linus Henze, CVE-2022-26766, fixed in iOS 15.5) attacked the certificate check instead of the parser, and it has the largest footprint. CoreTrust validated the CMS chain but never confirmed it terminated at an Apple root, so a certificate merely carrying the App Store extension, whoever issued it, made CoreTrust set the App Store flag and AMFI run the binary with nearly any entitlement. This is the primitive behind TrollStore: permanent, arbitrary code signing, no memory corruption at all. The archetype of the class, a logic bug that answers “may these bytes run?” with an unconditional yes.

And when the bug is in the kernel rather than the policy, the same component is the post-exploitation endgame. With kernel read/write in hand, an attacker does not necessarily need a fresh signing bug: append a cdhash to a loadable trust cache, or flip CS_PLATFORM_BINARY and clear CS_HARD | CS_KILL in a process’s p_csflags, or edit the AMFI label slot to grant an entitlement. Each of these is a data-only write that turns “I control kernel memory” into “I run whatever code I want.” On pre-A15 hardware, they all still land. The next section is about why, on current hardware, most of them do not.

State in 2026

The abstractions above are stable, but the ground under the post-exploitation moves has shifted hard, and the headline is that a kernel read/write is no longer enough to forge a verdict.

TXM owns the decision now. On A15 and M2 and later, the SPTM devices, the code-signing verdict left the XNU address space entirely. The Trusted Execution Monitor runs at a higher privilege than the kernel and holds the trust caches, the provisioning-profile registry, and the signature objects in memory the page-table monitor refuses to map writable to the kernel. The effect is blunt: trust-cache injection and p_csflags forging, the two moves that ended the offensive section, are dead on this hardware, because the bytes you would overwrite are not in memory XNU can write. Post-exploitation now needs a monitor bug on top of the kernel bug, a subject for the hardening post later in this series. tfp0 is again the start of the hard part.

The rest of the deltas, in brief:

What still works, and will keep working, is the logic. A parser differential or a chain-validation flaw in the policy bypasses memory tagging, kalloc_type, and the page-table monitor all at once, because it never corrupts anything: it convinces a correct machine of a false fact. That is the enduring lesson of this component. The JIT hole is also permanent by construction: a process holding dynamic-codesigning owns a legitimately writable-then-executable mapping, and a bug inside such a process, a browser’s JavaScript engine being the obvious one, reaches native code without touching any of this machinery.

Hands-on: dumping the policy off a real binary

You can watch this whole pipeline from a Mac, no jailbreak needed, because every value it turns on is dumpable from the signature and the kernelcache. We read a binary’s signature, pull out its cdhash, then open the OS’s trust cache, the allowlist of exactly those values that decides what runs as platform code.

1. Read the SuperBlob and the cdhash. codesign -dvvv prints the CodeDirectory summary and the cdhash for any signed binary. /bin/ls is a good first target, because it is one of Apple’s own platform binaries:

codesign -dvvv /bin/ls
Executable=/bin/ls
Identifier=com.apple.ls
Format=Mach-O universal (x86_64 arm64e)
CodeDirectory v=20400 size=741 flags=0x0(none) hashes=18+2 location=embedded
Hash type=sha256 size=32
CDHash=1205ca11b1c3f706109656bcf4e2c12439d843b7
Signature size=4442
Authority=Software Signing
Authority=Apple Code Signing Certification Authority
Authority=Apple Root CA
TeamIdentifier=not set

hashes=18+2 is 18 code slots plus 2 special slots. CDHash=1205ca11... is the 20 bytes everything downstream keys on. The Authority= chain is what CoreTrust validates, terminating at Apple Root CA, and the Software Signing leaf marks this as Apple’s own platform code, which is also why it carries no team identifier and no entitlements. Add --entitlements - when a binary actually has entitlements, to dump them alongside.

2. See the allowlist itself: the trust cache. The other half of that first pipeline step is where those cdhashes are trusted. ipsw fw tc pulls the trust caches an IPSW ships, one per system disk image (the mounted-image caches from earlier). Point it at an IPSW, not a decompressed kernelcache; --remote streams a URL without downloading the whole file:

ipsw fw tc iPhone10,3,iPhone10,6_15.0_19A346_Restore.ipsw
# --remote '<IPSW URL>' streams it instead of downloading
UUID:       E45C2F07-B759-44D4-BBD5-B3844FDBBED6
Version:    1
NumEntries: 2407
    0023c7654da7272bbd68953586f1a299b8bed350 sha256
    00262ea6bb7dcf7ee984c8280a6e5e5ac7a14584 sha256
    0037fc2307eae66eaf7139916862dc2b7336b43f sha256
    ...

This IPSW carries three, one per system image; the main OS volume’s is the big one, 2,407 cdhashes, each a 20-byte value exactly like the one /bin/ls gave up in step 1, sorted for a binary search. On iOS a binary runs as platform code precisely when its cdhash is one of these, with no CMS validation at all. That presence, and nothing cryptographic, is why so much of the OS never touches CoreTrust.

3. Try to grant yourself an entitlement, and watch AMFI decide. The steps so far read Apple’s policy off finished binaries. Now go the other way and try to give yourself a capability by signing it in. This is a macOS demonstration, because macOS will run locally-signed code at all; on iOS the binary would be killed for having no trust-cache entry and no Apple signature, long before entitlements came up. That relaxed first gate is what lets us isolate the entitlement check.

A freshly compiled binary is ad-hoc signed by the linker and carries no entitlements. Give it a benign one, the macOS debug entitlement com.apple.security.get-task-allow, re-sign ad-hoc, and it runs:

printf 'int main(void){return 0;}\n' > hello.c && clang -o hello hello.c
echo '{"com.apple.security.get-task-allow":true}' | plutil -convert xml1 -o allowed.plist -
codesign -s - --entitlements allowed.plist -f ./hello
./hello; echo "exit: $?"
# exit: 0

That entitlement is self-declared: any signer, ad-hoc included, may carry it, because it grants no authority the system has to vouch for. Now ask for one that does. platform-application marks a binary as Apple’s own platform code, the CS_PLATFORM_BINARY from the pipeline, so a self-signed binary must not be able to claim it:

echo '{"platform-application":true}' | plutil -convert xml1 -o restricted.plist -
codesign -s - --entitlements restricted.plist -f ./hello
./hello; echo "exit: $?"
# zsh: killed  ./hello
# exit: 137

A SIGKILL, before main. With log stream --predicate 'sender == "kernel"' open in another Terminal, the reason prints as the process dies:

kernel: mac_vnode_check_signature: /private/tmp/hello: code signature validation failed fatally:
  Code has restricted entitlements, but the validation of its code signature failed.
kernel: validation of code signature failed through MACF policy: 1

platform-application is a restricted entitlement, so carrying it forces the signature to be authorized to carry it, and an ad-hoc signature is authorized by nobody. Note the check: mac_vnode_check_signature, failing through MACF policy, the exact hook and framework from the top of this post.

The instinct is that a real signing identity fixes this. It does not. Sign the same binary with a genuine Apple Development identity, same entitlement:

codesign -s "Apple Development: Adam Taguirov" --entitlements restricted.plist -f ./hello
./hello; echo "exit: $?"
# exit: 137   (the same mac_vnode_check_signature kill)

Still dead. Holding a real certificate is not the same as being authorized for the entitlement. A developer can unlock some restricted entitlements with a provisioning profile, the signed document from the amfid section that binds entitlements to certificates and devices; platform-application is not one of them, it is reserved for Apple’s own platform binaries and no third-party profile grants it. Self-signing, ad-hoc or developer, lets you write any entitlement into the blob but confers no authority to use a restricted one. Manufacturing that authority, making the kernel believe a binary holds an entitlement it was never granted, is exactly what a bug like Psychic Paper bought.

4. Turn the enforcement off, and see what that takes. Everything above watches AMFI decide. The sanctioned way to make it stop deciding is the boot argument from the MACF section, amfi_get_out_of_my_way: set it, and AMFI’s hooks return “allow” without checking, so unsigned code runs. The switch itself is trivial. What a machine makes you do before it will honor it is the part worth seeing.

On a Mac the argument goes in NVRAM, which the boot loader passes to the kernel:

sudo nvram boot-args="amfi_get_out_of_my_way=0x1 cs_enforcement_disable=1"

On a stock machine that command changes nothing. The kernel ignores AMFI-disabling boot-args unless System Integrity Protection is already off, and SIP comes off only from recoveryOS with csrutil disable. On Apple Silicon there is a step in front of even that: recoveryOS refuses to disable SIP until you lower the machine’s security policy from Full to Reduced in Startup Security Utility. So the real sequence is boot recoveryOS, lower the security policy, csrutil disable, reboot, set the boot-arg, reboot again. Only then does the off-switch flip.

On iOS none of that is available. A production iPhone will not let you write boot-args, and its release kernel would ignore them if you could. The argument is honored only on Apple’s own development-fused hardware, or on a device whose boot chain you have already broken: a checkm8-class Boot ROM bug that lets you patch iBoot and inject boot-args, or a kernel already patched by a jailbreak. That is the thread straight back to the first post. You can only relax code signing if you can influence the boot chain, and the boot chain is the thing built to stop you. The off-switch is real, but it sits behind the security state the boot chain establishes, never in front of it.

Where this leaves us

We can now answer the question the post opened with. When a process is created, the kernel computes its cdhash and walks a pipeline: the trust cache first (the allowlist that runs the base OS with no cryptography), then CoreTrust (an in-kernel CMS check anchored to a pinned Apple root), then amfid with the provisioning profiles. The outcome is written as flags and a label into the process’s credentials, along with the entitlements it was granted. AMFI runs this, plugged into MACF alongside the sandbox, and its richest bugs are not memory corruption but logic: two parsers, or a certificate check, made to disagree.

That last handoff is the next post. AMFI has decided what this binary is allowed to be, and stamped the answer, including its sandbox-exception entitlements, into cr_label. The sandbox is the other module reading that same label, and it decides the complementary question: now that the process is running, what is it allowed to touch? It is AMFI’s twin on the same framework, and escaping it is more often a matter of logic than of corruption, for reasons that will feel familiar by the end.

Notes and sources

Everything here is drawn from open source, vendor documentation, and published research.

Work like this is what we do under engagement.

contact@sigreturn.com Vulnerability Research