# DF-5605 — `lockowned()` shared-lock guess makes the ua_mixer_set/setrecsrc reentrancy dance unsound for shared holders (latent; DFly-specific divergence from FreeBSD `mtx_owned`)

**File:** `sys/bus/u4b/audio/uaudio_pcm.c` (pass 2, GLM 5.3)
**Severity:** Info (hardening / latent footgun — no in-tree manifestation today)
**Evidence classes:** production-code KLD harness on the audit QEMU guest + code
reading. No USB audio hardware required (and none needed — the defect is in
kernel lock API semantics + the decision shape, independent of any device).

## TL;DR

`ua_mixer_set()` (uaudio_pcm.c:139) and `ua_mixer_setrecsrc()`
(uaudio_pcm.c:159) decide whether to (re)acquire the mixer lock with:

```c
if (lockowned(lock)) {
        do_unlock = 0;          /* assume *we* hold it */
} else {
        do_unlock = 1;
        lockmgr(lock, LK_EXCLUSIVE);
}
```

DragonFly's `lockowned()` (kern_lock.c:1365) is documented as *"Return non-zero
if the caller owns the lock shared or exclusive. We can only guess re: shared
locks."* — for a lock held **shared by any thread** it returns true because
`(count & LKC_SMASK) != 0` without checking WHO holds it. So under a
foreign-shared holder, the calling thread — which holds **nothing** — concludes
"already owned" and runs `uaudio_mixer_set()` / `uaudio_mixer_setrecsrc()`
(uaudio.c:5390 / :5407, walking `sc->sc_mixer_root` and issuing USB requests)
with zero exclusion while another thread is inside a shared critical section.

The sound(4) core itself does NOT rely on `lockowned()` for this decision —
`mixer_set()`/`mixer_setrecsrc()` (mixer.c:273, mixer.c:346) use the precise
`lockstatus(m->lock, curthread) == LK_EXCLUSIVE`. The glue in uaudio_pcm.c is
the FreeBSD `mtx_owned()` idiom carried over verbatim; DFly's `lockowned()` is
not equivalent for shared locks.

**Not exploitable today:** `m->lock` is created by `snd_mtxcreate()`
(sound.c:92, `lockinit(..., LK_CANRECURSE)`) and every in-tree acquisition is
`LK_EXCLUSIVE`: mixer.c via `snd_mtxlock`, uaudio.c (the only external
consumer of `mixer_get_lock()`, uaudio.c:5359) at lines 2753/2772/3019, and
grep proves **zero** `LK_SHARED` uses under `sys/dev/sound/`. The bug becomes
live the day any code takes the mixer lock shared (e.g. a future
reader-side optimization).

## Reproduce (on the audit guest)

```
./build.sh     # in-tree KLD build of df5605_demo (production APIs only)
./run.sh       # kldload, 8s, kldunload, dmesg | grep DF5605
```

Expected decisive line (3/3 runs stable):

```
DF5605 P3-foreign-SHARED: lockowned=1 lockstatus=1 (LK_SHARED=1 ...) -> ua_mixer_set dance do_unlock=0 (runs uaudio_mixer_set with NO lock held)
```

`lockstatus=1` (LK_SHARED) proves `curthread` holds nothing, while the exact
`ua_mixer_set` decision shape computed `do_unlock=0` (skip acquire).

## Fix validation (module-level, no USB audio H/W)

* `lockowned_demo_fixed.c` — identical phases, dance swapped to
  `lockstatus(lock, curthread) == LK_EXCLUSIVE`: P3 becomes
  `do_unlock=1 (took 200 ticks)` — it **blocks behind the foreign shared
  holder and then acquires exclusively**. Bad shape gone.
* `fix.diff` applied to the guest's pristine `/usr/src/sys/bus/u4b/audio/uaudio_pcm.c`
  (both hunks at lines 136/156) and the uaudio module rebuilt with `-Werror` —
  compiles clean (`fix_build.log`).

## Files

* `lockowned_demo.c` / `Makefile` — baseline harness (shipped decision shape)
* `lockowned_demo_fixed.c` — fixed-decision twin (fix validation)
* `build.sh` / `run.sh` — exact commands
* `build.log`, `run.log`, `run.2.log`, `run.3.log` — full untrimmed outputs
* `fix_run.log`, `fix_build.log` — fix-validation outputs
* `env.txt` — guest uname/sysctl
* `code-reading.txt` — grep evidence that no shared holder exists in-tree
* `VERDICT.md`, `manifest.json`, `verdict.json`, `fix.diff`
