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

cs4281: attach-failure path after mixer_init() leaves a live 0666 /dev/mixerN over freed codec/softc - mixer-UAF family (10th instance)

Summary

mixer_init's return is ignored AND pcm_register remains fallible after it publishes the world-accessible mixer cdev; on pcm_register failure the bad: unwind ac97_destroys the codec and kfrees the softc while the cdev survives (only pcm_unregister ever destroys it). Every mixer ioctl then dispatches through freed codec kobj + freed softc. Identical to the 9 previously-filed instances. Register-first + bad_unregister fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4537 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
cs4281_probe.c β€” 2.0 KB view raw
build.sh β€” 133 B view raw
run.sh β€” 138 B view raw
build.log β€” 94 B view raw
run.log β€” 428 B view raw
env.txt β€” 604 B view raw
wedge_episode.txt β€” 777 B view raw
fix.diff β€” 1.3 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 4.0 KB view raw

DF-4537 β€” cs4281 attach-failure dangling /dev/mixerN UAF (DF-4156 family, 10th instance)

What this is

cs4281_pci_attach() publishes the world-accessible (0666) /dev/mixerN cdev via mixer_init() (cs4281.c:845, return value ignored) before pcm_register() (cs4281.c:847). If pcm_register() fails, the bad: unwind ac97_destroy()s the codec (cs4281.c:862) and kfrees the softc (cs4281.c:873) β€” but nothing destroys /dev/mixerN: only pcm_unregister() β†’ mixer_uninit() (sound.c:1205, mixer.c:795-796) ever does, and it cannot run after a failed pcm_register(). Newbus does not run detach for a failed attach, so the cdev dangles until reboot and dispatches any user's mixer ioctls into the freed codec (m->devinfo, mixer.c:732) whose kobj method table and devinfo softc are both freed (ac97.c:852-858) β€” kernel heap UAF, identical in shape to the nine already-filed family instances (DF-4156/4193/4299/4334/4344/4421/4457/ 4468/4504).

Build / run (on a DragonFly guest)

./build.sh     # cc -O -Wall -o /root/cs4281_probe /root/cs4281_probe.c
./run.sh       # /root/cs4281_probe ; pciconf -l | grep 1013

Expected output on a machine WITH a CS4281 whose attach fails

pcm_register() failure requires one of (sound.c:1021-1033): * pcm_veto_load stuck non-zero (sound core feeder-init anomaly), or * device_get_unit(dev) > PCMMAXUNIT (255) β€” both boot-time anomalies an unprivileged user cannot create. On such a machine probe: prints UNPRIV mixer ioctl OK for the dangling /dev/mixerN, demonstrating the freed-codec dispatch surface.

Actual output on this audit guest (2026-09-21)

No vendor-1013 PCI function exists in QEMU, so snd_cs4281_pci_probe() returns ENXIO and attach never runs: the probe documents the absent trigger environment (see run.log / env.txt). Verdict: untested β€” analysis-certain, hardware-gated.

Fix

fix.diff reorders pcm_register() ahead of codec creation/mixer publication and fails attach (via pcm_unregister unwind) on any later error, so no failure path can outlive the published mixer. It also checks mixer_init()/pcm_addchan() returns (see DF-4539). Applies clean with git apply against sys/ (never applied there by the audit).

VERDICT.md
↓ download raw

DF-4537 VERDICT β€” untested (hardware-gated), analysis certain

