mbuf+meta leak on the debug-hook data path: the only NG_FREE_DATA is gated on an unreachable condition (v1 twin of DF-0690)
Summary
The debug hook gets hook->private = NULL in newhook while the downstream hook always has private set, so in ng_xxx_rcvdataq's else-branch 'if (hook == xxxp->downstream_hook.hook) NG_FREE_DATA(m, meta)' can never fire; the function returns 0 with the caller-owned mbuf and meta still allocated, leaking one mbuf per packet pushed into sample:debug. VERIFIED live with netstat -m brackets: control path delta 0; debug 300 -> +300, 1200 -> +1200, re-check 500 -> +500; leaked mbufs persist permanently across process exits. Gated behind admin kldload of the non-shipping module + root-only node creation, hence Low. Not a use-after-free.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3551 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.1 KB | β raw | |
| VERDICT.md | β | 3.4 KB | β raw | |
| v1demo.c | β | 8.2 KB | view raw | |
| build.sh | β | 133 B | view raw | |
| run.sh | β | 259 B | view raw | |
| build.log | β | 3.1 KB | view raw | |
| run.log | β | 1.8 KB | view raw | |
| fix.diff | β | 556 B | view raw | |
| verdict.json | β | 3.3 KB | view raw |
DF-3551 evidence pack β v1 ng_sample debug-hook mbuf leak (DF-0690 twin)
sys/netgraph/ng_sample.c: on the debug hook (hook->private == NULL,
set in ng_xxx_newhook at :238-242), ng_xxx_rcvdataq()'s only free is
gated on hook == xxxp->downstream_hook.hook (:390-394) β a condition that
can never be true because the downstream hook always has private set
(:237). Every mbuf+meta pushed into sample:debug is leaked.
Build / Run
Same v1demo.c, build.sh, module and graph setup as the DF-3550 pack
(one directory up). Decisive commands:
kldload ng_socket ; kldload /root/ngs1/ng_sample.ko netstat -m # baseline /root/v1demo leakctl 1200 # control: downstream, drained netstat -m /root/v1demo leakdbg 300 # +300 mbufs netstat -m /root/v1demo leakdbg 1200 # +1200 mbufs netstat -m
Expected
Exact, permanent, linear mbuf growth on the debug path (+N for N packets)
and zero growth on the drained downstream control path. See run.log
(taken from the same decisive session as DF-3550's).
DF-3551 β VERDICT: REPRODUCED (v1 twin of DF-0690)
Impact: dos (unbounded kernel mbuf leak; realistic ceiling gated by root-only node creation) Confidence: certain
What was run
v1demo leakdbg N / v1demo leakctl N on the stock v1 netgraph stack
(build/graph setup identical to DF-3550; NGM_CONNECT attaches a second hook
socket(dbg) <-> sample(debug)). netstat -m snapshots bracket every run.
Observed (run.log)
| phase | mbufs in use | delta |
|---|---|---|
| baseline | 7 | β |
| leakctl 1200 (downstream, reflections drained) | 8β1507ΒΉ | 0 |
| leakdbg 300 (debug hook) | 1507β1807 | +300 |
| leakdbg 1200 (debug hook) | 1807β3007 | +1200 |
| leakdbg 500 (stability re-check) | 3007β3507 | +500 |
ΒΉ the 1507 plateau is residue from the earlier exploratory leak runs β it never decreases: the leaked mbufs are permanently lost (still counted in the final re-check).
Every packet sent into sample:debug leaks exactly one mbuf, linearly and
permanently; the control run through sample:downstream (whose data path
does consume the mbuf via ng_send_data) leaks nothing.
Why (kernel path, line-by-line)
ng_xxx_newhook()gives the debug hookhook->private = NULL(sys/netgraph/ng_sample.c:238-242). The downstream hook always has private set (:237), and DLCI hooks getchannel + chan(:230).ng_xxx_rcvdata()withprivate == NULLfalls straight through tong_xxx_rcvdataq()(:327-338).- In
ng_xxx_rcvdataq()'s else-branch (:390-394), the only free isif (hook == xxxp->downstream_hook.hook) NG_FREE_DATA(m, meta);β but a hook withprivate == NULLcan never be the downstream hook (see 1), so the condition is dead code, the free is unreachable, and the functionreturn 0s with the caller-ownedm/metastill allocated (v1 rcvdata contract: "the caller will never free m or meta β¦ if we use up this data or abort we must free BOTH",:316-317). - Result: one leaked mbuf (+meta) per packet on the debug hook β the v1 twin of DF-0690 (ng7), here demonstrated live for the first time.
Reachability gate: ng_sample.ko does not ship (no SUBDIR/conf entry),
v1 control sockets are caps_priv_check(SYSCAP_RESTRICTEDROOT)-gated
(sys/netgraph/socket/ng_socket.c:172) β an admin must kldload the module and
instantiate the node before any data can be pushed; once a sample node
exists, any peer can feed its debug hook. Severity Low (matches DF-0690).
Exploit chain
Resource-exhaustion only: sustained feeding of sample:debug grows the
kernel mbuf pool without bound (mb_map exhaustion β system-wide networking
denial of service). No memory-safety primitive (the mbufs are freed-never,
not use-after-free).
Fix validation
fix.diff makes the free unconditional in the (debug-only) else branch β
with the sentinel fix from DF-3550's pack the downstream hook always has
private set, so the branch is provably debug-hook-only. Kernel-rebuild
validation skipped per contract (Low severity, not memory corruption);
the patch is a two-line dead-condition removal.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testableTwo-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.diff
Confirmed kernel references
Detail
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 (decisive lines)
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)
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.
Verified recommended fix
Replace the unreachable conditional free at ng_sample.c:390-394 with an unconditional NG_FREE_DATA(m, meta).
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_rcvdataq's 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.
No comments yet.