# DF-4634 VERDICT — snd_clone_gc / snd_clone_destroy free entries under in-flight openers

## Bottom line

**status: reproduced** (impact: dos — unbounded unprivileged kernel cdev/devfs-node
leak via the same code path that yields the UAF write; the UAF executes on
every trapped round, corruption manifestation not observed within budget).

## How it was reproduced (QEMU guest, DragonFly 6.5-DEVELOPMENT, es1370 pcm0)

1. Guest booted with `-device es1370 -audiodev none,id=snd0`;
   `kldload snd_es137x` → pcm0, `/dev/dsp` `/dev/dsp0` 0666 (clone handler
   registered as "dsp0" by pcm_register, sys/dev/sound/pcm/sound.c:1138).
2. `probe2.c` established the SD_F_BUSY stall primitive: opening /dev/dsp0,
   setting 8 kHz/u8/mono + 16x8192 fragments, filling bufsoft, then close()
   holds SD_F_BUSY for the entire `chn_flush`→`chn_sync` drain — measured
   **16.3 s** (128 KB at 8 KB/s); 3x4096 fragments give the ~2.09 s used by
   the race.
3. `race2.c` (unprivileged user `maxx`): each round fills the staller, spawns
   4 victim `open("/dev/dsp0")` threads staggered around the staller's
   `close()`. Victims whose dev_clone handler completed before the close
   acquired SD_F_BUSY sleep in `PCM_WAIT` (dsp.c:475) with their entries in
   the pending-INVOKE state — the harness logs `trapped=4/4, close≈2090ms,
   vopen≈2090ms`.
4. When the staller's `dsp_close` finishes the drain it runs
   `snd_clone_release` + `snd_clone_unref` (dsp.c:806-813); refcount hits 0
   and `snd_clone_gc` (clone.c:421-437) prunes every victim entry that is
   !BUSY, INVOKE, and >750 ms old — the victims are ~2 s old — executing
   `destroy_dev(ce->devt)` + `kfree(ce)` **while the openers are still
   sleeping inside their opens**.
5. The victims then wake and run `snd_clone_acquire`/`snd_clone_ref`
   (dsp.c:481,688) on the freed `snd_clone_entry` (cdev->si_drv2 still points
   at it): UAF write (`ce->flags` RMW) + UAF read (`ce->parent`) + increment
   through that pointer. With the freed chunk un-reused these writes are
   silent (they land on the stale, still-coherent bytes) — no panic was
   observed in ~75 trapped rounds. Cross-object reuse analysis:
   recycled as another entry → a live entry gets BUSY-flagged (EBUSY storm);
   recycled as `struct dsp_cdevinfo` (48 B, same malloc bucket) →
   `ce->parent` reads `cdi->rdch` and `++c->refcount` increments
   `pcm_channel->speed` (offset 32) — the coincidental layout explains the
   silent rounds. A groomed reuse with a controlled chunk remains a plausible
   memory-corruption escalation (64-byte M_DEVBUF, attacker-influenced
   allocation timing via concurrent clones).
6. **Visible, persistent damage (the reproduced DoS)**: each prune's
   `destroy_dev` fires while the victim still holds the node's vnode locked
   in its VOP_OPEN. The `/dev/dsp0.N` node is never removed and the cdev is
   never fully released:
   - pre-fix measured growth: 196 → 211 → 259 → **282** leaked nodes over
     ~30 rounds (~3 per round, unbounded);
   - after the leak, `kldunload snd_es137x` / `kldunload sound` fail with
     `Device busy` forever (leaked cdevs pin the module) — observed;
   - reproduced across 3 independent runs.

## Fix validation (A/B on the same guest)

`fix.diff` (snd_clone_gc never frees INVOKE entries):

- vulnerable module (md5 e869fcdebe0137ee26a9de85429c0baf): 25-round
  baseline run — node count grows without bound (run.log);
- fixed module (md5 cf8c3fcacda2760826cc3126a64a7ba2): same binary, 40
  rounds, **104 victim-traps** (the stall+gc interplay still exercised —
  the fix path is genuinely taken), final node count **0**, `/dev/dsp0`
  opens normally (run.fixed.log).

fix_status: **fixed** (baseline leak reproduced; patched leak gone; normal
clone lifecycle intact).

## Honest limits

- No kernel panic/heap-corruption manifestation was observed; the UAF
  transition itself is proven by timing arithmetic (trap ⇒ refcount 0 ⇒ gc
  ⇒ prune criteria all satisfied ⇒ kfree under the sleeping opener) and by
  the devfs-side scar (leaked nodes) that only the prune-in-flight produces.
- Exploitation toward uid=0 would require deterministic reuse of the freed
  64-byte chunk between the gc and the victim wake (µs window) with a
  controlled `parent` pointer — plausible grooming (concurrent clone-entry
  allocation) but not demonstrated here.
- snd_clone_destroy (pcm_unregister/detach) has the same INVOKE gap but
  needs a privileged/driver event to race; noted, not exercised.

## Kernel refs

- sys/dev/sound/clone.c:421-437 (snd_clone_gc prune of INVOKE entries)
- sys/dev/sound/clone.c:381-384 (SND_CLONE_EXPIRED 750ms timing assumption)
- sys/dev/sound/clone.c:46-58 (design comment admitting the assumption)
- sys/dev/sound/clone.c:444-465 (snd_clone_destroy same gap)
- sys/dev/sound/pcm/dsp.c:475-481 (PCM_WAIT before snd_clone_acquire)
- sys/dev/sound/pcm/dsp.c:806-813 (close-side release/unref → gc)
- sys/dev/sound/pcm/channel.c:983-999 (chn_flush → chn_sync sleeping drain)
- sys/vfs/devfs/devfs_core.c:1440-1460 (destroy worker skips release_dev on
  unlink error — compounding leak)

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