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

ng7 ng_uncallout() permanently leaks the pptpgre rack/sack timer ng_item + node/hook references (DF-3521 ng7 twin, two stacked defects; escalates to the DF-3514 panic)

Summary

Every armed-then-cancelled ng7 pptpgre timer leaks one struct ng_item, one node reference and one hook reference: ng_uncallout gates the cleanup on callout_stop()>0 (returns 0 for armed stops on this kernel - DF-3515; callout_cancel shares the same early-exit, verified experimentally) and even when reached only uses the flag-only NG_FREE_ITEM macro which never calls ng_free_item. Normal-operation triggers: every SET_CONFIG reset, ack processing, piggy-backed ack, session-hook disconnect. VERIFIED as uid 65534 (ng7 data sockets have no privilege check at attach): 30k cycles -> netgraph zone 4 -> 5.76K/830K never returned, then DF-3514 panic on next socket creation (observed in-guest); windowing=0 control flat. Both fix layers validated (kern_timeout contract patch + one-line ng_free_item in ng_uncallout - each alone measured insufficient). Unpriv local ~0.5KB/cancel + permanent module pinning + node squatting; remote peer triggers via ack timing.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3523 Β· 12 files
FileTypeDescriptionSize
pptp_leak.c β€” 7.6 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 90 B view raw
run.log β€” 2.3 KB view raw
build.log β€” 215 B view raw
panic.txt β€” 2.2 KB view raw
fix.diff β€” 1.3 KB view raw
kern_timeout.diff β€” 1.3 KB view raw
fix_run.log β€” 380 B view raw
fix_build.log β€” 192 B view raw
env.txt β€” 406 B view raw
VERDICT.md β€” 4.1 KB ↓ raw
VERDICT.md
↓ download raw

DF-3523 β€” VERDICT

