uaudio_mixer_sysctl_handler uses sc->sc_mixer_lock across an unbounded user-copy window; a racing USB-audio detach NULLs and frees the lock before the sysctl OIDs are unregistered, causing lockmgr(NULL) -> kernel panic
Summary
The handler checks/locks/walks, sleeps in sysctl_handle_int user-copy, then re-reads sc->sc_mixer_lock and lockmgr()s it - but detach runs pcm_unregister->mixer_uninit->uaudio_mixer_uninit_sub (NULLs the lock at :5385, snd_mtxfree frees it) BEFORE the sysctl OIDs become undispatchable (device sysctl ctx freed only after driver detach returns). REPRODUCED on guest with QEMU emulated usb-audio + root sysctl write-hammer + host-side device_del cycling: stock kernel Fatal trap 12 at lockmgr_release+0x11 (lockmgr(NULL)) within <=10 cycles; instrumented driver logged the exact race-hit; fixed driver (own sysctl ctx freed before bus_generic_detach + NULL re-check) survived 25 identical cycles zero panics. Unpriv writes gated (EPERM before handler). Panic-only, privileged-writer precondition.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5253 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| hammer.c | β | 2.5 KB | view raw | |
| hammer2.c | β | 2.2 KB | view raw | |
| hammer3.c | β | 2.0 KB | view raw | |
| hog.c | β | 431 B | view raw | |
| spray.c | β | 743 B | view raw | |
| spray_kld.c | β | 2.2 KB | view raw | |
| cycle.py | β | 952 B | view raw | |
| build.sh | β | 94 B | view raw | |
| run.sh | β | 179 B | view raw | |
| build.log | β | 262 B | view raw | |
| run.log | β | 1.8 KB | view raw | |
| panic.txt | β | 1.4 KB | view raw | |
| env.txt | β | 45 B | view raw | |
| fix.diff | β | 3.8 KB | view raw | |
| hashes.txt | β | 526 B | view raw | |
| verdict.json | β | 4.6 KB | view raw | |
| VERDICT.md | β | 6.0 KB | β raw |
DF-5253 VERDICT β uaudio mixer sysctl handler lifetime race (REPRODUCED, panic)
Status
reproduced β stock-kernel panic under the exact code path cited, on the DFBSD QEMU guest with an emulated USB audio device; fix validated (panic gone).
- impact: panic (kernel NULL-deref in
lockmgron the sysctl write path; the class is a lock-lifetime TOCTOU β with allocator luck the same window is a freed-locklockmgr, i.e. UAF-write potential β but the NULL store inuaudio_mixer_uninit_sub()makes the deterministic outcome a NULL deref). - severity assigned: Low β the racing sysctl write requires privilege
(
SYSCAP_NOSYSCTL_WRgate at sys/kern/kern_sysctl.c:1446-1450), so the practical trigger is "root process writes a uaudio mixer sysctl while the device is unplugged". Unprivileged users cannot reach the wide window (their writes fail EPERM before the handler runs; reads return before the second lockmgr). Still a real kernel defect: a legitimate privileged sysctl write racing an unplug panics the kernel.
Root cause (path:line)
sys/bus/u4b/audio/uaudio.c:2736 uaudio_mixer_sysctl_handler():
2748: if (sc->sc_mixer_lock == NULL)
2749: return (ENXIO);
2753: lockmgr(sc->sc_mixer_lock, LK_EXCLUSIVE); /* use #1 */
... /* walk sc->sc_mixer_root */
2764: lockmgr(sc->sc_mixer_lock, LK_RELEASE);
2766: error = sysctl_handle_int(oidp, &temp, 0, req); /* crosses user memory: unbounded sleep */
2767: if (error != 0 || req->newptr == NULL)
2768: return (error);
2772: lockmgr(sc->sc_mixer_lock, LK_EXCLUSIVE); /* use #2 β re-reads the field! */
2777: pmc->wData[chan] = temp; /* stale pmc write */
Teardown chain, all while the mixer sysctl OIDs are still registered (they
are children of the pcm device's sysctl ctx, freed only afterwards in
device_sysctl_fini(), sys/kern/subr_bus.c:234-240):
uaudio_detach()(uaudio.c:1192) βbus_generic_detach()(:1214)- β
ua_detachβuaudio_detach_sub()(uaudio.c:1166) βpcm_unregister() - β
mixer_uninit()(sys/dev/sound/pcm/mixer.c:767): MIXER_UNINIT(m)(:809) βua_mixer_uninitβuaudio_mixer_uninit_sub()(uaudio.c:5378) βsc->sc_mixer_lock = NULL(:5385)snd_mtxfree(m->lock)(:811) βlockuninit+kfreeof the 32-byte M_DEVBUFstruct lock(sys/dev/sound/pcm/sound.c:92-108)kobj_delete((kobj_t)m)(:812) frees the snd_mixer- only then:
device_sysctl_fini()βsysctl_ctx_free()removes the OIDs.
So for the whole duration of mixer_uninit() + the tail of the pcm child
detach, dev.pcm.X.mixer.*.val OIDs resolve and dispatch into the handler,
whose second lockmgr re-reads a field that has just been NULLed/freed.
Handler dispatch is serialized against OID removal by the global sysctllock
(shared in handlers, exclusive in ctx teardown) β but nothing serializes the
handler against sc_mixer_lock's lifetime.
How it was proven (chronological, honest)
- Booted the guest with QEMU's emulated USB audio device on a PIIX3 UHCI
controller: real
uaudio0+pcm0attach, real mixer sysctls (dev.pcm.0.mixer.vol_0_0.valetc.). - Unprivileged hammer (hammer.c, 8 Γ in-process
sysctlbynameread+write, ~1.4M ops/s) + 100+ monitordevice_del/device_addcycles: no crash. - Instrumented uaudio.c on the guest (probe only: print when a handler reaches the 2nd lockmgr with the field NULL; print each uninit free): 20 cycles with the unprivileged hammer β 20 frees, 0 traversals.
- Root-cause of (2)/(3): sysctl writes are gated by
caps_priv_check(SYSCAP_NOSYSCTL_WR)at kern_sysctl.c:1446 β unprivilegedsysctlbyname(..., newp, ...)returns EPERM before the handler runs, soreq->newptris never non-NULL for them and the 2nd lockmgr is unreachable. - Same instrumented module + hammer run as root (write path live):
8 cycles β
DF5253: RACE HIT: handler reached 2nd lockmgr after teardown (pmc=0xfffff80117b6aa00 chan=1)β traversal proven on the real driver. - Stock module restored, rebooted, root hammer + 10 cycles β kernel panic
(panic.txt):
Fatal trap 12, fault VA 0x8,Stopped at lockmgr_release+0x11: movq 0x8(%rdi),%rsiβ i.e.lockmgr(NULL), exactly the cited use. - Fix validation: applied
fix.diff(uaudio-owned sysctl ctx, freed inuaudio_detach()beforebus_generic_detach(), which drains in-flight handlers via the sysctllock; plus a defensive NULL re-check in the handler), rebuilt uaudio.ko, installed, rebooted, root hammer + 25 cycles β zero panics, guest healthy, mixer functional after replug.
Side observation (different subsystem, root-triggered, out of scope for this
file): kldunload uaudio once panicked in sndstat_sysuninit β
sbuf_delete ("sbuf_delete called with uninitialized or corrupt sbuf") via
linker_file_unload β a sound.ko teardown bug worth its own ticket.
Exploit chain
Not applicable beyond panic: the reachable traversal requires a privileged
sysctl writer, so this is not an unprivileged privesc primitive. With the
NULL store removed (hypothetically), the same window would be a freed-lock
lockmgr (32-byte M_DEVBUF UAF-write class), which is why the fix removes
the window rather than relying on the NULL store.
kernel_refs
- sys/bus/u4b/audio/uaudio.c:2736-2786 (handler, both lockmgr uses)
- sys/bus/u4b/audio/uaudio.c:5378-5388 (uaudio_mixer_uninit_sub NULL store)
- sys/bus/u4b/audio/uaudio.c:1192-1227 (uaudio_detach ordering)
- sys/dev/sound/pcm/mixer.c:767-813 (mixer_uninit: MIXER_UNINIT β snd_mtxfree β kobj_delete)
- sys/kern/subr_bus.c:234-240 (device_sysctl_fini after driver detach)
- sys/kern/kern_sysctl.c:1446-1450 (write privilege gate β reachability limit)
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
fixedBaseline stock kernel panicked in <=10 device cycles under the root sysctl-write hammer (Fatal trap 12 in lockmgr_release, fault VA 0x8). After applying fix.diff to the guest /usr/src, rebuilding uaudio.ko, installing and rebooting: 25 identical cycles under the same hammer produced ZERO panics (25x 'pcm0: detached' completed, guest stayed healthy, mixer sysctls functional after each replug). Fix closes the window by removing the OIDs (draining handlers under the sysctllock) before the PCM/mixer teardown that frees the lock.
VERDICT.md steps 6-7; run.log (baseline panic + patched 25-cycle clean run summary); fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
panic.txt (stock-kernel 'Fatal trap 12 ... lockmgr_release+0x11' + instrumented 'DF5253: RACE HIT' line); VERDICT.md (full chain path:line); fix.diff (validated); run.sh/cycle.py (exact reproduction); env.txt (guest uname)
PoC changes
Original concept (CLI 'sysctl -w' hammer) replaced by in-process sysctlbyname read+write loop (hammer.c); discovered en route that unprivileged writes are EPERM-gated before the handler, so the decisive runs execute the hammer as root; QEMU usb-audio device attached via QEMU_EXTRA/piix3-uhci; device cycling driven from the host via the QEMU monitor (cycle.py).
Verified recommended fix
Give uaudio its own sysctl_ctx_list for the mixer control OIDs and free it in uaudio_detach() before bus_generic_detach() (drains in-flight handlers via the sysctllock before mixer_uninit frees the lock), plus a defensive NULL re-check of sc_mixer_lock before the handler's second lockmgr.
Verdict
REPRODUCED on the QEMU guest with an emulated USB audio device (QEMU -device usb-audio on piix3-uhci): uaudio_mixer_sysctl_handler() (sys/bus/u4b/audio/uaudio.c:2736) drops sc->sc_mixer_lock, sleeps in sysctl_handle_int() crossing user memory, then re-reads the field and lockmgr()s it; a USB audio detach racing that window runs mixer_uninit() -> uaudio_mixer_uninit_sub() (sc_mixer_lock = NULL, uaudio.c:5385) and snd_mtxfree() (mixer.c:811) BEFORE the sysctl OIDs are unregistered (device_sysctl_fini runs after driver detach, subr_bus.c:234-240), so the handler executes lockmgr(NULL) -> Fatal trap 12, 'Stopped at lockmgr_release+0x11: movq 0x8(%rdi),%rsi' (stock kernel, root sysctl-write hammer, <=10 device_del cycles; instrumented stock driver separately logged 'RACE HIT: handler reached 2nd lockmgr after teardown (pmc=..., chan=1)' in 8 cycles). Reachability is limited: the second lockmgr needs the sysctl WRITE path which is privilege-gated (caps_priv_check SYSCAP_NOSYSCTL_WR, kern_sysctl.c:1446), so unprivileged hammers never traversed it in 100+ cycles; severity is Low (privileged writer racing unplug). fix.diff (uaudio-owned mixer sysctl ctx freed in uaudio_detach BEFORE bus_generic_detach, draining in-flight handlers via the sysctllock, + defensive NULL re-check in the handler) validated: 25 cycles under the same root hammer = zero panics vs stock panic in <=10.
No comments yet.