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

emupchan_init feeds unchecked emu_valloc() NULL into emu_vinit() β€” latent NULL-pointer write (unreachable today, hardening)

Summary

emu_valloc returns NULL on 64-voice exhaustion; emupchan_init never checks and emu_vinit immediately writes m->start/m->channel through the pointer - page-0 write/panic. Currently unreachable: pcm_register caps play channels at 8 (2 voices each = 16 of 64), pcm_addchan is the sole CHANNEL_INIT caller, vchans use their own class. Fences a latent kernel NULL write that would appear if the channel-count invariant is ever raised. NULL-check fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4096 Β· 8 files
FileTypeDescriptionSize
README.md β€” 1.9 KB ↓ raw
VERDICT.md β€” 2.1 KB ↓ raw
probe.sh β€” 585 B view raw
run.log β€” 282 B view raw
env.txt β€” 184 B view raw
fix.diff β€” 389 B view raw
manifest.json β€” 987 B view raw
verdict.json β€” 2.1 KB view raw

DF-4096 β€” emupchan_init passes unchecked emu_valloc() NULL into emu_vinit() (hardening)

File: sys/dev/sound/pci/emu10k1.c (pass 2, GLM 5.3)

What

snd_mtxlock(sc->lock);                       /* emu10k1.c:823-828 */
ch->master = emu_valloc(sc);
ch->slave = emu_valloc(sc);
snd_mtxunlock(sc->lock);
r = (emu_vinit(sc, ch->master, ch->slave, sc->bufsz, ch->buffer))
    ? NULL : ch;

emu_valloc() (emu10k1.c:552-565) returns NULL when all 64 entries of sc->voice[] are busy. The NULL return is never checked; emu_vinit() (emu10k1.c:568-616) immediately does

buf = emu_memalloc(sc, sz, &tmp_addr);
if (buf == NULL) return -1;
...
m->start = emu_memstart(sc, buf) * EMUPAGESIZE;   /* m == NULL => NULL write */

i.e. a write through m->start at offset ~0x14 of NULL β€” kernel page-0 write/panic. Same shape for s (slave) at emu10k1.c:600-614.

Reachability (why Info, not Low)

Every registered play channel consumes exactly 2 voices; the framework channel-init count is fixed by pcm_register(dev, sc, sc->nchans, ...) with sc->nchans = audigy ? 8 : 4 (emu10k1.c:2087, 2139-2141), and pcm_addchan is the only invoker of CHANNEL_INIT (verified: emupchan_class has no other in-tree users; vchans use their own class in pcm/vchan.c). Maximum possible consumption is 16 of 64 voices β€” the exhaustion branch is unreachable today. It becomes live the day anyone raises the registered play-channel count above 32 or adds a dynamic channel-creation path, with no local error marker β€” hence filed as a hardening finding (CWE-476).

Fix

Check both allocations before dereferencing (see fix.diff) β€” return NULL so the pcm framework's pcm_chn_create failure path handles it cleanly.

Reproduce

Hardware-gated + currently unreachable (see VERDICT.md). Guest probe evidence in run.log (no emu10k1 device on the QEMU guest).

VERDICT.md
↓ download raw

DF-4096 VERDICT

Status: inconclusive on-guest (hardware-gated + currently unreachable) / certain at source level. Impact: none today (defense-in-depth NULL-deref guard).

Was it reproduced?

No, and it cannot be on this guest for two independent reasons:

  1. No emu10k1 hardware (run.log: no 0x1102 PCI device, kldload snd_emu10k1 fails on the DragonFly 6.5-DEVELOPMENT guest).
  2. The branch is unreachable even with hardware: voice exhaustion needs

    32 registered play channels, but pcm_register(dev, sc, sc->nchans, ...) registers at most 8 (emu10k1.c:2087, 2139-2141), each channel consuming 2 of 64 voices via emu_valloc() (emu10k1.c:552-565). emupchan_class has no other in-tree caller; vchans use their own channel class (pcm/vchan.c:240), so no dynamic re-init path exists.

Why filed at all

emupchan_init() (emu10k1.c:823-828) passes the unchecked emu_valloc() results straight into emu_vinit(), which writes m->start, m->channel, ... through the pointer (emu10k1.c:579-598) β€” a latent NULL-pointer write with zero diagnostic value if the invariant above is ever broken (e.g. a future "more channels" patch or a leaked-busy bug in voice bookkeeping). One if eliminates the class. CWE-476.

Fix validation

Not performable on-guest. fix.diff adds the NULL check and returns NULL (channel-init failure), which the pcm core handles via pcm_chn_create's existing error path (sound.c pcm_addchan β†’ pcm_chn_destroy). git apply --check verified against the read-only tree.

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; branch is unreachable even on hardware, so no runtime validation is possible by construction.

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

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (reachability proof incl. framework bounds); run.log (no emu10k1 device on guest); fix.diff (NULL check returning NULL to the pcm core's existing failure path)

PoC changes

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

Verified recommended fix

Check emu_valloc() results in emupchan_init and return NULL on exhaustion.

Verdict

Hardening (CWE-476), unreachable today: emupchan_init (emu10k1.c:823-828) feeds unchecked emu_valloc() results into emu_vinit(), which writes m->start/m->channel through the pointer (:579-598) β€” NULL write if voice exhaustion ever occurs. It cannot occur: pcm_addchan is the only CHANNEL_INIT invoker, pcm_register caps play channels at nchans = audigy?8:4 (:2087,:2139-2141), 2 voices per channel = 16 of the 64-voice pool; vchans use their own class (pcm/vchan.c:240). Filed Info so the latent NULL write is fenced off before anyone raises the channel count.