Status: reproduced (stock kernel). Impact: dos β€” permanent, unprivileged, unbounded kernel-memory leak (one struct ng_item + one node reference + one hook reference per cancelled armed pptpgre timer); at ~30k cycles the objcache(netgraph) exhausts and the leaked node refs drive the known DF-3514 idhash-rehash kernel panic (observed in this run, panic.txt). Fix validated (two layers, kernel #1 + ng7 module rebuild): identical workload -> leak count flat (3).

Root cause (certain; TWO stacked defects)

ng7 ng_callout() (ng_base.c:3248-3268) parks an ng_item holding NG_NODE_REF + NG_HOOK_REF as the callout argument. ng_uncallout() (ng_base.c:3273-3296) frees it only inside if (rval > 0), and even then only via NG_FREE_ITEM(item).

  1. Layer 1 β€” DF-3515 contract violation (stock kernel): callout_stop() of an ARMED, not-yet-fired callout returns 0 (kern_timeout.c _callout_update_spinlocked CALLOUT_STOP branch unloads the callout without setting CALLOUT_PREVENTED; _callout_cancel_or_stop then early-exits return 0 at :888-891). callout_cancel() has the same early-exit (verified experimentally: swapping stop->cancel did not fix anything). So on stock, the cleanup is never reached.
  2. Layer 2 β€” ng7 port defect (any kernel): NG_FREE_ITEM in netgraph7 is a flag-only macro (netgraph.h:817-820; the same defect DF-3513 found in NG_BT_ITEMQ_DRAIN). The real destructor is ng_free_item() (reached via ng_unref_item, netgraph2.h:77-94); the callout-arg item has refs==0 (born zeroed, never sent), so the correct release is ng_free_item(item) directly. Measured: with ONLY layer 1 fixed (kernel #1, stock ng_base), 30k cycles still leaked 5.76K allocs / 830 KB β€” identical to stock.

Consumer sites in ng_pptpgre.c (all normal-operation): reset via every NGM_PPTPGRE_SET_CONFIG (:396 -> :968-969), ack processing (:770), piggy-backed ack in xmit (:596), session-hook disconnect (:494).

Verification narrative

  1. Stock kernel (X86_64_GENERIC #0), modules built in-guest, harness pptp_leak.c wires ng_socket -> pptpgre:upper + session_0000 and then runs the whole attack loop as uid 65534 (ng7 data sockets carry no privilege check): 30k arm/cancel cycles (SET_CONFIG -> 1-byte data write arms rackTimer -> SET_CONFIG cancels it) -> vmstat -m netgraph zone InUse 4 -> 5.76K / 830 KB, never returns (run.log).
  2. Persistence: kldunload ng_pptpgre -> Device busy (leaked node refs pin the module); a later NGM_NAME pptp0 -> Address already in use (the dead node's name stays registered β€” ng_unref_node only unlists at last ref).
  3. Escalation observed: after the 30k leak, the next netgraph control-socket creation panicked the kernel via the DF-3514 sink: Warning: objcache(netgraph) exhausted on cpu0! -> panic: hashdestroy: hash not empty (ngc_attach -> ng_make_node_common), guest down (panic.txt).
  4. CONTROL (fresh boot, identical code path, enableWindowing=0 so the rack timer is never armed): 30k cycles -> Count 3 -> 3 flat.
  5. Fix validation: kern_timeout.c fix applied in-guest (kernel #1, make nativekernel RC=0) + ng_free_item(item) added to ng_uncallout (module rebuild). 30k cycles -> Count flat at 3 (vs 5.76K). Intermediate step (kernel #1 + STOCK ng_base) measured 5.76K again β€” proving both layers are necessary.

Exploit chain / impact ceiling

Pure leak/DoS primitive (no corruption): ~0.5 KB of unreclaimable kernel memory per cancelled timer from an unprivileged local user, plus permanent module pinning and node-name squatting; at scale it crosses into the DF-3514 rehash panic (observed). No route to uid=0 β€” the leaked items are never dereferenced after their flag is set.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline stock: 30k cycles -> netgraph zone 4 -> 5.76K/830K (+DF-3514 panic at scale). Intermediate (kernel#1, stock ng_base): still 5.76K (layer 2 proven). Full fix (kernel#1 + ng7 fix.diff): 30k cycles -> Count flat at 3, exit 0, module unload clean.

fix_run.log, fix_build.log, VERDICT.md
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Sep 14 05:19:46 UTC 2026 (kern_timeout.diff) + ng7 ng_base.c ng_free_item module rebuild

Confirmed kernel references

Detail

Exploit chain

unprivileged local user (ng7 data sockets carry no privilege check) -> 30k fast SET_CONFIG/data/SET_CONFIG cycles -> 30k leaked items+refs (~0.5KB permanent kernel memory each incl. pinned node) -> objcache(netgraph) exhaustion -> next netgraph socket creation hits DF-3514 rehash panic (observed in-guest). No memory corruption; no uid=0 route.

Evidence (decisive lines)

run.log (stock 4->5.76K + flat control + kldunload busy), panic.txt (DF-3514 cascade via this leak), fix_run.log (kernel#1-only still 5.76K -> full fix flat 3), VERDICT.md

PoC changes

v1 seed (DF-3521 leak_test) was rewritten for ng7 ABI: ng_socket graph wiring via NGM_MKPEER/NGM_NAME/NGM_CONNECT to pptpgre:session_0000 (upper hook is taken by the control path), path addressing via hook 'c0' (bare node names parse as hooks), data writes addressed to hook 'data', per-run unique node names (leaked refs squat names).

Verified recommended fix

kern_timeout.c: inline-completed stop/cancel must report CALLOUT_PREVENTED (kern_timeout.diff); ng7 ng_uncallout: call ng_free_item(item) after NG_FREE_ITEM (fix.diff).

Verdict

ng7 ng_uncallout() permanently leaks the pptpgre rack/sack timer ng_item plus one node reference and one hook reference on every cancelled armed timer. TWO stacked defects: (1) the DF-3515 callout_stop contract violation makes callout_stop(armed) return 0 so the cleanup is never reached on stock (callout_cancel is affected identically - verified experimentally), and (2) even when reached, ng7's NG_FREE_ITEM is a flag-only macro - ng_free_item() is never called, so the item leaks on ANY kernel (measured: kernel #1 with stock ng_base still leaked 5.76K allocs/830KB per 30k cycles). Stock measurement: 30k arm/cancel cycles as uid 65534 -> netgraph zone 4 -> 5.76K/830K never returned; kldunload ng_pptpgre -> Device busy; dead node's name stays registered; at scale the leaked node refs drove the DF-3514 idhash rehash into a kernel panic (observed, panic.txt). Control run (windowing=0, timer never armed) flat 3->3. Fix validated with BOTH layers (kern_timeout.diff kernel rebuild + ng7 ng_free_item): 30k cycles -> flat at 3.