Most kernel exploits require heap manipulation. The zone allocator is the primary allocator in the XNU kernel. Understanding it = understanding how to control kernel memory layout.


Overview

XNU uses the zone allocator (zalloc) as its primary memory allocator. On top of the zone allocator, kalloc provides general-purpose allocation by size.

User code:  malloc(size)  → libmalloc (userspace)
Kernel:     kalloc(size)  → kalloc zones → zalloc → physical pages
            zalloc(zone)  → specific zone → physical pages

Zone Allocator (zalloc)

Concept

Zone = pool of fixed-size elements:

Zone "ipc_ports" (elem_size = 168 bytes):
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ port │ port │ FREE │ port │ FREE │ port │ port │ FREE │
│  #1  │  #2  │      │  #4  │      │  #6  │  #7  │      │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
         ↑               ↑               ↑
         └── Allocated ──┘── Free list ──┘

Zone Structure

struct zone {
    const char      *z_name;          // "ipc_ports", "kalloc.256", ...
    vm_size_t        z_elem_size;     // Element size
    zone_id_t        z_index;         // Zone index
    uint32_t         z_chunk_pages;   // Pages per chunk

    // Free list
    zone_element_t   z_recirc;        // Magazine/depot free lists
    uint32_t         z_elems_free;    // Count of free elements

    // Statistics
    uint64_t         z_elems_avail;   // Total elements
    uint64_t         z_alloc_count;   // Total allocations
    // ...
};

Allocation Flow

void *zalloc(zone_t zone) {
    // 1. Try magazine (per-CPU cache)
    elem = magazine_alloc(zone);
    if (elem) return elem;

    // 2. Try zone free list
    elem = zone_free_list_pop(zone);
    if (elem) return elem;

    // 3. Expand zone (allocate new pages)
    zone_expand(zone);
    elem = zone_free_list_pop(zone);
    return elem;
}

void zfree(zone_t zone, void *elem) {
    // 1. Clear element memory (security: prevent info leaks)
    bzero(elem, zone->z_elem_size);

    // 2. Push to magazine or free list
    magazine_free(zone, elem);
}

Free List

Pre-iOS 15: In-band free list (freelist pointer stored IN freed element)
  ┌──────────┐    ┌──────────┐    ┌──────────┐
  │ next ────┼───→│ next ────┼───→│ NULL     │
  │ (data)   │    │ (data)   │    │ (data)   │
  └──────────┘    └──────────┘    └──────────┘

Post-iOS 15: Out-of-band free list (bitmap or separate metadata)
  → Prevents freelist pointer overwrite attacks

Kalloc

Kalloc Zones

Kalloc wraps zalloc for general-purpose allocations:

kalloc(size) → selects the smallest zone that fits the size:

  Size range      Zone name         Actual allocation
  ─────────────────────────────────────────────────
  1-16 bytes  →   kalloc.16         16 bytes
  17-32       →   kalloc.32         32 bytes
  33-48       →   kalloc.48         48 bytes
  49-64       →   kalloc.64         64 bytes
  65-80       →   kalloc.80         80 bytes
  81-96       →   kalloc.96         96 bytes
  97-128      →   kalloc.128        128 bytes
  129-160     →   kalloc.160        160 bytes
  161-192     →   kalloc.192        192 bytes
  193-224     →   kalloc.224        224 bytes
  225-256     →   kalloc.256        256 bytes
  257-288     →   kalloc.288        288 bytes
  ...
  513-1024    →   kalloc.1024       1024 bytes
  1025-2048   →   kalloc.2048       2048 bytes
  2049-4096   →   kalloc.4096       4096 bytes
  4097-6144   →   kalloc.6144       6144 bytes
  6145-8192   →   kalloc.8192       8192 bytes
  >8192       →   kernel_map (vm_allocate, page-level)

kalloc_type (iOS 16+)

Type-isolated allocation – objects of the same C type go to the same zone:

// Old: all 256-byte objects share kalloc.256
struct ipc_port *p = kalloc(sizeof(struct ipc_port));  // kalloc.176
struct pipe_buf *b = kalloc(sizeof(struct pipe_buf));   // kalloc.176 (same zone!)