Finding shape (source chain, path:line)

  1. cs4281_pci_attach() calls mixer_init(dev, ac97_getmixerclass(), codec) at cs4281.c:845 with the return value ignored.
  2. mixer_init() publishes a world-accessible cdev: mixer.c:730-733 make_dev(&mixer_ops, PCMMKMINOR(unit,0,0), UID_ROOT, GID_WHEEL, 0666, "mixer%d", unit); pdev->si_drv1 = m; where m->devinfo is the AC97_CREATEd codec (cs4281.c:841) whose own devinfo is the driver softc sc.
  3. The next fallible call is pcm_register(dev, sc, 1, 1) at cs4281.c:847-848 β†’ goto bad.
  4. bad: (cs4281.c:860-873) runs ac97_destroy(codec) (ac97.c:852-858: kobj_delete(codec->methods) + kfree(codec)) and kfree(sc) (cs4281.c:873), then returns ENXIO.
  5. Nothing destroys /dev/mixerN: the only path is pcm_unregister() (sound.c:1205 β†’ mixer_uninit(), mixer.c:795-796 destroy_dev(pdev)), which requires a successful pcm_register(). Newbus does not call detach after a failed attach, so the cdev survives until reboot.
  6. Any user then open("/dev/mixerN") + ioctl(MIXER_READ/WRITE) β†’ mixer_ioctl β†’ ac97mixer_* β†’ AC97_READ/WRITE(codec) β†’ kobj dispatch through the freed codec->methods/codec->devinfo β†’ heap UAF (read/write of freed objects; the dispatched cs4281_rdcd/wrcd dereference the freed softc's bus_space tag/handle).

pcm_register() failure preconditions (sound.c:1021-1033): pcm_veto_load (feeder-init anomaly at boot) or unit > PCMMAXUNIT(255). Both are boot-time anomalies an unprivileged user cannot create on demand; they are the same gates the family instances DF-4156…DF-4504 documented.

Why untested (not reproduced / not false-positive)

  • Trigger hardware absent: the audit QEMU guest has no vendor-1013 (Cirrus) PCI function (run.log: PCI-ABSENT), so cs4281_pci_probe() returns ENXIO and attach β€” hence mixer_init() β€” never executes. snd_cs4281.ko exists in /boot/kernel and loads, but registers no device.
  • The pcm_register() failure itself is a boot-time anomaly (veto/unit-overflow), not creatable by an unprivileged user.
  • Static chain is complete and line-verified (above); nothing in it is speculative. Classification: untested, confidence certain, impact none demonstrated on this guest.

Environment-experiment note (why no kldload experiments in run.log)

An exploratory kldload snd_cs4281 (no hardware match) followed by kldunload on the pre-reset guest wedged it in DDB: panic: sbuf_delete called with uninitialized or corrupt sbuf via sndstat_sysuninit() at sound-module unload β€” the already-filed DF-4116 (sound core, not this file). The guest was reset (vm.sh reset with-src) and the final probe run was performed clean, without module loads. Excerpt preserved in wedge_episode.txt.

Fix validation

fix.diff (this pack) reorders pcm_register() before codec/mixer creation and unwinds via pcm_unregister() on every later failure; git apply --check passes. Baseline-vs-patched behaviour is not observable on this guest for the same hardware/precondition absence (no 1013:6005 function; pcm_register failure not creatable), so fix_status = not_testable. Canonical unwind order demonstrated by sound.c:1205 (pcm_unregister does mixer_uninit before codec teardown).

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

fix.diff reorders pcm_register() ahead of codec creation/mixer publication, checks mixer_init() and both pcm_addchan() returns, and adds a bad_unregister: unwind via pcm_unregister() (which performs mixer_uninit before codec teardown, cf. sound.c:1205); git apply --check is clean. Baseline vs patched behaviour is not observable on the audit guest for the same hardware/precondition absence (no 1013:6005 function, pcm_register failure not creatable).

findings/poc/DF-4537/fix.diff; canonical unwind order shown at sound.c:1205 (pcm_unregister does mixer_uninit before codec teardown)
↓ fix.diffper-fix-DF-4537

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4537/VERDICT.md (full source chain); cs4281.c:845 mixer_init ignored vs cs4281.c:847 pcm_register check vs cs4281.c:860-873 bad: ac97_destroy+kfree; mixer.c:730-733 make_dev 0666 + si_drv1=m(devinfo=codec); ac97.c:852-858 kobj_delete+kfree; sound.c:1021/1027 failure set; sound.c:1205 only pcm_unregister->mixer_uninit destroys the cdev; build.log/run.log/env.txt (guest-side absence probe); wedge_episode.txt (pre-reset kldload/kldunload abort, known DF-4116, not this file)

PoC changes

authored fresh probe (no seed); probe documents the absent trigger environment (no /dev/mixer* nodes, no vendor-1013 PCI function) rather than the UAF itself

Verified recommended fix

Move pcm_register() before AC97_CREATE/mixer_init() and fail attach (pcm_unregister unwind) on mixer_init()/pcm_addchan() error so no failure path exists after the mixer is published

Verdict

cs4281_pci_attach() ignores mixer_init()'s return at cs4281.c:845 (which publishes a 0666 /dev/mixerN whose si_drv1->devinfo is the AC97 codec, mixer.c:730-733) and keeps a fallible call after it: pcm_register() at cs4281.c:847 returns EINVAL/ENODEV when pcm_veto_load is stuck at its boot-time value (sound.c:1021) or when the pcm unit exceeds PCMMAXUNIT=255 (sound.c:1027). On that failure attach jumps to bad: and ac97_destroy(codec) at cs4281.c:862 kobj_delete()s the method table and kfrees the codec (ac97.c:852-858) plus kfree(sc) at cs4281.c:873, while /dev/mixerN still dispatches any user's ioctls into the freed codec/softc -- use-after-free with indirect-dispatch potential, identical to the DF-4156/4193/4299/4334/4344/4421/4457/4468/4504 family (10th instance). Untested rather than demonstrated: the trigger needs a real Cirrus 1013:6005 (CS4281) function whose attach reaches mixer_init and then fails pcm_register; the audit guest has no vendor-1013 PCI function (QEMU has no CS4281 emulation -- env.txt) and both pcm_register failure preconditions are boot-time anomalies an unprivileged user cannot create.