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

agg_attach() error path frees the AC97 codec while the 0666 /dev/mixerN mixer_init() created survives β€” post-failure mixer ioctl UAF through freed kobj

Summary

mixer_init runs before pcm_register and make_dev's /dev/mixerN 0666 holding m->devinfo=codec; when pcm_register subsequently fails, the bad: path runs ac97_destroy without mixer_uninit, so any unprivileged user touching the surviving node walks mixer_ioctl -> ac97_wrcd through freed heap and a kobj_delete'd dispatch table - heap-spray -> indirect-call control. Precondition not attacker-creatable (pcm_register failure state). Same-family symptom: agg_detach never destroys ess->codec at all (leak on every detach). Unwind-order fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4156 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 2.6 KB ↓ raw
maestro_probe.c β€” 1.5 KB view raw
build.sh β€” 111 B view raw
run.sh β€” 164 B view raw
build.log β€” 325 B view raw
run.log β€” 325 B view raw
env.txt β€” 370 B view raw
fix.diff β€” 815 B view raw
verdict.json β€” 2.8 KB view raw

DF-4156 β€” agg_attach() error path frees the AC97 codec while the 0666 /dev/mixerN it created survives

File: sys/dev/sound/pci/maestro.c (pass 2, GLM 5.3)

What

agg_attach() calls mixer_init() (which make_devs /dev/mixerN with mode 0666, mixer.c:730) before pcm_register() (maestro.c:1894-1903). If pcm_register() subsequently fails, the bad: label runs ac97_destroy(codec) (maestro.c:1923-1924) without first tearing the mixer down. /dev/mixerN keeps existing with pdev->si_drv1 = m and m->devinfo = codec, both dangling after the free.

Any unprivileged user that opens the surviving /dev/mixerN and issues a mixer ioctl walks:

mixer_ioctl (mixer.c:1196)
  -> mixer_ioctl_channel -> mix_* -> mixer_set (mixer.c)
     -> MIXER_SET(m, ...) -> ac97_set -> ac97_wrcd (ac97.c:330)
        -> AC97_WRITE(codec->methods, codec->devinfo, ...)   [both freed]

ac97_destroy() (ac97.c:852) does kobj_delete(codec->methods) and kfree(codec) β€” so the ioctl path dereferences freed heap and dispatches through a freed kobj: use-after-free read + indirect-call-through-freed- memory. The correct unwind order is demonstrated by pcm_unregister(), which calls mixer_uninit(dev) first (sound.c:1205).

Precondition (why this is Low)

pcm_register() only fails early (sound.c:1021-1027): pcm_veto_load stuck at 1 (root-feeder SYSINIT never ran β€” a boot-time anomaly), or device_get_unit(dev) > PCMMAXUNIT (255). Not attacker-creatable without prior root. With the precondition met, use of the UAF needs no privilege (0666 node).

Related symptom of the same asymmetry: agg_detach() never calls ac97_destroy(ess->codec) at all β€” the codec leaks on every detach (module unload). The fix addresses both.

Reproduce

build.sh   # cc maestro_probe.c
run.sh     # probes for the pcm/mixer precondition

Requires a machine with an ESS Maestro PCI audio function and a kernel where pcm_register() fails after mixer_init() succeeded. The audit guest has no Maestro hardware and no pcm devices (see run.log), so the trigger cannot be instantiated there; the defect itself is source-level certain.

Fix

fix.diff: call mixer_uninit(dev) before ac97_destroy(codec) when the mixer was initialized, and destroy the codec in agg_detach() (fixes the leak). Applies cleanly with git apply.

VERDICT.md
↓ download raw

DF-4156 VERDICT

status: untested (hardware/boot-precondition gated) | impact ceiling: UAF with control-flow potential

Reproduced?

