UAF/TOCTOU race: IRQ handler dereferences sc->mpu while detach frees it (mpu401_uninit runs before emu_intr_unregister)
| Field | Value |
|---|---|
| ID | DF-2116 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-416 Use After Free; CWE-367 Time-of-check Time-of-use |
| File | sys/dev/sound/pci/emu10kx-midi.c |
| Lines | 223-233 |
| Area | sound/pci |
| Confidence | likely |
| Discovered | 2026-07-25 |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
emu_midi_detach() calls mpu401_uninit(scp->mpu) β which NULLs
scp->mpu_intr and then kfree()s the mpu401 struct that scp->mpu
points at (mpu401.c:205) β before it calls emu_intr_unregister()
to disconnect the IRQ handler. While mpu401_uninit runs, the live IRQ
dispatch path (emu10kx.c:993-998 β emu_midi_card_intr β
mpu401_intr(scp->mpu)) can still fire on another CPU and dereference
the struct being freed. The sc->lock acquired in emu_muninit is
decorative because emu_midi_card_intr reads sc->mpu_intr with no
lock at all.
Root cause
Detach ordering is inverted. In emu_midi_detach
(emu10kx-midi.c:223-233):
line 229: mpu401_uninit(scp->mpu); /* tears down + frees scp->mpu */
line 230: emu_intr_unregister(...); /* disconnects handler -- TOO LATE */
mpu401_uninit (mpu401.c:197-207) first does CMD(m, MPU_RESET)
which writes the MPU port and can itself raise an interrupt, then calls
midi_uninit(m->mid) which (via mpu401_muninit β MPUFOI_UNINIT β
emu_muninit, emu10kx-midi.c:95-105) sets scp->mpu_intr = NULL under
sc->lock, and finally kfree(m, M_MIDI) (mpu401.c:205) frees the
very struct scp->mpu references.
During this entire window the IRQ slot in the parent card still points at
emu_midi_card_intr with softc = scp (emu_intr_register stored them
at emu10kx.c:930-931; not cleared until emu_intr_unregister at
emu10kx-midi.c:230). The top-level handler emu_intr
(emu10kx.c:981-1004) dispatches without sc->lock (lines
993-998), so on another CPU it can read ihandler[i].irq_func != NULL
and invoke emu_midi_card_intr(scp).
Inside emu_midi_card_intr (emu10kx-midi.c:115-129), line 119 reads
sc->mpu_intr with no lock; if the IRQ lands before emu_muninit
has run (i.e. during CMD(MPU_RESET) or the early part of
midi_uninit), sc->mpu_intr is still mpu401_intr and line 120 calls
mpu401_intr(sc->mpu) β racing against the concurrent free/teardown of
sc->mpu and sc->mpu->mid. mpu401_intr (mpu401.c:113-166) then
dereferences m->mid, m->cookie, and issues STATUS/READ/WRITE
through m, i.e. use-after-free on M_MIDI slab memory.
The LK_EXCLUSIVE lockmgr in emu_muninit (emu10kx-midi.c:100-102)
provides zero protection because the only reader of mpu_intr β the
interrupt handler β never acquires sc->lock.
Threat model & preconditions
- Attacker position: local. The race is entered on the detach path of
an emu10kx MIDI child device (module unload via
kldunload snd_emu10kx_midi, driver reattach, or system shutdown) while MIDI RX/TX interrupts are arriving β e.g. an unprivileged user holding/dev/midi0open and feeding data to keep the IRQ source busy widens the window for an admin-initiatedkldunload. - Privileges gained or impact: Because initiating detach itself
requires root (
kldunload) or physical hotplug, this is not an unprivβroot escalation; the realistic impact is a kernel panic / memory corruption (A:H) during a privileged unload operation. Escalation beyond DoS is not demonstrated: the freed object isM_MIDIslab and the only sinks arempu401_intr's hardware-register accesses andmidi_in/midi_outonm->mid(another freed pointer), which are not realistically groomable from userspace. - Required config or capabilities: a SoundBlaster Live!/Audigy
(emu10kx) card with
snd_emu10kx_midiloaded. - Reachability:
kldunload snd_emu10kx_midiwhile MIDI traffic is active on/dev/midi0.
Proof of Concept
PoC source: findings/poc/DF-2116/
Privileged race to panic the kernel during module unload (matches Low severity β root-triggerable DoS, not an escape primitive).
1) On a DragonFlyBSD guest with an emu10kx card and
snd_emu10kx_midi.ko loaded:
- Identify the MIDI device: ls -l /dev/midi*
- Confirm module: kldstat | grep snd_emu10kx_midi
2) Trigger source -- two cooperating processes:
/* midi_blast.c -- unprivileged, keeps RX/TX IRQs firing */
int main(void){
int fd = open("/dev/midi0", O_RDWR);
unsigned char note[] = {0x90, 0x3C, 0x40}; /* Note On */
for (;;){
write(fd, note, sizeof note); /* drive TX path + IRQs */
char buf[64];
read(fd, buf, sizeof buf); /* drain RX */
}
}
# cc -o midi_blast midi_blast.c
/* unload_race.sh -- run as root, races detach vs in-flight IRQ */
./midi_blast &
BLAST=$!
for i in $(seq 1 500); do
kldunload snd_emu10kx_midi 2>/dev/null
kldload snd_emu10kx_midi 2>/dev/null
done
kill $BLAST 2>/dev/null
3) Success criterion: kernel panic during one of the kldunload
iterations, signature along the lines of 'freelist entry ... out of
range' / 'uvm_fault' / 'mpu401_intr' on the stack, or use-after-free
detection under DEBUGMODE/KASAN.
The race window is the gap between entry to mpu401_uninit
(mpu401.c:201 CMD(MPU_RESET)) and emu_muninit nulling mpu_intr
(emu10kx-midi.c:101); continuous MIDI traffic maximizes the probability
that emu_intr fires inside it.
Reproduce on baremetal or KVM with a passed-through / emulated SB Live! HDA. Without the card the driver does not attach and the path is unreachable.
Impact
- Default config: not triggered unless
snd_emu10kx_midiis loaded and detached. - Blast radius: privileged local DoS (kernel panic during unload).
Recommended fix
Reorder detach so the IRQ source is masked and the dispatch slot cleared
before the mpu401 struct (scp->mpu) is torn down and freed.
emu_intr_unregister clears the inte_mask bit at EMU_INTE (stopping
new IRQ generation for this source) and NULLs irq_func/softc in the
dispatch table (emu10kx.c:961-967), so once it returns, emu_intr can
no longer call emu_midi_card_intr; the subsequent mpu401_uninit is
then safe. scp itself (the device softc) stays valid until newbus frees
it after detach returns, so the benign dispatch-level read-then-call race
in emu_intr is harmless.
--- a/sys/dev/sound/pci/emu10kx-midi.c
+++ b/sys/dev/sound/pci/emu10kx-midi.c
@@ -226,9 +226,15 @@ emu_midi_detach(device_t dev)
scp = device_get_softc(dev);
- mpu401_uninit(scp->mpu);
+ /*
+ * Disconnect the IRQ source BEFORE tearing down the mpu401, so that
+ * emu_midi_card_intr cannot fire and dereference sc->mpu / sc->mpu_intr
+ * while mpu401_uninit (which NULLs mpu_intr and kfree()s scp->mpu) runs.
+ * The sc->lock taken in emu_muninit does not help: the interrupt handler
+ * reads mpu_intr without any lock.
+ */
+ emu_intr_unregister(scp->card, scp->ihandle);
+ scp->ihandle = -1;
+ mpu401_uninit(scp->mpu);
- emu_intr_unregister(scp->card, scp->ihandle);
lockuninit(&scp->lock);
return (0);
}
Defense-in-depth (optional, separate change): make emu_midi_card_intr
robust by reading mpu_intr under a spinlock or via
atomic_load_consume, and have emu_muninit set the pointer via
atomic_store_release after a drain β but the two-line reorder above is
the minimal correct fix for this file's UAF.
References
sys/dev/sound/midi/mpu401.c:197-207,113-166βmpu401_uninitandmpu401_intr(the freed-struct sinks).sys/dev/sound/pci/emu10kx.c:961-967,981-1004βemu_intr_unregisterand the dispatch loop that callsemu_midi_card_intr.
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2116 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | file | 732 B | β raw | |
| build.sh | file | 161 B | view raw | |
| fix.diff | file | 169 B | view raw | |
| run.sh | file | 80 B | view raw |
DF-2116 - Verification Verdict
Status: reproduced (source-confirmed) Impact: corruption Confidence: certain
Verdict
Source-confirmed: emu_midi_detach (:229-230) calls mpu401_uninit which frees mpu401 struct BEFORE emu_intr_unregister removes handler; UAF window; sound-HW-gated
Fix Status
Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)
Source File
sys/dev/sound/pci/emu10kx-midi.c
Fix Validation
All 87 fix.diffs compiled together in a single batch kernel build
(make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors).
The combined patch is at findings/poc/batch_build/all_fixes.patch.
Fix verification
fixedbatch build rc=0
batch build rc=0
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
emu_midi_detach frees mpu401 before removing intr; UAF; sound-gated
Verified recommended fix
emu_midi_detach frees mpu401 before removing intr; UAF; sound-gated
Verdict
emu_midi_detach frees mpu401 before removing intr; UAF; sound-gated
No comments yet.