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

emu_intr2 tests and calls sc->mpu_intr/sc->mpu without the lock, racing emu_muninit + mpu401_uninit's kfree at detach β€” heap UAF on struct mpu401

Summary

emu_intr2 does 'if (sc->mpu_intr) (sc->mpu_intr)(sc->mpu);' locklessly. It is fired asynchronously by the mpu401_timeout callout, while detach clears sc->mpu_intr under sc->lock and then kfrees sc->mpu in mpu401_uninit. A CPU between the check and the call enters mpu401_intr(freed m) - UAF read/write of struct mpu401 plus callout_reset. The in-IRQ call site takes the lock and is safe; only the callout path races. Lock-wrap fix (no new lock order).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4095 Β· 8 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
probe.sh β€” 585 B view raw
run.log β€” 282 B view raw
env.txt β€” 184 B view raw
fix.diff β€” 261 B view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 2.6 KB view raw

DF-4095 β€” emu_intr2 calls sc->mpu_intr/sc->mpu locklessly, racing detach teardown (UAF)

File: sys/dev/sound/pci/emu10k1.c (pass 2, GLM 5.3) β€” DF-3867 family shape (the emu10kx sibling finding was a NULL-function-pointer call during unregister; here the analogous race is on the mpu401 callback plumbing).

What

static void                     /* emu10k1.c:1196-1203 */
emu_intr2(void *p)
{
        struct sc_info *sc = (struct sc_info *)p;

        if (sc->mpu_intr)
            (sc->mpu_intr)(sc->mpu);
}
  • sc->mpu_intr is set by mpu401_init(..., &sc->mpu_intr) in emu_midiattach() (emu10k1.c:1214) β€” mpu401 writes mpu401_intr into it (mpu401.c:188).
  • It is cleared under sc->lock by emu_muninit() (emu10k1.c:1175-1185), which runs during mpu401_uninit() ← emu_uninit() ← emu_pci_detach() (emu10k1.c:2038-2039, 2172).
  • mpu401_uninit() then kfrees the struct mpu401 that sc->mpu points at (mpu401.c:197-204).

emu_intr2 takes no lock. It is registered as the mpu401 soft-interrupt callback (m->si = softintr, mpu401.c:181) and is invoked asynchronously from the mpu401_timeout callout (mpu401.c:108-109), which is rearmed by mpu401_intr() whenever MIDI TX is enabled (mpu401.c:162-164, 241).

Race at detach with MIDI TX active:

callout(emu_intr2)                     emu_pci_detach
----------------                       ----------------
if (sc->mpu_intr)   != NULL
                                       emu_uninit -> mpu401_uninit
                                       -> midi_uninit -> emu_muninit:
                                          lock; sc->mpu_intr = 0; unlock
                                       kfree(m)          /* mpu401.c:203 */
(sc->mpu_intr)(sc->mpu)
  -> mpu401_intr(freed m)  /* UAF read/write of struct mpu401 */

The check-then-call on sc->mpu_intr is unsynchronized with the clear-and-free on the other CPU. Result: mpu401_intr() executes against a freed struct mpu401 (reads/writes its registers via STATUS()/READ()/ WRITE() macros that dereference m), i.e. a heap UAF. Note the hardware-IRQ path is safe β€” emu_intr() calls the same pointer while holding sc->lock (emu10k1.c:1225, 1258-1263), serialized against emu_muninit. Only the callout path through emu_intr2 is lockless.

(Companion hazard inside mpu401.c itself β€” mpu401_timeout reading m->si from possibly-freed m β€” is an mpu401.c defect and noted here only for context; this finding is the emu10k1.c lockless call site.)

Threat model

Requires an emu10k1-family card, an active MIDI TX stream, and device detach (or unload path that reaches emu_uninit) racing the 1-tick callout. Hardware-gated; narrow window; consequence is kernel UAF (panic; potential corruption given heap reuse). Severity Low (reachability), memory-safety class (UAF).

Fix

Serialize the check+call under the same mutex the teardown uses (see fix.diff). This mirrors the already-locked call in emu_intr() (emu10k1.c:1258-1263) β€” mpu401_intr already executes under sc->lock there, so no new lock-order is introduced.

