Kernel Memory Allocation (kalloc) Methods in Dopamine Explained: IOSurface-Based Primitives and API Design

Dopamine's kernel memory allocation relies on kalloc() and kalloc_with_options() functions that delegate to IOSurface-based primitives for global and local kernel buffer allocation.

Dopamine is a rootless jailbreak for iOS 15–17 that provides a unified kalloc API for kernel-mode memory allocation. Understanding these methods is essential for security researchers analyzing the jailbreak's exploitation primitives and memory management architecture. This guide examines the layered implementation, from public API entry points to low-level IOSurface Mach-port manipulations.

Core kalloc API: Entry Points and Options

Dopamine exposes two primary functions for kernel memory allocation in BaseBin/libjailbreak/src/primitives.c. The simpler kalloc() wraps kalloc_with_options() with global allocation semantics, while the latter provides explicit control over allocation scope.

kalloc(): Default Global Allocation

int kalloc(uint64_t *addr, uint64_t size) {
    return kalloc_with_options(addr, size, KALLOC_OPTION_GLOBAL);
}

This function is the default entry point used throughout Dopamine's codebase. It returns zero on success and populates *addr with the kernel virtual address of the allocated buffer.

kalloc_with_options(): Explicit Scope Control

int kalloc_with_options(uint64_t *addr, uint64_t size, kalloc_options options) {
    if (options == KALLOC_OPTION_GLOBAL && gPrimitives.kalloc_global)
        return gPrimitives.kalloc_global(addr, size);
    else if (options == KALLOC_OPTION_LOCAL && gPrimitives.kalloc_local)
        return gPrimitives.kalloc_local(addr, size);
    return -1;
}

The kalloc_options enum distinguishes two allocation strategies:

  • KALLOC_OPTION_GLOBAL – Allocates persistent kernel memory that survives Mach port destruction
  • KALLOC_OPTION_LOCAL – Allocates memory tied to IOSurface Mach port lifetime

The gPrimitives structure is populated during initialization in libjailbreak_IOSurface_primitives_init(), which registers the IOSurface-based implementations.

IOSurface Primitives: The Actual Allocator

The real kernel memory allocation logic resides in BaseBin/libjailbreak/src/primitives_IOSurface.m. Dopamine leverages IOSurface framework objects—normally used for graphics buffer sharing—to achieve arbitrary kernel memory allocation through Mach-port manipulation.

Global vs. Local Allocation

Method Flag Persistence Use Case
IOSurface_kalloc_global leak = true Survives port close Long-lived kernel structures
IOSurface_kalloc_local leak = false Freed with port Temporary allocations

Both variants call into version-specific helpers that handle iOS kernel alignment differences:

  • iOS 16+: IOSurface_kalloc_16up – 16-byte alignment (0x10)
  • iOS 15 and earlier: IOSurface_kalloc_15 – 15-byte alignment (0x15)

The runtime version detection uses @available(iOS 16.0, *) to select the appropriate code path.

Allocation Mechanism

The IOSurface primitive operates through four coordinated steps:

  1. Surface creation with a crafted IOSurfaceAddressRanges dictionary pointing to user-controlled pages
  2. Mach port generation via IOSurfaceCreateMachPort
  3. Kernel address extraction through IOSurface_get_ranges to obtain the buffer's kernel virtual address
  4. Conditional zeroing of range entries when leak=true, preventing automatic deallocation when the port is destroyed

This technique exploits the IOSurface kernel driver's trust of user-provided address ranges, converting them into allocated kernel buffers.

libkrw Integration: Plugin Architecture

Dopamine registers its kalloc implementation as a libkrw provider, enabling external tools to use standard kernel read/write primitives. The registration occurs in Packages/libkrw-provider/src/main.c:

handlers->kmalloc = (krw_kmalloc_func_t)(kalloc);
handlers->kdealloc = (krw_kdealloc_func_t)(kfree);

This bridge allows third-party jailbreak components to allocate kernel memory without direct dependency on Dopamine's internal headers.

Usage Patterns in Dopamine Codebase

C Exploit Modules: TrustCache Manipulation

