IOKit attack surface
TL;DR: IOKit is XNU’s C++ driver framework. Each driver exposes a
IOUserClientsubclass whoseexternalMethoddispatch table is reachable from userspace — and that dispatch table is where most macOS/iOS LPE and jailbreak bugs live.
What it is
IOKit is the Mach-based driver model used by graphics, audio, networking offload, USB, and Apple-silicon coprocessors. A user app calls IOServiceOpen to instantiate a driver-specific IOUserClient, then invokes typed methods via IOConnectCallMethod. Each method takes scalar inputs, optional structure input, scalar outputs, and a structure output. The kernel-side handler is C++ on top of OSObject/OSDictionary/OSArray, frequently dealing with shared memory mappings and async notifications via Mach ports.
Preconditions / where it applies
- Local code execution as a user with access to the driver’s
IOService(the sandbox controls which user clients you can open viacom.apple.security.iokit-user-client-classentitlements). - Major historical sources of macOS LPE and iOS jailbreaks (AppleAVE, IOSurface, AGX/AppleM2, AppleMobileFileIntegrity, AppleUSB).
- Relevant after a sandbox escape to reach root or kernel; see macos-sandbox-escape and apple-mitigations.
Technique
General methodology:
- Enumerate reachable services —
ioreg -llists the registry;IOServiceMatching("AppleFoo")+ sandbox profile tells you what you can open. - Pull the dispatch table — disassemble the driver kext or DriverKit
.dext, findgetTargetAndMethodForIndexor theIOExternalMethodDispatcharray. Each entry encodes input/output sizes; mismatches between declared sizes and actual handler assumptions are bugs. - Audit typed shared memory — many drivers
IOConnectMapMemorya shared region; the kernel re-reads fields after validation (classic TOCTOU). On Apple Silicon, PPL/SPTM may still let you race a non-protected field. - Look for object lifetime bugs —
OSObjectreference counting in user-driven async callbacks is a long-running source of UAFs (e.g. CVE-2020-9907 AVEVideoEncoder, CVE-2021-30883 IOMobileFrameBuffer “FORCEDENTRY adjacent”). - Trigger — build a small harness with
IOConnectCallStructMethod, fuzz scalars/structs guided by handler size checks.kmem-style read/write primitives often fall out of a single confused dispatcher.
Minimal client skeleton:
1
2
3
4
5
6
7
8
9
10
io_service_t svc = IOServiceGetMatchingService(kIOMainPortDefault,
IOServiceMatching("AppleSomething"));
io_connect_t conn;
IOServiceOpen(svc, mach_task_self(), 0 /* type */, &conn);
uint64_t in_scalars[2] = { 0x41, 0x42 };
size_t out_count = 4;
uint64_t out_scalars[4] = {0};
IOConnectCallMethod(conn, /*selector*/ 7,
in_scalars, 2, NULL, 0,
out_scalars, &out_count, NULL, NULL);
Modern Apple moves drivers to DriverKit (.dext running in userspace), which shrinks the kernel attack surface but introduces new XPC-style attack surface in the system extension itself.
Detection and defence
- Hardware mitigations (PAC, BTI, PPL/SPTM — see apple-mitigations) raise the bar significantly post-exploit. Bug-classes still exist; primitives are weaker.
- For defenders: kext load events show up via EndpointSecurity (
ES_EVENT_TYPE_NOTIFY_KEXTLOAD). Restrict third-party kexts via MDM; prefer DriverKit. - For developers: validate every userspace-supplied size before touching shared memory, use
OSSafeReleaseNULLconsistently, and adoptIOUserClient::externalMethodpatterns that pin object lifetimes.
References
- Apple — IOKit Fundamentals (archived) — base model.
- Project Zero — IOKit research posts — many writeups on AppleAVE, IOSurface, etc.
- HackTricks — macOS IOKit — offensive primer.