emu_midi_detach() ignores mpu401_uninit() failure: with the 0666 MIDI cdev open, detach reports success while newbus kfrees the softc the still-live cdev/mpu401/callout keep using (UAF + deterministic kldunload panic)
Summary
midi_uninit returns ENXIO without freeing when the world-writable cdev is open; mpu401_uninit propagates it without freeing - but detach returns 0 and newbus kfrees the softc. Left behind: the live cdev (si_drv1=m, any user), m->cookie==freed softc (mwrite derefs sc->card for MMIO writes, card_intr calls sc->mpu_intr), and the armed callout - after kldunload it jumps into unmapped module text. FRAMEWORK-LEVEL REPRODUCED on guest (real mpu401/midi code, exact teardown sequence): Stage A captured the ignored-ENXIO + freed-softc + callout-on-freed-memory; Stage B kldunload panic at the identical unmapped fault VA 2/2; fix (propagate rv) validated (unload refused, no panic). uid0 chain (forge sc->card for arbitrary writes + sc->mpu_intr for arbitrary call) documented, hardware-gated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5106 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| simid.c | β | 9.0 KB | view raw | |
| trig.c | β | 1.3 KB | view raw | |
| build.sh | β | 234 B | view raw | |
| build.log | β | 7.8 KB | view raw | |
| run.log | β | 1.1 KB | view raw | |
| run.2.log | β | 1.1 KB | view raw | |
| run.3.fixvalidation.log | β | 397 B | view raw | |
| run.stageA.console.log | β | 742 B | view raw | |
| run.stageA.trig.log | β | 189 B | view raw | |
| run.2.panic.console.log | β | 535 B | view raw | |
| panic.txt | β | 1.1 KB | view raw | |
| env.txt | β | 1.1 KB | view raw | |
| fix.diff | β | 937 B | view raw | |
| VERDICT.md | β | 7.3 KB | β raw | |
| README.md | β | 3.7 KB | β raw |
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->cookieand the armedm->timercallout reference the freed softc,m->sipoints 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)
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):
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):
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.
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:
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.Mstill exists, mode 0666,si_drv1 = m(midi_destroy never ran) β any user can keep reading/writing it;m->cookie= the freedstruct emu_midi_softcβ every mpufoi callback (emu_mread/emu_mwrite/emu_muninit) dereferences freed memory, andemu_mwritewrites MMIO throughsc->cardread out of the freed slab (emu10kx-midi.c:85-93βemu_wrptr(sc->card, ...));- the mpu401 callout (
m->timer) stays armed withm->si = emu_midi_intrinside the unloadable module (mpu401.c:162-166,mpu401.c:265-267); - on
kldunload snd_emu10kx(_midi)the module text is unmapped whilem, 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 failureemu_midi_detachdiscards;- 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.0still 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:
- freed ~120-byte device softc (M_BUS zone) whose contents the attacker re-grooms via same-size allocations;
- unprivileged fd I/O drives
emu_mread/emu_mwrite(cookie=freed):emu_wrptr(sc->card,...)computessc->cardfrom the freed slot β attacker-chosen bus-space tag/handle β arbitrary kernel-scope write; 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;- alternatively the unmapped-module
m->sijump 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;:232unconditionalreturn (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.
Fix verification
fixedBaseline (unfixed semantics): kldunload returns 0 then Fatal trap 12 panic - reproduced. Patched semantics (-DFIXED, identical code path to fix.diff: abort detach when mpu401_uninit() != 0): kldunload refused with 'Device not configured' (RC=1), console shows 'aborting detach (softc NOT freed, module NOT unloadable)', guest alive, unprivileged trigger still running on a consistent device, no UAF prints, no panic. The exact emu10kx-midi.c hunk itself cannot be boot-tested in QEMU (no emu10kx hardware) so the fix was validated on the faithful framework replica, which exercises the identical mpu401_uninit failure-and-abort contract.
findings/poc/DF-5106/run.3.fixvalidation.log; findings/poc/DF-5106/fix.diff; findings/poc/DF-5106/VERDICT.md (Fix validation section)
Confirmed kernel references
Detail
Exploit chain
unpriv user opens /dev/midiN.0 (0666) and writes (keeps mpu401 callout armed, m->busy=1) -> admin kldunload snd_emu10kx / card unplug -> emu_midi_detach: mpu401_uninit returns ENXIO, IGNORED -> softc kfree'd by newbus, cdev/mpu401/callout leaked alive -> [reproduced:] callout m->si(m->cookie) on freed softc (UAF read), and after module unmap indirect call into unmapped VA = kernel panic (Fatal trap 12, RIP=0xffffffff82600120, x2) -> [hardware-gated continuation on real emu10kx:] user I/O on still-open fd drives emu_mwrite/emu_mread with cookie=freed softc -> sc->card read from groomed freed slot -> emu_wrptr arbitrary kernel-scope write; sc->mpu_intr read from freed slot -> arbitrary kernel call -> uid=0
Evidence (decisive lines)
run.log (Stage A: 'mpu401_uninit() returned 6 ... <<< IGNORES this and returns 0', 'softc kfree'd', 'UAF: softintr on FREED softc 0xfffff801175f2f58 magic=51d10505'); run.2.log + panic.txt (Stage B: kldunload RC=0 -> Fatal trap 12, fault VA=instruction pointer=0xffffffff82600120 unmapped module text, current process=Idle i.e. callout context, db>); run.3.fixvalidation.log (with -DFIXED: unload refused 'Device not configured', GUEST_STILL_ALIVE, no free/no panic); env.txt (guest source md5 == audit tree; no emu10kx in guest)
PoC changes
Seed concept required a full rebuild: QEMU cannot emulate emu10kx so no /dev/midi device ever exists; replaced the hardware driver with simid.c, a kld that replicates the exact emu_midi_attach/emu_midi_detach call sequence (mpu401_init/mpu401_uninit + ignored-rv + softc kfree + module unload) against the real mpu401/midi framework, plus trig.c (unpriv open+write holding the 0666 cdev). First sim attempt taught the framework lesson: the softintr must call the mpu401 drain callback (as emu_midi_card_intr does) or the outq fills and midi_uninit takes the wake-and-destroy path instead of ENXIO; fixed to faithfully mirror emu10kx-midi.c:115-129. Build via in-tree bsd.kmod.mk (CFLAGS+= gets clobbered by kmod.mk; COPTS+= required for the -DFIXED variant).
Verified recommended fix
Propagate the mpu401_uninit() failure in emu_midi_detach(): rv = mpu401_uninit(scp->mpu); if (rv != 0) return (rv); (fix.diff, guest-validated)
Verdict
emu_midi_detach() (sys/dev/sound/pci/emu10kx-midi.c:229) discards mpu401_uninit()'s ENXIO that occurs whenever the 0666 MIDI cdev is open, then returns 0; newbus kfrees the device softc while the mpu401/snd_midi/cdev and the armed mpu401 callout still reference it. Reproduced on the guest at framework level (QEMU has no emu10kx HW): Stage A shows mpu401_uninit()==6 ignored, softc freed, then the live callout invoking m->si on the FREED softc while the unprivileged user keeps writing; Stage B shows kldunload returning 0 (silent success) followed within one tick by Fatal trap 12, RIP=0xffffffff82600120 inside the just-unmapped module text (callout jump through stale m->si), guest dead in DDB - reproduced twice at the identical fault VA. Fix semantics validated: aborting the detach on mpu401_uninit() failure refuses the unload and leaves the system healthy. On real SoundBlaster Live!/Audigy hardware the post-free sc->card MMIO write path (emu_mwrite) and the (sc->mpu_intr)(sc->mpu) call are attacker-groomable arbitrary-write/call primitives; completing uid0 was blocked only by the absence of emu10kx hardware in every VM.
No comments yet.