# DF-5106 VERDICT — emu_midi_detach() ignores mpu401_uninit() failure

**Status: REPRODUCED** (framework level; hardware-gated on the real driver)
**Impact: kernel memory corruption (use-after-free) + deterministic kernel
panic via `kldunload`; unprivileged-user-reachable consequence after a
privileged detach trigger.**

## The bug (code trace)

`sys/dev/sound/pci/emu10kx-midi.c:222-233`:

```c
static int
emu_midi_detach(device_t dev)
{
        struct emu_midi_softc *scp;

        scp = device_get_softc(dev);
        mpu401_uninit(scp->mpu);                 /* :229  rv DISCARDED */
        emu_intr_unregister(scp->card, scp->ihandle);
        lockuninit(&scp->lock);
        return (0);                              /* :232  success anyway */
}
```

`mpu401_uninit()` (`sys/dev/sound/midi/mpu401.c:196-207`) propagates
`midi_uninit()`'s error and does **not** free the mpu401 when it fails.
`midi_uninit()` (`sys/dev/sound/midi/midi.c:394-422`) fails with ENXIO when
`m->busy` and no reader/writer is asleep — i.e. exactly when an
unprivileged user simply holds `/dev/midiN.M` open (the cdev is created
0666 by `midi_init()`, `midi.c:364-366`; `midi_open()` sets `m->busy`,
`midi.c:653`). On that failure path the mpu401, the snd_midi (still in the
global `midi_devs` list) and the cdev all survive — but `emu_midi_detach`
returns 0, so newbus tears the device down anyway:
`kern/subr_bus.c:2148 device_set_driver(dev, NULL)` →
`kern/subr_bus.c:1943-1946 kfree(dev->softc, M_BUS)`.

Post-detach state (all verified live on the guest):

- `/dev/midiN.M` still exists, mode 0666, `si_drv1 = m` (midi_destroy never
  ran) — any user can keep reading/writing it;
- `m->cookie` = the freed `struct emu_midi_softc` — every mpufoi callback
  (`emu_mread`/`emu_mwrite`/`emu_muninit`) dereferences freed memory, and
  `emu_mwrite` writes MMIO through `sc->card` **read out of the freed slab**
  (`emu10kx-midi.c:85-93` → `emu_wrptr(sc->card, ...)`);
- the mpu401 callout (`m->timer`) stays armed with `m->si = emu_midi_intr`
  inside the unloadable module (`mpu401.c:162-166`, `mpu401.c:265-267`);
- on `kldunload snd_emu10kx(_midi)` the module text is unmapped while `m`,
  the cdev and the callout persist.

## Reproduction (guest, 3 baseline runs + 1 fix run)

Hardware note: QEMU does not emulate EMU10Kx, so `snd_emu10kx` cannot
attach; verification runs the **exact teardown sequence** of the driver
against the real mpu401/midi framework via `simid.c` (same
`mpu401_init`/`mpu401_uninit` calls and the same ignored-rv detach body).
Guest source md5-identical to the audit tree (env.txt).

**Stage A** (`run.log`): with uid-1001 `trig` holding `/dev/midi0.0` open
and writing, `sysctl hw.simid_detach=1` ran the detach body:

```
simid[sysctl]: mpu401_uninit() returned 6 (FAILURE - m NOT freed, midi cdev STILL LIVE)  <<< emu_midi_detach IGNORES this and returns 0
simid[sysctl]: softc kfree'd (newbus behaviour after detach()==0); mpu401/snd_midi/cdev/callout still alive
simid: UAF: softintr on FREED softc 0xfffff801175f2f58 magic=51d10505 mpu_intr=0xffffffff82627fd0
```

- `mpu401_uninit()` returned **6 (ENXIO)** exactly as traced — the failure
  `emu_midi_detach` discards;
