⬢ DragonFlyBSD Kernel Audit
DF-3521 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-3521",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "leak",
  "confidence": "certain",
  "verdict": "REPRODUCED deterministically in-guest: every cancelled (armed-then-stopped) rack/sack timer in ng_pptpgre permanently leaks its 8-byte timerPtr allocation, one node reference, and one priv->timers unit, because ng_pptpgre_stop_*_ack_timer() gates cleanup on callout_stop() returning non-zero and this kernels _callout_cancel_or_stop() returns 0 for inline-completed stops (kern_timeout.c:888-891, the DF-3515 contract violation) even though it cancelled the pending callout. Measured: +29.3k leaked allocations for 30k arm/cancel cycles; ~440 B per node lifecycle (priv+node+ptr) because priv->timers never drains so ng_pptpgre_rmnode never frees priv. Unbounded kernel memory leak under normal PPTP session operation (every ack, every SET_CONFIG). Fix (report CALLOUT_PREVENTED in the kern_timeout early exit) validated with a rebuilt kernel: leak per cycle drops to 0.",
  "exploit_chain": "",
  "evidence": "run.log (vmstat -m netgraph Count 503 -> 29.8K after 30k cycles -> 38.6K/1.54M after 3k lifecycles); fix_run.log (post-fix rerun, Count flat); leak_test.c cycle driver; build.log; VERDICT.md full trace",
  "kernel_refs": [
    "sys/netgraph/pptpgre/ng_pptpgre.c:437",
    "sys/netgraph/pptpgre/ng_pptpgre.c:525",
    "sys/netgraph/pptpgre/ng_pptpgre.c:679",
    "sys/netgraph/pptpgre/ng_pptpgre.c:762",
    "sys/netgraph/pptpgre/ng_pptpgre.c:768",
    "sys/netgraph/pptpgre/ng_pptpgre.c:786",
    "sys/netgraph/pptpgre/ng_pptpgre.c:889",
    "sys/netgraph/pptpgre/ng_pptpgre.c:977",
    "sys/kern/kern_timeout.c:888"
  ],
  "poc_changes": "Seeded draft did not exist for this ID; authored from scratch. Fixed during bring-up: used real struct ngm_mkpeer/ngm_connect/ngm_name from guest headers (hand-rolled connect struct had path[32] instead of path[512] -> EINVAL); node-name addressing requires trailing colon (pptp:, p0 hook-walk); node NAME itself must not contain a colon.",
  "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": 240,
  "guest_dirty": 1,
  "build_cmd": "cc -O2 -lpthread -o leak_test leak_test.c",
  "run_cmd": "./leak_test 30000 0 && ./leak_test 3000 1",
  "code_hash": "18bf9e55f92ea55153544a2df4a73d3406cf3f4c29a78cf046d4063b112bfa41",
  "notes": "Root-gated (ng control socket: caps_priv_check SYSCAP_RESTRICTEDROOT, ng_socket.c:172). Same contract family as DF-3515 (ng7 ng_uncallout) but a distinct consumer (v1 ng_pptpgre if(callout_stop()) pattern) not covered by DF-3515/DF-0596. Contrast evidence: DF-3518 hammer ran 52k node lifecycles with no armed timers and Count stayed flat - leak occurs iff a timer was armed then stopped.",
  "recommended_fix": "kern_timeout.c: in _callout_cancel_or_stop(), return ((c->flags & CALLOUT_PREVENTED) != 0) for sync requests completed inline, instead of unconditional 0 (restores the callout_stop(9) contract for every consumer).",
  "fix_status": "fixed",
  "fix_notes": "fix.diff applied to guest /usr/src (patch -p1, hunk at kern_timeout.c:885), kernel rebuilt (make -j6 nativekernel, RC=0, 35546-line log), installed, rebooted into DragonFly 6.5-DEVELOPMENT #1 Mon Sep 14 03:54:26 UTC 2026; identical leak_test rerun: netgraph Count=0 MemUse=0 after 30000 arm/cancel cycles AND 3000 full lifecycles (vs 29.8K/38.6K allocs, 1.54M on stock #0). See fix_run.log, kbuild_tail.log."
}