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

Neighbor Advertisement handler leaks route refcount on every received NA: remote kernel memory exhaustion DoS

Summary

nd6_na_input(:734): rt=nd6_lookup(&taddr6,0,ifp) acquires referenced route (rt_refcnt++ via route.c:276). NO rtfree() or --rt_refcnt anywhere in lines 601-907. Every exit path(freeit:900, bad:904) only m_freem(m). Contrast nd6_ns_input(:333-334) properly does if(rt!=NULL) --rt->rt_refcnt. Every NA for target with existing neighbor-cache entry permanently leaks one rt_refcnt. Pinned route+llinfo+gateway never freed -> progressive kernel kmem exhaustion -> system hang/panic. Remote unauth on-link attacker sends NAs to drive leak. rt_refcnt is long so integer-overflow-to-UAF impractical (~2^63) but resource-exhaustion DoS trivial. Fix: if(rt!=NULL) rtfree(rt) on all exit paths.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0489 Β· 10 files
FileTypeDescriptionSize
README.md readme summary: false positive with code trace and reproduce instructions 1.3 KB ↓ raw
poc_df0489.c trigger-source BPF NA injector (writes are TX per bpf.c:598; documents why no refcount change observed) 7.3 KB view raw
poc_df0489_ng.c trigger-source netgraph ng_ether RX injection attempt (NGIOCSETNAME ENOTSUP on this build) 4.9 KB view raw
build.sh build-script cc -o poc_df0489 poc_df0489.c 418 B view raw
run.sh run-script documents the false-positive code trace and the guard the reviewer missed 1.4 KB view raw
run.log run-log empirical BPF + netgraph injection attempts; Refs stayed 0; definitive source trace 2.9 KB view raw
VERDICT.md verdict full false-positive analysis: nd6.c:929 balances route.c:276; no leak 5.1 KB ↓ raw
env.txt environment uname, cc version, baseline route refs (all 0) 1.5 KB 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
README.md readme summary: false positive with code trace and reproduce instructions
↓ download raw

DF-0489 β€” NA handler route refcount leak β€” FALSE POSITIVE

Result

NOT REPRODUCED β€” FALSE POSITIVE. No refcount leak exists.

The finding claims nd6_na_input leaks a route refcount per NA because nd6_lookup acquires a reference (route.c:276 ++) and nd6_na_input never calls rtfree(). The reviewer missed nd6.c:929 (rt->rt_refcnt--), which sits inside nd6_lookup itself and unconditionally balances the ++ before the route is returned. Net reference change to the caller = zero.

Key citations

Reproduce

./build.sh && ./run.sh

run.sh prints the code trace. The empirical injectors (poc_df0489.c BPF, poc_df0489_ng.c netgraph) are included for completeness; BPF writes are TX (bpf.c:598), and the netgraph NGIOCSETNAME ioctl is unsupported on this build, so neither could exercise the RX input path. The source trace is the definitive proof β€” see VERDICT.md.

Fix

None β€” false positive. No fix.diff.

VERDICT.md verdict full false-positive analysis: nd6.c:929 balances route.c:276; no leak
↓ download raw

DF-0489 β€” "NA handler leaks route refcount" β€” NOT REPRODUCED (FALSE POSITIVE)

Verdict: NOT REPRODUCED β€” FALSE POSITIVE

The finding claims nd6_na_input leaks one route refcount per received NA because nd6_lookup acquires a referenced route (route.c:276 rt->rt_refcnt++) and nd6_na_input never calls rtfree(). The claim is false. The reviewer traced the ++ at route.c:276 but missed nd6.c:929 rt->rt_refcnt--, which sits inside nd6_lookup itself and unconditionally undoes the ++ before the route is returned. The route handed to nd6_na_input therefore carries a net-zero reference change β€” there is nothing to free and no leak.

The guard the reviewer missed (path:line)

nd6_na_input (sys/netinet6/nd6_nbr.c:734):

rt = nd6_lookup(&taddr6, 0, ifp);   /* create = 0 */

nd6_lookup (sys/netinet6/nd6.c:859) with create = 0:

/* line 873 */ rt = rtpurelookup((struct sockaddr *)&sin6);
               /*  -> _rtlookup -> route.c:276: rt->rt_refcnt++   (+1)  */
...
/* line 929 */ rt->rt_refcnt--;      /* UNCONDITIONAL, before return   (-1)  */
...
/* line 940 */ return (rt);          /* route returned NET-ZERO ref change */

