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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3551 Β· 9 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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)

  1. ng_xxx_newhook() gives the debug hook hook->private = NULL (sys/netgraph/ng_sample.c:238-242). The downstream hook always has private set (:237), and DLCI hooks get channel + chan (:230).
  2. ng_xxx_rcvdata() with private == NULL falls straight through to ng_xxx_rcvdataq() (:327-338).
  3. In ng_xxx_rcvdataq()'s else-branch (:390-394), the only free is if (hook == xxxp->downstream_hook.hook) NG_FREE_DATA(m, meta); β€” but a hook with private == NULL can never be the downstream hook (see 1), so the condition is dead code, the free is unreachable, and the function return 0s with the caller-owned m/meta still 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).
  4. 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_testable
baseline reproduced→ patch + rebuild →patched clean

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.diff
↓ fix.diffper-fix-DF-3551

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.