- GhostLock (CVE-2026-43499) est une vulnérabilité du noyau introduite dans Linux 2.6.39 et corrigée dans 7.1 ; elle permet à un attaquant local non privilégié de provoquer une UAF de pile avec de simples appels système de threading, exploitable pour obtenir les privilèges root et s’échapper d’un conteneur
- Dans le chemin proxy Requeue-PI,
remove_waiter()efface le champpi_blocked_ondecurrentau lieu de celui de la tâche réellement en attente, ce qui laisse, dans la tâche revenue en espace utilisateur, un pointeur vers une frame de pile libérée - En créant une boucle de dépendances PI avec trois futex et trois threads pour provoquer un rollback
-EDEADLK, puis en construisant un fauxrt_mutex_waiterdans le tampon de pile contrôlable dePR_SET_MM_MAP, on obtient une écriture de pointeur contrainte - L’exploit utilise
prefetchpour trouver les adresses de base de KASLR et de physmap, place de fausses structures et une pile ROP dans la CPU entry area (CEA), puis écraseinet6_protos[IPPROTO_UDP]pour détourner le flux de contrôle via un paquet IPv6 UDP en loopback - Les chercheurs ont reçu 92 337 $ dans Google kernelCTF pour un exploit d’élévation de privilèges et d’évasion de conteneur stable à 97 % ; toutes les distributions Linux non corrigées doivent être mises à jour vers la dernière version LTS
Portée de l’impact et aperçu de la vulnérabilité
- GhostLock est une vulnérabilité du noyau Linux découverte par VEGA, déclenchable par un utilisateur local non privilégié sans droits particuliers ni namespace utilisateur
- Elle a été introduite par le remaniement de rtmutex dans
8161239a8bcc, et la portée affectée va dev2.6.39-rc1àv7.1-rc1 - Elle a été corrigée en avril 2026 dans
3bfdc63936dd, et le seul paramètre de noyau nécessaire estCONFIG_FUTEX_PI=y - Un attaquant peut élever ses privilèges en suivant le processus suivant
- obtenir, uniquement avec des appels système de threading ordinaires, un pointeur noyau pendant vers de la mémoire de pile noyau
- créer une primitive contrainte permettant d’écrire un pointeur ou 8 octets nuls à une adresse presque arbitraire
- détourner une table de fonctions pour prendre le contrôle du flux d’exécution et obtenir les privilèges root
- Toutes les distributions Linux non corrigées étant affectées, il faut passer à la dernière version LTS
Pourquoi remove_waiter() nettoie la mauvaise tâche
remove_waiter(), danskernel/locking/rtmutex.c, a été écrit à l’origine pour le chemin où un thread bloqué nettoie lui-même son état d’attente- Dans le slow path normal, le
currenten cours d’exécution est la tâche propriétaire du waiter ; effacercurrent->pi_blocked_onest donc correct - Dans le chemin proxy Requeue-PI,
rt_mutex_start_proxy_lock()met en file unrt_mutex_waiterau nom d’une autre tâche endormie, puis l’annule en cas d’erreurcurrentest alors le requeuer qui a appeléFUTEX_CMP_REQUEUE_PI- le véritable waiter est une tâche distincte endormie dans
FUTEX_WAIT_REQUEUE_PI
- Lorsque
__rt_mutex_start_proxy_lock()renvoie-EDEADLK,remove_waiter()retire le waiter du verrou, mais met uniquementcurrent->pi_blocked_onàNULL - Le
pi_blocked_ondu véritable waiter continue de pointer vers lert_mutex_waitersitué sur sa propre pile noyau ; lorsque le waiter revient en espace utilisateur, cette frame de pile est considérée comme libérée - Ensuite, au moment où le parcours de la chaîne PI passe par cette tâche, il déréférence un objet de pile libéré
- lockdep vérifie seulement quel
pi_lockest détenu, sans vérifier à qui appartient ce lock, et ne détecte donc pas cette erreur
La boucle à trois futex qui provoque le rollback -EDEADLK
- Pour atteindre le chemin d’erreur, on construit une boucle de dépendances PI avec trois futex et trois threads
f_pi_chain: futex PI que le waiter verrouille en premierf_pi_target: futex PI que l’owner verrouille en premier et qui devient la cible du requeuef_wait: futex ordinaire sur lequel le waiter attend avecFUTEX_WAIT_REQUEUE_PI
- L’ordre de déclenchement est le suivant
- le waiter verrouille
f_pi_chain, puis se bloque dansFUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target), et lert_mutex_waiterest placé sur sa pile noyau - l’owner verrouille
f_pi_target, puis se bloque surf_pi_chain, détenu par le waiter - le thread main appelle
FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target)
- le waiter verrouille
- Lorsque le requeue proxy tente de relier le waiter à
f_pi_target, la bouclewaiter → f_pi_target → owner → f_pi_chain → waiterse referme - Le parcours de la chaîne PI renvoie
-EDEADLKet exécute le mauvais rollback, réveillant le waiter avec unpi_blocked_onpendant - La condition importante est que le requeuer effectue le rollback pendant que le waiter détient encore l’objet de pile ; une fois la boucle formée, le processus se poursuit de lui-même
- Une fois le waiter revenu en espace utilisateur, il n’y a plus de contrainte temporelle, et un parcours de chaîne peut être déclenché plus tard à tout moment avec
sched_setattr() - La configuration utilise trois threads, mais la course UAF elle-même peut être déclenchée même avec un seul cœur CPU
Primitive initiale fournie par l’UAF de pile
- Le pointeur pendant pointe vers le
rt_mutex_waiterqui se trouvait dans l’ancienne frameFUTEX_WAIT_REQUEUE_PI - En replaçant des octets contrôlables à la même profondeur de pile dans la même tâche, on peut faire en sorte que le noyau les déréférence comme un faux
rt_mutex_waiter - Selon la manière dont la fausse structure est placée, un seul accès permet d’obtenir deux primitives principales
- écrire un pointeur à une adresse presque arbitraire, sous contraintes
- écrire 8 octets nuls à une adresse presque arbitraire, sous contraintes
- Plusieurs déréférencements de pointeurs et contrôles d’intégrité sont effectués avant l’écriture, mais si les conditions sont remplies, le noyau ne plante pas après l’écriture et revient normalement
- Pour finaliser l’exploit, il faut à la fois réutiliser la frame de pile, passer les contrôles de structure du faux waiter et choisir une cible satisfaisant les contraintes d’écriture
Réutilisation d’un frame de pile libéré avec PR_SET_MM_MAP
- Le waiter appelle
prctl(PR_SET_MM, PR_SET_MM_MAP, ...)dès son retour de l’appel système futex prctl_set_mm_map()copie l’auxv fourni par l’utilisateur dans un tampon de pile de taille fixeunsigned long user_auxv[AT_VECTOR_SIZE]- Comme ce tampon se trouve à une profondeur de pile similaire à celle du waiter libéré, un gros bloc de qwords contrôlables et alignés se superpose à l’ancien
rt_mutex_waiter - La zone de chevauchement de l’auxv est configurée comme suit
tree: en fait un nœud rb qui, lors de la suppression, promeut le pointeur enfant choisiW0_BASEen racine de l’arbretask: défini sur&init_taskpour traverser sans risque les déréférencements de la recherche de chaînelock: fixé à&inet6_protos[IPPROTO_UDP] - 8afin d’aligner la cible d’écriturewake_state: défini à0
- L’auxv est placé dans un memfd et positionné de sorte que la copie franchisse une limite de page ; puis un thread frère crée une course avec
fallocate(PUNCH_HOLE)sur la page arrière pendant l’exécution deprctl, afin d’allonger la durée decopy_from_user - Un thread consumer sur un autre CPU appelle
sched_setattr()sur le waiter tant que le faux waiter reste sur la pile, ce qui déclenche la recherche de la chaîne PI - D’autres appels système utilisant de grandes variables locales contrôlables sur la pile, comme
clone,setsockopt,pselectoukeyctl, peuvent remplir le même rôle prctla été choisi parce que son tampon est grand, aligné et ne nécessite pas de namespace ; d’autres candidats sont inclus dans le code PoC public
Créer une écriture de pointeur contrainte via la suppression dans un rb-tree
- Même en contrôlant le faux waiter, on n’obtient pas immédiatement une écriture arbitraire complète ; la recherche de chaîne exécute le chemin suivant
- trouve le faux waiter via
task->pi_blocked_on - trouve le faux
rt_mutex_baseviafake waiter->lock rt_mutex_dequeue(lock, waiter)effectue une suppression rb-tree danslock->waiters
- trouve le faux waiter via
- On exploite la propriété selon laquelle, lorsqu’on supprime un nœud racine n’ayant qu’un seul enfant, cet enfant est écrit dans le slot racine
- Si
lockest fixé àtarget - 8, les données voisines sont interprétées comme les champs suivants dert_mutex_basetarget - 8:wait_lock, qui doit être lu comme non verrouillétarget:waiters.rb_root.rb_nodeà écrasertarget + 8:waiters.rb_leftmosttarget + 16:owner
- L’unique écriture exécutée au final est
*(uint64_t *)target = W0_BASE - L’adresse cible doit globalement satisfaire les conditions suivantes
- les 32 bits de poids faible de
target - 0x08doivent valoir0 - la valeur 64 bits de
target + 0x08doit valoir0 - la valeur du pointeur owner à
target + 0x10, hors flags de poids faible, doit valoir0
- les 32 bits de poids faible de
- Si le qword précédent ressemble à un spinlock verrouillé, le trylock échoue et l’exécution se termine sans rien écrire
- Si les valeurs suivantes pointent vers un top waiter ou un owner non contrôlé, ou vers une valeur non mappée, un kernel panic peut survenir
- Comme
W0_BASEdoit rester valide jusqu’à la fin des comparaisons, du requeueing, des mises à jour de priorité et du wakeup sans owner, on utilise l’alias direct-map du CEA
Fuite par prefetch et CPU entry area
-
Trouver les adresses de base KASLR et physmap
- Le temps d’exécution de
prefetchpour une adresse donnée varie selon que cette adresse est mappée ou non dans la table de pages courante - Un processus non privilégié peut estimer les emplacements mappés en mesurant les temps d’exécution dans la plage d’adresses du noyau ; le principe détaillé est décrit dans l’article sur prefetch
- Comme l’entropie de l’adresse de base de l’image du noyau Linux par défaut est d’environ 9 bits, des mesures répétées permettent de récupérer l’adresse de base KASLR avec une fiabilité proche de 100 %
- En théorie, les CPU disposant de
prefetchet dépourvus d’un KPTI approprié sont affectés, mais en pratique la technique est surtout utilisée sur x86 avec KPTI désactivé - L’image kernelCTF a KPTI désactivé ; même avec KPTI activé, combiner
prefetchavec EntryBleed permet de récupérer l’adresse de base de l’image noyau via le trampoline
- Le temps d’exécution de
-
Contournement de la randomisation des adresses CEA
- La CPU entry area (CEA) est une structure propre à chaque CPU x86 qui conserve les piles d’entrée et de gestion d’exceptions ainsi que le contexte des registres
- Lorsqu’un programme non privilégié déclenche une exception logicielle, son propre contexte de registres est écrit dans
pt_regssur la pile d’exception CEA, créant environ 120 octets de mémoire contiguë contrôlable - Avant Linux 6.2, l’adresse virtuelle du CEA était entièrement fixe, ce qui permettait de l’utiliser directement pour de fausses structures, absorber les effets de bord des déréférencements de pointeurs et construire une pile ROP
- Après la publication par Project Zero de Bringing back the stack attack, l’adresse virtuelle du CEA est fortement randomisée depuis Linux 6.2
- L’adresse virtuelle du CEA de chaque CPU est randomisée différemment, mais son adresse physique reste fixe ; connaître l’adresse de base du physmap permet donc de calculer l’alias direct-map
- En combinant
prefetch, la normalisation des limites candidates et la vérification des pages CEA attendues, on écarte les alias voisins et on obtientcea_direct = physmap_base + CPU1_CEA_BASE - Dans l’environnement de démarrage 3,5 Go de kernelCTF LTS
6.12.80, l’offset associé est0x11c517000(+0x1f58)
Réutiliser le CEA comme faux waiter et comme objets ultérieurs
- Avant la première écriture, un faux waiter et un lock auto-cohérents sont placés dans
W0du CEAtaskest défini sur&init_taskprioreçoit une valeur valide- le
wait_lockdu lock est rendu visible comme non verrouillé - l’owner est configuré pour traverser sans risque les étapes de dequeue, requeueing, mise à jour de priorité et wakeup
- Une fois l’écriture rb-tree terminée,
W0n’a plus besoin d’être un waiter ; le CEA peut alors être rempli de nouveau avec la structure requise par la cible écrasée - Le CEA est petit, environ 120 octets, mais il est efficace car il permet de placer des données à une adresse noyau fixe calculable
- NPerm et kernelsnitch peuvent remplir le même rôle dans un espace plus large
- L’exploit utilise une même zone CEA, séquentiellement ou simultanément, comme faux
rt_mutex_waiter, faux lock,inet6_protocol, slots de JOP et de pivot de pile, puis pile ROP finale
Prise de contrôle du flux d’exécution via inet6_protos[IPPROTO_UDP]
- Sur un Linux x86_64 courant, une fois l’adresse de base KASLR obtenue, on peut choisir un chemin court qui écrase une table de fonctions appropriée ou un objet qui la contient
- La zone autour de
inet6_protos[IPPROTO_UDP]dans la section de données inscriptible satisfait naturellement les contraintes nécessairesinet6_protos[16] == NULLdevient l’état non verrouillé du fauxwait_lockinet6_protos[17] == &udpv6_protocolest la véritable cible à écraserinet6_protos[18] == NULLdevient le fauxrb_leftmostinet6_protos[19] == NULLdevient le faux owner
- Une fois l’écriture terminée,
inet6_protos[IPPROTO_UDP]pointe vers un fauxinet6_protocoldans une page CEA - On projette à nouveau la CEA et on construit la structure comme suit
handler: défini sur le premier gadget de pivoterr_handler: non utiliséflags: définis àINET6_PROTO_NOPOLICY | INET6_PROTO_FINAL
- En envoyant un paquet IPv6 UDP en loopback qui écrit des données après un
connectvers::1, le noyau appelle le fauxhandler, ce qui permet de contrôler le compteur de programme
Pivot court et élévation de privilèges DirtyMode
- Sur la cible
lts-6.12.80de Google kernelCTF, aucun gadget de pivot de pile unique adapté n’a été trouvé ; une instruction load/call supplémentaire place donc l’adresse CEA dansrbp, puis le pivot se fait avecmov rsp, rbp; pop rbp; ret ret2usrou l’écrasement complet de/proc/%P/fd/xnécessite environ 10 qwords de gadgets, ce qui est trop volumineux pour l’espace CEA limité- L’étape finale utilise DirtyMode, qui modifie les bits de permissions en une seule écriture puis effectue le reste depuis l’espace utilisateur
- La cible d’écriture est
coredump_sysctls[1].modedans les données du noyau, c’est-à-dire le mode d’accès du sysctlcore_pattern - Comme il partage le même décalage KASLR que l’image du noyau, son adresse peut être calculée ; il suffit d’une valeur avec le bit d’écriture, le deuxième bit de poids faible, positionné
- Une courte chaîne
pop reg; mov [reg], reg; retmodifie la valeur du mode, puismsleepmet en pause en toute sécurité le thread détourné - Une fois
/proc/sys/kernel/core_patterninscriptible par tous les utilisateurs, un processus non privilégié écrit|/proc/%P/fd/666 %Pet fait crasher le helper, ce qui amène le noyau à exécuter le binaire de l’attaquant avec les privilèges root - En raison de contraintes d’alignement par lots, l’écriture initiale via rb-tree ne peut pas atteindre directement
coredump_sysctls[1].mode; la modification du mode est donc effectuée dans la courte étape ROP
Déroulé complet de l’exploit et résultat
- L’attaque se déroule dans l’ordre suivant
- fuite du slide de l’image noyau et de l’adresse de base physmap avec
prefetch - utilisation de GhostLock pour laisser un
rt_mutex_waiterpendant danspi_blocked_ondu waiter - réutilisation de la même frame de pile noyau avec
PR_SET_MM_MAPpour créer un faux waiter - écriture d’un pointeur CEA dans
inet6_protos[IPPROTO_UDP]via la suppression dans le rb-tree de rtmutex - placement dans la CEA du faux
inet6_protocol, des emplacements de pivot et de la pile ROP - appel du handler écrasé au moyen d’un paquet IPv6 UDP en loopback
- modification du bit de mode de
core_patternavec DirtyMode et finalisation de l’élévation de privilèges depuis l’espace utilisateur
- fuite du slide de l’image noyau et de l’adresse de base physmap avec
- Dans l’environnement distant de kernelCTF, le chemin combinant CEA et DirtyMode obtient le flag en environ 5 secondes
- L’exploit complet est publié dans le projet CyberMeowfia
- Sur Android, la réutilisation des frames de pile et les méthodes de contournement d’ASLR et de CFI diffèrent ; elles feront l’objet d’un article de suivi séparé
Chemins alternatifs et mesures d’atténuation
-
Espace ROP plus grand
- La mémoire basée sur NPerm peut servir de grande fausse pile après la prise de contrôle du flux d’exécution
- Des chemins plus lourds sont aussi possibles, comme la fuite heap-KASLR de Lukas Maar, mais ils ajoutent des étapes et allongent le temps d’exécution
- Dans kernelCTF, la chaîne la plus courte et la plus fiable est préférable ; la combinaison CEA et DirtyMode a donc été utilisée
-
Correctif du noyau
- Le correctif final prend le
pi_locket effacepi_blocked_onen se basant surwaiter->taskplutôt que surcurrent remove_waiter()enregistrewaiter_task = waiter->task, puis procède dans l’ordre suivant- verrouille
waiter_task->pi_lock - retire le waiter de la file rtmutex
- définit
waiter_task->pi_blocked_on = NULL - transmet aussi
waiter_task, au lieu decurrent, aurt_mutex_adjust_prio_chain()suivant
- verrouille
- Le correctif séparé envoyé par les chercheurs avant la v1 faisait transmettre explicitement par l’appelant la tâche propriétaire
- dans le chemin où la tâche se bloque elle-même, il transmet
current - lors d’un rollback proxy, il transmet la
taskcible du proxy - il n’efface
pi_blocked_onque si celui-ci pointe encore vers le waiter concerné, et le protège avec lepi_lockde la tâche
- dans le chemin où la tâche se bloque elle-même, il transmet
- Le correctif final prend le
-
RANDOMIZE_KSTACK_OFFSET- L’exploit repose sur un chevauchement déterministe entre la frame du waiter libérée et la frame
user_auxvsuivante - Activer
RANDOMIZE_KSTACK_OFFSETfait varier l’offset de pile, transformant cette étape en une devinette de 5 bits avec environ 1 chance sur 32 - Sur les deux cibles générales soumises, ce réglage était désactivé par défaut ; sur la cible avec atténuations, il était activé, si bien que ce chemin d’exploitation n’a pas été utilisé
- L’exploit repose sur un chevauchement déterministe entre la frame du waiter libérée et la frame
-
STATIC_USERMODE_HELPERSTATIC_USERMODE_HELPERbloque ce chemin DirtyMode précis- Toutefois, la même méthode peut être généralisée à d’autres réglages
/proc/sysdont les permissions d’accès sont contrôlées parctl_table::modeet dont la table se trouve dans des données noyau inscriptibles et prévisibles
Calendrier de divulgation
- 18 avril 2026 : la vulnérabilité et un correctif préliminaire sont transmis à
security@kernel.org - 20 avril 2026 : la vulnérabilité est corrigée par un autre patch
- 4 mai 2026 : le correctif v1 est rétroporté
- 30 juin 2026 : Google confirme la soumission à kernelCTF
- 7 juillet 2026 : l’analyse technique est publiée
- La vulnérabilité découverte par VEGA relève de la politique de divulgation standard 90+30 jours
1 commentaires
Commentaires sur Hacker News
J’ai testé sur 3 appareils utilisant Android 9, 13 et 16, avec différentes versions de Firefox antérieures à 150 : 2 sont partis en boucle de démarrage et ont dû passer en mode de récupération, et le 3e s’est éteint. La démo change le fond d’écran sur les appareils Pixel pris en charge, et la page de test est disponible sur IonStack
Quand on consulte des blogs ou des sites arbitraires sur un appareil personnel, il est plus sûr d’installer en plus du navigateur principal un navigateur basé sur Chromium comme Chromite, de désactiver dans les flags JavaScript et le décodeur vidéo à accélération matérielle souvent visé, puis d’utiliser le mode lecture sur les sites cassés. Sinon, on peut aussi garder une tablette dédiée
adbEn accédant à cette page de test, une sortie est apparue dans l’onglet Firefox, ce qui donnait l’impression que le code de preuve de concept s’était exécuté, mais ensuite le téléphone s’est figé et a refusé toute entrée. Seul le redémarrage fonctionnait. Je me demande comment l’événement de redémarrage peut encore être pris en compte dans une situation où le noyau semble gelé. L’écran est resté allumé en affichant une partie du résultat de l’exécution, puis l’économiseur d’écran s’est activé
Immense respect aux chercheurs en sécurité qui n’ont pas seulement trouvé l’exploit, mais qui, contrairement à copyfail, n’ont pas publié un script zero-day d’élévation locale de privilèges immédiatement exploitable par n’importe qui
J’ai essayé pendant plusieurs heures d’obtenir une élévation locale de privilèges (LPE) sur Rocky Linux 9, heureusement sans succès. À moins d’avoir beaucoup de temps libre ou un très haut niveau, cela semble difficile à utiliser dans une attaque réelle sur une distribution d’entreprise
Je me demande s’il serait possible d’utiliser cette faille pour déverrouiller le bootloader même sur des téléphones où il ne peut généralement pas l’être. Si oui, ce pourrait être l’un des plus grands événements de l’écosystème Android
Le titre aurait dû inclure LPE, au sens d’élévation locale de privilèges, afin que la plupart des gens puissent se rassurer et retourner tranquillement à leur week-end
Mais cette attaque-ci peut aussi être déclenchée depuis un processus fortement sandboxé, comme le processus navigateur isolé de Firefox. Il suffit à l’attaquant d’enchaîner une faille JavaScript pour exécuter du code local dans la sandbox isolée, puis cette faille-ci pour monter jusqu’au mode noyau : il faut donc mettre à jour à la fois Firefox et le noyau Linux
La phrase « Google a versé 92 337 dollars de récompense kernelCTF » a immédiatement attiré mon attention
Est-ce que cela signifie qu’une appli Android peut exécuter du code natif via le NDK pour obtenir les privilèges root, et que je me demande si SELinux aide à se défendre
On peut certes backporter des correctifs sur d’anciens noyaux, mais les notes de mise à jour des smartphones mentionnent rarement les CVE, si bien qu’un outil de vérification des vulnérabilités reste pratiquement le seul moyen de confirmer. Si une appli obtenue sur le Play Store ou ailleurs a été compromise, elle peut obtenir immédiatement les privilèges root ; les principes de confiance et d’audit au moment de l’installation restent donc importants
À l’avenir, cette vérification pourrait être ajoutée à tous les niveaux de Google Play Integrity, empêchant l’installation de nombreuses applis sur les téléphones non corrigés. C’est encore plus grave dans un navigateur, où une évasion de sandbox contourne aussi l’isolation des applis ; cela rappelle JailbreakMe sur iOS
Sur tout Linux des 15 dernières années, n’importe quelle appli capable d’exécuter du code natif peut obtenir les privilèges root sur l’appareil tant qu’une mise à jour du noyau n’est pas arrivée
Il est sidérant de voir que GhostLock a été introduit dans Linux 2.6.39 et n’a été corrigé qu’avec Linux 7.1
J’ai l’impression d’avoir déjà lu ces commentaires hier, mais leur heure de publication est affichée comme étant inférieure à 10 heures ; je me demande si l’affichage du temps sur HN est erroné
En consultant chaque jour la liste « underwater », c’est-à-dire les billets ayant reçu beaucoup de votes mais qui, pour une raison ou une autre, n’ont pas atteint la page d’accueil, celui-ci était tout en haut, donc il a été remis en avant. Cela paraît étrange, mais on n’a pas encore trouvé d’alternative moins déroutante