# DF-4473 — dm_table_load_ioctl mutates the inactive table under a SHARED lock: concurrent reloads corrupt the TAILQ

**REPRODUCED (kernel panic / list corruption race) + FIX VALIDATED.**

## The bug

Table readers/writers are supposed to be serialized by `table_mtx`:
`dm_table_get_entry()` takes it **LK_SHARED** (sys/dev/disk/dm/dm_table.c:65)
and holds it for the whole walk; `dm_table_destroy()`/`dm_table_switch_tables()`
take it LK_EXCLUSIVE. But `dm_table_load_ioctl()` is a **writer that runs
under the SHARED hold**:

```c
722  if (dmv->flags & DM_INACTIVE_PRESENT_FLAG)
723      dm_table_destroy(&dmv->table_head, DM_TABLE_INACTIVE);
726  tbl = dm_table_get_entry(&dmv->table_head, DM_TABLE_INACTIVE);  /* SHARED */
...
774  TAILQ_INSERT_TAIL(tbl, table_en, next);                          /* mutation */
```

Two concurrent reloads therefore both append to the same TAILQ at once.
`TAILQ_INSERT_TAIL` (sys/sys/queue.h:628) is four plain stores; racing
threads reading the same `tqh_last` overwrite each other's publish store,
leaving the tail pointer referencing a non-tail slot (and orphaning entries).
The conditional destroy at :722-723 usually serializes loads against each
other — but it is skipped whenever `DM_INACTIVE_PRESENT_FLAG` is clear,
i.e. exactly after `clear` (dm_table_clear_ioctl) or `resume`
(dm_ioctl.c:505 clears it).

## Reproduction (baseline, unpatched)

`./dm_loadrace 6 400 16` — 6 threads, 400 rounds of [barrier → one reload
with 16 error-target entries each → barrier → clear]:

```
panic: Bad tailq NEXT(0xfffff801173eebf8->tqh_last) != NULL, last 0xfffff80117492080
dm_table_load_ioctl() at dm_table_load_ioctl+0x6bf
dm_table_load_ioctl() at dm_table_load_ioctl+0x6bf
dmioctl() at dmioctl+0x2eb ...
Stopped at Debugger+0x7c
```

The QUEUEDEBUG tailq invariant (`QMD_TAILQ_CHECK_TAIL`, queue.h:629) fired
inside `TAILQ_INSERT_TAIL` in `dm_table_load_ioctl` — two racing inserts had
torn the tail. Without QUEUEDEBUG the same interleaving writes through a
stale `tqh_last` (mislinked list / orphaned entries that no destroy path can
free: permanent M_DM leak + `dm_target_busy` reference leak pinning loadable
target modules + pdev leaks). Guest required a hard reset.

## Impact ceiling

The demonstrated primitive is uncontrolled kernel list corruption from two
racing ioctls (write of a valid pointer through a wrong list slot). On this
code the reliable outcome is panic / leaked entries; a groomed exploitation
would additionally require controlling which kmalloc'd entry lands in the
wrong slot — not demonstrated. Precondition: operator group (mapper/control
is 0640 root:operator). Filed High: kernel memory corruption reachable from
a semi-privileged local user with a trivially-winnable race window (each
entry insert races inside a loop that repeats 16× per reload).

## Fix validation

`fix.diff` (shared with DF-4472/DF-4474): the load path now (a) takes
`dmv->dev_mtx` (recursive, only briefly used by busy/unbusy) exclusively for
the whole load, serializing concurrent loads on the device, and (b) links the
entry with `TAILQ_INSERT_TAIL` only **after** `dm_table_init()` fully
initialized it (readers walking under SHARED can never observe a half-built
entry), with the init-failure path cleaning the entry directly.

* Baseline: panic mid-run (above), guest down.
* Patched (`make` -Werror clean in sys/dev/disk/dm, kldload dm.ko):
  400/400 rounds, 2400/2400 reloads OK, **no panic**, guest healthy, and
  after `remove_all` the `dm` malloc type shows **no residual in-use
  objects** (no orphans). (run.log)

## Files

* `dm_loadrace.c` — PoC (barriered concurrent reloads + clear, M_DM check)
* `run.log` — baseline panic + patched clean run
* `panic.txt` — full serial-console excerpt
* `fix.diff` — combined dm_ioctl.c fix (validated)
* `env.txt`, `build.sh`, `run.sh`

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