β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4474

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4474 Β· 10 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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:

  1. dm_table_load_ioctl() β€” iter = prop_array_iterator(cmd_array) at :708 retains the whole inbound cmd_data array (sys/libprop/prop_array.c:525 prop_object_retain(pa)), yet every early return leaks it: * :713 ENOENT (device lookup failed) * :739-741 ENOENT (unknown target type) * :745-748 ENOMEM (dead code β€” M_WAITOK) * :783-789 (dm_table_init failure) Only the success path releases at :793.
  2. dm_table_deps_ioctl() β€” cmd_array = prop_array_create() at :602, ENOENT return at :606 without release.
  3. 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 run
  • fix.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

fixed
baseline reproduced→ patch + rebuild →patched clean

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.

run.log fix-validation section; fix.diff (prop_object_iterator_release/prop_object_release hunks)
↓ fix.diffper-fix-DF-4474

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.