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

ich_pci_attach() publishes the 0666 /dev/mixerN (mixer_init) before five failure paths whose bad: unwind frees the AC97 codec and softc β€” dangling mixer cdev UAF on open/ioctl (DF-4156/4193/4299/4334/4344 family, 6th instance, widest window in family)

Summary

mixer_init creates the 0666 /dev/mixerN dispatching into sc->codec; five later attach failures (bus_dma_tag_create x2, bus_dmamem_alloc, bus_dmamap_load, pcm_register) goto bad: which ac97_destroy()s the codec and kfrees the softc without destroying the mixer cdev. Any local user opening/ioctl-ing the surviving node dispatches through freed memory. Widest window in the family (five failure paths; mixer_init return ignored). Hardware/alloc-pressure dependent trigger, matching family precedent. pcm_register-before-mixer_init reorder fix (compile-validated on guest toolchain).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4421 Β· 10 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 8.7 KB ↓ raw
run.sh β€” 196 B view raw
trigger.hw.sh β€” 1.1 KB view raw
guest_check.log β€” 509 B view raw
env.txt β€” 192 B view raw
build.log β€” 7.8 KB view raw
fix.diff β€” 1.3 KB view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 4.4 KB view raw

DF-4421 β€” snd_ich attach-failure path leaves live 0666 /dev/mixerN over freed AC97 codec + softc (UAF on open/ioctl)

6th instance of the pcm attach-error family (DF-4156 maestro, DF-4193 es137x, DF-4299 emu10kx-pcm, DF-4334 via8233, DF-4344 atiixp), this time in sys/dev/sound/pci/ich.c.

TL;DR

ich_pci_attach() calls mixer_init() (ich.c:1031) β€” which publishes a 0666 /dev/mixerN whose si_drv1 chain reaches the AC97 codec β€” before five more failure paths that all jump to bad: (ich.c:1045, 1053, 1060, 1065, 1069). bad: frees the codec (ac97_destroy, ich.c:1096-1097) and the softc (kfree, ich.c:1118) but never tears the mixer cdev down. Any later open/ioctl on /dev/mixerN dispatches through the freed codec's kobj'd methods table β†’ use-after-free (read/write primitives through mixer_ioctl β†’ mix_getdevinfo β†’ freed struct ac97_info).

Not runtime-reproducible on the audit guest: QEMU (i440FX machine) presents no Intel ICH AC'97 controller (no class=0x0401 device; only 8086 chips 1237/7000/7010/7113 β€” see guest_check.log). snd_ich.ko loads but ich_pci_probe() never matches, so ich_pci_attach() never executes. Source-certain by construction and by symmetry with the 4 runtime-verified family members.

