# DF-5065 VERDICT — reproduced (deterministic panic), fix validated

## What was claimed

`dm_target_uninit()` (sys/dev/disk/dm/dm_target.c:277-295) tears down the
global target registry with **zero validation**: it frees *every* registered
`dm_target_t` regardless of `ref_cnt` (contrast `dm_target_remove`, which
carefully returns EBUSY at dm_target.c:181-184) and `lockuninit`s
`dm_target_mutex` while registered/referenced targets exist. Its only caller
is `dmdestroy()` (device-mapper.c:187-197), which ignores its return value
and always returns 0 to MOD_UNLOAD.

## Why it is exploitable — two reachable variants

**Variant 1 — deterministic, race-free (the one reproduced here).**
`linker_file_unload()`'s veto path does `file->refs--` *even when refs==1*
(sys/kern/kern_linker.c:514-519) — i.e. a **failed** `kldunload dm` (EBUSY
because a dm device exists, device-mapper.c:161) destroys dm.ko's base
reference. Trace, all verified line-by-line in-tree:

1. `kldload dm` — dm.ko refs=1 (kern_linker.c:456), userrefs=1 (:1353);
   builtin error/zero registered via SYSINIT (dm_target_error.c / zero.c,
   `DM_TARGET_BUILTIN`).
2. create a tableless device — `dm_dev_counter` 0→1 (dm_dev.c:193).
3. `kldunload dm` — refs==1 → not deferred (:494) → `module_unload` →
   `dm_modcmd` EBUSY → veto → **refs-- 1→0** (:518); userrefs restored (:856).
4. remove the device — counter 1→0 (dm_dev.c:71).
5. `kldload dm_target_linear` — MDT_DEPEND refs++ → dm refs 0→1
   (kern_linker.c:1367); the `refs > 1` deferral can never fire again.
6. `kldunload dm` — refs==1 → full unload attempt → counter==0 →
   `dmdestroy()` → `dm_target_uninit()` **frees linear's registered entry
   (heap) and lockuninit's the registry lock** → dm.ko unloaded, text freed.
7. `kldunload dm_target_linear` — `dm_target_remove("linear")` executes
   **freed dm.ko text** → Fatal trap 12.

The SYSUNINIT builtins (`dm_builtin_uninit`, device-mapper.c:617-624) also
call `dm_target_remove` after the lock was destroyed — they survive only
because `lockuninit` (kern_lock.c:1325-1333) doesn't poison the still-mapped
lock and the list walks empty — benign *by luck*, same root cause.

**Variant 2 — the DF-4751 race.** Any ioctl thread holding a busied target
(dm_ioctl.c:737 lookup, ref transferred to `table_en->target` at :759) when
the counter check is passed leaves live tables with dangling `dm_target_t`
pointers → `dm_table_destroy`'s `table_en->target->destroy(...)`
(dm_table.c:150-157) is a heap UAF. This variant is not separately
demonstrable on the guest (it requires the same interleaving as DF-4751 and
crashes in whichever site faults first), but the code path is unambiguous:
nothing in `dm_target_uninit` distinguishes ref_cnt>0 entries.

## Reproduction

- Baseline: `vm.sh reset with-src` → build (BUILD_OK) → `sh /tmp/run.sh` →
  **Fatal trap 12 on the first and only attempt** (no threads, no retry
  loops, no timing). panic.txt: IP=0xffffffff82604950, "supervisor read
  instruction, page not present" — the freed dm.ko text region (dm.ko was
  mapped at 0xffffffff82600000, size 0x6000, per kldstat in the patched run;
  DF-4751's freed-text signatures were in the same region). Current process
  = the `kldunload dm_target_linear` syscall. Guest down at `db>`.
- The console printed `Device Mapper unloaded` *while dm_target_linear was
  loaded and registered* — the smoking gun that uninit tore the registry
  down with a foreign module's entry in it.

## Fix validation (fix.diff — dm_target.c + device-mapper.c, both in dm.ko)

- Applied to the guest's /usr/src, `make obj && make` in
  sys/dev/disk/dm (-Werror clean), dm.ko installed, guest fresh otherwise.
- Identical PoC (`sh /tmp/run.sh`): step 5 now prints
  `kldunload: can't unload file: Device busy` (dm_target_uninit → EBUSY →
  dmdestroy propagates → dm_modcmd vetoes); kldstat shows dm.ko Refs=0 but
  **loaded**; step 6 `kldunload dm_target_linear` returns 0 cleanly through
  live text; the dependency cascade then unloads dm.ko itself (correct
  order); `STILL_UP`.
- Design note: the fix also makes `dmdestroy` unregister the builtin
  error/zero targets explicitly *before* the registry check (their SYSUNINITs
  run after the MOD_UNLOAD handler, so without this the "registry must be
  empty" rule would make dm permanently unloadable). Normal unload keeps
  working — demonstrated by the clean cascade unload at the end of the
  patched run. Known cosmetic degradation: if the veto fires, error/zero
  stay unregistered while dm remains loaded (tables using them would then
  fail to load); strictly safer than the UAF, and only reachable in the
  corrupted-refs state.
- **fix_status: fixed** — baseline panics, patched does not, exact same PoC.

## Exploit chain ceiling

Trigger side needs root (`kldload`/`kldunload`; `kern.securelevel` must be
≤0 — it is -1 on the guest and by default). The ioctl side alone is
operator-group reachable (0640 root:operator, device-mapper.c:181), but the
deterministic chain needs the privileged unload. Impact: freed-kernel-text
execution (deterministic) + freed-heap `dm_target_t` use (race variant) —
a code-execution-class primitive, but groomed by root against root ⇒
Medium (deterministic kernel panic at minimum; the memcorrupt-class
primitive is real and was demonstrated as a trap-12).

## Honesty notes

- `attempts` = 1 baseline + 1 patched; deterministic chain hit on the first
  try both times.
- The kern_linker.c:518 refs-- enabler is *out of this finding's file*;
  the finding is the missing in-file validation (the second veto), and the
  fix stands alone: with it, the chain dies even with the linker bug present
  (proven on the guest). The linker bug should still get its own finding —
  it affects every module that vetoes unloads.
- Guest reset `with-src` performed after the runs; no `sys/` file in the
  audit tree was modified (fix.diff authored against the read-only tree,
  applied only inside the disposable guest).

## 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.