// New: type-specific zones
struct ipc_port *p = kalloc_type(struct ipc_port, Z_WAITOK);  // zone: ipc_port
struct pipe_buf *b = kalloc_type(struct pipe_buf, Z_WAITOK);  // zone: pipe_buf (different!)

Impact on exploitation:

  • Pre-kalloc_type: allocate object A with the same size as freed object B – type confusion is easy
  • Post-kalloc_type: objects A and B must be the same type or in the same zone group – much harder

Heap Exploitation Techniques

1. Heap Spray

Purpose: Fill a zone with controlled data to increase the probability that the target object is adjacent.

// OOL message spray — spray the kalloc.256 zone
for (int i = 0; i < 1000; i++) {
    struct {
        mach_msg_header_t hdr;
        mach_msg_body_t body;
        mach_msg_ool_descriptor_t ool;
    } msg;

    msg.ool.address = spray_data;    // Controlled content
    msg.ool.size = 256;              // → kalloc.256
    msg.ool.type = MACH_MSG_OOL_DESCRIPTOR;
    msg.body.msgh_descriptor_count = 1;

    mach_msg(&msg.hdr, MACH_SEND_MSG, sizeof(msg), 0, 0, 0, 0);
}

2. Heap Grooming

Purpose: Arrange heap layout so the target object sits right after the vulnerable object.

Step 1: Spray to fill zone
  [spray][spray][spray][spray][spray][spray][spray]

Step 2: Create holes (free selected spray objects)
  [spray][    ][spray][    ][spray][    ][spray]

Step 3: Allocate victim objects into holes
  [spray][victim][spray][victim][spray][victim][spray]

Step 4: Free remaining spray, allocate vulnerable object
  [vuln][victim][     ][victim][     ][victim][     ]
    ↑       ↑
    │       └── Adjacent! Overflow from vuln corrupts victim
    └── Trigger vulnerability here

3. Zone Transfer (Cross-Zone Attack)

1. Allocate object X in zone A
2. Free object X → X returns to zone A's free list
3. Bug: dangling pointer to X still exists
4. Force zone A to release pages back to the page allocator
5. Force zone B to expand → zone B gets X's physical pages
6. Allocate object Y (different type) in zone B → Y occupies X's memory
7. Use dangling pointer to X → actually accessing Y → type confusion!

4. Freelist Poison (Pre-iOS 15)

Old-style in-band freelist:
  Free element: [next_ptr | padding...]

Overflow into freed element → overwrite next_ptr:
  [overflow data | CONTROLLED_ADDR | ...]

Next allocation returns CONTROLLED_ADDR → write-what-where primitive!

Post-iOS 15: the freelist is out-of-band, so this technique no longer works.

5. Probabilistic Grooming

When adjacency cannot be guaranteed:

1. Spray N objects (large N, e.g., 10000)
2. Free every other object → create N/2 holes
3. Allocate target objects → ~50% chance adjacent to spray
4. Trigger vulnerability
5. Check multiple targets → find which one was corrupted

Trade-off: reliability vs speed vs memory pressure

Mitigations Timeline

iOS Version Mitigation Impact
iOS 10 Zone poisoning (fill freed memory with pattern) Detect UAF sooner
iOS 11 Randomized zone free lists Make grooming harder
iOS 14 zone_require() – validate object belongs to expected zone Prevent zone transfer
iOS 15 Out-of-band free lists Kill freelist overwrite
iOS 15.4 Sequestered zones (sensitive zones isolated) Harder cross-zone
iOS 16 kalloc_type() – type-isolated zones Kill same-size type confusion
iOS 17 SPTM-enforced page types Prevent physical UAF escalation

Resources


Exercises

  1. Read zalloc.c: Trace the allocation path from zalloc() to element return
  2. Identify the zone for a struct: Given a kernel struct, compute its size, determine the kalloc zone
  3. Write a heap spray: Use OOL messages to spray a specific kalloc zone on macOS
  4. Analyze kalloc_type: Read the Apple blog post, understand how type isolation works
  5. Study a real exploit: Read the kfd source code, identify the heap grooming technique used