# DF-4284 — midi_unload() iterates midi_devs while midi_destroy() frees each entry → UAF read in the loop advance

## What

`midi_unload()` [sys/dev/sound/midi/midi.c:1469-1477] uses
`TAILQ_FOREACH(m, &midi_devs, link)` whose loop body calls
`midi_destroy(m, 1)` [midi.c:1474]. `midi_destroy()` removes the entry from
`midi_devs` [midi.c:1429] and **frees it** [midi.c:1436]. TAILQ_FOREACH
advances by evaluating `TAILQ_NEXT(m, link)` *after* the body — i.e. it
reads `m->link.tqe_next` from freed memory. The value read is whatever now
occupies the freed M_MIDI slab slot; with ≥2 midi devices the next loop
iteration then executes `lockmgr(&m->lock, LK_EXCLUSIVE)` /
`m->busy` on that pointer.

## Who can trigger

`midi_unload()` runs only from module unload (`kldunload` of the midi/
sound module) — **root** — and only destroys something when at least one
MIDI device is registered (again, only `snd_emu10kx` hardware registers
one). Impact ceiling: panic / undefined behavior driven by a
groom-controlled pointer during a privileged unload; not an unprivileged
primitive. Filed as a genuine lifetime defect (Low).

## Reproduce (model harness — runs on the guest)

```
cd findings/poc/DF-4284
./build.sh      # cc -O0 -g -Wall -o midi_unload_model midi_unload_model.c
./run.sh
```

Expected (see run.log):
- `reuse` — freed slot immediately reoccupied by a same-size "attacker"
  chunk with forged `link.tqe_next = 0x414141414140`; the loop advance
  reads it and the next iteration derefs `m->unit` at **0x414141414150**
  → SIGSEGV at exactly the attacker-chosen address (exit 99). Kernel
  equivalent: `lockmgr(&m->lock)` on an attacker-influenced pointer.
- `stale` — plain `free()`; tqe_next happens to survive on this libc, so
  the loop walks stale-but-plausible pointers (still a UAF read, value
  merely uncontrolled).
- `fixed` — fix.diff semantics (stash `TAILQ_NEXT` before destroy): all
  three devices visited and destroyed cleanly, exit 0.

TAILQ macros in the harness are verbatim from sys/sys/queue.h (kernel
build configuration; TRACEBUF/diagnostic bits omitted).

## Fix

`fix.diff` — replace TAILQ_FOREACH with an explicit loop that stashes
`TAILQ_NEXT(m, link)` before `midi_destroy(m, 1)` can free `m`.
(The dead `exit1` path that would re-release m->lock after a successful
destroy is unreachable — midi_destroy() always returns 0 — and is left
untouched by this minimal diff; noted in VERDICT.md.)
