Guaranteed kernel panic in sndstat_sysuninit on sound.ko unload: SNDSTAT_FLUSH deletes a never-initialized sbuf (sndstat_buflen=0 static init vs sbuf-only-created-in-open invariant)
Summary
sndstat.c tracks its single global sbuf with the invariant 'sndstat_buflen == -1 <=> sbuf not allocated', but the static initializer sets sndstat_buflen = 0 while the sbuf is only created by sbuf_new() in sndstat_open(). Unloading sound.ko before any /dev/sndstat open reaches SNDSTAT_FLUSH with buflen==0 and calls sbuf_delete() on the all-zero static sbuf -> KASSERT panic. Root-only but deterministic on the stock INVARIANTS kernel config - observed live 5x total across prior runs (the recurring sndstat_sysuninit lead from the emu10kx/channel.c/snd_hda runs) + 2 fresh minimal repros; A/B control on one boot isolates the single state variable; ONE-LINE fix (buflen = -1) applied to the guest /usr/src, sound.ko rebuilt -Werror-clean, trigger returns UNLOADED-CLEAN rc=0 3/3 cycles, unload-while-held still EBUSY (protection intact). This closes the long-standing sndstat_sysuninit lead.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4116 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 4.2 KB | β raw | |
| trigger.sh | β | 457 B | view raw | |
| run.sh | β | 129 B | view raw | |
| run.log | β | 3.2 KB | view raw | |
| panic.txt | β | 657 B | view raw | |
| panic.2.txt | β | 588 B | view raw | |
| build.log | β | 28.3 KB | view raw | |
| fix.diff | β | 2.0 KB | view raw | |
| env.txt | β | 772 B | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.7 KB | view raw |
DF-4116 β Guaranteed kernel panic in sndstat_sysuninit on sound.ko unload
File: sys/dev/sound/pcm/sndstat.c β pass 2 (GLM 5.3), LIVE-verified + fix-validated on the audit guest (DF 6.5-DEVELOPMENT X86_64_GENERIC, 2026-09-18). This is the root-cause resolution of the panic previously observed three times as a side note (DF-4026 panic.sndstat-unload.txt, DF-4115 pack Β§5, and the emu10kx run).
THE BUG
sndstat.c tracks its single global sbuf with the invariant
"sndstat_buflen == -1 <=> sbuf not allocated" (SNDSTAT_FLUSH,
sndstat.c:74-79), but the static initializer sets
sndstat_buflen = 0 (sndstat.c:68) β "allocated, empty" β while the
sbuf is only actually created by sbuf_new() inside sndstat_open()
(sndstat.c:146). If sound.ko is unloaded before any process has
opened /dev/sndstat since load:
* sound_modevent MOD_UNLOAD (sound.c:1433) calls
sndstat_acquire(curthread) and never releases β deliberately
handing sndstat ownership to the teardown β so si_drv1 holds the
unloading process's pid;
* the module SYSUNINIT (sndstat.c:466-476) runs sndstat_uninit(),
whose PID guard (sndstat.c:444) therefore PASSES (P == P);
* SNDSTAT_FLUSH() (sndstat.c:453) sees buflen == 0 != -1 and calls
sbuf_delete() on the never-initialized, all-zero static sbuf;
* subr_sbuf.c:100-101 KASSERT(s->s_buf != NULL) fires:
panic: sbuf_delete called with uninitialized or corrupt sbuf
sys/config/X86_64_GENERIC:56 ships `options INVARIANTS`, so this
panics the STOCK kernel config, deterministically, on every
`kldload sound; kldunload sound` (or any snd_* driver unload that
cascades sound.ko via kern_linker.c:545) with no intervening
/dev/sndstat open. Root-only trigger; impact = reliable kernel
panic (admin-action DoS), no memory corruption on non-INVARIANTS
kernels (sbuf_delete no-ops on the zeroed struct there).
BUILD: none (kernel module already present; trigger is kldload/kldunload)
RUN (root):
sh trigger.sh # stock: panics; patched: "UNLOADED-CLEAN rc=0"
FIX: one line β static int sndstat_buflen = -1; (fix.diff),
validated on a rebuilt sound.ko: panic gone, 3/3 clean cycles,
busy-protection (unload while open => EBUSY) intact.
ARTIFACTS panic.txt / panic.2.txt two independent stock-kernel captures run.log full session log (A/B control + fix tests) build.log full patched-module build (-Werror clean) fix.diff the one-line fix (validated) verdict.json machine verdict (reproduced / panic / certain)
DF-4116 VERDICT β sndstat_sysuninit guaranteed panic on sound.ko unload
STATUS: reproduced (deterministic, 2 fresh captures this session + 3 prior independent observations); fix VALIDATED on rebuilt module. impact: panic (root-triggered admin-action DoS; stock X86_64_GENERIC carries INVARIANTS, so the KASSERT is live on the shipping config) confidence: certain
-
ROOT CAUSE (line-verified, every hop) sndstat.c:68
static int sndstat_buflen = 0;The SNDSTAT_FLUSH macro (sndstat.c:74-79) defines the invariant "buflen != -1 => sndstat_sbuf is allocated". The static initializer violates it: 0 means "allocated, empty" but the sbuf is only created by sbuf_new() in sndstat_open() (sndstat.c:146), which also sets buflen = 0 (sndstat.c:151) β the ONLY legitimate producer of the 0 state. sound.c:1433 sound_modevent MOD_UNLOAD: sndstat_acquire(curthread) and no matching release β by design: acquire parks the unloading thread's pid in si_drv1 so the teardown may proceed. This is why the PID guard in sndstat_uninit() (sndstat.c:444, SNDSTAT_PID(sndstat_dev) != curthread->td_proc->p_pid) PASSES on every successful sound.ko unload (same thread runs MOD_UNLOAD and the SYSUNINITs via sys_kldunload -> linker_file_unload). sndstat.c:453 SNDSTAT_FLUSH() with buflen == 0 -> sbuf_delete() on the all-zero static sndstat_sbuf. subr_sbuf.c:756+ sbuf_delete -> assert_sbuf_integrity -> subr_sbuf.c:100-101 KASSERT(s->s_buf != NULL, "sbuf_delete called with uninitialized or corrupt sbuf") -> panic. INVARIANTS is in the stock config: sys/config/X86_64_GENERIC:56. -
WHY IT FIRES "EVERY TIME" YET WAS SEEN AS SPORADIC The panic requires buflen == 0 at unload, i.e. /dev/sndstat never opened-and-closed since sound.ko load. Sessions that cat /dev/sndstat (open+close sets buflen = -1 via SNDSTAT_FLUSH in sndstat_close, sndstat.c:171) unload cleanly. A/B on one boot (run.log RUN 2) isolates exactly this variable. Three earlier observations (emu10kx, DF-4026 channel.c session, DF-4115 snd_hda session) all had no sndstat reader between module load and unload.
-
REACHABILITY Root-only (kldload/kldunload). Variants that reach the same SYSUNINIT:
kldload sound; kldunload sound(minimal; RUN 1/2 here)kldunload snd_<drv>cascading sound.ko through linker_file_unload's dependency recursion (kern_linker.c:545) β the DF-4115/DF-4026 shape (their traces show the recursive linker_file_unload frame). On a hypothetical non-INVARIANTS kernel sbuf_delete() would no-op on the zeroed struct (not dynamic => nothing freed; subr_sbuf.c:759-764) and unload would succeed β hence severity Medium, not High: shipping config panics, stripped-config survives.
-
EXPLOITABILITY No memory-safety primitive: sbuf_delete on the zeroed static struct only trips the assert; no attacker-controlled free, no UAF window found in the flush path. Ceiling = reliable kernel panic as root during a routine admin action (module rollback after driver test). Not an unprivileged trigger.
-
FIX (validated) fix.diff: initialize the sentinel to "not allocated": -static int sndstat_buflen = 0; +static int sndstat_buflen = -1; Rebuilt sound.ko on the guest (make -Werror clean, md5 9422fb6783b984413d3a47ef55d4fbc7):
- exact trigger now returns UNLOADED-CLEAN rc=0 (was: panic)
- 3/3 additional load/unload cycles clean
- unload with /dev/sndstat held open still fails EBUSY (protection intact), clean unload after release FreeBSD resolved the whole class by rewriting sndstat to per-file cdevpriv sbufs (no global teardown at all); the one-liner is the minimal fix for DragonFly's current architecture.
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
fixedVALIDATED: one-line fix.diff (sndstat_buflen = -1) applied to guest /usr/src, sound.ko rebuilt with -Werror; the exact baseline trigger (kldload/kldunload sound, no sndstat open) returns rc=0 clean instead of panicking; 3/3 repeat cycles clean; unload while /dev/sndstat is held still fails EBUSY by design (no regression).
run.log FIX VALIDATION section; build.log (full -Werror build, sound.ko md5 9422fb6783b984413d3a47ef55d4fbc7); VERDICT.md section 5
Confirmed kernel references
Detail
Exploit chain
kldload any snd_* driver module; kldunload it without ever opening /dev/sndstat -> linker_file_unload unloads dependency sound.ko -> sndstat_sysuninit -> SNDSTAT_FLUSH with sndstat_buflen==0 -> sbuf_delete on never-sbuf_new'd static -> KASSERT panic. Local DoS by root.
Evidence (decisive lines)
findings/poc/DF-4743/DF-4116_live_repro/ (NOTES.txt = full A/B narrative + line refs; panic.txt = panic + backtrace)
Verdict
Independently re-reproduced live on the QEMU guest (DFly 6.4-SYNTH, INVARIANTS kernel) during the DF-4743 premise run: kldload snd_via82c686 on a fresh boot then kldunload -> panic 'sbuf_delete called with uninitialized or corrupt sbuf' at _assert_sbuf_integrity <- sbuf_delete <- sndstat_sysuninit <- linker_file_unload (recursive dependency unload of sound.ko). Controlled A/B: cat /dev/sndstat before kldunload -> clean unload (UNLOAD_AFTER_READ_RC=0); reload (fresh module statics) then kldunload without read -> panic again, guest down. Root cause line-verified at sndstat.c:66/68/74-79/146/171. Root-only trigger (kldload/kldunload), consistent with Medium severity.
No comments yet.