Kernel Heap & Zone Allocator
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
- Azeria Labs – Grooming the iOS Kernel Heap
- Azeria Labs – Heap Overflows and iOS Kernel Heap
- Apple – Towards Next-Gen XNU Memory Safety: kalloc_type
- XNU source:
osfmk/kern/zalloc.c,osfmk/kern/kalloc.c - Stefan Esser – iOS Kernel Exploitation (BlackHat 2011)
Exercises
- Read zalloc.c: Trace the allocation path from
zalloc()to element return - Identify the zone for a struct: Given a kernel struct, compute its size, determine the kalloc zone
- Write a heap spray: Use OOL messages to spray a specific kalloc zone on macOS
- Analyze kalloc_type: Read the Apple blog post, understand how type isolation works
- Study a real exploit: Read the kfd source code, identify the heap grooming technique used