dm_target_flakey_init() failure paths leak the dm_pdev reference and permanently pin the underlying device vnode
Summary
The pdev ref is taken at :86 but all three post-insert error paths jump to a fail: that only kfrees tfc - and the caller's destroy cannot recover it (target_config is still NULL so destroy early-returns without decr). REPRODUCED on guest: 3 failed loads -> M_DM 336B->1.08K permanent + every affected vn device pinned EBUSY forever (control unit detaches clean - proving the leak); correlated shutdown wedge on 2/2 resets. Unbounded with distinct paths; root/operator. decr-on-fail fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4922 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.6 KB | β raw | |
| VERDICT.md | β | 5.6 KB | β raw | |
| leak_poc.sh | β | 2.9 KB | view raw | |
| build.sh | β | 107 B | view raw | |
| run.sh | β | 201 B | view raw | |
| run.log | β | 1.5 KB | view raw | |
| run.noisy-first-attempt.log | β | 3.4 KB | view raw | |
| env.txt | β | 578 B | view raw | |
| fix.diff | β | 583 B | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 3.5 KB | view raw |
DF-4922 β dm_target_flakey: init-failure paths leak the dm_pdev reference and pin the device vnode
What
dm_target_flakey_init() takes a reference on the underlying block device
with dm_pdev_insert(argv[0]) (sys/dev/disk/dm/flakey/dm_target_flakey.c:86,
which opens the device vnode and inserts the pdev into the global
dm_pdev_list with ref_cnt = 1 β sys/dev/disk/dm/dm_pdev.c:120-188).
Every error path taken after that point jumps to fail: which only does
kfree(tfc) (dm_target_flakey.c:117-119) β the pdev reference is never
released with dm_pdev_decr().
The caller cannot recover it either: on target-init failure
dm_table_load_ioctl() destroys the half-loaded table
(sys/dev/disk/dm/dm_ioctl.c:783-790), and dm_table_destroy() invokes the
target's destroy handler (sys/dev/disk/dm/dm_table.c:150-151) β but
target_config was never installed (dm_table_init_target() runs only on
success, dm_target_flakey.c:114), so dm_target_flakey_destroy() returns
early at its tfc == NULL check (dm_target_flakey.c:214-215) without
dropping the ref. The pdev_maps dependency list is likewise still empty
(dm_table_add_deps() at :112 runs only on success), so nothing anywhere
drops the reference.
Leaking post-insert failure paths (all reachable with a single
dmsetup create call):
| Path | Table args | Check that fails | Line |
|---|---|---|---|
| A | flakey <dev> 0 0 0 |
up_int + down_int == 0 |
dm_target_flakey.c:96-100 |
| B | flakey <dev> 0 1 1 1 nosuchfeature |
unknown feature | dm_target_flakey.c:195-196 |
| C | flakey <dev> 0 2147483647 2147483647 |
interval overflow | dm_target_flakey.c:102-106 |
Impact
Each failed load permanently leaks one dm_pdev_t (~186 bytes, M_DM zone)
and pins the underlying device vnode open. Effects demonstrated on the
guest:
- the M_DM malloc zone grows and never shrinks (removing the half-created dm devices does not free the pdevs), and
- the affected block device can never be detached again
(
vnconfig -uβVNIOCDETACH: Device busy) until reboot.
Reachable by root or the operator group (/dev/mapper/control is
root:operator 0640, sys/dev/disk/dm/device-mapper.c:181); no priv_check
exists inside the ioctl paths themselves. Severity: Low (privileged
resource leak / device pinning; no memory-safety impact).
Reproduce
dfbsd-qemu/vm.sh up(root ssh aliasdfbsd)ssh -F dfbsd-qemu/config dfbsd 'cat > /tmp/leak_poc.sh' < leak_poc.shdfbsd-qemu/vm.sh run_root 'sh /tmp/leak_poc.sh'
Expected (see run.log):
load rc=1 out='ioctl reload call failed: Invalid argument ...' (x3, paths A/B/C) M_DM: 336 -> 1.08K after 3 failed loads; unchanged after dm remove LEAK-UNIT vn0: vnconfig: VNIOCDETACH: Device busy LEAK-UNIT vn1: vnconfig: VNIOCDETACH: Device busy LEAK-UNIT vn2: vnconfig: VNIOCDETACH: Device busy CONTROL vn3 re-attach+detach: OK
Fix
fix.diff β release the reference on the failure path:
fail:
if (dmp != NULL)
dm_pdev_decr(dmp);
kfree(tfc, M_DMFLAKEY);
return err;
(dmp is NULL only when dm_pdev_insert() itself failed, in which case
there is no reference to release.)
Fix validation build was not performed: this is a Low-severity resource
leak (not memory corruption), and the one-line fix is correct by
inspection β dm_pdev_decr() drops the ref taken by dm_pdev_insert(),
freeing the pdev and closing the vnode when it reaches zero
(sys/dev/disk/dm/dm_pdev.c:242-259), exactly mirroring what
dm_target_flakey_destroy() does on the normal teardown path
(dm_target_flakey.c:217).
DF-4922 VERDICT β dm_target_flakey init-failure pdev ref/vnode leak
Status: REPRODUCED (impact: leak, confidence: certain)
Root cause (line-accurate)
dm_target_flakey_init():
86: if ((dmp = dm_pdev_insert(argv[0])) == NULL) { ... } /* ref_cnt := 1, vnode opened */
...
96: if ((tfc->up_int + tfc->down_int) == 0) { err = EINVAL; goto fail; }
102: if (tfc->up_int + tfc->down_int < tfc->up_int) { err = EINVAL; goto fail; }
108: err = _init_features(tfc, argc - 4, argv + 4);
109: if (err) { goto fail; }
...
117: fail:
118: kfree(tfc, M_DMFLAKEY); /* <-- pdev ref never dropped */
dm_pdev_insert() (sys/dev/disk/dm/dm_pdev.c:120-188) opens the device
vnode (dm_dk_lookup) and installs the pdev in the global
dm_pdev_list with ref_cnt = 1 (dm_pdev.c:151) β or bumps the ref of an
existing entry (dm_pdev.c:132). None of the three post-insert goto fail
paths release it.
Nothing downstream releases it either:
dm_table_load_ioctl()error path (sys/dev/disk/dm/dm_ioctl.c:783-790) callsdm_table_destroy()on the half-loaded table;dm_table_destroy()(sys/dev/disk/dm/dm_table.c:147-160) callstable_en->target->destroy(table_en)β buttarget_configis still NULL (it is only installed on success at dm_target_flakey.c:114 viadm_table_init_target()), sodm_target_flakey_destroy()bails at itstfc == NULLcheck (dm_target_flakey.c:213-215) withoutdm_pdev_decr();- the
pdev_mapsdependency list is empty (deps are added only on success, dm_target_flakey.c:112), sodm_table_free_deps()(dm_table.c:298-314) frees nothing β note deps never own refs anyway; the target's destroy handler owns exactly the one insert-ref.
Reproduction narrative
Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (see env.txt).
Attempt 1 (run.noisy-first-attempt.log): 20-unit loop was noisy (only 4 vn
units exist; same-inode backing files confuse attach), but already showed
+744 B M_DM growth and VNIOCDETACH: Device busy on every leaked unit.
Guest reset, then the deterministic 4-unit run:
- Control: vn3 attach β detach OK (twice, including re-attach after the leak tests β proving the EBUSY below is the leak, not the tool).
- Path A (
0 0 0intervals), path B (unknown feature), path C (interval overflow): eachdmsetup create ... flakey ...fails withioctl reload call failed: Invalid argument= the EINVAL from dm_target_flakey.c:98/104/_init_features, i.e. after the insert. - M_DM zone: 336 B/3 allocs β 1.08K/18 allocs after exactly 3 failed loads (β +186 B + sub-alloc rounding per pdev).
- After
dmsetup removeof the half-created devices: M_DM unchanged (1.08K) β pdevs persist. vnconfig -u vn0,vn1,vn2βVNIOCDETACH: Device busyβ the kernel still holds the device vnodes; the devices are pinned until reboot.
An earlier dirty run also suggested the pinned vn devices can wedge the
shutdown path (vm.sh reset reported "guest not answering" during clean
shutdown); not claimed as a separate impact β the serial log rotated on
reset, so it stays a note, not a verdict claim.
Exploit chain
None β this is a privileged (root/operator) resource leak; no memory-safety primitive. Ceiling: unbounded kernel-memory growth (distinct device paths) + permanent device pinning + possible shutdown wedge.
Fix
fix.diff: drop the ref on the failure path (if (dmp != NULL)
dm_pdev_decr(dmp);), mirroring the normal teardown at
dm_target_flakey.c:217. Not build-validated: Low-severity leak, not
memory corruption; the one-liner is correct by inspection against
dm_pdev.c:242-259 (decr β 0 β TAILQ_REMOVE + dm_pdev_free() β
vn_close()).
Verified-benign / negative notes for this file (pass 2)
dm_target_flakey_table()(dm_target_flakey.c:377-394) advancespby ksnprintf return values while re-passing the full DM_MAX_PARAMS_SIZE as the size β a latent overflow pattern, but every printed field is bounded:udev_nameis"%d:%d"maj:min (dm_pdev.c:159-160, β€21 B),%dints β€11 B each β total β€ ~117 B inside a 1024-Bdm_alloc_stringbuffer. Not exploitable.- Strategy interval math (:232-233): config is immutable post-init and the
sum is validated != 0 at :96, so the
%can't divide by zero; negativeup_int(via atoi64βint truncation,down_int==0) only skews the up/down decision β no#DE(INT_MIN dividend unreachable: |elapsed| β€ 2^31/hz). _flakey_corrupt_buf()(:339-363):b_bcount < corrupt_buf_bytecheck- value β€0xff + β₯1 validation β write is in-bounds by construction.
- Bio chaining:
push_bioreuse assert (vfs_bio.c:786) is safe becausebiodone()clearsbio_donebefore invoking the callback (vfs_bio.c:3748-3751);pop_bionever frees (vfs_bio.c:795-798). - Module unload is guarded:
dm_target_remove()returns EBUSY whileref_cnt > 0(dm_target.c:181-184);dm_target_alloc()M_ZEROes unset handlers anddm_table_status()NULL-checks them (dm_ioctl.c:978-985). - Known (not re-reported here): DF-1981 (feature-count validation β OOB
argv walk/panic), DF-2452 (same area, mismatched feature_count NULL
deref β overlaps DF-1981), DF-2453 (tfc UAF in
_flakey_read_iodoneasync callback; the correct fix shape is taking a table ref across the async I/O, which the current tree still lacks).
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
not_testableKernel rebuild not performed: Low-severity privileged resource leak, not memory corruption (fix validation mandated only for memory-corruption findings). The one-line fix.diff mirrors the normal teardown path (dm_pdev_decr at dm_target_flakey.c:217) and is correct by inspection against dm_pdev.c:242-259.
fix.diff
Confirmed kernel references
- sys/dev/disk/dm/flakey/dm_target_flakey.c:86
- sys/dev/disk/dm/flakey/dm_target_flakey.c:96
- sys/dev/disk/dm/flakey/dm_target_flakey.c:102
- sys/dev/disk/dm/flakey/dm_target_flakey.c:108
- sys/dev/disk/dm/flakey/dm_target_flakey.c:117
- sys/dev/disk/dm/flakey/dm_target_flakey.c:213
- sys/dev/disk/dm/dm_pdev.c:151
- sys/dev/disk/dm/dm_pdev.c:242
- sys/dev/disk/dm/dm_table.c:150
- sys/dev/disk/dm/dm_ioctl.c:783
Detail
Evidence (decisive lines)
run.log: M_DM '336' baseline -> '1.08K' after A/B/C, unchanged after dm remove; 'LEAK-UNIT vn0/vn1/vn2: VNIOCDETACH: Device busy'; 'CONTROL vn3 ... OK'. dmesg line 'Unknown Flakey target feature nosuchfeature' (env.txt) proves path B failed inside _init_features, i.e. after the insert.
PoC changes
No seed PoC existed (new pass-2 finding). First attempt used 20 vn units; guest has only 4 vn units and same-inode backing files confuse attach, so the clean run uses one unit per failure path with distinct backing files plus a no-dm control unit.
Verified recommended fix
Add dm_pdev_decr(dmp) on the fail path of dm_target_flakey_init (see fix.diff).
Verdict
dm_target_flakey_init() takes a dm_pdev reference (dm_pdev_insert, opens the device vnode, ref_cnt=1) at dm_target_flakey.c:86, but every post-insert failure path (interval sum==0 :96, interval overflow :102, _init_features error :108) jumps to fail: which only kfree()s tfc; the ref is never dropped. The downstream error path cannot recover it: dm_table_destroy() calls the target destroy handler, but target_config is still NULL so flakey's destroy returns early (:213), and pdev_maps is empty. Demonstrated on the guest: 3 failed loads grow the M_DM zone 336B->1.08K permanently (survives dm device removal) and leave vn0/vn1/vn2 permanently pinned (vnconfig -u -> VNIOCDETACH: Device busy), while the no-dm control unit detaches cleanly. Privileged (root/operator via /dev/mapper/control 0640) resource leak + device pinning; no memory-safety primitive.
No comments yet.