macOS Mach port exploitation walkthrough

macOS Mach port exploitation walkthrough

TL;DR: Mach ports are the bedrock of macOS/iOS IPC, and their port-right semantics (receive / send / send-once / port-set / dead-name) have produced some of the most consequential kernel and sandbox-escape bugs of the past decade. This walkthrough chains the concepts top-to-bottom: kernel object (ipc_port_t), port-right confusion, OOL_MEMORY descriptors, MIG-generated stubs, voucher abuse, fake-port construction, and how sandboxed apps reach privileged Mach services via launchd. Treat it as a composite walkthrough assembled from public CVE write-ups and the Pegasus / Triangulation chains. Companions: mach-port-exploitation, mach-and-xpc, macos-xpc-internals, macos-kernel-heap-zone, macos-kernel-debugging, osee-roadmap, and (for the mobile sibling) ios-baseband-attacks.

Why it matters

Mach IPC is the substrate XPC, the WindowServer, launchd, and most privileged daemons rely on. Confuse the kernel about who owns which port right, or get it to dereference a forged ipc_port_t, and you usually get arbitrary kernel R/W — modulo PAC on arm64e (see pac-arm64e-bypass).

Offensively this hits two places: sandbox escape (convince launchd or a broker to hand you a send right you should not have — see macos-sandbox-escape) and kernel LPE (corrupt or forge a kernel port for tfp0-equivalent primitives — see macos-kernel-heap-zone and macos-kernel-debugging). Defensively, port-right state is messy and the same bug classes (port-right UAF, OOL trickery, MIG mistakes) keep returning.

Mach IPC fundamentals

A port is a kernel message queue. Tasks see port names (small ints, mach_port_name_t) indexing the task’s IPC space. Each name carries rights:

  • Receive (MACH_PORT_RIGHT_RECEIVE) — exactly one holder; dequeues.
  • Send (MACH_PORT_RIGHT_SEND) — many holders; enqueues messages.
  • Send-once (MACH_PORT_RIGHT_SEND_ONCE) — single-shot reply channel.
  • Port-set (MACH_PORT_RIGHT_PORT_SET) — bag of receive rights for one mach_msg syscall.
  • Dead-name (MACH_PORT_RIGHT_DEAD_NAME) — what a send right becomes when the receive side dies.

The kernel object is ipc_port_t (struct ipc_port), allocated from the ipc.ports zone — see macos-kernel-heap-zone for grooming behaviour.

Port-right confusion — the classic UAF shape

The bug pattern that has produced many exploits looks like this:

  1. A kernel API path takes a port name from userland and resolves it to an ipc_port_t without fully validating right type/lifetime.
  2. Userland destroys the underlying receive right (or causes the port to become a dead name).
  3. The kernel still holds a reference that now points at a stale or replaced object.
  4. Userland sprays the ipc.ports zone (or a sibling zone via cross-zone reallocation, post-kalloc_type mitigations permitting) to land a controlled object at the freed slot.
  5. The kernel uses the stale ipc_port_t — message send, ip_lock, convert_port_to_* — and the attacker wins primitives.

CVE-2019-8605 (“SockPuppet”) and the family around CVE-2020-9991 are canonical reading. More recently, the Triangulation chain (CVE-2023-32434 and friends) showed that port-right and TrustCache logic continues to be fragile.

OOL_MEMORY descriptors — historical R/W primitive

Mach messages carry typed descriptors. Among them:

  • MACH_MSG_OOL_DESCRIPTOR / OOL_VOLATILE — out-of-line memory transferred by virtual copy.
  • MACH_MSG_OOL_PORTS_DESCRIPTOR — an array of port rights transferred out-of-line.

Two bug classes have repeatedly fallen out of OOL handling:

  1. Forged kernel pointers via OOL_PORTS. Older kernels would, in some paths, dereference attacker-influenced pointers from OOL port arrays before fully validating right type — turning a controlled qword into a forged ipc_port_t.
  2. Type confusion between OOL_MEMORY and OOL_PORTS. Crafting a message whose declared descriptor type disagrees with the kernel’s actual handling produced read/write of kernel memory or arbitrary port-right creation.

Modern mitigations (PAC on ipc_port_t fields on arm64e, kalloc_type isolation, ZONE_SECURITY_OPTIONS, and pointer-authenticated ip_object/ip_kobject references) have made naive forgery harder but not impossible. See pac-arm64e-bypass.

MIG (Mach Interface Generator) stubs

Most kernel-exposed Mach interfaces are defined in .defs files and auto-generated by MIG into request/reply parsing stubs. MIG stubs are a historical bug factory:

  • Out-of-bounds descriptor counts — if a stub trusted the user-supplied descriptor count, you could under- or over-walk the message body.
  • Incorrect consume on error semantics — when a MIG handler returns an error, who frees the port rights and OOL memory in the message? Get this wrong and you have a double-free or use-after-free of port rights.
  • Reply-port confusion — a handler that uses the request’s reply port for unintended purposes can leak send rights cross-process.

