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

mixer_reinit and mixer_hwvol_* dereference the mixer cdev pointer with no NULL guards β€” NULL-deref panic on resume/reinit of a mixer-less pcm device

Summary

mixer_reinit/hwvol_init/hwvol_mute/hwvol_step fetch the mixer cdev and immediately dereference si_drv1 with no checks, although the dev is NULL whenever mixer_init failed or after mixer_uninit - mixer_uninit itself demonstrates the required guard. A driver whose MIXER_INIT failed handling a resume event panics. LIVE-verified negative space: ~1.06M hostile ioctls as uid 1001 over the ES1370 0666 surface - zero panics, errno classes exactly {0,ENXIO,EINVAL}. NULL-guard x4 fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4293 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 6.1 KB ↓ raw
mixbattery.c β€” 6.6 KB view raw
mixbattery2.c β€” 1.9 KB view raw
build.sh β€” 133 B view raw
run.sh β€” 199 B view raw
build.log β€” 9 B view raw
run.log β€” 1.0 KB view raw
run.2.log β€” 112 B view raw
env.txt β€” 542 B view raw
fix.diff β€” 1.0 KB view raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 3.5 KB view raw

DF-4293 β€” mixer_reinit / mixer_hwvol_* dereference the mixer cdev pointer with no NULL guards (driver-lifecycle NULL-deref panic)

Pass-2 finding for sys/dev/sound/pcm/mixer.c (0666 /dev/mixerN core). The pass-2 brief's families (a) dispatch bounds, (b) name-table walks, (c) volume packing, (e) count refresh, (f) sndstat β€” were audited and found clean (see VERDICT.md negative-space section, backed by the live hostile-ioctl battery in this pack). The one real defect is the unguarded device-lifecycle path below.

The bug

mixer.c functions consumed by sound drivers at resume/reinit time fetch the mixer cdev and dereference it with no checks:

Function Lines Unguarded deref
mixer_reinit mixer.c:828-830 m = pdev->si_drv1; (pdev and si_drv1 both unchecked)
mixer_hwvol_init mixer.c:881-882 same
mixer_hwvol_mute mixer.c:915-916 same
mixer_hwvol_step mixer.c:955-956 same

