
One IPv6 ND option with length zero. One missing check. Daemon walks backward and lives in the loop. Reported to OpenBSD, fixed, CVE assigned.
| CVE | CVE-2026-41285 |
| Bug class | Infinite loop via integer underflow |
| Root cause | ND option parser does nd_opt_len * 8 - 2 without checking len==0 |
| Component | sbin/slaacd/engine.c, usr.sbin/rad/engine.c |
| Impact | Permanent DoS of IPv6 SLAAC (slaacd) or RA service (rad) |
| Required | Any device on the same L2 network segment |
| Tested | OpenBSD 7.8 GENERIC amd64 |
RFC 4861 §4.6 says ND options with length zero are invalid and must be
silently discarded. The kernel's own nd6_options() in sys/netinet6/nd6.c
does this correctly. But the raw ICMPv6 packet still gets delivered to
userland sockets before that check matters.
slaacd and rad both parse ND options themselves. The loop looks like:
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
}
When nd_opt_len == 0: the expression 0 * 8 - 2 promotes to int → -2.
The guard (-2 > len) is always false (signed comparison, len is positive).
Then len += 2 and the pointer goes back by 2 bytes. Loop never advances.
CPU pegs at 100%. Forever.
The kernel validated its own copy. The userland daemon got the raw original. Nobody told slaacd.
rcctl restart slaacd or rebootpython3 poc/kill_slaacd.py <interface>
Requires scapy. Sends one Router Advertisement with a single ND option where
nd_opt_len = 0. That's it. The option type doesn't matter (PoC uses type
200 / unknown).
For the full end-to-end proof (shows SLAAC working -> exploit -> SLAAC dead):
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.
Same attack surface as CVE-2022-27881 and CVE-2022-27882 (earlier slaacd infinite loops in engine.c, also ND option parsing). This is a new instance of the same class of bug - the previous fixes didn't cover all the parsing loops.
Check nd_opt_len == 0 before doing arithmetic on it. Break out of the loop.
This is what the kernel already does in nd6_options():
if (nd_opt->nd_opt_len == 0)
break; // or: goto bad;
Suggested patch for slaacd's parse_ra(), debug_log_ra(), and rad's RS
parser - all three loops need the same one-line guard.
This is a local-network DoS. If you're on the same L2 segment as an OpenBSD box running slaacd, one packet freezes its IPv6. Don't send it to networks you don't own. If you run OpenBSD, check for a patch or add the len==0 guard yourself.
Daniel Wade - GitHub · Twitter/X · Bluesky · nadsec.online