Kernel heap leak on dm ioctl error paths: unreleased prop_array_iterator (retaining the entire inbound cmd_data tree) in dm_table_load_ioctl; unreleased cmd_array in deps/status ENOENT paths
Summary
The iterator created at :708 allocates M_TEMP AND retains the inbound cmd_data array; every early return of table_load leaks it (ENOENT/unknown-target/ENOMEM/init-failure). deps (:602->606) and status (:895->899) leak their cmd_arrays on ENOENT. VERIFIED on guest: +1 permanent M_TEMP object per failed reload (never drops), ~3.5MB retained per failed reload with 4000-entry trees (~208MB held after churn) - unbounded kernel heap exhaustion within minutes. Fix validated: InUse unchanged after 200 failed reloads + 60k deps/status calls. Release-on-every-early-return fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4474 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dm_errleak.c | β | 5.5 KB | view raw | |
| build.sh | β | 91 B | view raw | |
| run.sh | β | 128 B | view raw | |
| run.log | β | 2.7 KB | view raw | |
| fix.diff | β | 4.3 KB | view raw | |
| env.txt | β | 414 B | view raw | |
| VERDICT.md | β | 2.8 KB | β raw | |
| README.md | β | 899 B | β raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-4474 PoC β proplib object leaks on dm ioctl error paths
Build (guest, after vm.sh reset with-src + kldload dm):
cc -o dm_errleak dm_errleak.c -lprop
Run:
./dm_errleak 100 20000 # failed-reload rounds, deps/status rounds (400-entry trees)
./dm_errleak 60 0 4000 # few rounds, huge 4000-entry trees (page-accounting variant)
Expected (unpatched):
- M_TEMP InUse grows by exactly +1 object per failed reload and never drops (470 β 570 β 670 ...); with 4000-entry trees the free-page count drops by ~3.5MB per failed reload and does not return.
- deps/status ENOENT loops leak their cmd_array (code-evident; per-object zone accounting masks the count).
Expected (patched with fix.diff): InUse unchanged (470 β 470) after 200 failed reloads + 60000 deps/status calls; free pages healthy.
Requires root (or operator group).
DF-4474 β dm ioctl error paths leak proplib objects (iterator + retained array tree; deps/status cmd_array)
REPRODUCED (permanent kernel heap leak) + FIX VALIDATED.
The bug
Three error paths in sys/dev/disk/dm/dm_ioctl.c drop proplib objects without releasing them:
dm_table_load_ioctl()βiter = prop_array_iterator(cmd_array)at :708 retains the whole inbound cmd_data array (sys/libprop/prop_array.c:525prop_object_retain(pa)), yet every early return leaks it: *:713ENOENT (device lookup failed) *:739-741ENOENT (unknown target type) *:745-748ENOMEM (dead code β M_WAITOK) *:783-789(dm_table_init failure) Only the success path releases at :793.dm_table_deps_ioctl()βcmd_array = prop_array_create()at :602, ENOENT return at :606 without release.dm_table_status_ioctl()βcmd_array = prop_array_create()at :895, ENOENT return at :899 without release.
Reproduction (baseline, unpatched)
- Permanent linear leak, +1 M_TEMP object (~40B) per failed reload: InUse 470 β 570 β 670 β 740 tracking 100 + 100 + 60 failed reloads exactly, never dropping (run.log). The object is the leaked iterator.
- With fat cmd_data payloads the retained tree costs ~MBs per call: 60 failed reloads carrying 4000-entry (~1.4MB XML) cmd_data trees consumed ~294MB of kernel memory (v_free_count 821767 β 746491 pages), of which ~208MB was still allocated after unrelated churn β consistent with the iterator pinning the array tree (prop_array.c:525). M_TEMP zone accounting under-reports the retained tree, so the page counter is the honest signal.
An operator-group user can loop failed reloads with large cmd_data payloads and exhaust kernel memory (OOM / allocation-failure panics) within minutes.
Fix validation
fix.diff (shared with DF-4472/DF-4473) releases the iterator on every
early return of dm_table_load_ioctl and releases cmd_array on the
deps/status ENOENT paths.
- Baseline: +1 object per failed reload (470 β 740 over the series), ~MBs retained per fat failed reload.
- Patched (
make-Werror clean, kldload): 200 failed reloads + 60000 deps/status ENOENT calls leave InUse unchanged at 470; free-page count healthy (826089). (run.log)
Files
dm_errleak.cβ PoC (fat failed reloads + deps/status ENOENT loops, vmstat/page accounting)run.logβ baseline leak series + patched zero-leak runfix.diffβ combined dm_ioctl.c fix (validated)env.txt,build.sh,run.sh
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: +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.
run.log fix-validation section; fix.diff (prop_object_iterator_release/prop_object_release hunks)
Confirmed kernel references
Detail
Evidence (decisive lines)
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)
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.
Verified 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.
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.
No comments yet.