Le 20 octobre 2026, un rapport de la Zero Day Initiative destiné à l’équipe de sécurité du noyau Linux a été envoyé par erreur sur des listes de diffusion publiques. Il décrivait une élévation de privilèges locale, enregistrée sous le numéro ZDI-CAN-33367, et un programme de démonstration a circulé dans la foulée. Dix jours plus tard, la faille n’a toujours pas de numéro CVE, le correctif proposé n’est pas encore intégré au noyau officiel, et le noyau de ma distribution publié le 28 octobre ne le contient pas. Elle permet à n’importe quel utilisateur local de modifier des fichiers qu’il ne peut que lire (un programme setuid, /etc/passwd) et donc de devenir root.

Ce billet explique le mécanisme, sans détail d’exploitation, parce qu’il illustre une famille de failles qui revient régulièrement : celles où une optimisation qui évite de copier des données finit par écrire là où elle ne devrait pas. Il décrit ensuite la mesure que j’ai appliquée en attendant un noyau corrigé, et pourquoi j’ai écarté celle que la plupart des guides recommandent.

Une famille de failles

FailleAnnéeLe principe commun
Dirty COW (CVE-2016-5195)2016une course dans la copie sur écriture permet d’écrire dans une page projetée en lecture seule
Dirty Pipe (CVE-2022-0847)2022un drapeau mal réinitialisé dans un tube permet d’écrire dans les pages du cache d’un fichier
Dirty Frag (CVE-2026-43284)2026le déchiffrement ESP « sur place » écrit dans des fragments réseau qui pointent vers le cache de fichiers
ZDI-CAN-33367octobre 2026le correctif de Dirty Frag repose sur un marqueur que skb_zerocopy() ne recopie pas

Toutes ces failles exploitent le même point faible : le cache de pages. Quand un programme lit un fichier, le noyau en garde une copie en mémoire, et toutes les lectures suivantes, par n’importe quel utilisateur, sont servies depuis cette copie. Les droits d’accès sont vérifiés à l’ouverture du fichier ; ensuite, le noyau manipule les pages en mémoire. Si un chemin du noyau écrit dans une de ces pages par erreur, le contenu du fichier change pour tout le système, sans que les droits aient jamais été consultés, et sans que le fichier sur disque soit modifié : un contrôle d’intégrité qui relit le disque après avoir vidé le cache ne voit rien.

Le mécanisme, étape par étape

1. Éviter les copies

Copier des données coûte cher. Pour envoyer un fichier sur le réseau, le noyau sait éviter de le copier : avec splice() ou sendfile(), le paquet réseau ne contient pas les octets du fichier mais une référence vers les pages du cache qui les contiennent. Dans le noyau, un paquet est représenté par une structure appelée skb (socket buffer), dont les données peuvent être réparties en fragments, chacun pointant vers une page mémoire. Un fragment peut donc pointer vers une page du cache d’un fichier que l’utilisateur a seulement le droit de lire. Tant que personne n’écrit dans ce fragment, c’est sans danger.

2. Déchiffrer sur place

IPsec protège des paquets IP en les chiffrant avec le protocole ESP. À la réception, le module esp4 (ou esp6) déchiffre le paquet. Pour aller vite, il le déchiffre sur place : le texte clair remplace le texte chiffré, dans la même mémoire. Si cette mémoire est un fragment qui pointe vers une page du cache d’un fichier, le « déchiffrement » écrit dans le fichier. L’attaquant contrôle la clé et les données chiffrées, puisqu’il configure lui-même sa liaison IPsec : il choisit donc ce que le déchiffrement produit.

3. Le correctif de Dirty Frag, et son marqueur

C’est exactement Dirty Frag, corrigée plus tôt en 2026. Le correctif a ajouté un marqueur, SKBFL_SHARED_FRAG, posé sur un paquet dont les fragments pointent vers une mémoire partagée avec quelqu’un d’autre. Avant de déchiffrer, ESP vérifie ce marqueur : s’il est présent, il copie d’abord les données dans une mémoire à lui (copie sur écriture), puis déchiffre la copie.