No β€” and it cannot be on this guest: pciconf -lv shows no ESS Maestro multimedia function and there are no /dev/dsp, /dev/mixer nodes (run.log); snd_maestro never attaches. Additionally the trigger needs pcm_register() to fail after mixer_init() succeeded β€” on this tree that means pcm_veto_load stuck at 1 (root-feeder SYSINIT anomaly) or a device unit > PCMMAXUNIT(255) (sound.c:1021-1027), neither of which an unprivileged user can create. The evidence is therefore source-level, and the source is unambiguous:

  1. maestro.c:1894 mixer_init(dev, ac97_getmixerclass(), codec).
  2. mixer.c:730-733 make_dev(..., UID_ROOT, GID_WHEEL, 0666, "mixer%d"), pdev->si_drv1 = m, m->devinfo = codec (mixer.c:635).
  3. maestro.c:1901-1903 pcm_register() may fail β†’ goto bad.
  4. maestro.c:1923-1924 bad: β†’ ac97_destroy(codec) β€” no mixer_uninit.
  5. ac97.c:852-859 frees codec->methods (kobj_delete) and codec.
  6. Post-error, any user opens the surviving 0666 /dev/mixerN: mixer_ioctl (mixer.c:1196) β†’ mixer_set β†’ MIXER_SET β†’ ac97_set β†’ ac97_wrcd(codec) β†’ AC97_WRITE(codec->methods, codec->devinfo, ...) β€” deref of freed heap + indirect dispatch through freed kobj memory.

Contrast the correct unwind: pcm_unregister() calls mixer_uninit(dev) before anything else (sound.c:1205); mixer_uninit does destroy_dev(pdev) and frees the snd_mixer (mixer.c:767-815) β€” it is safe to call at exactly this point in the failure path because the snddev softc and mixer cdev both exist once mixer_init returned 0.

Exploit chain (if precondition met)

spray kernel heap to reclaim the freed ac97_info + kobj cache lines β†’ issue SOUND_MIXER_WRITE_VOLUME on /dev/mixerN β†’ AC97_WRITE dispatches through attacker-influenced kobj memory β†’ control flow. Chain untested (no trigger environment); impact recorded as ceiling, not demonstration.

Fix validation

Not possible on this guest (no hardware, no trigger). fix.diff applies cleanly (git apply --check) and mirrors the pcm_unregister() order. Bonus: adds the missing ac97_destroy(ess->codec) to agg_detach() (codec currently leaks on every detach).

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

No trigger environment on the audit guest (no Maestro hardware; pcm_register failure precondition not creatable), so baseline vs patched behaviour cannot be observed. fix.diff applies cleanly (git apply --check) and mirrors the pcm_unregister() unwind order (mixer_uninit before codec free).

findings/poc/DF-4156/fix.diff (git apply --check clean); sound.c:1205 shows the canonical order
↓ fix.diffper-fix-DF-4156

Confirmed kernel references

Detail

Evidence (decisive lines)

maestro.c:1894-1903 (mixer_init then pcm_register), maestro.c:1922-1924 (bad: ac97_destroy without mixer_uninit), mixer.c:730-733 (make_dev 0666 + si_drv1), mixer.c:635 (m->devinfo=codec), ac97.c:852-859 (kobj_delete+kfree), sound.c:1021-1027 (pcm_register failure set), sound.c:1205 (correct unwind order in pcm_unregister); run.log (no maestro/pcm on guest)

PoC changes

authored fresh probe (no seed); probe documents precondition absence on guest

Verified recommended fix

Call mixer_uninit(dev) before ac97_destroy(codec) on the attach failure path when the mixer was initialized; destroy the codec in agg_detach()

Verdict

agg_attach() destroys the AC97 codec on the pcm_register()-failure error path without first tearing down the mixer that mixer_init() created, leaving the 0666 /dev/mixerN with dangling si_drv1->devinfo and a kobj_delete'd methods table; any subsequent mixer ioctl on that node is a UAF with indirect-dispatch potential. Trigger precondition (pcm_register failing after mixer_init: pcm_veto_load stuck / unit > PCMMAXUNIT) is a boot-time anomaly an unprivileged user cannot create, and the audit guest has no Maestro hardware, so untested rather than demonstrated.