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)
PoC verification
Evidence pack
findings/poc/DF-0489 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
sys/netinet6/nd6_nbr.c:734βrt = nd6_lookup(&taddr6, 0, ifp);(create=0)sys/netinet6/nd6.c:873βrtpurelookupβ_rtlookupβroute.c:276++sys/netinet6/nd6.c:929βrt->rt_refcnt--;(the guard the reviewer missed)git blame: line 929 is in commit6cc80ee9(same as the rest of the audited file).
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.
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):
-
BPF write injection (
poc_df0489.c): injected 5000 NA frames. BPF writes callifp->if_output(sys/net/bpf.c:598) β i.e. TX/output, not RX/input. The NAs went out the interface, never reachingnd6_na_input(icmp6 Input histogram showed zero neighbor-adverts). RouteRefsstayed at 0. -
netgraph ng_ether RX injection (
poc_df0489_ng.c): loadedng_ether+ng_socket. TheNGIOCSETNAMEioctl needed to name the injection socket node returnedENOTSUPon this build, so thevtnet0: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.
Recommended fix
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_testablenot_applicable -- false positive.
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.
No comments yet.