# 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()`:

```c
 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)
  calls `dm_table_destroy()` on the half-loaded table;
* `dm_table_destroy()` (sys/dev/disk/dm/dm_table.c:147-160) calls
  `table_en->target->destroy(table_en)` — but `target_config` is still
  NULL (it is only installed on success at dm_target_flakey.c:114 via
  `dm_table_init_target()`), so `dm_target_flakey_destroy()` bails at its
  `tfc == NULL` check (dm_target_flakey.c:213-215) without `dm_pdev_decr()`;
* the `pdev_maps` dependency list is empty (deps are added only on success,
  dm_target_flakey.c:112), so `dm_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 0` intervals), path B (unknown feature), path C (interval
  overflow): each `dmsetup create ... flakey ...` fails with
  `ioctl 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 remove` of 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) advances `p`
  by 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_name` is `"%d:%d"` maj:min
  (dm_pdev.c:159-160, ≤21 B), `%d` ints ≤11 B each → total ≤ ~117 B
  inside a 1024-B `dm_alloc_string` buffer. 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; negative
  `up_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_byte` check
  + value ≤0xff + ≥1 validation ⇒ write is in-bounds by construction.
* Bio chaining: `push_bio` reuse assert (vfs_bio.c:786) is safe because
  `biodone()` clears `bio_done` before invoking the callback
  (vfs_bio.c:3748-3751); `pop_bio` never frees (vfs_bio.c:795-798).
* Module unload is guarded: `dm_target_remove()` returns EBUSY while
  `ref_cnt > 0` (dm_target.c:181-184); `dm_target_alloc()` M_ZEROes unset
  handlers and `dm_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_iodone`
  async 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.
