DF-2615 / verdict.json
{ "finding_id": "DF-2615", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "verdict": "REPRODUCED (memory leak, Info-class) on an instrumented kernel and FIX VALIDATED on a rebuilt kernel. Mechanism confirmed exactly as filed: kdmsg_iocom_uninit allocates a LNK_PING (kern_dmsg.c:280-281) which kdmsg_msg_write_locked TAILQ_INSERTs onto iocom->msgq (:2018-2021) because EXITNOACC is clear, the thread-join loop (:284-294) is a no-op with no threads, and nothing drains msgq before the embedded iocom is freed with hmp \u2014 instrumentation shows per cycle: uninit entered rd_td=0 wr_td=0 flags=00000003, after ping write msgq.first=0xfffff801168ef4c0, uninit exit msgq_nleak=1; 20/20 cycles leak exactly one kdmsg_msg each (run_base.dmesg_after). IMPORTANT precondition correction: stock mount_hammer2(8) auto-starts the hammer2 service daemon and passes cluster_fd >= 0 (mount_hammer2.c:225-247), so plain mount -t hammer2 cycles do NOT leak (write thread sets EXITNOACC at kern_dmsg.c:545 and the PING self-drains \u2014 verified live); the leak is taken when the mount proceeds without a cluster connection (daemon unavailable -> cluster_fd=-1 at mount_hammer2.c:162-167, or any direct mount(2) caller) \u2014 reproduced with df2615_trigger.c (mount(2) with cluster_fd=-1). The leak is invisible to vmstat on a stock kernel because the msgs per-device malloc type is kmalloc_destroyed at unmount (hammer2_vfsops.c:1176/1893) while the allocation lives. Fix (set EXITNOACC + kdmsg_drain_msgq in uninit, mirroring the write-thread exit path :545-554, msglk held): same 20-cycle trigger on the rebuilt kernel gives after ping write msgq.first=<ptr> (same path), uninit exit msgq_nleak=0 x20, 0 DF2615_LEAK lines, CYCLES_DONE=20 \u2014 leak eliminated, mount/umount behavior unchanged; the DF-2614 cluster-mount trigger also runs clean. Impact ceiling: slow root-driven kernel heap growth under mount cycling; no attacker-controlled data, no dereference after free (pure leak) \u2014 Info stands.", "exploit_chain": "root mount(2) hammer2 with cluster_fd=-1 (or mount_hammer2 with service daemon unavailable) -> no iocom threads -> umount -> kdmsg_iocom_uninit queues LNK_PING on msgq -> threads absent so nothing drains -> kdmsg_msg leaked with embedded iocom when hmp is freed; repeat per mount/umount cycle", "evidence": [ "findings/poc/DF-2615/run_base.dmesg_after \u2014 decisive baseline: 20x 'DF2615_LEAK: 1 kdmsg_msg(s) queued at iocom_uninit exit' with msgq.first pointers and flags=00000003 (no EXITNOACC, no threads)", "findings/poc/DF-2615/run_fix.dmesg_after \u2014 decisive fixed run: 0 leak lines, 20x 'uninit exit msgq_nleak=0', ping still queued then drained", "findings/poc/DF-2615/instrument.diff \u2014 verification counters (not the fix)", "findings/poc/DF-2615/fix.diff \u2014 EXITNOACC + kdmsg_drain_msgq(iocom) in kdmsg_iocom_uninit (pristine-tree diff)", "findings/poc/DF-2615/build.log / fix_build.log \u2014 instrumented and fixed kernel builds (nativekernel X86_64_GENERIC)" ], "kernel_refs": [ "sys/kern/kern_dmsg.c:264", "sys/kern/kern_dmsg.c:280", "sys/kern/kern_dmsg.c:284", "sys/kern/kern_dmsg.c:545", "sys/kern/kern_dmsg.c:554", "sys/kern/kern_dmsg.c:619", "sys/kern/kern_dmsg.c:2018", "sys/vfs/hammer2/hammer2_iocom.c:66", "sys/vfs/hammer2/hammer2_vfsops.c:1341", "sys/vfs/hammer2/hammer2_vfsops.c:1778", "sys/vfs/hammer2/hammer2_vfsops.c:1176", "sys/vfs/hammer2/hammer2_vfsops.c:1893" ], "poc_changes": "no seed existed. df2615_trigger.c written fresh (direct mount(2) with cluster_fd=-1 after live testing proved stock mount(8) cycles do NOT leak \u2014 see VERDICT.md precondition correction). Instrumentation v1 (counter at uninit exit only) returned 0 lines because stock mount(8) mounts are daemon-connected; v2 added entry/mid probes which exposed wr_td alive + flags=0x8003 on mount(8) mounts, isolating cluster_fd<0 as the true leak precondition.", "attempts": 4, "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 (baseline #0 stock; leak proven on instrumented #2/#4; fix validated on #3/#5)", "runtime_sec": 4800, "guest_dirty": 0, "build_cmd": "cc -O -I/usr/src/sys -o df2615_trigger df2615_trigger.c && cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel KERNCONF=X86_64_GENERIC", "run_cmd": "dfbsd-qemu/vm.sh run_root vnconfig -c vn0 /root/poc/df2615/img; dmesg > /tmp/b; /root/poc/df2615/df2615_trigger 20; dmesg > /tmp/a; grep -c DF2615_LEAK /tmp/a; vnconfig -u vn0", "code_hash": "58a07f9c1e1a475c0c272e8f36aa125a25816f078e5be5185b84d94326e4b93e", "notes": "Three nativekernel rebuilds (#2 instrumented, #4 baseline-recapture, #3/#5 fix). Also observed, out of scope: global M_HAMMER2 (HAMMER2-mount) zone grows ~3.5KB per mount/umount cycle even on daemon-connected mounts \u2014 a separate accounting leak not covered by this finding; noted for future audit. Side observation: mount(2) via generic mount(8) zeroes hammer2_mount_info, so cluster_fd=0 (stdin) can be held as the cluster descriptor \u2014 related to why VOLDATA DUMP prints appear at root boot (see DF-2614 notes).", "recommended_fix": "in kdmsg_iocom_uninit (kern_dmsg.c, after the thread-join/cache cleanup, before dropping msg_fp): iocom->flags |= KDMSG_IOCOMF_EXITNOACC; kdmsg_drain_msgq(iocom);", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #5: Sat Aug 29 11:12:52 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "baseline (instrumented, fix reverted, kernel #4): 20 leak lines / 20 cycles, exit nleak=1 each; patched (kernels #3/#5): 0 leak lines, exit nleak=0 x20, ping still queued (msgq.first non-NULL) then drained, 20/20 cycles succeed \u2014 leak eliminated, no behavior change", "fix_evidence": "findings/poc/DF-2615/run_fix.dmesg_after vs run_base.dmesg_after; fix_build.log" } |