4. Le marqueur perdu

fichier en lecture seule ──lecture──▶ page du cache (mémoire)
        │ splice() / envoi sans copie : le paquet RÉFÉRENCE la page, il ne la copie pas
        ▼
paquet réseau (skb) ── fragment ──▶ même page          marqueur SHARED_FRAG posé ✔
        │ NFQUEUE ou Open vSwitch : skb_zerocopy() transfère le fragment vers un nouveau skb
        ▼
nouveau skb ── fragment ──▶ même page                  marqueur PERDU ✘
        │ réception ESP (IPsec) : « pas partagé, je déchiffre sur place »
        ▼
le « déchiffrement » écrit dans la page du cache ──▶ le fichier lu par tous est modifié

La nouvelle faille tient dans une fonction, skb_zerocopy(), qui existe depuis le noyau 3.14. Elle sert à transférer le contenu d’un paquet vers un autre sans le copier, en déplaçant les références vers les fragments. Elle est utilisée notamment par NFQUEUE, le mécanisme qui envoie des paquets du pare-feu vers un programme en espace utilisateur, et par Open vSwitch. Elle transfère les fragments, mais pas le marqueur : le nouveau paquet pointe vers les mêmes pages du cache, sans indiquer qu’elles sont partagées. ESP, plus loin, ne voit pas le marqueur, déchiffre sur place, et l’écriture atteint le cache du fichier. Le correctif proposé tient en deux lignes : recopier le marqueur.

Ce schéma est fréquent. Un correctif ajoute une information de sécurité à une structure ; tous les chemins qui créent ou recopient cette structure doivent la propager ; un seul oubli, dans une fonction écrite dix ans plus tôt, et le correctif ne protège plus rien sur ce chemin.

Pourquoi un utilisateur ordinaire peut tout faire

sudo -u nobody unshare -Urn true && echo "espace de noms utilisateur : autorisé"
espace de noms utilisateur : autorisé
sysctl user.max_user_namespaces
user.max_user_namespaces = 125388

Configurer IPsec, une file NFQUEUE ou des règles de contrôle de trafic demande normalement des droits d’administration réseau (la capacité CAP_NET_ADMIN). Mais les espaces de noms utilisateur, activés par défaut sur la plupart des distributions, permettent à un utilisateur ordinaire de créer un espace où il est root, avec son propre espace de noms réseau dans lequel il a cette capacité. Il ne peut agir que sur ses interfaces à lui, mais c’est suffisant : la chaîne décrite ci-dessus se déroule entièrement dans son espace réseau, et c’est le noyau, commun à tous, qui écrit dans le cache de pages, commun à tous lui aussi.

La mesure recommandée, et pourquoi je ne l’ai pas prise

La mesure la plus citée consiste à interdire les espaces de noms utilisateur aux utilisateurs ordinaires (user.max_user_namespaces=0). Elle coupe la chaîne à la racine, et pas seulement pour cette faille. Je l’ai écartée parce qu’elle casse des choses que j’utilise : les services systemd durcis avec PrivateUsers=, et les conteneurs Podman sans privilège sur une de mes machines, qui reposent précisément sur ce mécanisme.

Couper la chaîne au bon maillon

# /etc/modprobe.d/zdi-can-33367.conf (déployé par Ansible)
install esp4 /bin/false
blacklist esp4
install esp6 /bin/false
blacklist esp6
install nlmon /bin/false
blacklist nlmon
install act_pedit /bin/false
blacklist act_pedit

