# 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

1. `dfbsd-qemu/vm.sh up` (root ssh alias `dfbsd`)
2. `ssh -F dfbsd-qemu/config dfbsd 'cat > /tmp/leak_poc.sh' < leak_poc.sh`
3. `dfbsd-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:

```c
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).
