
Одна опция IPv6 ND с нулевой длиной. Одна пропущенная проверка. Демон движется назад и живёт в цикле. Сообщено в OpenBSD, исправлено, присвоен CVE.
Один специально сформированный ICMPv6-пакет из локальной сети навсегда зависает slaacd и/или rad. Автоконфигурация SLAAC-адресов IPv6 умирает, пока кто-то вручную не перезапустит демон. Без аутентификации, без привилегий — 18 байт ICMPv6-нагрузки.
| CVE | CVE-2026-41285 |
| Bug class | Бесконечный цикл из-за целочисленного андерфлоу |
| Root cause | Парсер ND-опций выполняет nd_opt_len * 8 - 2 без проверки len==0 |
| Component | sbin/slaacd/engine.c, usr.sbin/rad/engine.c |
| Impact | Постоянный DoS IPv6 SLAAC (slaacd) или RA-сервиса (rad) |
| Required | Любое устройство в том же сегменте сети L2 |
| Tested | OpenBSD 7.8 GENERIC amd64 |
RFC 4861 §4.6 гласит, что ND-опции с нулевой длиной недействительны и должны молча отбрасываться. Собственная функция ядра nd6_options() в sys/netinet6/nd6.c делает это правильно. Но сырой ICMPv6-пакет всё равно доставляется в сокеты пользовательского пространства до того, как эта проверка возымеет эффект.
slaacd и rad сами разбирают ND-опции. Цикл выглядит так:
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
}
Когда nd_opt_len == 0: выражение 0 * 8 - 2 повышается до int → -2. Проверка (-2 > len) всегда ложна (знаковое сравнение, len положителен). Затем len += 2, и указатель возвращается на 2 байта назад. Цикл никогда не продвигается. CPU загружается на 100%. Навсегда.
Ядро проверяло свою собственную копию. Демон пользовательского пространства получил сырой оригинал. Никто не сказал об этом slaacd.
rcctl restart slaacd или перезагрузкиpython3 poc/kill_slaacd.py <interface>
Требуется scapy. Отправляет одно Router Advertisement с единственной ND-опцией, где nd_opt_len = 0. Вот и всё. Тип опции не имеет значения (PoC использует тип 200 / неизвестный).
Для полного сквозного доказательства (показывает работающий SLAAC -> эксплойт -> мёртвый SLAAC):
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.
Та же поверхность атаки, что и CVE-2022-27881 и CVE-2022-27882 (более ранние бесконечные циклы slaacd в engine.c, тоже разбор ND-опций). Это новый экземпляр того же класса ошибок — предыдущие исправления не покрывали все циклы разбора.
Проверяйте nd_opt_len == 0 перед выполнением арифметических операций с ним. Выходите из цикла. Именно это ядро уже делает в nd6_options():
if (nd_opt->nd_opt_len == 0)
break; // or: goto bad;
Предлагаемый патч для parse_ra() и debug_log_ra() в slaacd, а также для RS-парсера rad — всем трём циклам нужна одна и та же однострочная проверка.
Это DoS в локальной сети. Если вы находитесь в том же L2-сегменте, что и OpenBSD-машина с запущенным slaacd, один пакет замораживает её IPv6. Не отправляйте его в сети, которые вам не принадлежат. Если вы используете OpenBSD, проверьте наличие патча или добавьте проверку len==0 самостоятельно.
Daniel Wade - GitHub · Twitter/X · Bluesky · nadsec.online