How to reproduce (on hardware with an ICH AC'97 controller)

Preconditions: any machine with an Intel ICH/ICH2/.../ICH7, SiS 7012, nForce, AMD-768/8111 AC'97 controller (PCI class 0401, e.g. 8086:2415), plus one of:

  • memory pressure sufficient to fail bus_dmamem_alloc/bus_dmamap_load (ich.c:1060/1065), or
  • bus_dma_tag_create failure (ich.c:1045/1053), or
  • 33+ sound devices so pcm_register hits PCMMAXUNIT β†’ ENODEV (sound.c:1027-1033), or
  • a boot where feeder init left pcm_veto_load=1 β†’ pcm_register returns EINVAL (sound.c:1021-1024).
# with hw.snd.maxunit exhausted (33rd sound card) or any alloc failure:
kldload snd_ich            # attach fails AFTER mixer_init succeeded
ls -l /dev/mixer0          # device still exists, 0666 root:wheel
dd if=/dev/mixer0 bs=64 count=1 | hexdump -C   # UAF read path
# or any SNDCTL_MIXER_* ioctl via /dev/mixer0  β†’ UAF through freed codec

On the audit guest this is impossible β€” no AC'97 PCI function exists, so the trigger never reaches ich_pci_attach() (guest_check.log).

Artifacts

  • VERDICT.md full narrative + root-cause table + negative results
  • guest_check.log proof of hardware gate (no 0401 PCI device, no pcm dev)
  • run.sh exact guest check commands (what was run)
  • trigger.hw.sh the real-hardware trigger script
  • fix.diff family-consistent fix (pcm_register before mixer_init)
  • env.txt guest environment
  • manifest.json machine-readable catalog
  • verdict.json machine verdict (consumed by audit/persist_poc.py)

Fix

Reorder pcm_register() ahead of mixer_init() (no failure path may remain after the mixer cdev is published) and check mixer_init()'s return β€” the same shape accepted for DF-4334 (via8233). See fix.diff.

VERDICT.md
↓ download raw

DF-4421 VERDICT β€” snd_ich /dev/mixerN UAF on attach-failure path

Status: inconclusive (code defect certain; kernel runtime hardware-gated) Impact ceiling: use-after-free reachable by any local user via a 0666 cdev (open/ioctl), with kernel read/write through the freed struct ac97_info methods table. Trigger requires the driver's attach to fail after mixer_init() β€” hardware/alloc-pressure dependent, hence Low severity, matching the family precedent.

Root cause (path:line)

# Fact Reference
1 Codec created ich.c:1004 sc->codec = AC97_CREATE(dev, sc, ich_ac97)
2 0666 mixer cdev published, devinfo = codec ich.c:1031 mixer_init(dev, ac97_getmixerclass(), sc->codec); mixer.c:687; make_dev(..., 0666, "mixer%d") mixer.c:726-727; pdev->si_drv1 = m mixer.c:728; m->devinfo = codec via mixer_obj_create mixer.c:711
3 Failure paths AFTER the cdev exists: bus_dma_tag_create Γ—2 ich.c:1045-1050, 1053-1058 β†’ goto bad
4 bus_dmamem_alloc / bus_dmamap_load ich.c:1060-1063, 1065-1067 β†’ goto bad
5 pcm_register (EINVAL via pcm_veto_load; ENODEV via PCMMAXUNIT) ich.c:1069-1070 β†’ goto bad; sound.c:1021-1033
6 bad: frees the codec ich.c:1096-1097 ac97_destroy(sc->codec) β†’ kobj_delete + kfree (ac97.c:852-859)
7 bad: frees the softc ich.c:1118 kfree(sc, M_DEVBUF)
8 Nothing destroys /dev/mixerN bad: path ich.c:1095-1119 has no mixer_uninit/destroy_dev; mixer teardown only happens via pcm_unregister (sound.c), which is never reached
9 Dangling dispatch any open("/dev/mixerN")/ioctl β†’ mixer_ops β†’ si_drv1 (snd_mixer, whose backing m->devinfo was the codec) β†’ ac97mix_... kobj methods on freed memory

Note ich.c is worse than atiixp (DF-4344, which had exactly one post-mixer failure point): five distinct gotos exist after mixer_init(), and ich.c additionally ignores mixer_init()'s return value entirely (ich.c:1031) β€” a failure there is silent (that direction is safe only because mixer_init cannot fail after make_dev; verified mixer.c:726-764).

Why not reproduced (hardware gate)

The audit guest is QEMU i440FX (guest_check.log): PCI inventory contains only 8086:1237/7000/7010/7113 β€” no class=0x0401 AC'97 audio function. ich_pci_probe() (ich.c:861-879) matches none of the ich_devs[] IDs against any device, so ich_pci_attach() never executes. snd_ich.ko itself loads cleanly (kldstat shows it; module compiled from the same source), proving the code is deployed in the module set β€” only the trigger hardware is absent. Same situation as verified family members DF-4334/DF-4344 on this guest.

Exploitability discussion (on real hardware)

  • The dangling object is reachable through a 0666 cdev: any local UID (no group membership) can open it.
  • Primitive: UAF read/write of moderate quality β€” mixer ioctls write attacker-scaled values into codec registers (ac97_wrcd β†’ bus_space_write_2 through freed-but-stale pointers), read back codec state. The kobj methods table itself is freed (kobj_delete), so the dispatch already dereferences freed memory on the first ioctl.
  • Grooming: struct ac97_info is M_AC97 (kmalloc ~ small size class); between attach failure and the mixer open there is no window pressure β€” an attacker who can cause the alloc failure (memory pressure is the realistic remote-ish knob) then sprays the size class.
  • Realistic ceiling: local privesc UAF, but the trigger needs an attach-time failure β€” not attacker-schedulable at will. Family rated Low; consistent here.

Negative results (pass-2 classes hunted and killed β€” ich.c)

  • ichchan_init writes sc->ch[num] fields BEFORE the default: return (NULL) bounds check (ich.c:375-384 vs 410-411) β€” would be an OOB write into trailing sc_info fields (ac97rate/dtbl/ich_lock) for num>=3. Killed: channel_init is invoked exactly once per pcm_addchan (sound.c:585 via pcm_chn_create), and ich.c calls pcm_addchan at most 3Γ— (ich.c:1072-1075), so num ∈ {0,1,2} always. Unreachable.
  • ichchan_setspeed (speed * 48000) (ich.c:467): no uint32 overflow β€” pcm core clamps via RANGE(hwspeed, caps->minspeed, caps->maxspeed) (channel.c:1983) with ich caps 8000..48000 β†’ max 2.304e9 < 2^32. ac97rate guarded to (32000,64000) at ich.c:463-464.
  • ichchan_getptr pos = civ*blksz (ich.c:557-560): in-bounds β€” ich_filldtbl clamps blksz*blkcnt <= sndbuf maxsize (ich.c:305-306) before the dtbl build; blkcnt ∈ [2,32] always (clamped ich.c:969-972, default 2), so no div-by-zero either. Post-calibrate blksz/blkcnt mismatch (ich.c:733-737 vs 776) self-heals: any later ichchan_setblocksize β†’ ich_filldtbl re-clamps (ich.c:493-500), and even a hypothetical OOB hwptr is neutralized by the modulo in chn_dmaupdate (channel.c:387).
  • ich_setmap (ich.c:818-824): reads only segs[0]; the BDL dma tag is created with nsegments=1 (ich.c:1045-1047), so the callback can never see more than one segment. No ICH_BD_CAPS-style seg-count issue.
  • BDLE length field (ich.c:316-317): blksz ≀ maxsize/blkcnt ≀ 32768 (blkcnt β‰₯ 2), SIS7012 sample_size=1 (ich.c:903) β†’ ≀ 32768 fits the 16-bit length field below IOC bit 31.
  • ich_rdcd/ich_wrcd reg mask & 0xff (ich.c:269, 280): stays inside the 256-byte NAMBAR/MMBAR mapping; ich_waitcd bounded 1000Γ—DELAY(1) (ich.c:252-257). Ignoring ich_waitcd's ETIMEDOUT (ich.c:270, 281) reads stale codec data on timeout β€” functional, not memory-unsafe (upstream-identical).
  • ich_intr LVI arithmetic (ich.c:637-649): all quantities reduced modulo blkcnt/ICH_DTBL_LENGTH before the 8-bit register write; CIV read masked % blkcnt (ich.c:557); uninit ch[2].imask==0 (M_ZERO softc) skips cleanly when !hasmic.
  • ich_pci_resume ich_init(sc) == -1 (ich.c:1198) is a dead check (ich_init returns 0/ENXIO, never -1): failed re-init not caught, but later mixer_reinit failure still returns ENXIO; no memory-safety consequence (upstream-identical cosmetic bug).
  • 1 << (i - 1) at ich.c:966-968: UB when blocksize hint > bufsz makes blkcnt 0 β€” but hint is root-only (resource_int_value from device.hints), and on x86-64 the shift count masks to 31 β†’ 0x80000000 β†’ clamped to ICH_MAX_BLKCNT=32 (ich.c:971-972). Self-healing; not attacker-reachable. (Hardening note only.)
  • Detach-vs-calibrate intrhook race (ich.c:1083-1090 hook established only during cold boot; ich_pci_detach ich.c:1122-1144 never disestablishes): the hook drains within seconds of boot before userland can kldunload β€” unreachable window.
  • Suspend/resume trigger loop over ch[0..2] with uninitialized ch[2] (ich.c:1176-1183, 1212-1219): run_save/run are 0 via M_ZERO; loop bodies gated on them. Safe.

Fix validation

fix.diff applies the DF-4334 family shape to ich.c: * move pcm_register() to before mixer_init() (all DMA/dtbl setup stays before pcm_register, which needs sc->hasmic from the extcaps block β€” order: codec β†’ extcaps β†’ dtbl/dma β†’ pcm_register β†’ mixer_init (checked) β†’ pcm_addchan), * check mixer_init()'s return (its failure leaves no cdev β€” verified mixer.c:726-764 has no failure path after make_dev).

git apply --check passes against the read-only sys/ tree. Additionally the patched driver was COMPILE-validated on the guest: the patched ich.c was pushed into the guest's /usr/src and make in /usr/src/sys/dev/sound/driver/ich compiled it with -Werror (cc 8.3) and linked snd_ich.ko (25784 bytes) β€” full untrimmed output in build.log (BUILD_RC=0). Guest source restored to pristine afterward (md5 2a290625f264317f5ce793fbb381a197 matches the audit tree). A behavioral patched-kernel run remains impossible on this guest: ich_pci_attach requires an ICH/SIS/nForce/AMD AC'97 controller the guest cannot present, so baseline/patched behavior is not demonstrable here (fix_status: not_testable, compile-validated). The semantics of the reorder were verified against sound.c:1016-1040 (pcm_register has no mixer dependency) and mixer.c (mixer_init has no channel dependency, and cannot fail after make_dev).

Guest state

snd_ich.ko was left loaded during the check (harmless: probes nothing, attaches nothing). Not unloaded because kldunload of snd_* on this guest panics in sndstat teardown β€” known DF-4116 (recorded in DF-4344's run); no reset needed since no panic occurred and no persistent state changed.

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 (git apply --check OK) reorders pcm_register() ahead of mixer_init() and checks its return; validated to COMPILE cleanly with -Werror on the guest toolchain (cc 8.3, patched snd_ich.ko built and linked, build.log). Behavioral baseline/patched runs are impossible on this guest: ich_pci_attach requires an AC'97 controller the guest cannot present, so the failure path never executes at runtime (fix_status not_testable, compile-validated).

fix.diff; build.log (full -Werror compile + link, BUILD_RC=0); VERDICT.md 'Fix validation' section citing mixer.c:726-764 (no make_dev failure path) and sound.c:1016-1040 (pcm_register has no mixer dependency)
↓ fix.diffper-fix-DF-4421

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md root-cause table (ich.c:1031-1118, mixer.c:687-737, sound.c:1016-1040); guest_check.log (no 0401 PCI device, snd_ich loads, 0 pcm devices); build.log (fix-compiled snd_ich.ko, -Werror clean); fix.diff (git apply --check OK)

PoC changes

no seed; family trigger shape (DF-4334/DF-4344) adapted; hardware absent so guest probe substituted

Verified recommended fix

Move pcm_register() before mixer_init() and check mixer_init()'s return (DF-4334 family shape), eliminating all failure paths after the 0666 mixer cdev is published

Verdict

Code defect certain, kernel runtime hardware-gated: ich_pci_attach() calls mixer_init() (ich.c:1031, publishing a 0666 /dev/mixerN whose si_drv1 devinfo chain reaches the AC97 codec, mixer.c:726-728) before five failure paths that all goto bad (bus_dma_tag_create x2 ich.c:1045/1053, bus_dmamem_alloc ich.c:1060, bus_dmamap_load ich.c:1065, pcm_register ich.c:1069 which fails EINVAL via pcm_veto_load or ENODEV via PCMMAXUNIT, sound.c:1021-1033); the bad: unwind then ac97_destroy()s the codec (ich.c:1096-1097, kobj_delete+kfree per ac97.c:852-859) and kfrees the softc (ich.c:1118) while the mixer cdev stays registered -- any later mixer open/ioctl dispatches through freed memory. Identical construct to the verified family members (DF-4156 maestro, DF-4193 es137x, DF-4299 emu10kx-pcm, DF-4334 via8233, DF-4344 atiixp); this is the 6th instance, with the widest window in the family (five post-mixer gotos vs atiixp's one) and mixer_init's return ignored entirely. Not runtime-reproducible on the audit guest: QEMU i440FX has no AC'97 controller (no class 0401 PCI function; only 8086:1237/7000/7010/7113 present), so ich_pci_probe never matches and ich_pci_attach never executes -- snd_ich.ko itself loads cleanly (kldstat), proving deployment (guest_check.log). Ceiling on real hardware: local unprivileged UAF read/write through a 0666 cdev, trigger requires an attach-time failure (memory pressure / 33rd sound unit / broken feeder boot) -- hence Low, matching family precedent.