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

cmi_attach error path after mixer_init leaves a live 0666 /dev/mixerN pointing at freed sc β€” UAF on open/ioctl (DF-4156 family, 7th instance)

Summary

mixer_init at :986 publishes the world-writable /dev/mixerN whose devinfo is sc, before pcm_register at :989; pcm_register can fail (pcm_veto_load EINVAL, PCMMAXUNIT) and the bad: unwind frees sc without mixer_uninit - any unpriv open/ioctl dispatches through freed sc (type-confused bus_space tag+handle). 7th instance of the verified family, narrowest window (one post-mixer goto). Fix compile-validated on guest (patched snd_cmi.ko -Werror RC=0). pcm_register-first + pcm_unregister-on-mixer-fail fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4457 Β· 12 files
FileTypeDescriptionSize
README.md β€” 1.4 KB ↓ raw
VERDICT.md β€” 5.9 KB ↓ raw
trigger.hw.sh β€” 1.4 KB view raw
run.sh β€” 164 B view raw
build.sh β€” 137 B view raw
build.log β€” 4.7 KB view raw
run.log β€” 362 B view raw
guest_check.log β€” 630 B view raw
env.txt β€” 630 B view raw
fix.diff β€” 948 B view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 4.3 KB view raw

DF-4457 β€” cmi_attach error path after mixer_init leaves a live 0666 /dev/mixerN pointing at freed sc (UAF on open/ioctl)

7th instance of the DF-4156/4193/4299/4334/4344/4421 sound-driver family, in sys/dev/sound/pci/cmi.c.

Reproduce (on hardware with a CMI8738-class card)

  1. sh trigger.hw.sh β€” preconditions and expected dangling-cdev state.
  2. Success criterion: after a failed snd_cmi attach that got past mixer_init(), ls -l /dev/mixerN still shows the 0666 cdev and any unprivileged open+ioctl(MIXER_WRITE(...)) dispatches through the freed softc (garbage bus_space reads/writes / panic under INVARIANTS).

On the audit QEMU guest

sh run.sh β€” proves the gate: no CMedia/audio PCI device, snd_cmi.ko cannot load (midi dependency mismatch). Runtime reproduction is impossible on this guest; the defect is proven by source trace (VERDICT.md).

Build (fix compile-validation)

sh build.sh (inside guest, with the fixed cmi.c staged at /root/cmi.fixed.c) β€” baseline and fix both build -Werror clean.

Files

  • VERDICT.md β€” full narrative, root-cause table, negative results
  • trigger.hw.sh β€” real-hardware trigger
  • run.sh + run.log / guest_check.log β€” guest hardware-gate proof
  • build.sh + build.log β€” pristine & patched module builds
  • fix.diff β€” git-apply-able unified diff (validated: applies clean, compiles -Werror clean as a module on the guest)
  • env.txt β€” guest environment
VERDICT.md
↓ download raw

DF-4457 β€” PoC Verification Verdict

File: sys/dev/sound/pci/cmi.c (pass 2) Family: DF-4156 (maestro) / DF-4193 (es137x) / DF-4299 (emu10kx) / DF-4334 (via8233) / DF-4344 (atiixp) / DF-4421 (ich) β€” 7th instance. Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR), cc 8.3.

Verdict: INCONCLUSIVE at runtime (code defect Certain; hardware-gated)

Root cause (source-trace proof)

cmi_attach() calls mixer_init(dev, &cmi_mixer_class, sc) at cmi.c:986 and pcm_register(dev, sc, 1, 1) at cmi.c:989. mixer_init() (mixer.c:687-764) creates a world-writable cdev:

mixer.c:730  pdev = make_dev(&mixer_ops, PCMMKMINOR(unit,0,0),
mixer.c:731              UID_ROOT, GID_WHEEL, 0666, "mixer%d", unit);
mixer.c:732  pdev->si_drv1 = m;         /* m->devinfo = sc (mixer.c:78) */

