# DF-4322 — dsp_clone() shared `static struct cdev *dev` race (cross-card clone handoff)

**File:** `sys/dev/sound/pcm/dsp.c` (dsp_clone), reached from the pcm core
registry created in `sys/dev/sound/pcm/sound.c` (`pcm_register` ->
`devfs_clone_handler_add(devtoname(d->dsp_clonedev), dsp_clone)`, sound.c:1138,
plus the global "dsp" handler from `sound_modevent`, sound.c:1425).

**Kernel:** DragonFly dfbsd 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026
(X86_64_GENERIC)

## How to reproduce

Guest needs TWO registered pcm units (here: pcm0 = QEMU `ES1370` ->
snd_es137x, pcm1 = `intel-hda`+`hda-duplex` -> snd_hda). Launch with
`dfbsd-qemu/vm_snd.sh`, then as root: `kldload snd_es137x; kldload snd_hda`.
Verify `/dev/dsp0` and `/dev/dsp1` exist.

```
build.sh   # cc -O2 -pthread -o /tmp/dsp_race /tmp/dsp_race.c   (as user maxx)
run.sh     # /tmp/dsp_race                                       (as user maxx)
```

Expected (race hit): a line

```
[!] RACE HIT: opened /dev/dsp1, fd is clone of card 0 (devnode /dev/dsp0.vp0, name pcm0:virtual:dsp0.vp0)
```

plus `N cross-card mismatches >= 1` and exit status 1. The race is rare
(observed ~1 hit per 10^5 open cycles); repeat runs or longer RUN_SECONDS if
no hit in the first window. `run.2.log`/`run.3.log`/`run.4.log` show
hit-free characterization runs (~36k/56k/38k cycles).

## What the PoC does

Parallel threads hammer `open("/dev/dsp0")` and `open("/dev/dsp1")`. Every
successful open goes through the devfs clone handler `dsp_clone()`; after each
open the fd is `fstat()`ed and the unit byte of the minor (`minor & 0xff`,
`PCMUNIT`, sound.h:115) is compared against the unit that was actually opened.
On mismatch, `SNDCTL_AUDIOINFO` (dev=-1) is issued to name the backing
channel (`ai.devnode`/`ai.name`).

Observed hit: an `open("/dev/dsp1")` returned a cdev whose minor encodes
unit 0 and whose channel is `pcm0:virtual:dsp0.vp0` — i.e. the clone handler
for card 1 handed card 0's clone cdev to devfs for the open.

## Root cause

`dsp_clone()` keeps the outgoing cdev in a function-`static` variable:

```c
static struct cdev *dev = NULL;            /* dsp.c:2381 */
...
ce = snd_clone_alloc(d->clones, &dev, &cunit, udcmask);   /* dsp.c:2495 */
if (ce != NULL) {
        ...
        dev = make_only_dev(...);                          /* dsp.c:2506 */
        ...
}
...
ap->a_dev = dev;                                          /* dsp.c:2520 */
```

devfs invokes clone handlers with `devfs_lock` *released*
(sys/vfs/devfs/devfs_core.c:2329-2336), and each pcm unit has its own
`snddev_info` lock/busy-flag, so two opens on two different pcm units run
`dsp_clone()` fully concurrently while sharing this one global. Whichever
thread stores `dev` last before the other's `ap->a_dev = dev` read wins, and
the wrong card's cdev is returned to devfs for the open. FreeBSD's dsp_clone
uses a *local* `struct cdev *dev = NULL;` — the `static` is a DragonFly port
regression.

Consequences:
1. Demonstrated (unprivileged, this PoC): cross-card channel confusion — an
   fd opened via /dev/dsp1 operates on pcm0's channel (and vice versa).
2. Traced, not demonstrated: if `snd_clone_alloc()` returns NULL via its
   early exits (clone manager disabled mid-flight, e.g. during
   `pcm_unregister`'s `snd_clone_disable()` at sound.c:1200) it does not set
   `*dev` (clone.c:627-629), so `ap->a_dev` is the *stale* static pointer —
   possibly a cdev already destroyed by `snd_clone_destroy()`/GC
   (sound.c:1226, clone.c:431/457) — handing devfs a freed cdev (UAF).

Both pcm devices are 0666 by default, so (1) crosses no permission boundary
on a default install; its security weight is that /dev/dspN opens are not
bound to card N. (2) requires a concurrent `pcm_unregister` (driver detach —
root) racing an open.

## Fix

Make `dev` local again (matches FreeBSD):

```diff
--- a/sys/dev/sound/pcm/dsp.c
+++ b/sys/dev/sound/pcm/dsp.c
@@ -2379,7 +2379,7 @@ dsp_clone(struct dev_clone_args *ap)
 	int i, unit, udcmask, cunit, devtype, devhw, devcmax, tumax;
 	size_t len;
 	char sname[64];
 	int err = EBUSY;
-	static struct cdev *dev = NULL;
+	struct cdev *dev = NULL;
```

`fix.diff` carries this patch (authored against the read-only sys/ tree; not
applied there).

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