# DF-5106 — emu_midi_detach() ignores mpu401_uninit() failure (emu10kx-midi.c)

**Finding**: `sys/dev/sound/pci/emu10kx-midi.c:229` — `emu_midi_detach()`
discards the return value of `mpu401_uninit()`. When any process holds the
MIDI cdev open (`/dev/midiN.M`, created 0666 by `midi_init()`),
`mpu401_uninit()` → `midi_uninit()` returns ENXIO **without freeing the
mpu401, the snd_midi, or destroying the cdev** — yet `emu_midi_detach()`
still returns 0. newbus then kfrees the device softc
(`kern/subr_bus.c:2148` → `device_set_driver(dev, NULL)` →
`kfree(dev->softc, M_BUS)`) while:

- the cdev stays live with `si_drv1 = m` (any user can keep using it),
- `m->cookie` and the armed `m->timer` callout reference the freed softc,
- `m->si` points into the now-unloadable driver module.

## What is in this pack

| file | what |
|---|---|
| `simid.c` | kld module replicating the exact emu_midi_attach/emu_midi_detach call sequence against the REAL mpu401/midi framework (hardware-independent part of the driver) |
| `trig.c` | unprivileged trigger: opens `/dev/midiN.M` (0666) and writes MIDI bytes |
| `build.sh` | (documented) in-tree kld build actually used on the guest |
| `run.log` | Stage A console log: ENXIO ignored → softc freed → UAF callback on freed softc |
| `run.2.log` | Stage B: `kldunload` returns 0 → **kernel panic** (call through stale `m->si` into unmapped module text) |
| `run.3.fixvalidation.log` | same scenario with the fix semantics (-DFIXED): unload refused, no free, guest alive |
| `panic.txt` | the Fatal trap 12 dump |
| `env.txt` | guest identity, md5s proving guest source == audit target, no-emu10kx evidence |
| `fix.diff` | one-hunk fix for `sys/dev/sound/pci/emu10kx-midi.c` |

## Build (on the DragonFly guest, root)

```sh
mkdir -p /usr/src/sys/dev/sound/driver/simid
cp simid.c /usr/src/sys/dev/sound/driver/simid/
cat > /usr/src/sys/dev/sound/driver/simid/Makefile <<'EOF'
.PATH: ${.CURDIR}/../../midi
KMOD=	simid
SRCS=	device_if.h bus_if.h mpufoi_if.h
SRCS+=	simid.c
COPTS+=	-DFIXED          # only for the fix-validation build

.include <bsd.kmod.mk>
EOF
cd /usr/src/sys/dev/sound/driver/simid && make obj && make
cp /usr/obj/usr/src/sys/dev/sound/driver/simid/simid.ko /tmp/poc/
cc -O -o /tmp/poc/trig /tmp/poc/trig.c
```

## Run

Stage A (deterministic, non-fatal):
```sh
kldload /tmp/poc/simid.ko
# as unprivileged user:
/tmp/poc/trig 90 /dev/midi0.0 &
# as root:
sysctl hw.simid_detach=1     # the emu_midi_detach body: ENXIO printed + IGNORED
sysctl hw.simid_spray=1      # heap churn after the free
sysctl hw.simid_peek=1       # deliberate read of the freed softc slot
```

Stage B (full kldunload scenario → panic):
```sh
kldload /tmp/poc/simid.ko
# as unprivileged user:
/tmp/poc/trig 300 /dev/midi0.0 &
# as root:
kldunload simid              # returns 0 (!), kernel panics <=1 tick later
```

Expected (stage B): `simid[kldunload]: mpu401_uninit() returned 6 ... IGNORES`,
`softc kfree'd`, then `Fatal trap 12: page fault while in kernel mode`,
`instruction pointer = 0xffffffff82600120` (unmapped module text),
`current process = Idle` (softclock/callout context), `db>` prompt — guest dead.

## Why a simulation module

QEMU does not emulate the EMU10Kx PCI audio device, so the real
`snd_emu10kx` driver never attaches in a VM. The bug, however, lives in the
hardware-independent teardown sequence (`mpu401_uninit` → `midi_uninit` →
newbus softc free → leaked cdev/callout/`m->si`), all of which is framework
code that runs identically for any mpu401 consumer. `simid.c` reproduces
that sequence verbatim (same `mpu401_init`/`mpu401_uninit` calls, same
ignored-return detach body, same module-unload behaviour), so the observed
behaviour is the framework's true behaviour for this exact bug.
