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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5253 Β· 17 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 lockmgr on the sysctl write path; the class is a lock-lifetime TOCTOU β€” with allocator luck the same window is a freed-lock lockmgr, i.e. UAF-write potential β€” but the NULL store in uaudio_mixer_uninit_sub() makes the deterministic outcome a NULL deref).
  • severity assigned: Low β€” the racing sysctl write requires privilege (SYSCAP_NOSYSCTL_WR gate 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 + kfree of the 32-byte M_DEVBUF struct 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)

  1. Booted the guest with QEMU's emulated USB audio device on a PIIX3 UHCI controller: real uaudio0 + pcm0 attach, real mixer sysctls (dev.pcm.0.mixer.vol_0_0.val etc.).
  2. Unprivileged hammer (hammer.c, 8 Γ— in-process sysctlbyname read+write, ~1.4M ops/s) + 100+ monitor device_del/device_add cycles: no crash.
  3. 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.
  4. Root-cause of (2)/(3): sysctl writes are gated by caps_priv_check(SYSCAP_NOSYSCTL_WR) at kern_sysctl.c:1446 β€” unprivileged sysctlbyname(..., newp, ...) returns EPERM before the handler runs, so req->newptr is never non-NULL for them and the 2nd lockmgr is unreachable.
  5. 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.
  6. 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.
  7. Fix validation: applied fix.diff (uaudio-owned sysctl ctx, freed in uaudio_detach() before bus_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

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

Baseline 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
↓ fix.diffper-fix-DF-5253

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.