mixer_get_devt() (mixer.c:117-125) returns snddev->mixer_dev, which is NULL whenever mixer_init() failed (e.g. the driver's MIXER_INIT method returned an error β€” mixer_obj_create mixer.c:644-649 then returns NULL and mixer_init returns -1 without creating the cdev, mixer.c:709-711) or the device is mid-teardown (mixer_uninit NULLs d->mixer_dev at mixer.c:814).

The in-file precedent for the required guard is mixer_uninit itself (mixer.c:776: if (d == NULL || pdev == NULL || pdev->si_drv1 == NULL) return EBADF;) β€” the four functions above simply omit it.

Callers are driver resume/reinit handlers, e.g. sys/dev/sound/pci/als4000.c:909, cmi.c:1075, solo.c:965, vibes.c:673, cs4281.c:935, sys/dev/sound/pci/hda/hdaa.c:6576. A driver whose mixer failed to initialize (misbehaving codec / failed MIXER_INIT) that then resumes or reinits its channels calls mixer_reinit(dev) β†’ pdev->si_drv1 with pdev == NULL β†’ Fatal trap 12 kernel panic (CWE-476). This is the pcm-core-side expression of the DF-4156/4193 "driver misbehaves at init" family the pass-2 brief asked to hunt.

Threat model / severity

Trigger requires a driver lifecycle event (Suspend/Resume β€” root via acpiconf/ACPI, or devd-driven reinit) on hardware whose mixer_init failed, i.e. privileged or hardware-dependent β€” hence Low, not a 0666-device attack. The 0666 ioctl surface itself was hammered live (see run.log / run.2.log) and is clean.

Reproduce

Code-trace certain; not triggerable on this guest (single ES1370 card with a healthy mixer; suspend/resume not exercisable in this QEMU setup). The pack instead live-verifies the negative space: two hostile ioctl batteries against /dev/mixer0 + /dev/dsp0 as uid 1001 β€” all 256 device bytes read+write, hostile recsrc/mixerinfo/audioinfo payloads, and ~1M concurrent mixer operations β€” zero panics, zero unexpected errnos.

VERDICT.md
↓ download raw

DF-4293 VERDICT β€” mixer.c pass-2

Status: inconclusive (defect trace-certain; trigger not exercisable on this guest; 0666 surface live-tested clean)

THE FINDING (trace)

  • mixer_reinit mixer.c:822-845 β€” pdev = mixer_get_devt(dev); m = pdev->si_drv1; at :828-829 with no NULL check on either; snd_mtxlock(m->lock) at :830. mixer_get_devt (mixer.c:117-125) returns snddev->mixer_dev which is NULL when mixer_init failed (mixer.c:709-711 returns -1 before make_dev at :730) or after mixer_uninit (mixer.c:814 d->mixer_dev = NULL;).
  • Same pattern: mixer_hwvol_init :881-882, mixer_hwvol_mute :915-916, mixer_hwvol_step :955-956.
  • Guarded contrast in the same file: mixer_uninit :776 (d == NULL || pdev == NULL || pdev->si_drv1 == NULL), mixer_open :1044, mixer_ioctl :1205, mixer_ioctl_cmd :1275, and every dsp.c call site (dsp.c:1124, 1852, 1865 check d->mixer_dev != NULL).
  • Callers (resume/reinit): als4000.c:909, cmi.c:1075, solo.c:965, vibes.c:673, cs4281.c:935, hdaa.c:6576.
  • Trigger: mixer_init failure (driver MIXER_INIT returns error β†’ mixer_obj_create frees m, mixer.c:644-649) + driver resume path calling mixer_reinit β†’ NULL->si_drv1 read at offset β†’ Fatal trap 12 (CWE-476).
  • Severity Low: requires driver lifecycle event (root-driven suspend/resume or devd reinit) on hardware whose mixer failed β€” the "driver misbehaves" family (DF-4156/4193 analog in the pcm core). Not unprivileged.
  • Not triggerable on this guest: one healthy ES1370 mixer; no suspend/resume path exercisable in this QEMU setup; a synthetic kmod poking mixer_reinit(dev-of-mixerless-pcm) would just restate the trace.

NEGATIVE SPACE (what was hunted and killed β€” with live evidence)

Batteries run as uid 1001 on /dev/mixer0 + /dev/dsp0 (ES1370, pcm0, 0666 cdevs): see run.log (round 1) and run.2.log (round 2).

  1. Dispatch bounds (brief item a) β€” j = cmd & 0xff spans 0..255; every path bounds-checks: mixer_set mixer.c:255 (dev >= SOUND_MIXER_NRDEVICES(25) rejected), mixer_get :334 (dev < 25 && devs bit), special bytes 0xfb..0xff (STEREODEVS/CAPS/RECMASK/DEVMASK/RECSRC, soundcard.h:1075-1080) handled by explicit cases :1312-1326 / :1304. Live: 247Γ—ENXIO + 2571Γ—EINVAL, 0 unexpected errnos across all 256 j values Γ— 8 payloads Γ— 3 devices. No 32-bit conversion shims exist in this file (DragonFly native ioctls only).
  2. Name tables (item b) β€” mix_getdev/mix_setdevcbyname do not exist in this DragonFly mixer.c. The name-table walks present are bounded: mixer_lookup :132-135 iterates snd_mixernames[25] with strncmp bounded by strlen(name) ≀ 8 < 32-byte stack buffer; sysctl handler sysctl_hw_snd_hwvol_mixer :848-873 gets a NUL-terminated buffer (kern_sysctl.c:1176-1181 enforces newlen < arg2 and writes the NUL); m->hwvol_mixer only ever holds mixer_lookup results ∈ [0,24] (:861-867) or SOUND_MIXER_VOLUME (init :884) β†’ snd_mixernames[m->hwvol_mixer] at :856 is in-bounds. bootverbose prints guard every index (:744-745, :750).
  3. Volume arithmetic / u8 packing (item c) β€” mixer_set :259-260 clamps both channels to ≀100 with min(); m->level[] writers are all clamped (mixer_set :326, hwvol step :935-945 clamps 0..100 both channels, mix_set_locked routes through mixer_set); parent/child products ≀ 100Γ—100 (:283-284, :297-298) β€” no overflow of the u16 packing l | (r << 8). mixer_get reads bounded :334. Live: hammer wrote ~1M hostile values, all reads back ≀ 0x6464.
  4. devinfo lifetime vs driver misbehavior (item d) β€” mixer_obj_create zeroes m (M_ZERO :625) so enuminfo is not an uninit-leak source; failing MIXER_INIT frees m and mixer_init returns before make_dev (:644-649, :709-711); dsp.c call sites NULL-check d->mixer_dev (dsp.c:1124/1852/1865); devfs guards a NULL clone return (devfs_vnops.c:906 if (ndev != NULL)), so mixer_clone :1359 handing out a NULL mixer_dev cannot crash devfs. The si_drv1 check-then-use window in mixer_ioctl :1205β†’:1208 racing mixer_uninit :795 is destroy_dev-drained (destroy_dev blocks while open fds hold the cdev, and m is freed only after destroy_dev returns :796β†’:812) β€” only a NULL-deref (not UAF) window remains, needing a concurrent root-driven detach; noted, not filed.
  5. reinit/resume count refresh (item e) β€” mixer_reinit's refresh loop :838-839 is bounded (i < 25, level[i] pre-validated); the defect is the entry guard, filed above.
  6. sndstat (item f) β€” mixer.c contains no sndstat integration (that lives in sound.c/sndstat.c); nothing to attack here.
  7. mix_setrecdevs strindex write-before-bounds-check (:512 writes strindex[nvalues], check at :526) β€” unreachable: i < SOUND_MIXER_NRDEVICES(25) bounds nvalues ≀ 25 << OSS_ENUM_MAXVALUE(255) and total name bytes ≀ 25Γ—8=200 < strings[3000] (soundcard.h:1850-1859). Hardening note only.

Live-run summary

Round 1 (run.log): all 256 j values Γ— {read, 8 hostile write payloads, dsp-routing}; SNDCTL_DSP_{SET,GET}_RECSRC with {0,1,-1,2,100000,INT_MAX, INT_MIN,1<<30,24,25,26,255,256}; MIXERINFO/AUDIOINFO/ENGINEINFO with dev ∈ {-1,0,1,2,100,INT_MAX,-2,-100,3}; SYSINFO; CARDINFO(-5); RECSRC_NAMES; SOUND_MIXER_INFO; OSS_GETVERSION; 3Γ—60k concurrent hammer. β†’ tally ok=66 enxio=247 einval=2571 other=0; guest alive.

Round 2 (run.2.log): /dev/mixer clone-alias open (works; DEVMASK read β€” one transient -1 under 3000-open churn, standalone probe rc=0 clean β€” error path only, no memory-safety effect), 2 churn threads Γ— 3000 open/close + 4 hammer threads Γ— 200k β†’ guest alive, dmesg clean.

env: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, QEMU 7.2 KVM, -device ES1370 (pcm0 ), snd_es137x.ko+sound.ko loaded, /dev/mixer0 crw-rw-rw- root:wheel.

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

not_testable
baseline no→ patch + rebuild →patched clean

Low severity, defect not triggerable on this guest (needs mixer-less pcm device + resume event); per contract fix-build validation is reserved for reproduced memory-corruption findings. fix.diff (4 NULL guards mirroring mixer_uninit:776) authored from trace.

findings/poc/DF-4293/fix.diff (not applied to sys/; authored post-verification from line-accurate trace)
↓ fix.diffper-fix-DF-4293

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (trace + full negative-space section); run.log/run.2.log (live hostile-ioctl batteries, uid 1001, guest survived); env.txt (ES1370 pcm0, 0666 /dev/mixer0); fix.diff (4x NULL guard)

PoC changes

mixbattery.c: fixed oss_sysinfo field nmixers->nummixers for the guest's soundcard.h; everything else compiled as written

Verified recommended fix

Add pdev==NULL || pdev->si_drv1==NULL guards (ENXIO/return) to mixer_reinit and the three mixer_hwvol_* entry points, mirroring mixer_uninit:776

Verdict

mixer_reinit (mixer.c:828-830), mixer_hwvol_init (:881-882), mixer_hwvol_mute (:915-916) and mixer_hwvol_step (:955-956) dereference mixer_get_devt(dev)->si_drv1 with no NULL guards, although snddev->mixer_dev is NULL whenever mixer_init failed (mixer.c:709-711 returns before make_dev when the driver's MIXER_INIT fails) or after mixer_uninit (:814), and mixer_uninit itself demonstrates the required guard (:776). Driver resume/reinit handlers (als4000.c:909, cmi.c:1075, solo.c:965, vibes.c:673, cs4281.c:935, hdaa.c:6576) then NULL-deref -> Fatal trap 12 panic (CWE-476). Low severity: the trigger needs a privileged/hardware lifecycle event on a mixer-less pcm device (the DF-4156/4193 driver-misbehavior family in the pcm core), not the 0666 device. NOT triggerable on this guest (single healthy ES1370 mixer; no suspend/resume) -> inconclusive, trace-certain. The 0666 ioctl surface itself WAS live-tested as uid 1001 (QEMU -device ES1370 + snd_es137x): two hostile batteries (all 256 mixer device-bytes read+write+dsprouting with 8 hostile payloads, SNDCTL_DSP_*/MIXERINFO/AUDIOINFO/SYSINFO with hostile and negative indices, ~1.06M concurrent mixer ops incl. clone-alias churn) produced zero panics and zero unexpected errnos (ok=66 enxio=247 einval=2571 other=0; round 2 clean) β€” run.log/run.2.log. Pass-2 negative space (dispatch bounds, name tables, volume packing, devinfo lifetime, count refresh, sndstat) documented killed in VERDICT.md.