emu_intr_unregister() uses caller-controlled slot index with no bounds check -> OOB read + four-field NULL write cluster at ihandler[-1] (NULLs sc->reg, sc->irq, sc->ih)
Summary
emu_intr_register() returns -1 when all 32 ihandler[] slots are taken. emu10kx-pcm.c stores that value unchecked into ch->ihandle at START and passes it to emu_intr_unregister() at STOP, which indexes sc->ihandler[hnumber] with no bounds check at all. PROVEN on the guest kernel ABI: ihandler[-1] spans exactly {sc->reg, sc->irq, sc->ih}; the -1 path reads a nonzero inte_mask (high dword of the irq pointer) so the guard does not fire, then NULLs all three pointers and smears pointer bits into the EMU_INTE hardware write. Subsequent detach does bus_release_resource(...,NULL)/bus_teardown_intr(...,NULL) -> NULL-deref panic. Slot exhaustion is fed by a real leak: the card-level handler is never unregistered (emu_pcm_uninit is a no-op), leaking up to 6 slots per root kldload/kldunload cycle. Requires physical SB Live!/Audigy hardware + root for the cycling. NOTE: the DragonFly port has NO ioctl/mmap surface (ops stripped to open/close/read, node 0640 root:wheel) - the FreeBSD EMU10KX_IOCTL family does not exist here.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3866 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| rec_cycle.c | β | 1.9 KB | view raw | |
| layout_proof.c | β | 2.7 KB | view raw | |
| build.sh | β | 65 B | view raw | |
| run.sh | β | 89 B | view raw | |
| build.log | β | 6.0 KB | view raw | |
| run.log | β | 1.1 KB | view raw | |
| env.txt | β | 180 B | view raw | |
| fix.diff | β | 2.2 KB | view raw | |
| README.md | β | 2.0 KB | β raw | |
| VERDICT.md | β | 4.0 KB | β raw | |
| verdict.json | β | 4.9 KB | view raw |
DF-3866 β emu_intr_unregister unvalidated index β OOB write cluster at ihandler[-1]
File: sys/dev/sound/pci/emu10kx.c:948-978 (+ unchecked callers in sys/dev/sound/pci/emu10kx-pcm.c)
Build
On the DragonFly guest (module snd_emu10kx + /usr/src present β this repo's QEMU image):
cd findings/poc/DF-3866
./build.sh # builds: rec_cycle (userland trigger tail) + kmod/lay.ko (layout proof)
Run
./run.sh # 1) hardware gate 2) kernel layout proof 3) would-be trigger
Expected output
- Hardware gate β on a guest without Creative Labs (0x1102) PCI audio:
GATE: no Creative Labs PCI device present GATE: cannot open /dev/dsp0: No such file or directory GATE: target device absent -> trigger impossible on this guest (no emu10kx hw) - Kernel layout proof (needs no hardware β compiles the verbatim field
prefix of
struct emu_sc_infoagainst the running kernel ABI):DF3866: sizeof(struct emu_intr_handler)=24 DF3866: offsetof(reg)=280 irq=288 ih=296 ihandler=304 DF3866: ihandler[-1] spans [280,304) DF3866: PROVEN: ihandler[-1] overlays {reg, irq, ih} ... - On real hardware, after root-side slot exhaustion (kldload/kldunload
cycling of
snd_emu10kx, which leaks one ihandler slot per pcm child), a record-channel START returns -1 and the next STOP feedsemu_intr_unregister(sc, -1)β OOB read ofihandler[-1].inte_mask(high dword ofsc->irq, nonzero β passes the guard) β four NULL OOB writes NULLsc->reg,sc->irq,sc->ihβ panic at nextbus_release_resource/bus_teardown_intr(emu10kx.c:3524-3527).
Artifacts
rec_cycle.cβ userland rec START/STOP cycle (the trigger tail)layout_proof.c+kmod/β kernel module printing the true offsetsbuild.sh/run.sh,build.log/run.logβ recorded guest sessionfix.diffβ bounds check + caller guards + slot-leak fix (NOT applied; sys/ is read-only)VERDICT.md,manifest.json,verdict.json
DF-3866 VERDICT
Status: inconclusive (runtime-gated: no emu10kx hardware in the QEMU guest)
Defect certainty: source-level certain β OOB write geometry proven on the guest kernel ABI.
What the bug is
emu_intr_register() (sys/dev/sound/pci/emu10kx.c:919-946) returns -1 when all
32 ihandler[] slots are taken. Its callers in emu10kx-pcm.c store that value
unchecked into ch->ihandle (emu10kx-pcm.c:983, :1137) and later pass it to
emu_intr_unregister() (emu10kx-pcm.c:992, :1162). emu_intr_unregister()
(emu10kx.c:948-978) indexes sc->ihandler[hnumber] with no bounds check
whatsoever β first read (emu10kx.c:956), then four writes (emu10kx.c:964-967).
Why it corrupts memory (proven)
The guest kernel was made to compile a module (layout_proof.c) declaring the
verbatim field prefix of struct emu_sc_info (emu10kx.c:307-334). Recorded
output (run.log):
DF3866: sizeof(struct emu_intr_handler)=24
DF3866: offsetof(reg)=280 irq=288 ih=296 ihandler=304
DF3866: ihandler[-1] spans [280,304)
DF3866: PROVEN: ihandler[-1] overlays {reg, irq, ih}
So ihandler[-1] = { .softc=&sc->reg, .intr_mask=lo32(sc->irq),
.inte_mask=hi32(sc->irq), .irq_func=&sc->ih }. When hnumber == -1:
sc->ihandler[-1].inte_maskreads the high dword of thestruct resource *irqpointer β 0xfffffeβ¦ on x86_64, i.e. nonzero β so theinte_mask == 0early-out (emu10kx.c:956) does NOT save us;x &= ~ihandler[-1].inte_mask(emu10kx.c:962) smears pointer bits into the EMU_INTE hardware write (clears random interrupt-enable bits);ihandler[-1].inte_mask = 0; .intr_mask = 0NULLssc->irq(both halves),ihandler[-1].softc = NULLNULLssc->reg,ihandler[-1].irq_func = NULLNULLssc->ih(emu10kx.c:964-967);- next detach does
bus_release_resource(dev, SYS_RES_IOPORT, ..., NULL)/bus_teardown_intr(dev, NULL, NULL)(emu10kx.c:3524-3527) β NULL-deref panic; even before that, the corruptedsc->irqpoisons every later use.
How the pool exhausts (precondition)
Concurrent unprivileged use alone cannot exhaust the pool (β€6 card-level + β€6
rec-channel slots < 32; rec START/STOP is balanced because chn_abort()
channel.c:955 refuses to trigger a stopped channel). But the card-level handle
registered at emu10kx-pcm.c:1453 is never unregistered β emu_pcm_uninit()
(emu10kx-pcm.c:1292-1296) is a no-op. Every kldunload/kldload snd_emu10kx
cycle (root, devctl-equivalent) leaks up to 6 slots; after ~6 cycles
emu_intr_register starts returning -1 and the OOB fires on the next rec
STARTβSTOP.
Why it is inconclusive at runtime
The QEMU guest has no Creative Labs PCI device (pciconf -l shows no
0x1102 vendor). Loading snd_emu10kx.ko succeeds but emu_pci_probe()
(emu10kx.c:3013-3039) matches nothing β no softc is ever allocated, no
/dev/emu10kx and no pcm children exist. The runtime trigger (rec_cycle on
/dev/dsp) therefore stops at open(): No such file or directory β recorded in
run.log. Reproduction requires physical SB Live!/Audigy hardware.
Exploitability ceiling
Both gates (root for the kld cycling, hardware presence) precede the memory
corruption, and the write values are fixed NULLs into a driver-private softc β
an attacker who already has root gains nothing. Realistic impact:
kernel NULL-deref panic / device bricking after privileged module cycling.
Severity: Low (memcorrupt bucket for tracking). The primary fix is the
one-line bounds check in emu_intr_unregister (see fix.diff, which also
fixes the leak and the unchecked callers).
Fix validation
Not possible on this guest (no hardware) β fix_status not_testable.
fix.diff is authored against the read-only sys/ tree and was never applied.
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 validation impossible on this guest: the bug never executes without Creative Labs hardware (baseline already gated). fix.diff is authored against the read-only sys/ tree, never applied; it adds the bounds check, both caller guards, and the emu_pcm_uninit() unregister that stops the leak feeding the exhaustion.
findings/poc/DF-3866/fix.diff; VERDICT.md 'Fix validation' section; run.log GATE lines
Confirmed kernel references
- sys/dev/sound/pci/emu10kx.c:919
- sys/dev/sound/pci/emu10kx.c:945
- sys/dev/sound/pci/emu10kx.c:948
- sys/dev/sound/pci/emu10kx.c:956
- sys/dev/sound/pci/emu10kx.c:964
- sys/dev/sound/pci/emu10kx.c:3524
- sys/dev/sound/pci/emu10kx-pcm.c:983
- sys/dev/sound/pci/emu10kx-pcm.c:992
- sys/dev/sound/pci/emu10kx-pcm.c:1137
- sys/dev/sound/pci/emu10kx-pcm.c:1162
- sys/dev/sound/pci/emu10kx-pcm.c:1453
- sys/dev/sound/pci/emu10kx-pcm.c:1292
Detail
Exploit chain
root: kldunload/kldload snd_emu10kx xN (each cycle leaks 1-6 ihandler slots via never-unregistered card-level handle) -> emu_intr_register() returns -1 -> rec-channel PCMTRIG_START stores ch->ihandle=-1 -> PCMTRIG_STOP calls emu_intr_unregister(sc,-1) -> ihandler[-1] OOB read (hi32(sc->irq)!=0 passes guard) -> four NULL OOB writes NULL sc->reg/sc->irq/sc->ih -> next detach panics in bus_teardown_intr/bus_release_resource. No unprivileged path to exhaustion exists (concurrent use needs <=12 of 32 slots); no uid0 chain.
Evidence (decisive lines)
run.log lines 'DF3866: PROVEN: ihandler[-1] overlays {reg, irq, ih}' and 'GATE: target device absent'; layout_proof.c (kernel module, verbatim softc prefix); VERDICT.md full trace; fix.diff (bounds check + caller guards + leak fix)
PoC changes
Fixed ioctl name SNDCTL_DSP_SETTRIG -> SNDCTL_DSP_SETTRIGGER (DragonFly soundcard.h); kmod Makefile needed SRCS += bus_if.h device_if.h to generate interface headers; fixed an arithmetic slip in the layout-proof assertion (oh-16 not oh-sz+16)
Verified recommended fix
Bounds-check hnumber in emu_intr_unregister() (return -1 if <0 or >=EMU_MAX_IRQ_CONSUMERS), guard emu_intr_register() failure at emu10kx-pcm.c:983/1137, and unregister the card-level handle in emu_pcm_uninit()
Verdict
Runtime reproduction is hardware-gated: the QEMU guest has no Creative Labs (0x1102) PCI device, snd_emu10kx.ko loads but emu_pci_probe() matches nothing, so no softc, /dev/emu10kx or pcm device ever exists and the /dev/dsp trigger stops at open(ENOENT) (run.log). The defect itself is source-level certain and its memory-corruption geometry is PROVEN on the guest kernel ABI: emu_intr_register() returns -1 on slot exhaustion (emu10kx.c:945), emu10kx-pcm.c stores it unchecked (emu10kx-pcm.c:983,1137) and passes it to emu_intr_unregister() (emu10kx-pcm.c:992,1162), which indexes sc->ihandler[hnumber] with no bounds check (emu10kx.c:956,964-967). The kernel-compiled layout proof shows ihandler[-1] spans exactly {sc->reg, sc->irq, sc->ih}; the -1 path reads the nonzero high dword of the irq pointer (bypassing the inte_mask==0 guard), NULLs reg/irq/ih, and smears pointer bits into the EMU_INTE register write; detach then NULL-derefs in bus_release_resource/bus_teardown_intr (emu10kx.c:3524-3527). Pool exhaustion is reachable because the card-level handler registered at emu10kx-pcm.c:1453 is never unregistered (emu_pcm_uninit is a no-op at emu10kx-pcm.c:1292-1296), leaking up to 6 slots per root kldunload/kldload cycle. Both gates (hardware + root for exhaustion) cap real-world impact at a root-triggered panic/DoS: severity Low, memcorrupt bucket.
No comments yet.