The virtual memory subsystem manages translation between virtual addresses (used by code) and physical addresses (actual RAM). Page tables are the data structure holding this mapping. Bugs in the VM subsystem lead to physical read/write – the most powerful primitive.


Why You Need to Understand VM

  • Physical UAF (PUAF) exploits target bugs in the VM subsystem
  • Page table manipulation is how to bypass PPL/SPTM
  • physrw (physical read/write) is achieved through dangling page table entries
  • KASLR relies on VM – understanding VM = understanding how to leak the kernel base
  • pmap code is a target for many kernel bugs

ARM64 Address Translation

Translation Table Walk

Virtual Address (48-bit):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ L1 idx β”‚ L2 idx β”‚ L3 idx β”‚ Page   β”‚ Page offset  β”‚
β”‚ [47:39]β”‚ [38:30]β”‚ [29:25]β”‚ [24:14]β”‚ [13:0]       β”‚
β”‚ 9 bits β”‚ 9 bits β”‚ 5 bits β”‚ 11 bitsβ”‚ 14 bits      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   (iOS uses 16KB pages β†’ offset = 14 bits)

Translation:
  TTBR1_EL1 (kernel) or TTBR0_EL1 (user)
       β”‚
       β–Ό
  L1 Table ──[L1 idx]──→ L1 Entry β†’ L2 Table base
                              β”‚
                              β–Ό
                         L2 Table ──[L2 idx]──→ L2 Entry β†’ L3 Table base
                                                    β”‚
                                                    β–Ό
                                               L3 Table ──[L3 idx]──→ L3 Entry
                                                                         β”‚
                                                                         β–Ό
                                                                   Physical Page
                                                                    + offset
                                                                         β”‚
                                                                         β–Ό
                                                                   Physical Address

Page Table Entry (PTE) Format

L3 Page Table Entry (arm64, 16KB granule):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 63    59β”‚58  55β”‚54  52β”‚51          14β”‚13  12β”‚11  10β”‚9  8β”‚...β”‚
β”‚ ignored β”‚ SW   β”‚ res  β”‚ Output Addr  β”‚ res  β”‚  AP  β”‚ SH β”‚...β”‚
β”‚ by HW   β”‚ use  β”‚      β”‚ (phys page)  β”‚      β”‚      β”‚    β”‚   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ ...β”‚7  6β”‚5β”‚4  2β”‚1β”‚0β”‚
β”‚    β”‚ resβ”‚ β”‚MAIRβ”‚ β”‚Vβ”‚
β”‚    β”‚    β”‚ β”‚idx β”‚ β”‚aβ”‚
β”‚    β”‚    β”‚ β”‚    β”‚ β”‚lβ”‚
β”‚    β”‚    β”‚ β”‚    β”‚ β”‚iβ”‚
β”‚    β”‚    β”‚ β”‚    β”‚ β”‚dβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key fields:
  Bit 0 (Valid)     : 1 = entry valid, 0 = fault on access
  Bits 4:2 (AttrIdx): Memory attribute index (MAIR register)
  Bits 7:6 (AP)     : Access Permission
                       00 = EL1 R/W, EL0 no access
                       01 = EL1 R/W, EL0 R/W
                       10 = EL1 R/O, EL0 no access
                       11 = EL1 R/O, EL0 R/O
  Bits 9:8 (SH)     : Shareability (Inner/Outer/None)
  Bit 10 (AF)       : Access Flag (set by HW on first access)
  Bit 54 (XN)       : Execute Never (EL1)
  Bit 53 (PXN)      : Privileged Execute Never
  Bits 51:14        : Physical address of page (shifted)

Address Permissions Summary

AP[7:6] EL1 (kernel) EL0 (user)
00 Read/Write No access
01 Read/Write Read/Write
10 Read-only No access
11 Read-only Read-only

XNU Virtual Memory Subsystem

VM Map

// vm_map = address space representation
struct vm_map {
    struct vm_map_header    hdr;        // Sorted list of vm_map_entry
    pmap_t                  pmap;       // Machine-dependent page tables
    vm_map_size_t           size;       // Virtual size
    // ...
};

// vm_map_entry = one contiguous mapping
struct vm_map_entry {
    struct vm_map_entry *prev, *next;   // Doubly-linked list
    vm_map_offset_t     vme_start;      // Start virtual address
    vm_map_offset_t     vme_end;        // End virtual address
    vm_prot_t           protection;     // Current protection
    vm_prot_t           max_protection; // Maximum allowed protection
    // ...
    union {
        vm_object_t     vme_object;     // Backing object (file, anonymous)
    };
};

