# 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.
