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)
PoC verification
Evidence pack
findings/poc/DF-4096 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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:
- No emu10k1 hardware (run.log: no 0x1102 PCI device,
kldload snd_emu10k1fails on the DragonFly 6.5-DEVELOPMENT guest). - 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 viaemu_valloc()(emu10k1.c:552-565).emupchan_classhas 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
- sys/dev/sound/pci/emu10k1.c:823-828 (unchecked valloc β vinit)
- sys/dev/sound/pci/emu10k1.c:552-565 (emu_valloc NULL on exhaustion)
- sys/dev/sound/pci/emu10k1.c:574-580 (emu_vinit dereferences m)
- sys/dev/sound/pci/emu10k1.c:2087,2139-2141 (channel count bound: max 8)
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; branch is unreachable even on hardware, so no runtime validation is possible by construction.
fix.diff (unified, 'git apply --check' OK)
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.
No comments yet.