# DF-2436 — dm_target_crypt_destroy use of uninitialized heap memory on partial-init failure

## Summary
`dm_target_crypt_init()` (`sys/dev/disk/dm/crypt/dm_target_crypt.c`) allocates the
crypt config `priv` WITHOUT `M_ZERO` (line 489), publishes it to
`table_en->target_config` at line 513 (`dm_table_init_target`), then has 5
`goto notsup` error paths (lines 521/533/543/552/564) that are reachable AFTER
the publish but BEFORE the fields `ivgen`, `ivgen_priv`, `crypto_session`,
`status_str`, `read_mpipe`, `write_mpipe` are initialized (lines 547/549/577/580).
The caller `dm_table_load_ioctl` (`dm_ioctl.c:783-785`) reacts to the init error
by calling `dm_table_destroy` → `dm_target_crypt_destroy`, which reads those
uninitialized fields — `dmtc_destroy_mpipe` → `mpipe_done` → `lwkt_gettoken` on
the garbage `mpipe->token` → panic (INVARIANTS). CWE-908.

## Privilege
Root/operator-only. `/dev/mapper/control` is `0640 root:operator`
(`device-mapper.c:181`), the `dm` module is demand-loaded via root-only
`kldload`, and `dm_target_crypt` auto-loads from there. Verified: unprivileged
`maxx` (uid 1001, not in wheel/operator) gets `Permission denied`. This is a
root→kernel hardening gap / local DoS, **not** an unprivileged→root escalation.
There is no unprivileged path, so uid0 is a valid-hard-blocked non-goal.

## Reproduce
```sh
./build.sh && ./run.sh         # as root, after `kldload dm` (run.sh does it)
```
- **Build:** `cc -O2 -o dm_crypt_uninit dm_crypt_uninit.c -lprop`
- **Run as root** (must be root or operator-group to open the control dev).
- **Expected on the BUGGY (unpatched) kernel:** guest PANICS in `mpipe_done`
  during `dm_target_crypt_destroy` — ssh dies, `boot.log` shows
  `panic: assertion "count & TOK_COUNTMASK" failed ... mpipe_done() ...
  dm_target_crypt_destroy()`. (`panic.txt` is the captured signature.)
- **Expected on the FIXED kernel:** reload returns `rv=45 (ENOTSUP)` cleanly,
  guest stays up, no panic. (`fix_run.log`.)

## How the PoC works
`dm_crypt_uninit` opens `/dev/mapper/control`, creates a dm device, then
`reload`s a `crypt` table with params `"aes-xts-bogusiv <hexkey> 0 /dev/md0 0"`.
`dm_target_crypt_init` then:
1. allocates `priv` without `M_ZERO` (raw heap)            [line 489]
2. `dm_pdev_insert("/dev/md0")` succeeds                   [line 492]
3. `dmtc_find_crypto_cipher("aes","xts",256)` succeeds     [line 498]
4. `dm_table_init_target` PUBLISHes `priv` (garbage)       [line 513]
5. `hex2key` succeeds                                      [line 515]
6. `iv_mode="bogusiv"` NOT in `ivgens` → `goto notsup`     [line 533]
The caller sees ENOTSUP, calls `dm_table_destroy` → destroy reads the
uninitialized `read_mpipe.write_mpipe.status_str.ivgen.crypto_session` fields
→ `mpipe_done` → `lwkt_gettoken` on the garbage token → panic.

## Fix
See `fix.diff`: add `M_ZERO` to the `kmalloc` at line 489 so all fields start
NULL/0; then `dm_target_crypt_destroy` is a safe no-op on any partial-init path
(`mpipe_done` is guaranteed safe on a zero'd `malloc_pipe` per
`kern_mpipe.c:117-121`; `status_str`/`ivgen`/`crypto_session` are NULL so their
guards/frees short-circuit). Validated by rebuilding the `dm_target_crypt`
module and re-running the PoC — panic → clean ENOTSUP return, guest survives.
