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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4192 Β· 15 files
FileTypeDescriptionSize
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_LOCK before thread A (START) inserted c: CHN_REMOVE = SLIST_REMOVE walks while (SLIST_NEXT(curelm) != c) past the end of the list β†’ curelm = NULL β†’ NULL->sle_next at 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).

VERDICT.md
↓ download raw

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):

  1. 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 wchan pcm0 (lockmgr_exclusive) reached from dsp_open, mixer_ioctl, dsp_ioctl (ddb_blocked_stacks.txt) β€” everything queued behind the stuck lock holder.
  2. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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.
↓ fix.diffper-fix-DF-4192

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