GhostLock, un UAF de pila presente durante 15 años en todas las distribuciones de Linux
(nebusec.ai)- GhostLock (CVE-2026-43499) es una vulnerabilidad del kernel introducida en Linux 2.6.39 y corregida en 7.1; permite que un atacante local sin privilegios provoque un UAF de pila usando solo llamadas al sistema de threading comunes, y lo aproveche para obtener privilegios de root y escapar de contenedores
- En la ruta proxy de Requeue-PI,
remove_waiter()borrapi_blocked_ondecurrenten lugar de hacerlo del task que realmente está esperando, dejando en el task que vuelve al espacio de usuario un puntero que apunta a un stack frame ya liberado - Con tres futex y tres hilos se crea un ciclo de dependencias PI para forzar un rollback con
-EDEADLK, y luego se construye unrt_mutex_waiterfalso en el buffer de pila controlable dePR_SET_MM_MAPpara obtener una escritura de puntero restringida - El exploit usa
prefetchpara encontrar las direcciones base de KASLR y physmap, coloca estructuras falsas y una pila ROP en la CPU entry area (CEA), y luego sobrescribeinet6_protos[IPPROTO_UDP]para secuestrar el flujo de control con paquetes loopback UDP de IPv6 - Los investigadores recibieron $92,337 en Google kernelCTF por un exploit de escalada de privilegios y escape de contenedor con 97% de estabilidad, y todas las distribuciones Linux sin parchear deben actualizarse a la LTS más reciente
Alcance del impacto y resumen de la vulnerabilidad
- GhostLock es una vulnerabilidad del kernel de Linux descubierta por VEGA, y puede ser activada por un usuario local sin privilegios sin requerir permisos adicionales ni namespaces de usuario
- Fue introducida con la reelaboración de rtmutex en
8161239a8bcc, y su alcance abarca desdev2.6.39-rc1hastav7.1-rc1 - Fue corregida en abril de 2026 en
3bfdc63936dd, y la única configuración del kernel necesaria esCONFIG_FUTEX_PI=y - Un atacante puede escalar privilegios mediante el siguiente proceso
- Obtiene un puntero colgante del kernel que apunta a memoria de la pila del kernel usando solo llamadas al sistema de threading comunes
- Crea una primitiva restringida capaz de escribir un puntero o 8 bytes en cero en una dirección casi arbitraria
- Secuestra una tabla de funciones para tomar control del flujo de ejecución y obtener privilegios de root
- Como todas las distribuciones Linux sin parchear están afectadas, se debe actualizar a la versión LTS más reciente
Por qué remove_waiter() limpia el task equivocado
remove_waiter()enkernel/locking/rtmutex.cfue escrito originalmente para la ruta en la que el hilo bloqueado limpia por sí mismo su estado de espera- En la ruta lenta normal, el
currenten ejecución es el task dueño del waiter, por lo que borrarcurrent->pi_blocked_ones correcto - En la ruta proxy de Requeue-PI,
rt_mutex_start_proxy_lock()inserta en la cola unrt_mutex_waiteren nombre de otro task dormido, y si ocurre un error hace rollback- En ese momento,
currentes el requeuer que llamó aFUTEX_CMP_REQUEUE_PI - El waiter real es otro task distinto que está dormido en
FUTEX_WAIT_REQUEUE_PI
- En ese momento,
- Cuando
__rt_mutex_start_proxy_lock()devuelve-EDEADLK,remove_waiter()quita el waiter del lock pero solo pone enNULLcurrent->pi_blocked_on - El
pi_blocked_ondel waiter real sigue apuntando alrt_mutex_waiterubicado en su propia pila del kernel, y cuando el waiter vuelve al espacio de usuario ese stack frame se considera liberado - Después, en el momento en que una exploración de la cadena PI pasa por ese task, desreferencia un objeto de pila ya liberado
- lockdep solo verifica qué
pi_lockestá tomado, no de quién es ese lock, por lo que no puede detectar este error
Ciclo de tres futex para producir el rollback con -EDEADLK
- Para llegar a la ruta de error, se construuye un ciclo de dependencias PI con tres futex y tres hilos
f_pi_chain: futex PI que el waiter bloquea primerof_pi_target: futex PI que el owner bloquea primero y que será el destino del requeuef_wait: futex normal en el que el waiter espera conFUTEX_WAIT_REQUEUE_PI
- La secuencia de activación es la siguiente
- El waiter bloquea
f_pi_chainy luego queda bloqueado enFUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target), dejando elrt_mutex_waiteren su propia pila del kernel - El owner bloquea
f_pi_targety luego queda bloqueado enf_pi_chain, que está en posesión del waiter - El hilo principal llama a
FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target)
- El waiter bloquea
- Cuando el requeue proxy intenta enlazar al waiter con
f_pi_target, se cierra el ciclowaiter → f_pi_target → owner → f_pi_chain → waiter - La exploración de la cadena PI devuelve
-EDEADLKy ejecuta el rollback incorrecto, de modo que el waiter despierta conservando unpi_blocked_oncolgante - La condición clave es que el requeuer haga rollback mientras el waiter todavía conserva el objeto en la pila; una vez completado el ciclo, el proceso avanza por sí solo
- Cuando el waiter vuelve al espacio de usuario, ya no hay presión de tiempo, y luego se puede provocar una exploración de la cadena en cualquier momento con
sched_setattr() - Aunque la configuración usa tres hilos, la carrera UAF en sí puede activarse incluso en un solo núcleo de CPU
Primitivas iniciales que aporta el UAF de pila
- El puntero colgante apunta al
rt_mutex_waiterque estaba en el stack frame anterior deFUTEX_WAIT_REQUEUE_PI - Si se vuelven a colocar bytes controlables en la misma profundidad de pila del mismo task, se puede hacer que el kernel los desreferencie como un
rt_mutex_waiterfalso - Dependiendo de cómo se organice la estructura falsa, una sola desreferenciación permite obtener dos primitivas principales
- Se puede escribir un puntero en una dirección casi arbitraria con ciertas restricciones
- Se pueden escribir 8 bytes en cero en una dirección casi arbitraria con ciertas restricciones
- Antes de la escritura se realizan varias desreferenciaciones de punteros y comprobaciones de integridad, pero si se cumplen las condiciones el kernel puede volver normalmente sin colapsar incluso después de escribir
- Para completar el exploit, es necesario reutilizar el stack frame, hacer que el waiter falso supere las comprobaciones estructurales y elegir un objetivo que satisfaga las restricciones de escritura
Reutilización de un frame de pila liberado con PR_SET_MM_MAP
- El waiter llama a
prctl(PR_SET_MM, PR_SET_MM_MAP, ...)inmediatamente después de volver de la syscallfutex prctl_set_mm_map()copia el auxv proporcionado por el usuario en el búfer de pila de tamaño fijounsigned long user_auxv[AT_VECTOR_SIZE]- Como este búfer se coloca a una profundidad de pila similar a la del waiter liberado, un bloque qword grande, alineado y controlable se superpone sobre el
rt_mutex_waiteranterior - La región superpuesta del auxv se configura así
tree: se convierte en un nodo rb que eleva al hijo apuntado elegido al borrar,W0_BASE, a la raíz del árboltask: se establece en&init_taskpara pasar de forma segura por la desreferenciación del recorrido de la cadenalock: se fija como&inet6_protos[IPPROTO_UDP] - 8para apuntar al objetivo de escriturawake_state: se establece en0
- El auxv se coloca en un memfd y se organiza para que la copia cruce un límite de página; luego, un hilo hermano lanza una carrera con
fallocate(PUNCH_HOLE)sobre la página trasera mientras se ejecutaprctlpara alargar el tiempo decopy_from_user - El hilo consumer en otra CPU llama a
sched_setattr()sobre el waiter mientras el waiter falso sigue en la pila, para recorrer la cadena PI - Otras syscalls que usan variables locales de pila grandes y controlables, como
clone,setsockopt,pselectykeyctl, también pueden cumplir la misma función - Se eligió
prctlporque el búfer es grande, está alineado y no requiere namespaces; candidatos adicionales están incluidos en el código PoC público
Crear una escritura de puntero restringida mediante borrado de rb-tree
- Incluso controlando el waiter falso, no se obtiene de inmediato una escritura arbitraria completa, y el recorrido de la cadena ejecuta la siguiente ruta
- encuentra el waiter falso en
task->pi_blocked_on - encuentra el
rt_mutex_basefalso enfake waiter->lock rt_mutex_dequeue(lock, waiter)realiza el borrado del rb-tree enlock->waiters
- encuentra el waiter falso en
- Se aprovecha la propiedad de que, al borrar un nodo raíz con un solo hijo, ese hijo se escribe en el slot de la raíz
- Si
lockse fija entarget - 8, los datos circundantes se interpretan como los siguientes campos dert_mutex_basetarget - 8:wait_lock, que debe leerse como desbloqueadotarget:waiters.rb_root.rb_node, que será sobrescritotarget + 8:waiters.rb_leftmosttarget + 16:owner
- Como resultado, la única escritura que se ejecuta es
*(uint64_t *)target = W0_BASE - La dirección objetivo debe cumplir aproximadamente las siguientes condiciones
- los 32 bits bajos de
target - 0x08deben ser0 - el valor de 64 bits en
target + 0x08debe ser0 - el valor del puntero owner en
target + 0x10, excluyendo los flags bajos, debe ser0
- los 32 bits bajos de
- Si el qword anterior parece un spinlock bloqueado, el trylock falla y termina sin escribir nada
- Si los valores posteriores apuntan a un top waiter u owner no controlado, o a un valor no mapeado, puede producirse un kernel panic
W0_BASEdebe seguir siendo válido hasta que terminen la comparación, el requeueing, la actualización de prioridad y el wakeup sin owner, por lo que se usa el alias direct-map de la CEA
Filtración con prefetch y CPU entry area
-
Encontrar la base de KASLR y physmap
- El tiempo de ejecución de
prefetchsobre una dirección específica cambia según si esa dirección está mapeada en las tablas de páginas actuales - Si un proceso sin privilegios mide los tiempos en el rango de direcciones del kernel, puede inferir dónde hay mapeos; el principio detallado está resumido en el paper de prefetch
- Como la entropía de la base de la imagen estándar del kernel Linux es de alrededor de 9 bits, con mediciones repetidas se recupera la base de KASLR con una confiabilidad cercana al 100%
- En teoría, cualquier CPU con
prefetchy sin una KPTI adecuada está afectada, pero en la práctica esta técnica se usa principalmente en x86 con KPTI desactivado - Las imágenes de kernelCTF tienen KPTI desactivado, y aun con KPTI activado, combinar
prefetchcon EntryBleed permite recuperar la base de la imagen del kernel mediante el trampoline
- El tiempo de ejecución de
-
Evadir la aleatorización de la dirección CEA
- La CPU entry area (CEA) es una estructura por CPU en x86 que guarda pilas y contextos de registros para entrada y manejo de excepciones
- Si un programa sin privilegios provoca una excepción de software, puede escribir su propio contexto de registros en el
pt_regsde la pila de excepciones de la CEA y crear alrededor de 120 bytes continuos de memoria controlable - Antes de Linux 6.2, la dirección virtual de la CEA era completamente fija, por lo que podía usarse directamente para estructuras falsas, absorber efectos secundarios de desreferenciación de punteros y construir una pila ROP
- Tras la publicación de Project Zero, Bringing back the stack attack, desde Linux 6.2 la dirección virtual de la CEA quedó fuertemente aleatorizada
- La dirección virtual de la CEA de cada CPU se aleatoriza de forma distinta, pero la dirección física permanece fija, así que si se conoce la base de physmap se puede calcular el alias direct-map
- Combinando
prefetch, normalización de límites candidatos e inspección de páginas CEA esperadas, se descartan aliases cercanos y se obtienecea_direct = physmap_base + CPU1_CEA_BASE - En el entorno de arranque de 3.5 GB de kernelCTF LTS
6.12.80, el offset relevante es0x11c517000(+0x1f58)
Reutilizar la CEA como waiter falso y objetos posteriores
- Antes de la primera escritura, se coloca en
W0de la CEA un waiter falso y un lock falso autoconsistentestaskse establece en&init_taskpriorecibe un valor válidowait_lockdel lock se hace parecer desbloqueado- el owner se configura para pasar de forma segura por dequeue, requeueing, actualización de prioridad y wakeup
- Una vez terminada la escritura del rb-tree,
W0ya no necesita seguir siendo un waiter, por lo que la CEA puede rellenarse de nuevo con la estructura que requiera el objetivo sobrescrito - La CEA es pequeña, de unos 120 bytes, pero es eficiente porque permite colocar datos en una dirección fija del kernel que puede calcularse
- NPerm y kernelsnitch también pueden cumplir la misma función en un espacio más amplio
- El exploit usa secuencial o simultáneamente una sola región CEA como
rt_mutex_waiterfalso, lock falso,inet6_protocol, slots de JOP y stack pivot, y la pila ROP final
Toma de control del flujo con inet6_protos[IPPROTO_UDP]
- En Linux x86_64 típico, tras obtener la dirección base de KASLR, se puede elegir una ruta corta para sobrescribir una tabla de funciones adecuada o un objeto que la contenga
- El área de datos escribible alrededor de
inet6_protos[IPPROTO_UDP]satisface de forma natural las restricciones necesariasinet6_protos[16] == NULLse convierte en el estado desbloqueado de unwait_lockfalsoinet6_protos[17] == &udpv6_protocoles el objetivo real a sobrescribirinet6_protos[18] == NULLse convierte en unrb_leftmostfalsoinet6_protos[19] == NULLse convierte en un owner falso
- Cuando termina la escritura,
inet6_protos[IPPROTO_UDP]apunta a uninet6_protocolfalso dentro de la página CEA - Se vuelve a rociar CEA para construir la estructura de la siguiente manera
handler: se asigna como el primer gadget de pivoteerr_handler: no se usaflags: se establece comoINET6_PROTO_NOPOLICY | INET6_PROTO_FINAL
- Tras hacer
connecta::1y escribir datos, al enviar un paquete de loopback UDP IPv6 el kernel invoca elhandlerfalso y permite controlar el contador de programa
Pivote corto y escalación de privilegios con DirtyMode
- En el objetivo
lts-6.12.80de Google kernelCTF no se encontró un gadget adecuado de pivote de pila único, así que se hace un pivote cargando la dirección CEA enrbpmediante un load/call adicional y luego usandomov rsp, rbp; pop rbp; ret - Sobrescribir por completo
ret2usro/proc/%P/fd/xrequiere alrededor de 10 qwords de gadgets, demasiado para el espacio limitado de CEA - En la etapa final se usa DirtyMode, que cambia los bits de privilegios con una sola escritura y deja el resto del proceso al espacio de usuario
- El objetivo de escritura es
coredump_sysctls[1].modeen los datos del kernel, es decir, el modo de acceso del sysctlcore_pattern - Como comparte el mismo desplazamiento KASLR que la imagen del kernel, su dirección puede calcularse, y basta con un valor que tenga activado el segundo bit menos significativo, que es el bit de escritura
- Una cadena corta de
pop reg; mov [reg], reg; retcambia el valor del modo y luegomsleepdetiene de forma segura el hilo secuestrado - Una vez que
/proc/sys/kernel/core_patternpuede ser escrito por cualquier usuario, un proceso sin privilegios puede escribir|/proc/%P/fd/666 %Py hacer fallar el helper, con lo que el kernel ejecuta el binario del atacante con privilegios de root - La escritura inicial sobre el rb-tree no puede alcanzar directamente
coredump_sysctls[1].modepor restricciones de disposición, así que el cambio de modo se realiza en una etapa corta de ROP
Flujo completo del exploit y resultado
- El ataque sigue esta secuencia
- Filtra la dirección base del slide de la imagen del kernel y la base de physmap con
prefetch - Usa GhostLock para dejar un
rt_mutex_waitercolgante enpi_blocked_ondel waiter - Reutiliza el mismo frame de pila del kernel con
PR_SET_MM_MAPpara crear un waiter falso - Usa la eliminación del rb-tree de rtmutex para escribir un puntero CEA en
inet6_protos[IPPROTO_UDP] - Coloca en CEA un
inet6_protocolfalso, el slot de pivote y la pila ROP - Invoca el
handlersobrescrito con un paquete de loopback UDP IPv6 - Cambia los bits de modo de
core_patterncon DirtyMode y completa la escalación de privilegios en espacio de usuario
- Filtra la dirección base del slide de la imagen del kernel y la base de physmap con
- En el entorno remoto de kernelCTF, la ruta que combina CEA y DirtyMode obtiene la flag en unos 5 segundos
- El exploit completo está publicado en el proyecto CyberMeowfia
- En Android, la reutilización de frames de pila y la forma de evadir ASLR y CFI son distintas, y se tratarán en una publicación posterior independiente
Rutas alternativas y mitigaciones
-
Espacio ROP más grande
- La memoria basada en NPerm puede usarse como una pila falsa grande después de tomar control del flujo
- También son posibles rutas más pesadas, como la filtración de heap-KASLR de Lukas Maar, pero añaden pasos y aumentan el tiempo de ejecución
- En kernelCTF conviene usar la cadena más corta y confiable, por lo que se elige la combinación de CEA y DirtyMode
-
Parche del kernel
- El parche final toma
pi_lockbasándose enwaiter->tasken lugar decurrent, y limpiapi_blocked_on remove_waiter()guardawaiter_task = waiter->tasky luego procesa en este orden- Bloquea
waiter_task->pi_lock - Elimina el waiter de la cola rtmutex
- Establece
waiter_task->pi_blocked_on = NULL - En la llamada posterior a
rt_mutex_adjust_prio_chain(), también pasawaiter_tasken lugar decurrent
- Bloquea
- Una corrección alternativa enviada por los investigadores antes de la v1 hacía que el llamador pasara explícitamente la tarea propietaria
- En la ruta donde la tarea se bloquea a sí misma, pasa
current - En el rollback por proxy, pasa la
taskobjetivo del proxy - Solo limpia
pi_blocked_oncuando todavía apunta a ese waiter y lo protege con elpi_lockde la tarea
- En la ruta donde la tarea se bloquea a sí misma, pasa
- El parche final toma
-
RANDOMIZE_KSTACK_OFFSET- El exploit depende de que el frame del waiter liberado y el frame posterior de
user_auxvse superpongan de forma determinista - Si se activa
RANDOMIZE_KSTACK_OFFSET, el desplazamiento de la pila cambia y esta etapa pasa a ser una adivinanza de 5 bits con probabilidad aproximada de 1/32 - En los dos objetivos generales enviados esta opción estaba desactivada por defecto, y en el objetivo con mitigación estaba activada, por lo que esa ruta del exploit no se usa
- El exploit depende de que el frame del waiter liberado y el frame posterior de
-
STATIC_USERMODE_HELPERSTATIC_USERMODE_HELPERbloquea esta ruta específica de DirtyMode- Aun así, como los permisos de acceso se controlan mediante
ctl_table::modey la tabla está en datos escribibles y predecibles del kernel, el mismo método puede generalizarse a otras configuraciones de/proc/sys
Cronograma de divulgación
- 18 de abril de 2026: se envían la vulnerabilidad y un parche preliminar a
security@kernel.org - 20 de abril de 2026: la vulnerabilidad se corrige con otro parche
- 4 de mayo de 2026: se hace backport de la corrección v1
- 30 de junio de 2026: Google confirma la entrega a kernelCTF
- 7 de julio de 2026: se publica el análisis técnico
- A la vulnerabilidad descubierta por VEGA se le aplica la política estándar de divulgación de 90+30 días
1 comentarios
Opiniones en Hacker News
Lo probé en 3 dispositivos con Android 9, 13 y 16, y distintas versiones de Firefox anteriores a la 150: 2 quedaron en un bucle de arranque y tuve que entrar en modo de recuperación, y el otro se apagó. La demo cambia el fondo de pantalla de los dispositivos Pixel compatibles, y la página de prueba está disponible en IonStack.
Al leer blogs o sitios cualquiera en un dispositivo personal, es más seguro instalar un navegador basado en Chromium como Chromite, separado del navegador principal, desactivar JavaScript y los decodificadores de video acelerados por hardware —que suelen ser blanco de ataques— desde las flags, y usar el modo lectura en sitios rotos. Otra opción es tener una tablet dedicada.
adbpara verificarlo y luego publicaré los resultados completos.Al entrar a la página de prueba, apareció salida en la pestaña de Firefox, como si se hubiera ejecutado el código de prueba de concepto, pero después el teléfono se congeló y rechazó toda entrada. Lo único que funcionó fue reiniciar, y me da curiosidad cómo puede responder al evento de reinicio incluso cuando el kernel parece haberse colgado. La pantalla quedó encendida mostrando parte del resultado de la ejecución hasta que se activó el protector de pantalla.
Un gran reconocimiento a los investigadores de seguridad que no solo descubrieron el exploit, sino que, a diferencia de copyfail, no publicaron un script de escalamiento local de privilegios zero-day que cualquiera pudiera usar de inmediato.
Intenté durante varias horas lograr escalamiento local de privilegios (LPE) en Rocky Linux 9, pero por suerte no tuve éxito. Si no se tiene mucho tiempo libre o un nivel técnico muy alto, parece difícil usarlo en ataques reales contra distribuciones empresariales.
Me pregunto si esta vulnerabilidad permitiría desbloquear el bootloader incluso en teléfonos donde normalmente no se puede. Si fuera posible, podría ser una de las mejores cosas que le hayan pasado al ecosistema Android.
Creo que deberían haber puesto LPE, por escalamiento local de privilegios, en el título, así la mayoría podría quedarse tranquila y volver a disfrutar el fin de semana.
Pero este ataque también puede activarse dentro de un proceso fuertemente aislado por sandbox, como el proceso de navegador aislado de Firefox. A un atacante le basta con encadenar un ataque de dos etapas: primero ejecutar código local dentro del sandbox aislado mediante una vulnerabilidad de JavaScript, y luego subir hasta modo kernel con esta vulnerabilidad. Por eso hay que actualizar tanto Firefox como el kernel de Linux.
Me llamó la atención desde la parte que dice que “Google pagó 92,337 dólares como recompensa de kernelCTF”.
Me pregunto si esto significa que una app de Android puede ejecutar código nativo con el NDK y obtener permisos de root, y si SELinux ayuda a defenderse.
Se pueden backportear parches a kernels antiguos, pero como los changelogs de actualizaciones de smartphones rara vez mencionan CVE, las herramientas de detección de vulnerabilidades son prácticamente el único modo de confirmarlo. Si una app instalada desde Play Store o desde fuera fue comprometida, podría obtener root de inmediato, así que sigue siendo importante verificar la confianza y la auditoría al instalar.
Es posible que en el futuro esta comprobación se agregue a todos los niveles de Google Play Integrity, impidiendo instalar varias apps en teléfonos sin parchear. En navegadores, donde es difícil evitar sitios cualquiera y anuncios, es más grave porque escapar del sandbox también permite saltarse el aislamiento de apps; es similar a JailbreakMe en iOS.
Cualquier app capaz de ejecutar código nativo en Linux de los últimos 15 años puede obtener root hasta que llegue una actualización del kernel al dispositivo.
Es impactante que GhostLock haya entrado en Linux 2.6.39 y recién se haya corregido en Linux 7.1.
Siento que ya había leído los comentarios el día anterior, pero todos muestran tiempos de publicación dentro de las últimas 10 horas; me pregunto si el indicador de tiempo de HN está mal.
Este artículo estaba al principio de la lista “underwater” que reviso a diario, es decir, la lista de posts que recibieron muchos votos pero que por algún motivo no llegaron a la portada, así que lo volví a exponer. Se ve raro, pero todavía no apareció una alternativa menos confusa.