# DF-4614 — ips command-queue lifecycle double-free / UAF (unchecked ips_cmdqueue_init + unconditional kfree in ips_cmdqueue_free)

## What
`sys/dev/raid/ips/ips.c:414` (init return ignored), `ips.c:139-141`
(`kfree(sc->commandarray)` unconditional, pointer not cleared),
`ips.c:125` (UAF read of `command_phys_addr` on re-entry),
`ips.c:447-451 → 462 → 533` (second trigger), `ips.c:121-138` (array freed
even while commands outstanding).

## Trigger A — first init fails (ips.c:414)
`sc->max_cmds = 1; ips_cmdqueue_init(sc);` — the return value is **ignored**.
On failure (`bus_dmamem_alloc` ips.c:164, `bus_dmamap_create` ips.c:181, or
`command_phys_addr == 0` ips.c:171 — i.e. DMA map exhaustion / load failure at
attach) the internal error path already ran `ips_cmdqueue_free(sc)`
(ips.c:190), which `kfree`s `sc->commandarray` (ips.c:140) **without setting
it to NULL** and marks the adapter `IPS_OFFLINE` (ips.c:137).
`ips_adapter_init` then keeps going: `ips_adapter_reinit` (ips.c:416) still
succeeds (the card itself is healthy), and the very next command
(`ips_ffdc_reset`, ips.c:421) fails with EIO because of IPS_OFFLINE →
`goto error` → `ips_adapter_free(sc)` (ips.c:462) → `ips_cmdqueue_free(sc)`
**again** (ips.c:533):
* ips.c:124-125 walks `sc->commandarray[i]` — **use-after-free read**;
* if the freed chunk was reused and `command_phys_addr != 0`,
  ips.c:127-134 run `bus_dmamap_unload`/`bus_dmamem_free`/`bus_dmamap_destroy`
  on garbage pointers — arbitrary free;
* ips.c:140 `kfree(sc->commandarray)` — **double free** → kernel heap
  corruption.

## Trigger B — second init fails (ips.c:447-451)
The re-sized init after `ips_get_adapter_info` is checked, but its failure
path has the identical shape: internal free already happened → `goto error`
→ `ips_adapter_free` → second `ips_cmdqueue_free` → same double free.

## Trigger C — detach with commands in flight
`ips_cmdqueue_free` executes `kfree(sc->commandarray)` at ips.c:140 **even
when `used_commands != 0`** (the `if` at ips.c:122 only guards the loop and
the error value). Detach/unload with an outstanding command frees the array
under a live interrupt handler (ips.c:570/573, ips.c:773-775 walk it), and
`ips_adapter_free` then *returns EBUSY leaving the driver loaded on a
dangling array* (ips.c:533-537) — subsequent I/O is UAF.

## Harness (control-flow proof, run on the audit guest)
`doublefree_harness.c` mirrors the exact call sequence with stubbed bus_dma
and a double-free-detecting allocator. Built and executed on the DragonFly
guest itself:

```
$ cc -O0 -g -o harness harness.c && ./harness
[!] DOUBLE FREE: kfree(#2) on 0x800490ec0 already freed at kfree(#1)
exit=42
```

This proves the call sequence `ips_adapter_init → (failed) ips_cmdqueue_init
→ ips_cmdqueue_free → … → ips_adapter_free → ips_cmdqueue_free` reaches two
kfrees of the same pointer (and an intervening UAF read). It does **not**
execute the real in-kernel driver — the guest has no ServeRAID controller
(`env.txt`), so the kernel path itself is `not_reproduced` on this machine.

## Contents
`VERDICT.md`, `doublefree_harness.c`, `build.log`, `run.log`, `env.txt`,
`fix.diff`, `manifest.json`, `verdict.json`.
