β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2116

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/midi0 open and feeding data to keep the IRQ source busy widens the window for an admin-initiated kldunload.
  • 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 is M_MIDI slab and the only sinks are mpu401_intr's hardware-register accesses and midi_in/midi_out on m->mid (another freed pointer), which are not realistically groomable from userspace.
  • Required config or capabilities: a SoundBlaster Live!/Audigy (emu10kx) card with snd_emu10kx_midi loaded.
  • Reachability: kldunload snd_emu10kx_midi while 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_midi is loaded and detached.
  • Blast radius: privileged local DoS (kernel panic during unload).

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

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2116 Β· 4 files
FileTypeDescriptionSize
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
VERDICT.md file
↓ download 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

fixed
baseline reproduced→ patch + rebuild →patched clean

batch build rc=0

batch build rc=0
↓ fix.diffcombined 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