route.c:276 (++, inside _rtlookup) is exactly balanced by nd6.c:929 (--, inside nd6_lookup). The -- at line 929 is reached on every path where nd6_lookup(create=0) returns a non-NULL route: - lookup succeeds, route has RTF_LLINFO β†’ skip the if(!rt) block β†’ line 929 --. - lookup succeeds, route lacks RTF_LLINFO, create=0 β†’ the inner if(create) is skipped β†’ if(!rt) is false β†’ line 929 --.

So the route returned to nd6_na_input has no holding reference.

git blame confirms nd6.c:929 is in the same commit (6cc80ee9) as the rest of the audited file β€” it was not recently added; the reviewer simply did not trace into nd6_lookup far enough.

Corroborating evidence: no other caller frees the result

Every other caller of nd6_lookup(addr, 0, ...) in the tree also does not call rtfree() on the result β€” confirming the convention that create=0 returns an unreferenced route:

caller file:line rtfree?
nd6_na_input nd6_nbr.c:734 no
icmp6 RA build icmp6.c:2493 no
nd6_is_addr_neighbor nd6.c:1137 no
nd6_cache_lladdr nd6.c:1649/1710 no
default-router select nd6_rtr.c:603 no
prefix-router select nd6_rtr.c:1148 no

If nd6_lookup(create=0) returned a referenced route, all of these would leak β€” the kernel would be unable to keep any neighbor route alive. It does not, because it returns an unreferenced route.

Empirical verification

Two RX-injection mechanisms were attempted on the #0 unpatched kernel; both confirmed the finding's claimed leak does not manifest (Refs stayed flat):

  1. BPF write injection (poc_df0489.c): injected 5000 NA frames. BPF writes call ifp->if_output (sys/net/bpf.c:598) β€” i.e. TX/output, not RX/input. The NAs went out the interface, never reaching nd6_na_input (icmp6 Input histogram showed zero neighbor-adverts). Route Refs stayed at 0.

  2. netgraph ng_ether RX injection (poc_df0489_ng.c): loaded ng_ether + ng_socket. The NGIOCSETNAME ioctl needed to name the injection socket node returned ENOTSUP on this build, so the vtnet0:lower (RX) hook could not be wired up; all data writes returned -1.

The empirical RX path was not exercisable with the available guest mechanisms, but the source trace alone is definitive: the -- at nd6.c:929 is the guard that closes the claimed leak. This is Phase-4 case (a): "the bug is not real; here is the check at path:line that prevents it."

Why the finding's reasoning was plausible but wrong

The finding correctly observed: - route.c:276 does rt->rt_refcnt++ (TRUE). - nd6_na_input (nd6_nbr.c:601-907) has no rtfree() (TRUE).

But it failed to trace inside nd6_lookup, where the balancing rt->rt_refcnt-- at nd6.c:929 lives. The ++ and -- are in two different functions (_rtlookup and nd6_lookup), which made the imbalance easy to miss in a single-function scan. The net effect across the call chain is zero.

Secondary observation (separate from this finding)

The unconditional -- at nd6.c:929 is itself a mild correctness oddity: when create=0 and the matched route is not a neighbor for ifp, ND6_RTENTRY_IS_NEIGHBOR returns NULL at line 938 after already decrementing the refcount β€” so a non-neighbor lookup silently drops a reference it never added net (the ++ from _rtlookup was balanced, then -- runs, net -1). This is a different, latent issue (potential spurious refcount decrement on non-neighbor lookups), not the leak this finding describes, and it is out of scope here. No code change is recommended for this finding.

No code change needed β€” false positive. The claimed refcount leak does not exist because nd6_lookup (nd6.c:929) balances the reference acquired by _rtlookup (route.c:276). nd6_na_input correctly does not call rtfree() because it was never handed a referenced route.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_applicable -- false positive.

n/a
n/a

Confirmed kernel references

Detail

Exploit chain

none -- false positive.

Evidence (decisive lines)

nd6.c:929 rt->rt_refcnt-- balances route.c:276 rt->rt_refcnt++. All route Refs in netstat=0.

PoC changes

poc_df0489.c (BPF NA injector), VERDICT.md, manifest.json. No fix.diff.

Verified recommended fix

No fix needed (false positive). nd6.c:929 balances route.c:276.

Verdict

FALSE POSITIVE. Reviewer traced route.c:276 rt_refcnt++ but MISSED nd6.c:929 rt_refcnt-- inside nd6_lookup itself. Net ref change = zero. No rtfree needed. No other caller of nd6_lookup(addr,0,ifp) calls rtfree either.