L’autre approche consiste à empêcher le chargement des modules du noyau dont la chaîne a besoin. Un module bloqué de cette façon ne peut plus être chargé à la demande, même par root dans un espace de noms : la directive install … /bin/false remplace le chargement par une commande qui échoue, et blacklist empêche le chargement automatique par alias. Le choix des modules demande de vérifier ce qui est réellement utilisé.

  • esp4 et esp6 : c’est là que se produit l’écriture. Sans ESP, pas de déchiffrement sur place, donc pas d’écriture dans le cache. Aucun de mes serveurs n’utilise IPsec : mes tunnels sont en WireGuard, qui n’a rien à voir avec ESP ;
  • nlmon et act_pedit : deux modules utilisés par les démonstrations publiées pour mettre en place la chaîne, inutiles sur mes serveurs ;
  • pas nfnetlink_queue : NFQUEUE est l’un des deux chemins qui perdent le marqueur, mais Suricata l’utilise en mode prévention d’intrusion sur trois de mes serveurs. Le bloquer couperait le filtrage. Couper la chaîne à l’étape ESP suffit.
modprobe -n -v esp4          # ce que ferait un chargement
install /bin/false           # → bloqué
lsmod | grep -E '^(esp4|esp6|nlmon|act_pedit|nfnetlink_queue) '
nfnetlink_queue        36864  1      # chargé : utilisé par Suricata en mode IPS

La vérification se fait en deux temps : modprobe -n -v montre ce que ferait un chargement (ici, exécuter /bin/false, donc échouer), et lsmod confirme qu’aucun des modules bloqués n’est déjà chargé, car un module déjà en mémoire n’est pas affecté par le fichier de configuration. Si l’un l’avait été, il aurait fallu le décharger, ou redémarrer.

Suivre l’arrivée du vrai correctif

dnf -q repoquery --changelogs --latest-limit 1 kernel-core | grep -ciE 'skb_zerocopy|SHARED_FRAG'
0      # le noyau 6.12.0-211.69.1 disponible le 30 octobre ne contient pas le correctif

Cette mesure est une attente. Pour savoir quand la retirer, il faut surveiller les noyaux de la distribution. En l’absence de numéro CVE, les outils qui comparent des listes de CVE ne servent à rien : c’est le journal des modifications du paquet noyau qu’il faut lire, en y cherchant le nom de la fonction ou du marqueur. Le noyau publié le 28 octobre corrige seize autres CVE mais pas celle-ci. Un dérivé de Red Hat a publié un noyau corrigé dans son dépôt de test ; un autre propose un correctif appliqué à chaud. Quand un noyau corrigé arrivera, je l’installerai, je redémarrerai, puis je viderai la liste des modules bloqués.

Et si l’on ne peut rien bloquer ?

Sur une machine qui utilise IPsec, la mesure ne s’applique pas. Il reste à interdire les espaces de noms utilisateur à ceux qui n’en ont pas besoin, à limiter les comptes locaux (la faille demande un accès local : un compte, ou une application web compromise), et à surveiller les signes d’exploitation : création d’espaces de noms par des comptes de service, chargement des modules concernés, modification d’un binaire setuid détectée par la comparaison de son empreinte en mémoire avec celle du paquet (rpm -V).

Ce que j’en retiens

  • le cache de pages est partagé par tous : toute écriture accidentelle dans une de ses pages contourne les droits d’accès ;
  • les optimisations « sans copie » sont la source récurrente de ces failles, depuis Dirty COW ;
  • un correctif qui repose sur un marqueur n’est complet que si tous les chemins le propagent ;
  • les espaces de noms utilisateur transforment beaucoup de failles « réservées à root » en failles accessibles à tous ;
  • bloquer un module demande d’abord de vérifier qu’on ne s’en sert pas ;
  • sans CVE, c’est le journal des modifications qui dit si un noyau est corrigé.

Sources : analyse publiée sur GitHub (FranklinF25/zdi-can-33367) ; billets de TuxCare, CloudLinux, Sysdig et AlmaLinux, octobre 2026.


Ce billet fait partie d’une série de tutoriels d’administration système (tag « pas à pas »).
Série « Pas à pas », tutoriel 4/57. · ← Tutoriel précédent · Tutoriel suivant → · English version