ua_mixer_set/ua_mixer_setrecsrc reentrancy dance gates on lockowned(), which on DragonFly returns true for a lock held shared by ANY thread β under a foreign-shared holder the mixer callbacks run with zero exclusion (latent; DFly-specific divergence from FreeBSD mtx_owned)
Summary
The (re)acquire decision uses lockowned(), which on DFly answers 'someone holds it shared' not 'curthread owns it' (kern_lock.c self-documents 'We can only guess'): a thread holding nothing concludes 'already owned' and runs the mixer body (walk + synchronous USB control requests) with zero exclusion while a foreign thread sits in a shared section. The sound(4) core uses the precise lockstatus(lock,curthread)==LK_EXCLUSIVE for the identical decision - this glue carries the FreeBSD mtx_owned idiom verbatim, and mtx_owned IS exact, so it is a porting divergence. Dormant today (lock created LK_CANRECURSE, every in-tree acquisition exclusive); goes live the moment any reader-side shared acquisition is added. Production-API harness 3/3: foreign-shared holder -> do_unlock=0 with lockstatus proving curthread holds nothing. lockstatus fix validated at module level.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5605 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| lockowned_demo.c | β | 4.1 KB | view raw | |
| lockowned_demo_fixed.c | β | 2.9 KB | view raw | |
| Makefile | β | 69 B | β download | |
| build.sh | β | 137 B | view raw | |
| run.sh | β | 160 B | view raw | |
| build.log | β | 7.2 KB | view raw | |
| run.log | β | 729 B | view raw | |
| run.2.log | β | 342 B | view raw | |
| run.3.log | β | 513 B | view raw | |
| fix_run.log | β | 497 B | view raw | |
| fix_build.log | β | 2.1 KB | view raw | |
| code-reading.txt | β | 1.1 KB | view raw | |
| env.txt | β | 192 B | view raw | |
| fix.diff | β | 465 B | view raw | |
| README.md | β | 3.8 KB | β raw | |
| VERDICT.md | β | 5.6 KB | β raw | |
| verdict.json | β | 4.3 KB | view raw |
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:
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 tolockstatus(lock, curthread) == LK_EXCLUSIVE: P3 becomesdo_unlock=1 (took 200 ticks)β it blocks behind the foreign shared holder and then acquires exclusively. Bad shape gone.fix.diffapplied 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 commandsbuild.log,run.log,run.2.log,run.3.logβ full untrimmed outputsfix_run.log,fix_build.logβ fix-validation outputsenv.txtβ guest uname/sysctlcode-reading.txtβ grep evidence that no shared holder exists in-treeVERDICT.md,manifest.json,verdict.json,fix.diff
DF-5605 VERDICT
Status: reproduced (latent-pattern demonstration; impact none today) Confidence: certain β both the divergence (KLD harness, 3/3 stable) and the non-manifestation (exhaustive grep of every holder of the lock) are proven.
What was claimed
ua_mixer_set() (sys/bus/u4b/audio/uaudio_pcm.c:139) and
ua_mixer_setrecsrc() (uaudio_pcm.c:159) gate lock (re)acquisition on
lockowned(lock). On DragonFly, lockowned() (sys/kern/kern_lock.c:1365-1378)
returns true when the lock is held shared by ANY thread β
if (count & LKC_XMASK) return (lk_lockholder == td); else return ((count &
LKC_SMASK) != 0); β with the source comment itself admitting "We can only
guess re: shared locks." Under a foreign-shared holder, curthread holds
nothing, yet the dance computes do_unlock = 0 and the body
(uaudio_mixer_set(), uaudio.c:5390 β walks sc->sc_mixer_root, calls
uaudio_mixer_ctl_set() which issues synchronous USB control requests) runs
with no exclusion while another thread is inside a shared critical section.
The precise check the sound(4) core uses for the same decision is
lockstatus(m->lock, curthread) == LK_EXCLUSIVE (mixer.c:273, mixer.c:346).
This glue is the FreeBSD mtx_owned() idiom (FreeBSD's uaudio_pcm.c has the
identical dance; mtx_owned() there is exact); DFly's lockowned() is not
semantically equivalent for shared locks β a DFly-specific divergence.
How it was verified
Baseline harness (findings/poc/DF-5605/lockowned_demo.c)
Production-API KLD (lockmgr/lockowned/lockstatus/kthread_create only; lock
created with lockinit(..., LK_CANRECURSE) exactly like snd_mtxcreate(),
sound.c:92-99). Four phases; ua_mixer_set_dance() is byte-for-byte the
decision shape of uaudio_pcm.c:136-148:
- P1 lock free:
lockowned=0β dance acquires. Correct. - P2 self-held exclusive:
lockowned=1,lockstatus=2β dance skips reacquire. Correct (and LK_CANRECURSE makes blind reacquire safe too). - P3 foreign-held SHARED (kthread holds LK_SHARED, tsleeps):
lockowned=1 lockstatus=1 (LK_SHARED)β dancedo_unlock=0, runs the body with NO lock held. DIVERGENCE. 3/3 runs (run.log, run.2.log, run.3.log). - P4 after release:
lockowned=0β correct again.
Fix validation
- Fixed-decision twin (
lockowned_demo_fixed.c): same phases, dance useslockstatus(lock, curthread) == LK_EXCLUSIVE. P3 becomesdo_unlock=1 (took 200 ticks)β blocks behind the foreign shared holder, then acquires exclusive. The bad decision shape is gone (fix_run.log). - fix.diff on the real driver: both hunks apply at lines 136/156 of the
guest's pristine
/usr/src/sys/bus/u4b/audio/uaudio_pcm.c; module rebuilt in-tree with-Werrorβ uaudio.ko links clean (fix_build.log). A driver runtime test is impossible on this guest (no USB audio hardware), so kernel-level fix_status is module-build + decision-shape validated.
Why impact is none today (the honest ceiling)
The lock in question is m->lock from mixer_obj_create() (mixer.c:634).
Exhaustive survey of every acquisition path:
- created via
snd_mtxcreate()βlockinit(m, desc, 0, LK_CANRECURSE)(sound.c:92-99); all sound(4) access issnd_mtxlock/snd_mtxunlock(exclusive); grep -rn LK_SHARED sys/dev/sound/β zero hits;- the only external consumer of
mixer_get_lock()in the tree is uaudio.c:5359 (sc->sc_mixer_lock = mixer_get_lock(m)), taken exclusively at uaudio.c:2753, 2772, 3019.
So no in-tree code ever holds the mixer lock shared; the unsound branch is dormant. It activates the moment anyone adds a shared acquisition (a natural future optimization β e.g. reader-side mixer-state access), which is precisely why it is filed as Info hardening with a two-line fix that also aligns this file with the precise check the sound core already uses for the identical decision.
Exploit chain
None β no manifestation today; not memory corruption. The finding's value is locking a latent protocol hole before it can go live.
Negative results from the same pass (weight of the clean verdict)
ua_probe(uaudio_pcm.c:190-206): ivars NULL +SCF_PCMchecked; sole producer (uaudio.c:1051-1066) sets ivars beforedevice_add_childattach runs β no type confusion, no TOCTOU.- Channel glue (uaudio_pcm.c:39-124): pure forwarders; no size math in this
file.
ua_chan_setblocksizeβ uaudio.c:2485 ignores the requested size (recomputes from validated descriptors);ua_chan_setfragmentsβ uaudio.c:2493 refuses (returns 1);ua_chan_setspeedβ clamps to nearest validated alt rate;ua_chan_setformatβ no-op. No attacker-fed size reaches sndbuf via this file. - Buffer lifecycle:
ua_chan_init/freeforward to uaudio.c:2420/2471 (kmalloc + sndbuf_setup binding / kfree after usbd_transfer_unsetup drain); detach orderingua_detachβuaudio_detach_sub(uaudio.c:1167) runs pcm_unregister-first with a retry loop; sound(4) serializes method dispatch under the chn lock. No UAF path through the glue. - Mixer-uninit deadlock candidate killed:
mixer_uninit()callsMIXER_UNINITunlocked (mixer.c:804, "mixer uninit can sleep --hps"), sousbd_transfer_unsetup()underua_mixer_uninitcannot self-deadlock on the xfer lock (== m->lock). - No kloop/callout machinery in this file.
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
fixedfix.diff (both hunks at uaudio_pcm.c:136/156) applied to the guest's pristine driver and compiled clean with -Werror into uaudio.ko (fix_build.log). Decision-shape runtime validation via lockowned_demo_fixed.c: in the identical foreign-shared setup the fixed dance blocks behind the holder (200 ticks) and acquires exclusively - the bad do_unlock=0 decision is gone (fix_run.log). Driver-path runtime test impossible on this guest (no USB audio hardware); module-level validation is the strongest available evidence class per the family standard.
fix_build.log (clean -Werror build of patched uaudio_pcm.c); fix_run.log (DF5605FIX P3: do_unlock=1, took 200 ticks)
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log/run.2.log/run.3.log decisive line 'DF5605 P3-foreign-SHARED: lockowned=1 lockstatus=1 ... do_unlock=0 (runs uaudio_mixer_set with NO lock held)'; fix_run.log 'DF5605FIX P3-foreign-SHARED: ... do_unlock=1 (took 200 ticks)'; fix_build.log (patched uaudio_pcm.c compiles -Werror clean into uaudio.ko); code-reading.txt (no shared holders in-tree)
PoC changes
Authored the harness from scratch (no seed): replicated the uaudio_pcm.c:136-148 decision shape verbatim, lock created with snd_mtxcreate's exact flags (lockinit LK_CANRECURSE), foreign-shared holder via kthread_create+LK_SHARED+tsleep. Fixed-dance twin authored for fix validation. During fix-diff application, an interactive patch session half-applied a hand-authored diff to the guest tree; regenerated fix.diff mechanically (diff -u) and restored the pristine file before the recorded clean apply+build.
Verified recommended fix
Replace lockowned(lock) with lockstatus(lock, curthread) == LK_EXCLUSIVE in ua_mixer_set() and ua_mixer_setrecsrc() (uaudio_pcm.c:139/159), matching the precise check the sound(4) core uses for the identical decision at mixer.c:273/346.
Verdict
The reentrancy decision in ua_mixer_set() (uaudio_pcm.c:139) and ua_mixer_setrecsrc() (uaudio_pcm.c:159) uses lockowned(), which on DragonFly returns true for a lock held shared by ANY thread (kern_lock.c:1365, 'We can only guess re: shared locks'), so under a foreign-shared holder the calling thread concludes it already owns the lock and runs uaudio_mixer_set()/uaudio_mixer_setrecsrc() with zero exclusion. A production-API KLD harness replaying the exact decision shape proved the divergence deterministically (P3: lockowned=1 while lockstatus(curthread)=LK_SHARED -> do_unlock=0, 3/3 runs), and a fixed-decision twin using the precise lockstatus(...)==LK_EXCLUSIVE check (the same check the sound core itself uses at mixer.c:273/346) blocks behind the shared holder and acquires properly. Impact is none TODAY because every in-tree acquisition of the mixer lock is LK_EXCLUSIVE (zero LK_SHARED under sys/dev/sound; sole external consumer uaudio.c:5359 takes it exclusively at 2753/2772/3019) - filed as Info hardening of a latent protocol hole with a two-line fix, validated to compile against the real driver.
No comments yet.