Pmap (Physical Map)

// pmap = machine-dependent page table management
struct pmap {
    tt_entry_t          *tte;           // Top-level page table pointer
    uint64_t            asid;           // Address Space ID (for TLB)
    // ...
};

Key pmap functions:

// Create mapping: virtual β†’ physical
pmap_enter_options(pmap, va, pa, prot, fault_type, flags, options);

// Remove mapping
pmap_remove(pmap, start, end);

// Change protection
pmap_protect(pmap, start, end, prot);

// Query mapping
pmap_find_phys(pmap, va);  // Returns physical page number

TLB (Translation Lookaside Buffer)

TLB = cache for page table entries. The CPU does not walk page tables on every memory access – it checks the TLB first.

CPU needs virtual β†’ physical translation
  β”œβ”€β†’ Check TLB (fast path)
  β”‚     β”œβ”€β†’ TLB hit: use cached translation
  β”‚     └─→ TLB miss: walk page tables (slow path)
  β”‚           └─→ Insert result into TLB
  └─→ Access physical memory

TLB invalidation (TLBI instruction) is needed when:

  • Page table entries are changed
  • Context switch (ASID change)

Bug pattern: Kernel changes a PTE but forgets to invalidate TLB – stale translation is still used – security violation.


Physical Use-After-Free (PUAF)

Concept

1. Process A maps virtual address VA β†’ physical page P
2. Bug: kernel frees physical page P but DOES NOT remove the mapping from process A's page tables
3. Kernel reallocates physical page P for another purpose (kernel object, page table, ...)
4. Process A still reads/writes P through VA β†’ reads/writes kernel data!

PUAF β†’ physrw Escalation

If page P becomes a Level 3 page table:
  β”œβ”€β†’ Process A writing to P = modifying page table entries
  β”œβ”€β†’ Create a new PTE pointing to ANY physical address
  β”œβ”€β†’ Map all of DRAM into the address space
  └─→ Full physical read/write!

If page P becomes a kernel object:
  β”œβ”€β†’ Process A writing to P = corrupting a kernel object
  └─→ Escalate to kread/kwrite

Defenses (iOS 17+ SPTM)

SPTM ensures:
  - Physical pages track their "type" (user, kernel, page table, ...)
  - User pages CANNOT be reallocated as kernel pages until next boot
  - Page table modifications MUST go through SPTM (EL2)
  β†’ PUAF from user β†’ kernel is blocked
  β†’ PUAF user β†’ user still possible (less useful)

Page Lifecycle

Free page pool
  β”‚
  β”œβ”€ vm_page_grab() β†’ Allocate physical page
  β”‚   └─ pmap_enter() β†’ Map into address space
  β”‚
  β”œβ”€ pmap_remove() β†’ Remove mapping
  β”‚   └─ vm_page_free() β†’ Return to free pool
  β”‚
  └─ PUAF bug: vm_page_free() WITHOUT pmap_remove()
       β†’ Page returned to free pool but still mapped!

Kernel Memory Regions

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Kernel Virtual Address Space           β”‚
β”‚                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚  β”‚ Kernelcache         β”‚ Fixed mapping  β”‚
β”‚  β”‚ (__TEXT, __DATA)     β”‚ KASLR slide   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚  β”‚ Zone maps           β”‚ Dynamic       β”‚
β”‚  β”‚ (kalloc, ipc, ...)  β”‚               β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚  β”‚ Kernel map          β”‚ General       β”‚
β”‚  β”‚ (large allocations) β”‚ purpose       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚  β”‚ MMIO regions        β”‚ Device        β”‚
β”‚  β”‚ (hardware registers)β”‚ mappings      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Resources


Exercises

  1. Manual page table walk: Given a virtual address, TTBR value, and page table dumps – compute the physical address by hand
  2. Read pmap.c: Trace pmap_enter_options() – understand what each parameter does
  3. Analyze a PUAF exploit: Read the kfd source code, trace the PUAF primitive (smith/landa/physpuppet)
  4. Draw a memory map: For a running process, use vmmap (macOS) to draw the full address space layout
  5. Compute PTE values: Given a physical address + desired permissions, construct the PTE value by hand