# DF-1640 — VERDICT

## Verdict: REPRODUCED (panic / local DoS via slab double-free)

## Root cause
`sys/dev/disk/dm/dm_ioctl.c:771-791`:
```c
/* declared before loop, init NULL at :695 */
char *str;
...
while ((target_dict = prop_object_iterator_next(iter)) != NULL) {
    ...
    prop_dictionary_get_cstring(target_dict, DM_TABLE_PARAMS, &str); /* :771 */
    ...
    if ((ret = dm_table_init(target, table_en, str)) != 0) {
        ...
        kfree(str, M_TEMP);   /* :786 */
        ...
    }
    kfree(str, M_TEMP);       /* :791 -- unconditional */
}
```
`prop_dictionary_get_cstring` (confirmed at `sys/libprop/prop_dictionary_util.c:185-198`)
writes `*cpp` **only on success**.  On a table entry that omits `params`, the call
fails and `str` retains its previous value.  After iteration 1 freed `str` at :791,
iteration 2 (no params) reuses the dangling pointer and `kfree`s it again →
**double-free** in `M_TEMP` kmalloc-32.

## Evidence (baseline, unpatched dm.ko)
```
panic: memory chunk 0xfffff8008d580dd0 is already free!
cpuid = 2
chunk_mark_free() at chunk_mark_free+0xae 0xffffffff80655dbe
chunk_mark_free() at chunk_mark_free+0xae 0xffffffff80655dbe
_kfree() at _kfree+0x262 0xffffffff806580e2
Stopped at      Debugger+0x7c:  movb    $0,0xbdaf09(%rip)
db>
```
The `chunk_mark_free` INVARIANTS check (`kern_slaballoc.c`) catches the double-free
and panics before any heap grooming can land.  Triggered by operator-group `maxx`:
`create` device, then `reload` with `cmd_data=[{zero,params=AAAA...},{zero,no params}]`.

## Exploit-chain assessment
- **Primitive:** double-free of a 17-byte `M_TEMP` string (kmalloc-32 bucket).
- **On GENERIC (INVARIANTS ON):** the second `kfree` trips `chunk_mark_free`'s
  already-free check and panics immediately.  The two `kfree` calls are in the same
  syscall (consecutive loop iterations) with no scheduler intervention between them,
  so there is **no reclamation window** to groom a victim object between frees.
- **On `noinv` (INVARIANTS OFF):** the double-free would silently corrupt the slab
  freelist (chunk appears twice); cross-syscall `M_TEMP`/kmalloc-32 spray could
  reclaim it into two objects → type confusion → potential `uid0`.  This is a
  **non-default-kernel** escalation path.
- **Outcome:** `panic` on default GENERIC (valid INVARIANTS blocker).  No `uid0` on
  default kernel.  The write-class primitive is confirmed but neutralized by
  INVARIANTS on the realistic target.

## PoC changes
Authored `dm_poc.c` from scratch.  The `1640` case crafts a `reload` with two table
entries: the first has `params="AAAAAAAAAAAAAAAA"` (17 bytes → kmalloc-32), the
second omits `params` entirely, exercising the stale-`str` → double-free path.

## Fix (fix.diff)
```diff
+       str = NULL;
        prop_dictionary_get_cstring(target_dict, DM_TABLE_PARAMS, &str);
        ...
-       kfree(str, M_TEMP);       /* error path */
+       if (str != NULL)
+           kfree(str, M_TEMP);
        ...
-       kfree(str, M_TEMP);       /* normal path */
+       if (str != NULL)
+           kfree(str, M_TEMP);
```
Matches the finding's proposed fix (`str=NULL when get_cstring fails + only kfree if
str!=NULL`).  Resetting `str=NULL` each iteration breaks the stale-pointer chain;
the NULL guards make both `kfree` sites safe.

## Fix validation
Patched `dm.ko`, re-ran PoC:
```
DF-1640 reload(double-free str): rc=-1 errno=22 (Invalid argument)
EXIT=0
```
With the fix, the second entry's `dm_table_init(target, table_en, NULL)` returns
EINVAL at `dm_table.c:814` (`if (params == NULL) return EINVAL`), the error path
returns cleanly, and no double-free occurs.  Guest stayed up.  **fix_status = fixed.**
