DF-4474 / verdict.json
{ "finding_id": "DF-4474", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "dm_table_load_ioctl creates prop_array_iterator(cmd_data) at dm_ioctl.c:708, which retains the entire inbound cmd_data array (prop_array.c:525), but every early return (:713 device ENOENT, :741 unknown-target ENOENT, :745 ENOMEM, :784 init failure) leaks the iterator and its retained tree; dm_table_deps_ioctl (:602/:606) and dm_table_status_ioctl (:895/:899) likewise leak their freshly created cmd_array on ENOENT. Verified on the guest: M_TEMP InUse grows by exactly +1 permanent object per failed reload (470->570->670->740 across runs), and failed reloads carrying 4000-entry cmd_data trees leave ~3.5MB of kernel memory allocated each (v_free_count page accounting; M_TEMP zone counters under-report the retained tree). Operator-group user can loop fat failed reloads to exhaust kernel heap. Fix (release iter on all early returns; release cmd_array on deps/status ENOENT) validated: InUse unchanged at 470 after 200 failed reloads + 60000 deps/status calls.", "exploit_chain": "", "evidence": "run.log (baseline: +1 InUse per failed reload series 470->740, page-accounting -294MB for 60 fat reloads with ~208MB not returned; patched: 470->470 after 200 failed reloads + 60k deps/status)", "kernel_refs": ["sys/dev/disk/dm/dm_ioctl.c:707-713", "sys/dev/disk/dm/dm_ioctl.c:739-741", "sys/dev/disk/dm/dm_ioctl.c:601-606", "sys/dev/disk/dm/dm_ioctl.c:895-899", "sys/libprop/prop_array.c:513-529"], "poc_changes": "Authored fresh (pass-2 finding, no seed). Added entries-count argument for the fat-tree page-accounting variant after M_TEMP zone accounting proved to under-report retained trees.", "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": 1500, "guest_dirty": 0, "build_cmd": "cc -o dm_errleak dm_errleak.c -lprop", "run_cmd": "./dm_errleak 100 20000 ; ./dm_errleak 60 0 4000", "code_hash": "bf1838c88678da390e3cb4e727eec32c2b78930c8649e8b3f528c8b01ee4b3ab", "notes": "Slow-but-unbounded kernel heap leak from a semi-privileged (operator group) position; rate amplified ~100000x by large cmd_data payloads (tree retained via iterator reference). kmemstats zone accounting masks part of the retention -- page counters are the honest signal.", "recommended_fix": "Release the prop_array_iterator on every early return of dm_table_load_ioctl and release cmd_array on the deps/status ENOENT paths.", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Baseline: +1 permanent M_TEMP object per failed reload (470->740 series), MBs retained per fat failed reload. Patched (dm.ko rebuilt from fix.diff, -Werror clean): InUse unchanged (470->470) after 200 failed reloads + 60000 deps/status ENOENT calls; free pages healthy.", "fix_evidence": "run.log fix-validation section; fix.diff (prop_object_iterator_release/prop_object_release hunks)" } |