If pcm_register() fails it returns non-zero (EINVAL via pcm_veto_load, sound.c:1021; ENODEV via unit > PCMMAXUNIT=255, sound.c:1027) and cmi_attach goto's bad: (cmi.c:1004-1016), which destroys the dma tag, tears down the irq, releases reg/irq resources, snd_mtxfree(sc->lock) and kfree(sc, M_DEVBUF) β€” without mixer_uninit(dev). The 0666 /dev/mixerN cdev stays registered with si_drv1 -> m -> devinfo == freed sc:

  • any later open("/dev/mixerN") / ioctl(MIXER_*) dispatches cmimix_set/cmimix_init/cmimix_setrecsrc (cmi.c:654-750) β†’ mix_getdevinfo(m) == freed sc β†’ cmi_rd/cmi_wr(sc, ...) reads and writes through the freed ~264-byte M_DEVBUF chunk (sc->st, sc->sh, i.e. type-confused bus_space tag/handle read from freed heap).
  • the mixer keeps its OWN m->lock (mixer.c:71-72), so the UAF is not masked by the freed sc->lock.

Why not runtime-reproduced on this guest

  1. No hardware: QEMU presents no CMedia 13f6 device and no audio class 0x040x function at all (guest_check.log / run.log: pciconf -l shows only 1237/7000/7010/7113/1111:1234/virtio). cmi_probe() never matches β†’ cmi_attach() never executes.
  2. Module gate: kldload snd_cmi on this guest fails with KLD snd_cmi.ko: depends on midi - not available or version mismatch (serial console) β€” deployment itself is broken on the snapshot, so even a hypothetical device could not reach the driver.
  3. Trigger gate (real hardware): the sole post-mixer_init failure path is pcm_register(), which only fails via pcm_veto_load (0 after sound(4) feeder init, feeder.c:126 β€” unreachable during PCI attach) or >255 sound units. Identical trigger shape to DF-4193/4334/4344/4421.

Ceiling on real hardware: local unprivileged UAF read/write through a 0666 cdev gated by an attach-time failure β€” Low, matching family precedent.

Negative results (classes hunted and killed in cmi.c, pass 2)

  • cmi_intr() NULL-channel (cmi.c:562-568): sc->pch.channel is set during pcm_addchanβ†’pcm_chn_createβ†’chn_initβ†’cmichan_init (sound.c:695-…, cmi.c:345-374) i.e. still within attach at cmi.c:994-995; before that, CH0/CH1 INTR_STATUS bits require DMA enabled which cmi_init (cmi.c:874-878) disables. Family-wide unguarded chn_intr(sc->pch.channel) (es137x.c:750, via8233.c:833, atiixp.c:712) β€” no cmi differential. Killed.
  • pcm_addchan return ignored (cmi.c:994-995): robustness only β€” channel-less device, no chn_intr(NULL) reachable (DMA never enabled).
  • cmichan_getptr modulo (cmi.c:529-531): sndbuf_resize floor is blkcnt>=2 Γ— blksz>=16 (buffer.c sndbuf_resize EINVAL guard) so sndbuf_getsize() >= 32 β€” no div-by-zero; physptr arithmetic wraps within u32 and stays bounded by % sz.
  • cmi_dma_prog u16 sample counts (cmi.c:261-267): sz ≀ 65536 (pcm_getbuffersize max 65536, cmi.c:969; dma tag maxsize sc->bufsz, cmi.c:971-977), bps ∈ {1,2,4} β†’ s,i ≀ 65535, no truncation; i>=0 because sz β‰₯ 2*bps.
  • cmimix_set cmt[dev] OOB (cmi.c:684): dev bounded by SOUND_MIXER_NRDEVICES in mixer.c before MIXER_SET dispatch (mixer_set validates mix->devs & (1<<dev)), and cmi's own mix_setdevs only enables cmt[i].bits != 0 entries.
  • cmpci_rate_to_regvalue (cmi.c:222-247): i ≀ 7, cmi_rates[7] valid.
  • cmi_resume lock recursion (cmi.c:1067-1092): snd_mtxcreate uses LK_CANRECURSE (sound.c) β€” safe.
  • cmi_detach (cmi.c:1021-1047): pcm_unregister first (drains channels+cdevs), then teardown; sc->mpu_reg NULL-guarded; mpu401 paths dead (if (0) at cmi.c:957-958) β€” cmi_mread/cmi_mwrite hardcoded port 0x330 with NULL bus_space tags would be broken if ever enabled, but unreachable (note only).
  • DMA tag: nsegments=1, maxsize=sc->bufsz ≀ 64K consistent with CMPCI DMA limits (cmi.c:969-977). No seg-count mismatch.
  • SPDIF: no ioctl surface in cmi.c β€” sysctl spdif_enabled only (cmi_initsys, cmi.c:754-769); cmi_spdif_speed register-only.

