β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5605

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5605 Β· 17 files
FileTypeDescriptionSize
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 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
VERDICT.md
↓ download raw

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) β†’ dance do_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

  1. Fixed-decision twin (lockowned_demo_fixed.c): same phases, dance uses lockstatus(lock, curthread) == LK_EXCLUSIVE. P3 becomes do_unlock=1 (took 200 ticks) β€” blocks behind the foreign shared holder, then acquires exclusive. The bad decision shape is gone (fix_run.log).
  2. 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 is snd_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_PCM checked; sole producer (uaudio.c:1051-1066) sets ivars before device_add_child attach 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/free forward to uaudio.c:2420/2471 (kmalloc + sndbuf_setup binding / kfree after usbd_transfer_unsetup drain); detach ordering ua_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() calls MIXER_UNINIT unlocked (mixer.c:804, "mixer uninit can sleep --hps"), so usbd_transfer_unsetup() under ua_mixer_uninit cannot 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-5605

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.