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)
PoC verification
Evidence pack
findings/poc/DF-4156 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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:
maestro.c:1894mixer_init(dev, ac97_getmixerclass(), codec).mixer.c:730-733make_dev(..., UID_ROOT, GID_WHEEL, 0666, "mixer%d"),pdev->si_drv1 = m,m->devinfo = codec(mixer.c:635).maestro.c:1901-1903pcm_register()may fail βgoto bad.maestro.c:1923-1924bad:βac97_destroy(codec)β nomixer_uninit.ac97.c:852-859freescodec->methods(kobj_delete) andcodec.- 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_testableNo 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
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.
No comments yet.