pcm-core chn_trigger() trigger-flag/busy-list race (reached via es137x /dev/dsp): unprivileged kernel panic and unrecoverable system-wide hang
Summary
Concurrent SNDCTL_DSP_SETTRIGGER START/STOP ioctls on one shared 0666 /dev/dsp descriptor let two threads interleave inside chn_trigger(): one flips c->trigger under CHN_LOCK then drops the lock before the PCM_LOCK-protected busy-list insert; the other (STOP) enters in the window and CHN_REMOVEs a channel that was never inserted - SLIST_REMOVE's predecessor walk runs off the list end -> NULL->sle_next deref at offset 0x138 -> panic; with other channels queued the same interleaving builds a cyclic list and the walk spins forever holding the channel lock (all dsp/mixer clients block; power-cycle required). Defect is in the pcm CORE (sound.ko channel.c:2198-2219) and applies to EVERY pcm driver; the NULL-safe CHN_INSERT/REMOVE_SAFE macros exist but are used nowhere. REPRODUCED LIVE twice on the QEMU ES1370 guest as unprivileged (panic + full-hang variant with DDB spin trace). SAFE-macro fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4192 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.8 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| trighammer.c | β | 2.0 KB | view raw | |
| es_batt.c | β | 5.2 KB | view raw | |
| build.sh | β | 190 B | view raw | |
| run.sh | β | 222 B | view raw | |
| run.log | β | 1.8 KB | view raw | |
| panic.txt | β | 630 B | view raw | |
| ddb_ps_stuck_threads.txt | β | 28.7 KB | view raw | |
| ddb_blocked_stacks.txt | β | 715 B | view raw | |
| env.txt | β | 627 B | view raw | |
| fix.diff | β | 704 B | view raw | |
| fix_build.log | β | 1.2 KB | view raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-4192 β pcm chn_trigger() trigger-flag/busy-list race: unprivileged kernel panic + unrecoverable system hang
Discovered during the pass-2 (GLM 5.3) audit of sys/dev/sound/pci/es137x.c,
live-verified on the QEMU ES1370 card. The defect is in the pcm core
(sys/dev/sound/pcm/channel.c, compiled into sound.ko), not in es137x
itself β it is reachable through any pcm sound driver via the 0666
/dev/dsp device.
The bug (sys/dev/sound/pcm/channel.c:2190-2225)
chn_trigger() updates the software trigger flag c->trigger under
CHN_LOCK(c), then releases the channel lock before taking PCM_LOCK(d)
to insert/remove c from the per-device channels.pcm.busy list:
case PCMTRIG_START:
if (c->trigger != PCMTRIG_START) {
c->trigger = go; /* flag flips HERE */
CHN_UNLOCK(c); /* ---- window ---- */
PCM_LOCK(d);
CHN_INSERT_HEAD(d, c, channels.pcm.busy); /* list HERE */
...
case PCMTRIG_STOP:
case PCMTRIG_ABORT:
if (c->trigger == PCMTRIG_START) {
c->trigger = go;
CHN_UNLOCK(c);
PCM_LOCK(d);
CHN_REMOVE(d, c, channels.pcm.busy); /* SLIST walk */
Two threads issuing SNDCTL_DSP_SETTRIGGER START/STOP concurrently on the
same fd (same pcm channel β dsp.c serializes per channel only via
CHN_LOCK, which chn_trigger drops mid-call) desynchronize the flag from
list membership:
- Thread B (STOP) wins
PCM_LOCKbefore thread A (START) insertedc:CHN_REMOVE=SLIST_REMOVEwalkswhile (SLIST_NEXT(curelm) != c)past the end of the list βcurelm = NULLβNULL->sle_nextat offset 0x138. - Interleaved insert/remove on a non-empty list builds a cycle β the same
walk spins forever in kernel mode holding the channel lock, wedging
every subsequent dsp/mixer user (they pile up in
lockmgr_exclusive).
DFly ships NULL-safe variants (CHN_INSERT_HEAD_SAFE / CHN_REMOVE_SAFE,
channel.h:193-221) but chn_trigger does not use them.
Making the guest attackable (QEMU audio)
The audit guest boots headless with no sound hardware. Boot it with a temporary launcher (vm.sh untouched β DF-3879 pattern):
qemu-system-x86_64 -enable-kvm -m 4096 -smp 6 \ -drive file=disk.qcow2,if=virtio,format=qcow2 \ -netdev user,id=n0,hostfwd=tcp:127.0.0.1:5522-:22 \ -device virtio-net-pci,netdev=n0 \ -audiodev none,id=snd0 -device ES1370,audiodev=snd0 \ -display none -monitor unix:boot-mon.sock,server,nowait \ -serial unix:/tmp/ser.sock,server=on,wait=off -daemonize
In the guest: kldload snd_es137x β pcm0: <AudioPCI ES1370> and
/dev/dsp, /dev/mixer appear 0666 root:wheel.
Build (on the guest, as any user)
cc -O2 -pthread -o /tmp/trighammer /tmp/trighammer.c
Run (unprivileged)
/tmp/trighammer 30 # 6 threads: 4Γ SETTRIGGER START/0 race,
# SETFRAGMENT/SPEED churn, read/write I/O
Success criterion (baseline, stock sound.ko):
* kernel panic Fatal trap 12 ... fault virtual address = 0x138 at
chn_trigger+0x263 (movq 0x138(%rdx),%rax, rdx==NULL) β see panic.txt;
typically within 10-60 s, and
* alternatively/intermittently the whole system hangs silently: ssh dies,
serial stops; NMI into DDB shows a CPU spinning at chn_trigger+0x26a
(cmpq %rbx,%rax) inside the same SLIST walk, with every dsp/mixer
client blocked behind it (ddb_blocked_stacks.txt).
With the patched module (fix.diff): no panic, no system wedge across
10 min of hammering; the PoC process itself can still end up blocked in a dsp ioctl (residual flag/state desync β see VERDICT.md "Fix validation").
es_batt.c is the wider 5-process OSS ioctl battery that first triggered the hang (mixer dev 0..255, SETFRAGMENT extremes, speed 0..0x7fffffff, format storm, open/close churn).
DF-4192 β VERDICT (pass 2, GLM 5.3; audit target sys/dev/sound/pci/es137x.c, defect in pcm core)
VERDICT: REPRODUCED β unprivileged local kernel panic AND unrecoverable system-wide hang via SNDCTL_DSP_SETTRIGGER race in chn_trigger(). confidence: certain. impact: panic (availability-only; no corruption primitive beyond the NULL-offset read at fault VA 0x138 β the SLIST walk's pointer write only ever targets valid list nodes).
TIMELINE (all on DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026, X86_64_GENERIC, QEMU 7.2 KVM, -device ES1370, kldload snd_es137x):
- Baseline battery (es_batt.c, 5 processes as user "maxx", stock sound.ko
md5 c4a52a5bab2572173ead879d9869a53c):
* Run 1: full system wedge ~90-120 s in. ssh dead, serial frozen, QEMU
at ~143% host CPU. NMI into DDB: "Stopped at chn_trigger+0x26a:
cmpq %rbx,%rax" β CPU0 spinning inside the CHN_REMOVE predecessor
walk, holding the channel lock.
* Run 2: partial wedge. DDB
ps: five es_batt threads sleeping on wchanpcm0(lockmgr_exclusive) reached from dsp_open, mixer_ioctl, dsp_ioctl (ddb_blocked_stacks.txt) β everything queued behind the stuck lock holder. - Focused repro (trighammer.c: 4 threads racing SETTRIGGER START/PCM_ENABLE_ALL vs 0 on one shared fd + SETFRAGMENT/SPEED + r/w I/O, unprivileged): kernel panic within β€60 s on the first run β panic.txt: Fatal user address access from kernel mode from trighammer Fatal trap 12: page fault while in kernel mode fault virtual address = 0x138 instruction pointer = 0x8:0xffffffff82644233 (chn_trigger+0x263) current process = 953 0x138 is the offset of the channels.pcm busy-list link inside struct pcm_channel; rdx==NULL is the SLIST_REMOVE walk cursor that ran off the end of the list (the channel being "removed" was never inserted β its inserter was preempted in the CHN_UNLOCK window at channel.c:2199-2201).
ROOT CAUSE: sys/dev/sound/pcm/channel.c:2190-2225 β the software trigger
flag c->trigger is flipped under CHN_LOCK(c), but the corresponding
insert/remove on d->channels.pcm.busy happens after dropping CHN_LOCK and
taking PCM_LOCK(d). A concurrent SNDCTL_DSP_SETTRIGGER on the same channel
(dsp.c serializes on CHN_LOCK only) interleaves the two halves:
* STOP-before-insert β SLIST_REMOVE walks past list end β NULL deref at
link offset (panic; fault VA 0x138 proves the exact field).
* interleaved insert/remove on a non-empty list β cyclic list β the same
walk spins forever in kernel mode with preemption, holding the channel
lock β silent total system hang (wedge samples at chn_trigger+0x26a and
+0x263).
The NULL-safe list macros CHN_INSERT_HEAD_SAFE / CHN_REMOVE_SAFE already
exist in channel.h:193-221 but were used nowhere.
ES137X SPECIFICS: the driver is only the delivery vehicle; the race is in the framework and applies to every pcm driver (verified reachability via the QEMU-emulated ES1370, /dev/dsp mode 0666). es137x's own ioctl surface (mixer register indices via mixtable[], fragment/rate arithmetic, ring sizing) survived the full battery without a single driver-local fault β see the file's negative notes in the audit result.
EXPLOIT CHAIN: none worth pursuing β availability only. The corruption window writes pointer values only into valid list nodes; the wild access is a read at a fixed offset from NULL. No path to uid=0 identified; the value of this bug is denial of service (panic or permanent hang requiring power cycle, from any local user, on default-config machines with audio).
FIX VALIDATION: * Baseline reproduced: YES (panic + wedge, above). * Patched module: sound.ko rebuilt on the guest from /usr/src/sys with CHN_INSERT_HEAD_SAFE + CHN_REMOVE_SAFE substituted at channel.c:2201 and 2218 (fix.diff; module md5 0d076339abf53061007aa1deeecedfd7 installed to /boot/kernel/sound.ko; fix_build.log). * Re-run of the exact PoC on the patched module: NO panic and NO system wedge across >10 minutes of hammering (3Γ60 s trighammer + 30 s + battery attempts; serial log zero "Fatal trap"; DDB samples show idle CPUs, not chn_trigger spins) β fix_patched_reproduced=0 (the bad behavior is GONE). * Residual (noted honestly): post-fix, the PoC process itself can still fail to exit β a thread ends up blocked in a dsp ioctl (join never returns). The _SAFE macros prevent the memory fault and the list cycle, but the flag/list desync itself remains; a full fix needs the trigger transition to be made atomic w.r.t. the busy list (e.g., do list bookkeeping while holding PCM_LOCK across the flag check, or track list membership explicitly). Availability impact of the residual is process-local, not system-wide.
ENVIRONMENT: dfbsd-qemu/disk.qcow2 @ with-src snapshot; launcher with -audiodev none,id=snd0 -device ES1370,audiodev=snd0; guest kldload snd_es137x (snd_es137x.ko from /boot/kernel); /dev/dsp + /dev/mixer 0666. Guest left clean: vm.sh revert with-src + vm.sh up (no audio device).
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
fixedfix.diff swaps CHN_INSERT_HEAD/CHN_REMOVE for the CHN_INSERT_HEAD_SAFE/CHN_REMOVE_SAFE macros (already present in channel.h:193-221, previously unused) at channel.c:2201/2218. Patched sound.ko (md5 0d076339abf53061007aa1deeecedfd7) built on the guest from /usr/src/sys and installed to /boot/kernel. Exact PoC re-run on the patched module: NO panic and NO system-wide wedge across >10 minutes of hammering (3x60s + 30s trighammer plus battery attempts; zero 'Fatal trap' on serial; DDB samples idle) - the previously-observed bad behavior is GONE (fix_patched_reproduced=0 denotes the BUG did not reproduce). Residual, noted honestly: the PoC process itself can still end up permanently blocked in a dsp ioctl because the flag/list desync itself persists (_SAFE only prevents the memory fault and the cycle); a complete fix should make the trigger transition atomic with respect to the busy-list bookkeeping.
fix.diff; fix_build.log (build transcript, module md5s, installed-symbol check); VERDICT.md 'FIX VALIDATION' section.
Confirmed kernel references
Detail
Evidence (decisive lines)
panic.txt (full 'Fatal trap 12 ... 0x138 ... chn_trigger+0x263' with current process = PoC pid 953); run.log (baseline wedge runs + DDB samples); ddb_ps_stuck_threads.txt + ddb_blocked_stacks.txt (dsp_open/mixer_ioctl/dsp_ioctl threads blocked behind the spinning lock holder); VERDICT.md (complete narrative incl. disassembly of the spin loop d230-d23d from guest sound.ko); fix_build.log (patched module md5 0d076339...); env.txt (0666 nodes, unpriv attacker).
PoC changes
n/a - PoC written from scratch for this finding (trighammer.c); es_batt.c is the wider discovery battery.
Verified recommended fix
Use the existing CHN_INSERT_HEAD_SAFE/CHN_REMOVE_SAFE macros in chn_trigger() (channel.c:2201,2218); longer term, make the c->trigger transition atomic with the channels.pcm.busy list bookkeeping.
Verdict
REPRODUCED: an unprivileged local user races concurrent SNDCTL_DSP_SETTRIGGER START/STOP ioctls on a shared 0666 /dev/dsp descriptor; chn_trigger() (sys/dev/sound/pcm/channel.c:2190-2225) flips c->trigger under CHN_LOCK but performs the channels.pcm.busy insert/remove after dropping CHN_LOCK under PCM_LOCK, so the two halves interleave: (a) STOP-before-insert makes CHN_REMOVE/SLIST_REMOVE walk past the list end -> NULL->sle_next deref at struct offset 0x138 -> Fatal trap 12 kernel panic (captured: fault VA 0x138, RIP chn_trigger+0x263, process=unprivileged trighammer); (b) interleaved insert/remove builds a cyclic list -> the same walk spins forever in kernel mode holding the channel lock -> silent unrecoverable whole-system hang (captured via DDB: spin at chn_trigger+0x26a/+0x263 plus all dsp/mixer clients blocked in lockmgr_exclusive). Verified live on QEMU ES1370 (kldload snd_es137x); the defect is in the pcm core sound.ko, reachable through any pcm driver, es137x being the audited vehicle. Availability-only: no uid=0 chain exists (wild access is a fixed-offset read from NULL; list writes stay inside valid nodes).
No comments yet.