For audit work, generated MIG code lives under the kernel build tree (e.g. osfmk/mach/*_server.c). Reading the generator’s logic against the .defs files in xnu/osfmk/mach/ is the cheapest way to find missed mig_no_reply edges.

Vouchers

mach_voucher_t was added to attach attributes (importance, bank tickets, ATM, user-data) to messages. Voucher code paths have repeatedly produced exploitable bugs:

  • Voucher UAF — recycling a voucher whose attribute manager still holds a reference (Ian Beer’s mach_voucher_extract_attr_recipe work is the classic write-up).
  • Bank-attribute confusion — abused in iOS jailbreaks to corrupt importance ledgers and ultimately port references.

When auditing, watch every convert_*_to_voucher* and the attribute manager’s release / extract callbacks. They are subtle and the lifetime graph is non-obvious.

Sandboxed app reaching a Mach service via launchd

Reachability for a sandboxed attacker:

  1. The sandbox profile (sandboxd / Seatbelt) controls which Mach service names are visible via bootstrap_look_up — see macos-sandbox-escape.
  2. launchd owns the bootstrap port and hands out send rights based on plist MachServices entries.
  3. If allowed, the app gets a send right and can send arbitrary messages.
  4. The target service’s MIG/XPC handler is the attack surface — see macos-xpc-internals and mach-and-xpc.

Even with kernel mitigations, the common real-world path is a userland Mach service bug reached via an over-permissive sandbox profile. Triage which services your sandbox can see first; sandbox-exec -p and sbtool-style introspection help.

Fake-port construction (concept, not how-to)

Once you have a kernel R/W primitive (from any earlier bug), the next step in most modern chains is to fabricate an ipc_port_t whose fields lie about what kind of object it represents — typically pointing ip_kobject at a task_t you control. This yields a “fake tfp0” pattern. PAC on arm64e forces you to either reuse a legitimate signed pointer (signing gadget) or to abuse a kernel path that re-signs for you. This is where chains converge with pac-arm64e-bypass.

Real-world historical references

These public chains are the canonical study material:

  • Pegasus / FORCEDENTRY (2021) — kernel side leveraged a Mach voucher / port issue post-BlastDoor sandbox escape.
  • Triangulation (2023, CVE-2023-32434 + 38606) — kernel R/W via Mach port-related logic, with TrustCache disabled via MMIO write.
  • Ian Beer’s mach_portal (2016) — port-right UAF in task_swap_mach_voucher.
  • SockPuppet (2019, CVE-2019-8605) — XNU port-rights UAF.

Read all four end-to-end before claiming you understand the bug class.

Workflow to study

  1. Read xnu/osfmk/ipc/ipc_port.c and ipc_object.c until you can sketch the receive- and send-right lifecycles from memory.
  2. Build a debug XNU and attach lldb via a VM (see macos-kernel-debugging); breakpoint ipc_port_alloc, ipc_port_destroy, ipc_port_dealloc_kernel.
  3. Reproduce a historical CVE in a VM (SockPuppet is friendliest). Step through the UAF window; inspect the ipc.ports zone.
  4. Fuzz a chosen MIG interface with malformed descriptors — prioritise descriptor-count and OOL boundary cases.
  5. Re-read Pegasus and Triangulation write-ups with the above context.
  6. Practice fake-port construction on your debug kernel before any mitigated target.

This is OSEE-adjacent on the macOS side — see osee-roadmap.

Defensive baseline

For platform-side defenders or first-party auditors:

  • Treat MachServices entries as a security boundary. Every entry is sandbox-reachable surface; review the consuming code with the same rigour as a network service.
  • Where possible move to XPC with typed messages and audit-token checks (xpc_connection_get_audit_token — see macos-xpc-internals).
  • Use kalloc_type correctly: do not type-pun structs into the same kalloc class as ipc_port_t.
  • On arm64e, ensure all ipc_port_t fields you store are PAC-signed with diversifiers that include the object identity.
  • Detection-side, abnormal lookups to high-value Mach services from low-privilege bundle IDs are a strong telemetry signal — see detection-engineering-pyramid-of-pain.

References

  • Apple, XNU source — osfmk/ipc/ (ipc_port.c, ipc_object.c, mach_msg.c): https://github.com/apple-oss-distributions/xnu
  • Ian Beer, “mach_portal” (Project Zero): https://googleprojectzero.blogspot.com/2017/04/exception-oriented-exploitation-on-ios.html
  • Project Zero, Triangulation analysis (Kaspersky GReAT + P0 follow-ups): https://securelist.com/operation-triangulation-the-last-hardware-mystery/111669/
  • Brandon Azad, “A survey of recent iOS kernel exploits”: https://googleprojectzero.blogspot.com/2020/06/a-survey-of-recent-ios-kernel-exploits.html
  • Citizen Lab + Google P0 on FORCEDENTRY: https://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html
  • Dionysus Blazakis / @bazad, “The road to iOS Mach voucher exploitation”: https://bazad.github.io/2018/10/ios-mach-voucher-uaf-overview/