- the softc was freed while the framework objects stayed alive;
- the still-armed mpu401 callout then invoked `m->si(m->cookie)` on the
  **freed** softc (kernel read of freed 120-byte M_DEVBUF/M_BUS-class
  memory through the framework) — the unprivileged user's writes keep this
  arming indefinitely;
- `/dev/midi0.0` still present, trig (uid 1001) still running — the leak.
- The 64-block spray did not land on this particular freed slot in this run
  (peek showed stale content) — content control of the freed slot is
  standard kmalloc-zone grooming, demonstrated here only as a stale read.

**Stage B** (`run.2.log`, `panic.txt` — reproduced twice, identical fault
address both times): with trig writing, `kldunload simid` returned **0**
(the "clean success" the admin sees) and the kernel panicked within one
callout tick:

```
simid[kldunload]: mpu401_uninit() returned 6 (...)  <<< IGNORES this and returns 0
simid[kldunload]: softc kfree'd ...
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xffffffff82600120     <- unmapped module text
instruction pointer   = 0x8:0xffffffff82600120 <- RIP jumped via stale m->si
current process       = Idle                   <- softclock/callout context
db>
```

`0xffffffff82600120` is inside the just-unloaded module's mapping — the
callout's indirect call through `m->si`. On the real driver this is
`kldunload snd_emu10kx` (or card removal) while any user holds the MIDI
device open: silent "success" → guaranteed panic.

**Fix validation** (`run.3.fixvalidation.log`): rebuilding the module with
the exact semantics of `fix.diff` (abort the detach when
`mpu401_uninit()` fails, i.e. MOD_UNLOAD returns the error):

```
simid[kldunload]: mpu401_uninit() returned 6 (...)  <<< IGNORES this and returns 0
simid[kldunload,FIXED]: aborting detach (device stays attached, softc NOT freed, module NOT unloadable)
kldunload: can't unload file: Device not configured      (RC=1)
GUEST_STILL_ALIVE
```

No free, no UAF, no panic; the admin now correctly sees the unload refused.
(Baseline for the same run pair: identical unfixed build panicked at the
same VA — the accidental second unfixed run doubles as a stress re-run.)

## Exploit chain assessment (uid0?)

Primitive on real hardware, after the admin action (kldunload / card
unplug) while the attacker's fd is open:

1. freed ~120-byte device softc (M_BUS zone) whose contents the attacker
   re-grooms via same-size allocations;
2. unprivileged fd I/O drives `emu_mread/emu_mwrite(cookie=freed)`:
   `emu_wrptr(sc->card,...)` computes `sc->card` from the freed slot →
   attacker-chosen bus-space tag/handle → arbitrary kernel-scope write;
3. `emu_midi_card_intr` (or the callout) calls
   `(sc->mpu_intr)(sc->mpu)` — both read from the freed slot → arbitrary
   kernel call with one argument;
4. alternatively the unmapped-module `m->si` jump after unload is a
   reusable-VA call primitive.

That is a plausible unpriv→root chain on a machine with a SoundBlaster
Live!/Audigy. It is **not completable on this guest**: the entire attack
surface requires the emu10kx PCI device to be present (probe never binds
without it; no cdev is ever created). This is a hard environmental blocker,
documented rather than hand-waved: every step up to (but excluding) the
final MMIO/call primitive was reproduced live.

## Root cause line

- `sys/dev/sound/pci/emu10kx-midi.c:229` — `mpu401_uninit()` return value
  discarded; `:232` unconditional `return (0)`.
- enabling conditions: `sys/dev/sound/midi/mpu401.c:196-207`,
  `sys/dev/sound/midi/midi.c:394-422` (ENXIO on busy), `midi.c:364-366`
  (0666 cdev), `sys/kern/subr_bus.c:2148,1943-1946` (softc kfree).

## Recommended fix

Propagate the failure (`fix.diff`): `rv = mpu401_uninit(scp->mpu);
if (rv != 0) return (rv);` — validated on the guest (run.3): unload then
fails cleanly, nothing is freed, nothing dangles, no panic.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
