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)
PoC verification
Evidence pack
findings/poc/DF-3523 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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).
- Layer 1 β DF-3515 contract violation (stock kernel):
callout_stop()of an ARMED, not-yet-fired callout returns 0 (kern_timeout.c_callout_update_spinlockedCALLOUT_STOP branch unloads the callout without setting CALLOUT_PREVENTED;_callout_cancel_or_stopthen early-exitsreturn 0at :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. - Layer 2 β ng7 port defect (any kernel):
NG_FREE_ITEMin netgraph7 is a flag-only macro (netgraph.h:817-820; the same defect DF-3513 found inNG_BT_ITEMQ_DRAIN). The real destructor isng_free_item()(reached viang_unref_item, netgraph2.h:77-94); the callout-arg item has refs==0 (born zeroed, never sent), so the correct release isng_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
- Stock kernel (X86_64_GENERIC #0), modules built in-guest, harness
pptp_leak.cwires 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 -mnetgraph zone InUse 4 -> 5.76K / 830 KB, never returns (run.log). - Persistence:
kldunload ng_pptpgre-> Device busy (leaked node refs pin the module); a laterNGM_NAME pptp0-> Address already in use (the dead node's name stays registered β ng_unref_node only unlists at last ref). - 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). - CONTROL (fresh boot, identical code path,
enableWindowing=0so the rack timer is never armed): 30k cycles -> Count 3 -> 3 flat. - Fix validation: kern_timeout.c fix applied in-guest (kernel #1,
make nativekernelRC=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
fixedBaseline 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
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.
No comments yet.