DF-3521 / verdict.json
{ "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." } |