# DF-4751 — VERDICT

**Status: REPRODUCED (kernel panic, use-after-free of module text; four
distinct crash signatures captured across baseline AND two attempted in-driver
fixes — the fixes demonstrably do NOT close it: `fix_failed`).**
Impact: privileged local kernel panic / potential code-execution-class UAF.
Severity Low (trigger requires root: `kldunload dm` on one side; the ioctl
side alone is operator-group reachable but cannot panic the box alone).

## Root cause (two compounding windows)

**Window 1 — the admitted counter race.**
`dm_modcmd(MOD_UNLOAD)` (sys/dev/disk/dm/device-mapper.c:161-165) reads
`dm_dev_counter` with no lock while the only writer, `dm_dev_insert()`
(sys/dev/disk/dm/dm_dev.c:193), increments under `dm_dev_mutex`. The
source even carries the comment `/* race window here */`. When an ioctl
"create" slips past the check, unload proceeds with live devices:
every crashing run's console shows
`malloc_uninit: 1984 bytes of 'dm' still allocated` — allocations that
outlived `dmdestroy()`.

**Window 2 — unpinned ops dispatch (framework-level).**
`dev_dioctl()` (sys/kern/kern_device.c:237-260) executes
`dev->si_ops->d_ioctl(&ap)` with no reference on the ops or module, and
`destroy_dev()`'s revector to `dead_dev_ops` happens asynchronously via the
devfs core (sys/vfs/devfs/devfs_core.c:755-765: "asynchronous entry point").
So a `NETBSD_DM_IOCTL` already executing `dmioctl`
(sys/dev/disk/dm/device-mapper.c:241) keeps running module text after the
linker frees `dm.ko` → Fatal trap 12.

## Reproduction

`dm_unloadrace` (proplib ioctl client, adapted from the DF-4473 harness):
thread A cycles create→remove on `df4751dev` (counter oscillates 0↔1, control
fd opened per iteration); thread B loops `kldunload dm` / `kldload dm`.
30-45 s runs on the QEMU guest:

- run 1 (stock): panic at `dev_dioctl+0x1b` — read through freed ops.
- run 2 (fresh `vm.sh reset with-src` guest, stock): panic `IP = 0x0` —
  call through zeroed/freed ops entry.
- run 3 (fix v1: `destroy_dev(dmcdev)` before the counter check + re-arm
  on counter>0): STILL panics, executing freed module text
  (`0xffffffff82601029`).
- run 4 (fix v2: v1 + `sync_devs()` — the kern_conf.c-documented unload
  contract): STILL panics — `Stopped at dmioctl+0x2bf` — i.e. an ioctl
  already inside `dmioctl` when the module text was freed.

`vm.sh status` → down after each crashing run; full signatures in panic.txt.

## Why the in-driver fixes failed (and what the real fix is)

`sync_devs()` (sys/kern/kern_conf.c:349-362) only waits for the devfs core
message queue; it cannot observe a thread that has already loaded
`si_ops->d_ioctl` and is executing it. Window 2 is a dev-dispatch framework
gap that no `dm_modcmd` reordering can close. Correct remediation:

- Framework: hold a reference on `dev_ops` (and its module) across every
  `dev_d*` dispatch — acquire in `compile_dev_ops`/`make_dev`, per-call
  acquire/release in `dev_dioctl` and siblings; or revector synchronously
  (`devfs_msg_send_sync`) and make the linker honor a per-open-cdev module
  reference so `kldunload` returns EBUSY while any control fd is open.
- Driver-side (defense in depth, shrinks but does not close): the v2 diff in
  fix.diff — destroy + `sync_devs()` + stable counter check + re-arm — plus
  an in-flight-ioctl drain (enter/exit bracket counter that MOD_UNLOAD
  waits on).

## Threat

Root can already crash the box many ways, so the panic itself is Low.
The interesting ceiling: the freed kld region is returned to the linker
allocator; execution continuing in freed-but-mapped text is a
code-execution-class UAF — with a groomed reuse it is in principle more
than a panic, but that requires root-timed racing against one's own
kldunload, so no unprivileged chain exists. Filed for correctness and
because the same framework gap (unpinned ops dispatch) affects every
loadable driver, incl. nmdm.ko (no MOD_UNLOAD handler at all).

## Honest notes

- 40+ unloads typically succeed benignly before the interleaving hits; the
  crash usually lands within 30-60 s of churn (4/4 runs crashed: 2 baseline,
  2 patched).
- The churn thread is the DF-4473 client trimmed to create/remove only —
  no table/target modules involved, so the panic is core-dm, not target code.

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