β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0418

Unbounded default-router and prefix list growth from spoofed RAs: RA-flooding kernel memory exhaustion DoS

Summary

defrtrlist_update(:689-702) kmalloc nd_defrouter per unique RA src linklocal, TAILQ_INSERT no upper bound. nd6_prelist_add(:765-782) kmalloc nd_prefix + pfxrtr_add(:727) per unique prefix per router. No per-router/prefix limit anywhere (nd6_maxndopt caps per-packet opts not cross-packet). On-link unauth attacker sends stream of RAs each with fresh randomized src+prefix -> unbounded kmalloc -> kernel memory exhaustion -> system hang/panic. Classic RA-flooding (RFC 6104/6105). Linux/FreeBSD added caps; DragonFly lacks them.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0418 Β· 11 files
FileTypeDescriptionSize
raflood.c trigger-source RA-flood PoC: enable accept_rtadv + inject forged RAs via /dev/tap0 7.5 KB view raw
fix.diff suggested-fix per-interface caps nd6_maxdefrouters/nd6_maxprefixes (nd6.c+nd6_rtr.c+nd6.h) 2.5 KB view raw
build.sh build-script cc -o raflood raflood.c 148 B view raw
run.sh run-script enable accept_rtadv + flood 2000 RAs on tap0 487 B view raw
run.log run-log baseline #0: ndp 6->4006, M_IP6NDP 485K 1.0 KB view raw
fix_build.log build-log single-fix kernel build rc=0 5.6 MB ↓ download
fix_run.log run-log patched #1: capped at 16 prefixes, M_IP6NDP 6.8K 373 B view raw
VERDICT.md verdict full mechanism + reproduction + fix validation 3.9 KB ↓ raw
env.txt environment uname 150 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict full mechanism + reproduction + fix validation
↓ download raw

DF-0418 β€” Unbounded default-router/prefix list growth from RA flood

Verdict: REPRODUCED (DoS) + FIX VALIDATED

Summary

defrtrlist_update() (sys/netinet6/nd6_rtr.c:689) kmallocs a fresh struct nd_defrouter for every unique RA source link-local address and TAILQ_INSERT_TAILs it with no upper bound. nd6_prelist_add() (nd6_rtr.c:765) does the same per unique prefix, and pfxrtr_add() (:727) per prefix-router pair. The only cap in the tree, nd6_maxndopt (nd6.c:128), limits the number of ND options per packet, not cross-packet accumulation. An on-link unauthenticated attacker who floods RAs with a fresh randomized source + prefix each causes unbounded M_IP6NDP kmalloc β†’ kernel memory exhaustion β†’ system hang/panic. This is the classic RFC 6104/6105 RA-flooding DoS.

Reachability (the realistic precondition)

nd6_ra_input() (nd6_rtr.c:200) accepts an RA only when !ip6_forwarding && (ND_IFINFO(ifp)->flags & ND6_IFF_ACCEPT_RTADV) (:221). ND6_IFF_ACCEPT_RTADV is set per-interface at init from the global net.inet6.ip6.accept_rtadv (nd6.c:210), which is 0 by default. So on a default DragonFly install RAs are NOT processed; the bug is reachable only on a host that has enabled SLAAC (accept_rtadv=1) β€” a common, realistic configuration for any IPv6 autoconfig host. The CVSS AV:A (adjacent) vector is correct: the attacker must be on-link.

Reproduction (baseline #0 unpatched kernel)

Forged RAs are injected as ingress on a tap0 interface (writing raw Ethernet+IPv6+ICMPv6 RA frames to /dev/tap0 delivers them to the kernel RX path, exercising nd6_ra_input exactly as a real on-link RA would). Each RA carries a unique fe80:: source and a unique 2001:db8:<i>::/64 prefix.

precondition: net.inet6.ip6.accept_rtadv=1 + ND6_IFF_ACCEPT_RTADV on tap0
before: ndp=6 entries, free pages=833542
flood 2000 RAs (unique src + prefix each)
after:  ndp=4006 entries, free pages=832973 (-~2.3MB for 2k RAs)
        M_IP6NDP slab = 485K current, 390M cumulative, ~tens of thousands of allocs

Memory scales linearly with #unique RAs; an attacker trivially exhausts GBs. At ~4200 cumulative RAs the slab showed 969K current / 390M cumulative and free pages had dropped ~10MB. This is an unbounded-growth DoS.

Exploit chain

Not memory corruption (no primitive derivable) β€” this is a resource-exhaustion DoS. No uid=0 chain applies. Realistic impact ceiling: permanent kernel memory exhaustion / OOM-kill / hang on any SLAAC (accept_rtadv=1) host from an on-link attacker; no privilege gain.

fix.diff adds two per-interface caps, nd6_maxdefrouters and nd6_maxprefixes (default 16 each β€” generous for legitimate multi-router/ multi-prefix deployments, RFC 4191), enforced by counting same-ifp entries in defrtrlist_update and nd6_prelist_add before the kmalloc. Over-cap entries are refused and logged. The caps could be promoted to sysctls (trivial) but are plain globals matching the existing nd6_maxndopt style.

Fix validation (single-fix kernel #1, built + booted)

baseline #0  : 2000 RAs -> tap0 grew to ~thousands of autoconf addrs,
               M_IP6NDP slab = 485K-969K current, thousands of allocs
patched #1   : 2000 RAs -> tap0 inet6 addrs = EXACTLY 16 (capped),
               M_IP6NDP slab = 6.8K / 68 allocs

The default-router/prefix kmalloc growth is bounded by the fix. (The remaining ~2000 neighbor-cache entries from 2000 distinct source MACs are a separate neighbor-discovery-cache resource, not the router/prefix lists this finding claims β€” out of scope here.)

Files

  • raflood.c β€” RA-flood PoC: enables ND6_IFF_ACCEPT_RTADV, injects forged RAs via /dev/tap0
  • fix.diff β€” per-interface caps (nd6.c globals + nd6_rtr.c checks + nd6.h externs)
  • run.log β€” baseline reproduction (ndp 6->4006)
  • fix_build.log β€” full single-fix kernel build output (rc=0)
  • fix_run.log β€” patched-kernel run (capped at 16)
  • env.txt β€” guest environment

Fix verification

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 18 10:22:25 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (live). Unbounded defrouter/prefix growth from RA flood -> M_IP6NDP 485K+ exhaustion. accept_rtadv=1. Fix: cap nd6_maxdefrouters/prefixes=16.