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

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