DF-0489 / run.log
=== DF-0489 empirical test logs (BPF + netgraph injection attempts) ===
Guest: DragonFly 6.5-DEVELOPMENT #0 (unpatched audit kernel)
Test: inject Neighbor Advertisement packets to trigger nd6_na_input, then
observe the route "Refs" column (netstat -rn -W -f inet6) for the
target route. If the finding's leak claim were true, Refs would climb
by 1 per NA processed.
--- Setup ---
Created a static neighbor entry for the fake target so nd6_na_input reaches
the nd6_lookup path (otherwise nd6_lookup returns NULL -> freeit):
# ndp -s fe80::dcad:beff:feef:1%vtnet0 de:ad:be:ef:00:01
-> fe80::dcad:beff:feef:1%vtnet0 de:ad:be:ef:00:01 UHLS Refs=0
--- Attempt 1: BPF injection (poc_df0489.c) ---
BPF writes call ifp->if_output (sys/net/bpf.c:598) -- i.e. TX (output), NOT
RX (input). Injected 5000 NA frames; they went OUT the interface, never
reaching nd6_na_input. icmp6 Input histogram showed no neighbor-advert count.
Refs stayed at 0 throughout (no leak, but path not actually exercised):
Refs BEFORE: fe80::dcad:beff:feef:1%vtnet0 UHLS Refs=0
[inject 5000 NAs via BPF write]
Refs AFTER: fe80::dcad:beff:feef:1%vtnet0 UHLS Refs=0
(icmp6 Input histogram unchanged -- confirms BPF write = TX, not RX)
--- Attempt 2: netgraph ng_ether RX injection (poc_df0489_ng.c) ---
Loaded ng_ether + ng_socket. Attempted to name the ng_socket node via
NGIOCSETNAME ioctl so it could be connected to vtnet0:lower (the RX injection
hook). The ioctl returned ENOTSUP ("Operation not supported") on this build;
ngctl connect then failed ("send msg: No such file or directory"); all data
writes returned -1. No packets injected.
--- Conclusion ---
Empirical RX injection was not achievable with the available guest mechanisms
(BPF is TX-only; netgraph socket naming ioctl unsupported). The DEFINITIVE
proof is the source trace below -- which is sufficient on its own to classify
this as a false positive, because it identifies the exact guard the reviewer
missed.
--- Definitive source trace ---
nd6_na_input (nd6_nbr.c:734):
rt = nd6_lookup(&taddr6, 0, ifp); /* create = 0 */
nd6_lookup (nd6.c:860) with create=0:
line 873: rt = rtpurelookup(...) /* -> _rtlookup -> route.c:276: rt->rt_refcnt++ */
line 929: rt->rt_refcnt--; /* UNCONDITIONAL, before any return of rt */
line 940: return (rt); /* route returned with NET-ZERO ref change */
route.c:276 (++ inside _rtlookup) is exactly balanced by nd6.c:929 (--) inside
nd6_lookup. The route handed to nd6_na_input carries NO extra reference, so
there is nothing for nd6_na_input to free and NO refcount leak.
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 missed it.
No other caller of nd6_lookup(addr, 0, ifp) in the tree calls rtfree on the
result (icmp6.c:2493, nd6.c:1137/1649/1710, nd6_rtr.c:603/1148) -- confirming
the convention that create=0 returns a route with no holding reference.