
Une option ND IPv6 avec une longueur nulle. Un contrôle manquant. Le démon recule et vit dans la boucle. Rapporté à OpenBSD, corrigé, CVE attribuée.
Un paquet ICMPv6 forgé provenant du réseau local gèle définitivement slaacd et/ou rad. L'autoconfiguration d'adresses IPv6 SLAAC meurt jusqu'à ce que quelqu'un redémarre manuellement le démon. Pas d'authentification, pas de privilèges, 18 octets de charge utile ICMPv6.
| CVE | CVE-2026-41285 |
| Classe de bug | Boucle infinie via sous-dépassement d'entier |
| Cause racine | L'analyseur d'options ND fait nd_opt_len * 8 - 2 sans vérifier len==0 |
| Composant | sbin/slaacd/engine.c, usr.sbin/rad/engine.c |
| Impact | DoS permanent d'IPv6 SLAAC (slaacd) ou du service RA (rad) |
| Requis | Tout appareil sur le même segment réseau L2 |
| Testé | OpenBSD 7.8 GENERIC amd64 |
La RFC 4861 §4.6 indique que les options ND de longueur zéro sont invalides et doivent être silencieusement rejetées. La fonction nd6_options() du noyau, dans sys/netinet6/nd6.c, fait cela correctement. Mais le paquet ICMPv6 brut est tout de même délivré aux sockets de l'espace utilisateur avant que cette vérification ne s'applique.
slaacd et rad analysent tous deux les options ND eux-mêmes. La boucle ressemble à ceci :
while (len > 0) {
// ...
optlen = nd_opt->nd_opt_len * 8 - 2; // nd_opt_len is uint8_t
if (optlen > len)
break;
len += 2;
// advance pointer by optlen... which is (uint32_t)-2 promoted from int
}
Quand nd_opt_len == 0 : l'expression 0 * 8 - 2 est promue en int → -2. La garde (-2 > len) est toujours fausse (comparaison signée, len est positive). Ensuite len += 2 et le pointeur recule de 2 octets. La boucle n'avance jamais. Le CPU sature à 100 %. Pour toujours.
Le noyau a validé sa propre copie. Le démon de l'espace utilisateur a reçu l'original brut. Personne n'a prévenu slaacd.
rcctl restart slaacd manuel ou un redémarragepython3 poc/kill_slaacd.py <interface>
Nécessite scapy. Envoie une annonce de routeur (Router Advertisement) avec une seule option ND où nd_opt_len = 0. C'est tout. Le type d'option n'a pas d'importance (le PoC utilise le type 200 / inconnu).
Pour la preuve de bout en bout complète (montre SLAAC fonctionnel -> exploitation -> SLAAC mort) :
python3 poc/prove_dos.py
── Before exploit ──
SLAAC addresses: 2001:db8:1:0:df6f:edeb:6e3a:2640, ...
Engine CPU: 0.0%
── After one packet ──
Engine CPU: 23.1% → 43.3% (climbing)
New RA with 2001:db8:2::/64 sent → no address configured
slaacd is dead. IPv6 autoconf: DEAD.
Même surface d'attaque que CVE-2022-27881 et CVE-2022-27882 (boucles infinies antérieures de slaacd dans engine.c, également liées à l'analyse des options ND). Il s'agit d'une nouvelle instance de la même classe de bug — les correctifs précédents ne couvraient pas toutes les boucles d'analyse.
Vérifiez nd_opt_len == 0 avant d'effectuer des calculs dessus. Sortez de la boucle. C'est ce que le noyau fait déjà dans nd6_options() :
if (nd_opt->nd_opt_len == 0)
break; // or: goto bad;
Correctif suggéré pour parse_ra() et debug_log_ra() de slaacd, et pour l'analyseur RS de rad — les trois boucles nécessitent la même garde d'une ligne.
Il s'agit d'un DoS de réseau local. Si vous êtes sur le même segment L2 qu'une machine OpenBSD exécutant slaacd, un paquet gèle son IPv6. Ne l'envoyez pas sur des réseaux qui ne vous appartiennent pas. Si vous utilisez OpenBSD, cherchez un correctif ou ajoutez vous-même la garde len==0.
Daniel Wade - GitHub · Twitter/X · Bluesky · nadsec.online