# DF-4283 — midi(4) kqueue filter detach removes knote from BOTH klists → NULL-deref panic

## What

`midi_kqfilter()` (sys/dev/sound/midi/midi.c:901-930) inserts each knote into
exactly ONE of `m->rkq.ki_note` (EVFILT_READ) or `m->wkq.ki_note`
(EVFILT_WRITE), but the shared detach callback `midi_filter_detach()`
(midi.c:932-941) removes the knote from **both** lists. `knote_remove()`
(sys/kern/kern_event.c:1902-1909) is a plain `SLIST_REMOVE`
(sys/sys/queue.h:208-219) with **no membership guard**: for a knote that is
not in the list it walks `curelm = SLIST_NEXT(curelm, ...)` until `curelm`
becomes NULL and dereferences it — page fault on the unmapped low page →
**kernel panic**. (Empty foreign list: immediate NULL deref; foreign list
holding a *different* knote: walk to the end, then NULL deref — both modes
demonstrated.)

Correct in-kernel idiom for comparison: sys/kern/tty.c:1308-1314 /
1334-1340 use one detach callback per list.

## Trigger (unprivileged, when a MIDI device exists)

/dev/midiN.M is created with mode **0666** (midi.c:364-366). Any local user:

```c
fd = open("/dev/midi0.1", O_RDWR);
kq = kqueue();
EV_SET(&kev, fd, EVFILT_READ, EV_ADD, ...); kevent(kq, &kev, 1, ...);
/* any of: EV_DELETE, close(kq), process exit  ->  filter detach -> panic */
```

Detach fires via `knote_detach_and_drop()` on EV_DELETE
(sys/kern/kern_event.c:1365-1368) and on kqueue teardown
(sys/kern/kern_event.c:706-708). It is enough to run `kevent(2)` once with
EV_ADD and exit the process.

## Hardware gating (why no live kernel trigger on the audit guest)

`/dev/midiN.M` exists only when some driver calls `midi_init()`. The only
in-tree caller chain is `snd_emu10kx` → `emu_midi_attach` → `mpu401_init()`
(sys/dev/sound/pci/emu10kx-midi.c:201 → sys/dev/sound/midi/mpu401.c:185 →
midi_init). QEMU cannot emulate an EMU10K1/EMU10K2 PCI audio card, so the
raw-MIDI node cannot appear on the audit guest. On real hardware with a
SoundBlaster Live!/Audigy (driver auto-loads), the panic is deterministic.

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

```
cd findings/poc/DF-4283
./build.sh          # cc -O0 -g -Wall -o midi_kq_model midi_kq_model.c
                    # cc -O0 -g -Wall -o kq_trigger kq_trigger.c
./run.sh
```

Expected (see run.log):
- `empty`  → SIGSEGV at faulting address 0x0, exit 99  (kernel: panic)
- `peer`   → SIGSEGV at faulting address 0x0, exit 99  (kernel: panic)
- `fixed`  → clean removal, exit 0   (fix.diff semantics)
- `control`→ clean, exit 0
- `kq_trigger /dev/midistat` → live kevent add/delete mechanics exercised
  on the real guest syscalls, guest stays alive (midistat has no kqfilter,
  ENODEV on EV_ADD — proving only the missing MIDI hardware blocks the
  live trigger).

The harness copies the queue macros and `knote_remove()` verbatim from
sys/sys/queue.h:191-219 and sys/kern/kern_event.c:1902-1909 (LWKT pool
token omitted — an SMP exclusivity primitive irrelevant to single-threaded
list semantics) and `midi_filter_detach()` verbatim from midi.c:932-941.

## Fix

`fix.diff` — split the detach callback per filter (mirrors the tty(4)
idiom). `midi_kq_model fixed` mode models the patched behavior: clean.