Reproduce

Hardware-gated (QEMU does not emulate emu10k1 β€” probe evidence in run.log). Source-level certainty of the missing lock; race exploitability is rated likely (window exists, but never triggered on this guest).

VERDICT.md
↓ download raw

DF-4095 VERDICT

Status: inconclusive on-guest (hardware-gated) / likely at source level. Impact: kernel heap use-after-free (mpu401 softc) during detach β€” panic class; exploitation would require heap grooming of a 1-callout-tick window.

Was it reproduced?

No. The guest has no emu10k1 hardware (run.log: no 0x1102 PCI device, kldload snd_emu10k1 fails), so the MIDI-TX + detach race cannot be exercised. The missing synchronization is, however, provable by inspection:

  1. emu_intr2() (sys/dev/sound/pci/emu10k1.c:1196-1203) tests and calls sc->mpu_intr with sc->mpu as argument β€” no snd_mtxlock(sc->lock).
  2. The teardown that invalidates both is: emu_pci_detach (emu10k1.c:2161) β†’ emu_uninit (:2172) β†’ mpu401_uninit (:2039) β†’ midi_uninit β†’ emu_muninit (emu10k1.c:1175-1185, clears sc->mpu_intr under sc->lock) β†’ kfree(m) (mpu401.c:203).
  3. emu_intr2 is reachable asynchronously via the mpu401 callout: mpu401_intr rearms mpu401_timeout (mpu401.c:162-164) which fires m->si = emu_intr2 (mpu401.c:108-109). A CPU can sit between the if (sc->mpu_intr) test and the indirect call while another CPU clears the pointer and kfrees sc->mpu.
  4. Control flow then enters mpu401_intr(m) with freed m β†’ STATUS()/READ()/WRITE() deref m (mpu401.c:113-155) β†’ UAF read + the callout-reset write at mpu401.c:163.

The sibling finding DF-3867 (emu10kx, emu_intr vs emu_intr_unregister) was the same family shape and was filed Low.

Why Low and not higher

  • Trigger requires physical Creative silicon + MIDI TX + detach timing.
  • Window is one callout tick; no userland-controllable heap-spray primitive presented itself for the 1-slot struct mpu401 reuse within the window. (The guest, being permissive, would allow grooming, but without the device the chain cannot even start.)

Fix validation

Not performable on-guest (no device). fix.diff adds the mutex around the check+call, matching the locking discipline already used by the in-IRQ call site (emu10k1.c:1258-1263) β€” no new lock order, mpu401_intr already runs under this lock from emu_intr().

kernel_refs

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored and git-apply-verified; race validation needs physical emu10k1 + MIDI TX + detach, unavailable on the guest.

fix.diff (unified, 'git apply --check' OK)
↓ fix.diffper-fix-DF-4095

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (full interleaving diagram + refs); run.log (guest: no 0x1102 PCI device, kldload fails); fix.diff (locks emu_intr2 β€” mirrors the locking already used by the safe in-IRQ call site, no new lock order)

PoC changes

No PoC seed existed; wrote probe.sh for the guest-side hardware-absence check.

Verified recommended fix

Wrap emu_intr2()'s check+call in snd_mtxlock/sc->lock, matching the already-locked in-IRQ call site.

Verdict

DF-3867-family race: emu_intr2() (emu10k1.c:1196-1203) tests-and-calls sc->mpu_intr with sc->mpu as argument with NO lock, while the teardown that clears sc->mpu_intr (emu_muninit :1175-1185, under sc->lock) and kfrees sc->mpu (mpu401_uninit -> kfree, mpu401.c:203) runs at detach. emu_intr2 is fired asynchronously by the mpu401_timeout callout (mpu401.c:108-109), so a CPU between the NULL-check and the indirect call can end up in mpu401_intr(freed m) β€” heap UAF read/write of struct mpu401. The in-IRQ call site (emu10k1.c:1258-1263) takes sc->lock and is safe; only the callout path is racy. Window is one callout tick and needs real Creative hardware with active MIDI TX during detach, so it is proven-likely at source level but untriggerable on the QEMU guest (no emu10k1 device).