# DF-1640 — Use-after-free + double-free of `DM_TABLE_PARAMS` string across table entries

## Summary
`dm_ioctl.c:688` declares `char *str;` and initializes it to NULL at line 695
(before the table-entry loop).  Inside the loop, line 771
`prop_dictionary_get_cstring(target_dict, DM_TABLE_PARAMS, &str)` only writes `*cpp`
**on success** (confirmed in `sys/libprop/prop_dictionary_util.c:185-198`: on failure
the output pointer is left untouched).  Line 791 `kfree(str, M_TEMP)` frees
unconditionally.  So on a second table entry that omits `params`, `str` still holds
the **dangling** pointer freed in iteration 1 → `kfree` frees it again →
**double-free** in `M_TEMP` slab zone.

## Severity / impact
- **Severity filed:** High
- **Verified impact:** `panic` (local DoS) on the default GENERIC kernel
  (`X86_64_GENERIC`, INVARIANTS ON).  The slab allocator's INVARIANTS check at
  `chunk_mark_free` (`kern_slaballoc.c`) catches the double-free immediately and
  panics before any heap grooming can land.
- **Primitive class:** write (double-free / slab freelist corruption) — but on
  GENERIC with INVARIANTS ON, the corruption never lands (caught at the second
  `kfree`).  On an INVARIANTS-OFF (`noinv`) kernel the double-free would silently
  corrupt the `M_TEMP` kmalloc-32 freelist; that is a **non-default-kernel**
  result.  The INVARIANTS trip is a valid blocker for default-GENERIC escalation.
- **Trigger credential:** operator group.
- **Precondition:** admin has loaded `dm` KLD module + a dm device must exist.

## Reproduce
```sh
kldload dm
pw groupmod operator -m <user>
./build.sh
./dm_poc create          # helper: create device "pocdev"
./run.sh                 # reload with two entries: 1st has params, 2nd omits params
# expected (BUG): panic: memory chunk ... is already free!
# expected (FIXED): EINVAL, guest stays up
```

## Mechanism (line-accurate)
1. Iteration 1 of the `while` loop (`dm_ioctl.c:730`): entry has `params="AAAA..."`.
   - `dm_ioctl.c:771` `get_cstring` succeeds → `str` = kmalloc'd "AAAA..." (17 bytes,
     `M_TEMP` kmalloc-32 bucket).
   - `dm_ioctl.c:783` `dm_table_init` — zero target has no `->init`, so str is
     ignored, ret=0.
   - `dm_ioctl.c:791` `kfree(str, M_TEMP)` — frees the chunk. **str is now dangling.**
2. Iteration 2: entry has **no** `params` key.
   - `dm_ioctl.c:771` `get_cstring` fails → `str` left unchanged (still dangling).
   - `dm_ioctl.c:791` `kfree(str, M_TEMP)` → **double-free** of the same chunk.
   - `chunk_mark_free` sees the chunk is already marked free →
     `panic: memory chunk 0x... is already free!`

## Exploit-chain assessment (write-class primitive)
- Bucket: `M_TEMP` / kmalloc-32 (17-byte string).
- The two `kfree` calls happen in the same syscall, iterations of the same loop,
  with no scheduler intervention between them.  There is **no reclamation window**
  to groom a victim object between the first and second free.
- On `noinv` (INVARIANTS OFF), the double-free would add the chunk to the freelist
  twice; a subsequent cross-syscall `kmalloc-32`/`M_TEMP` spray could reclaim it
  into two objects → type confusion.  This is a **non-default-kernel** escalation
  path (INVARIANTS OFF).  On default GENERIC it is a DoS (INVARIANTS catches it).
- **Outcome:** `panic` on GENERIC (valid INVARIANTS blocker).  No `uid0` on default
  kernel.

## Fix
`fix.diff`: (1) reset `str = NULL` at the start of each loop iteration before
`get_cstring`; (2) guard both `kfree(str)` sites with `if (str != NULL)`.  Matches
the finding's proposed fix.

## Fix validation
Patched `dm.ko` (combined with DF-1639+DF-1642 fixes), re-ran PoC: returns `EINVAL`
(the second entry's `dm_table_init` gets `str=NULL` → returns EINVAL at
`dm_table.c:814`), **no double-free**, guest stays up.
