DF-3551 / verdict.json
{ "finding_id": "DF-3551", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "Reproduced live on the stock v1 netgraph stack: every packet pushed into sample:debug leaks exactly one mbuf, permanently and linearly (+300, +1200, +500 across three runs; netstat -m in run.log), while the drained-downstream control run leaks zero. Root cause: ng_xxx_newhook gives the debug hook private==NULL (sys/netgraph/ng_sample.c:238-242) while the downstream hook always has private set (:237), so the only free in ng_xxx_rcvdataqs else-branch, if (hook == xxxp->downstream_hook.hook) NG_FREE_DATA(m, meta) (:390-394), is unreachable dead code and the function returns 0 with the caller-owned mbuf+meta still allocated (v1 rcvdata contract at :314-317 requires the receiver to free). v1 twin of DF-0690 (ng7, which was only source-confirmed/inconclusive), here demonstrated live. Realistic ceiling is kernel mbuf-pool exhaustion (system-wide networking DoS), but the module does not ship and node creation is root-gated (caps_priv_check, sys/netgraph/socket/ng_socket.c:172) - severity Low.", "exploit_chain": "Resource exhaustion only: root loads ng_sample.ko and instantiates a sample node (or any existing sample:debug hook is fed by a peer) -> sustained sends into sample:debug grow the kernel mbuf count without bound -> mb_map exhaustion -> networking DoS. No memory-safety primitive (never-freed, not use-after-free).", "evidence": "run.log (netstat -m deltas: control +0, debug +300/+1200/+500 exactly; leaked mbufs persist across runs), v1demo.c (leakdbg/leakctl modes), fix.diff (unconditional NG_FREE_DATA in the debug-only else branch)", "kernel_refs": [ "sys/netgraph/ng_sample.c:237", "sys/netgraph/ng_sample.c:238", "sys/netgraph/ng_sample.c:314", "sys/netgraph/ng_sample.c:390", "sys/netgraph/ng_sample.c:393" ], "poc_changes": "Same v1demo.c driver as DF-3550 (leakdbg/leakctl modes); initial run mistakenly sent to the sample-side hook name instead of the local socket hook - fixed by tracking the our-side hook (sd/dbg) separately.", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 1800, "guest_dirty": 0, "build_cmd": "make -C /root/ngs1 KMOD=ng_sample; cc -O2 -Wall -o /root/v1demo /root/v1demo.c (same as DF-3550)", "run_cmd": "kldload ng_socket; kldload /root/ngs1/ng_sample.ko; netstat -m; /root/v1demo leakctl 1200; netstat -m; /root/v1demo leakdbg 300; netstat -m; /root/v1demo leakdbg 1200; netstat -m", "code_hash": "1517851c923420332e62c40c89ead87fbaeb6967e76d529de564364011b5e929", "fix_status": "not_tested", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Two-line fix (drop the dead downstream comparison, free unconditionally in the provably debug-only else branch, fix.diff, git-apply --check clean); kernel rebuild validation skipped per contract (Low severity, not memory corruption).", "fix_evidence": "fix.diff", "notes": "The ng7 twin DF-0690 was only source-confirmed (inconclusive); this v1 verification closes the loop for the v1 half of the template pair.", "recommended_fix": "Replace the unreachable conditional free at ng_sample.c:390-394 with an unconditional NG_FREE_DATA(m, meta)." } |