Fix validation

fix.diff reorders pcm_register() ahead of mixer_init() and adds pcm_unregister(dev) on the mixer_init-failure path (pcm_unregister itself calls mixer_uninit, sound.c:1197ff, and handles zero channels β€” CHN_FOREACH over empty list). Validated to COMPILE cleanly with -Werror on the guest toolchain:

  • baseline snd_cmi.ko: RC=0 (build.log)
  • patched snd_cmi.ko (fix applied): RC=0, -Werror clean (build.log, guest_fixbuild output preserved in build.log)

Behavioral baseline/patched runs impossible on this guest (no CMI hardware β€” see above), hence fix_status=not_testable, compile-validated. git apply --check passes against the pristine read-only tree. Guest /usr/src restored to pristine after the build (md5 3a977e0dea384942133aa5707146f390 matches tree).

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 (git apply --check OK against pristine tree) reorders pcm_register() before mixer_init() and adds pcm_unregister(dev) when mixer_init() fails (pcm_unregister calls mixer_uninit itself and handles the zero-channel case, sound.c:1146-1215). Compile-validated on the guest: patched snd_cmi.ko builds -Werror clean, RC=0 (build.log). Behavioral baseline/patched runs impossible on this guest: cmi_attach requires CMedia hardware the guest cannot present (fix_status not_testable, compile-validated).

fix.diff; build.log (patched module full compile+link, CMI_BUILD_RC=0); VERDICT.md 'Fix validation' section
↓ fix.diffper-fix-DF-4457

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md root-cause walk (cmi.c:986-1016, mixer.c:687-764, sound.c:1016-1040); run.log + guest_check.log (no CMedia/audio PCI device, snd_cmi unloadable on snapshot, no /dev/mixer*); build.log (baseline AND fix.diff-patched snd_cmi.ko build -Werror clean, RC=0); fix.diff (git apply --check OK)

PoC changes

no seed; family trigger shape (DF-4334/4344/4421) adapted to cmi's single post-mixer failure path; hardware absent so guest gate-check substituted

Verified recommended fix

Move pcm_register() ahead of mixer_init() and pcm_unregister() on the mixer_init failure path so no live cdev remains over freed memory (DF-4334/4421 family shape)

Verdict

Code defect certain, kernel runtime hardware-gated: cmi_attach() calls mixer_init(dev, &cmi_mixer_class, sc) (cmi.c:986), which publishes a 0666 /dev/mixerN whose si_drv1 -> snd_mixer m has m->devinfo = sc (mixer.c:730-732, mixer.c:78), BEFORE pcm_register(dev, sc, 1, 1) (cmi.c:989). pcm_register can fail (EINVAL via pcm_veto_load sound.c:1021; ENODEV via unit>PCMMAXUNIT=255 sound.c:1027), and the bad: unwind (cmi.c:1004-1016) then destroys the dma tag, tears down the irq, releases resources, snd_mtxfree(sc->lock) and kfree(sc, M_DEVBUF) WITHOUT mixer_uninit(dev) -- the mixer cdev stays registered over freed memory and any unprivileged open/ioctl dispatches cmimix_* through mix_getdevinfo(m) == freed sc (cmi_rd/cmi_wr with a type-confused bus_space tag/handle read out of the freed ~264-byte chunk; the mixer's own m->lock survives, so nothing masks the UAF). Identical construct to the verified family DF-4156/4193/4299/4334/4344/4421; this is the 7th instance with the narrowest window (exactly one post-mixer goto: pcm_register). Not runtime-reproducible on the audit guest: QEMU presents no CMedia 13f6 / class-040x PCI function (guest_check.log), and this snapshot's snd_cmi.ko itself fails kldload with 'depends on midi - not available or version mismatch' (serial console, run.log), so cmi_attach never executes. Ceiling on real hardware: local unprivileged UAF read/write through a 0666 cdev, gated by an attach-time failure -- Low, matching family precedent.