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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3866 Β· 11 files
FileTypeDescriptionSize
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

  1. 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)
  2. Kernel layout proof (needs no hardware β€” compiles the verbatim field prefix of struct emu_sc_info against 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} ...
  3. 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 feeds emu_intr_unregister(sc, -1) β†’ OOB read of ihandler[-1].inte_mask (high dword of sc->irq, nonzero β†’ passes the guard) β†’ four NULL OOB writes NULL sc->reg, sc->irq, sc->ih β†’ panic at next bus_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 offsets
  • build.sh / run.sh, build.log / run.log β€” recorded guest session
  • fix.diff β€” bounds check + caller guards + slot-leak fix (NOT applied; sys/ is read-only)
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

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:

  1. sc->ihandler[-1].inte_mask reads the high dword of the struct resource *irq pointer β€” 0xfffffe… on x86_64, i.e. nonzero β€” so the inte_mask == 0 early-out (emu10kx.c:956) does NOT save us;
  2. x &= ~ihandler[-1].inte_mask (emu10kx.c:962) smears pointer bits into the EMU_INTE hardware write (clears random interrupt-enable bits);
  3. ihandler[-1].inte_mask = 0; .intr_mask = 0 NULLs sc->irq (both halves), ihandler[-1].softc = NULL NULLs sc->reg, ihandler[-1].irq_func = NULL NULLs sc->ih (emu10kx.c:964-967);
  4. 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 corrupted sc->irq poisons 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_testable
baseline no→ patch + rebuild →patched clean

Fix 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
↓ fix.diffper-fix-DF-3866

Confirmed kernel references

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.