The TrustCache extension code in BaseBin/libjailbreak/src/trustcache.c demonstrates standard allocation and cleanup:

uint64_t kern_addr = 0;
if (kalloc(&kern_addr, 0x4000) != 0) {
    // allocation failed
}
// ... populate and use kernel buffer ...
kfree(kern_addr, 0x4000);   // explicit deallocation

Swift Wrapper: iDownloadKRW Module

Dopamine's Swift bindings expose kalloc to higher-level components:

public func kalloc(size: UInt) throws -> UInt64 {
    var kallocAddr: UInt64 = 0
    let r = c_kalloc(&kallocAddr, UInt64(size))
    guard r == 0 else {
        throw KRWError.customError(description: "kalloc_data_external failed to allocate!")
    }
    return kallocAddr
}

This wrapper converts C return codes into Swift errors while preserving the raw kernel address for subsequent operations.

Exploit-Specific Helpers

Some modules use thin convenience wrappers for common allocation sizes:

uint64_t page = kalloc_page();   // allocates 0x4000 bytes with KALLOC_OPTION_GLOBAL

This pattern appears in page table manipulation code where 16KB aligned allocations are required.

Key Implementation Files

Component Path Purpose
Public API BaseBin/libjailbreak/src/primitives.c kalloc(), kalloc_with_options(), kfree()
IOSurface primitives BaseBin/libjailbreak/src/primitives_IOSurface.m Actual kernel allocation via Mach ports
Initialization BaseBin/libjailbreak/src/main.c Primitive registration into gPrimitives
libkrw bridge Packages/libkrw-provider/src/main.c Plugin standardization
Example consumer BaseBin/libjailbreak/src/trustcache.c Real-world allocation patterns
Swift interface Application/Dopamine/Exploits/idownloadd/src/idownloadd/iDownloadKRW.swift High-level language binding

Summary

  • Dopamine's kalloc API provides kalloc() and kalloc_with_options() as the primary kernel memory allocation interface
  • IOSurface primitives implement the actual allocation through Mach-port manipulation of graphics buffer objects
  • Global vs. local distinction controls allocation persistence, with global allocations deliberately "leaked" for long-term kernel use
  • Version-aware alignment handles iOS 16+ (16-byte) versus earlier kernels (15-byte) automatically
  • libkrw integration exposes the allocator to standardized kernel read/write frameworks
  • Multi-language support spans C exploit code, Swift wrappers, and external plugin consumers

Frequently Asked Questions

What is the difference between kalloc and kalloc_with_options in Dopamine?

The kalloc() function is a convenience wrapper that always uses KALLOC_OPTION_GLOBAL, allocating persistent kernel memory. The kalloc_with_options() function allows explicit selection between global (persistent) and local (port-bound) allocation strategies. Most Dopamine internal code uses kalloc(), while kalloc_with_options() is reserved for scenarios requiring precise lifetime control.

How does IOSurface enable kernel memory allocation?

IOSurface framework objects accept user-provided address ranges that the kernel driver copies into allocated buffers. By crafting these ranges and manipulating the surface's Mach port, Dopamine causes the kernel to allocate memory that the user partially controls. The IOSurface_get_ranges function reveals the kernel virtual address of this buffer, completing the primitive.

Why does Dopamine distinguish between 16-byte and 15-byte alignment?

iOS 16 introduced changes to the IOSurface kernel driver's internal structure layout, affecting address alignment requirements. The @available(iOS 16.0, *) check in primitives_IOSurface.m selects IOSurface_kalloc_16up for modern kernels and IOSurface_kalloc_15 for iOS 15 and earlier, ensuring compatibility across supported jailbreak targets.

When should kernel memory be freed with kfree versus deallocated by closing the Mach port?

Use kfree() for allocations made with KALLOC_OPTION_GLOBAL, as these persist beyond port lifetime due to range zeroing. Allocations made with KALLOC_OPTION_LOCAL are automatically reclaimed when their associated IOSurface Mach port is destroyed, making explicit kfree() unnecessary and potentially unsafe for these objects.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →