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)
PoC verification
Evidence pack
findings/poc/DF-4095 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_intris set bympu401_init(..., &sc->mpu_intr)inemu_midiattach()(emu10k1.c:1214) β mpu401 writesmpu401_intrinto it (mpu401.c:188).- It is cleared under
sc->lockbyemu_muninit()(emu10k1.c:1175-1185), which runs duringmpu401_uninit()βemu_uninit()βemu_pci_detach()(emu10k1.c:2038-2039, 2172). mpu401_uninit()then kfrees thestruct mpu401thatsc->mpupoints 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).
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:
emu_intr2()(sys/dev/sound/pci/emu10k1.c:1196-1203) tests and callssc->mpu_intrwithsc->mpuas argument β nosnd_mtxlock(sc->lock).- 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, clearssc->mpu_intrunder sc->lock) βkfree(m)(mpu401.c:203). emu_intr2is reachable asynchronously via the mpu401 callout:mpu401_intrrearmsmpu401_timeout(mpu401.c:162-164) which firesm->si=emu_intr2(mpu401.c:108-109). A CPU can sit between theif (sc->mpu_intr)test and the indirect call while another CPU clears the pointer and kfreessc->mpu.- Control flow then enters
mpu401_intr(m)with freedmβ STATUS()/READ()/WRITE() derefm(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 mpu401reuse 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
- sys/dev/sound/pci/emu10k1.c:1196-1203 (lockless check+call)
- sys/dev/sound/pci/emu10k1.c:1175-1185 (locked clear in emu_muninit)
- sys/dev/sound/pci/emu10k1.c:1214 (mpu401_init wiring)
- sys/dev/sound/pci/emu10k1.c:2038-2039,2172 (detach teardown)
- sys/dev/sound/midi/mpu401.c:108-109,162-164,188,203 (callout fire, callback install, kfree)
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_testablefix.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)
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).
No comments yet.