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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4116 Β· 12 files
FileTypeDescriptionSize
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)

VERDICT.md
↓ download raw

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

  1. 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.

  2. 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.

  3. 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.
  4. 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.

  5. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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
↓ fix.diffper-fix-DF-4116

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.