# DF-3996 — VERDICT

**Finding:** When a second same-direction channel starts on envy24(4), the
DMA-emulator functions index the shared play/record DMA buffers with an
unclamped `dst = src/2 + ch->offset` (play) / `src = dst/2 + ch->offset`
(rec): the first loop iteration runs **before** the `%= dsize` wrap,
writing/reading out of bounds whenever `src/2 + offset >= dsize`.

**Status: reproduced (verbatim-code harness on the guest; live /dev/dsp
trigger impossible — the QEMU guest has no ICE1712 hardware).**
Confidence: certain. Impact on real hardware: kernel heap corruption
(controlled 32-bit writes up to ~82 KB past a 160 KB DMA allocation) +
kernel heap disclosure (~up to 196 KB past the record DMA buffer) →
privesc-grade.

## Root cause (path:line)

- `envy24chan_trigger` `PCMTRIG_START` else-branch,
  `sys/dev/sound/pci/envy24.c:1810-1812`:
  `ch->offset = ((ptr / ch->blk + 1) * ch->blk % (ch->size / 4)) * 4 / ch->unit;`
  executed when `sc->run[slot] != 1` — i.e. for every channel after the
  first of the same direction. offset can be as large as `dsize-1`
  (S32_LE: `8191/2 = 4095`, dsize = 4096).
- `envy24_p32sl` envy24.c:1414-1415: `src = readyptr/4; dst = src/2 + ch->offset;`
  then envy24.c:1421-1422 writes `dmabuf[dst * ENVY24_PLAY_CHNUM + slot{,+1}]`
  **before** `dst %= dsize` (1424). First-iteration dst ≤ (dsize-1) +
  (ssize/2-1)... concretely for S32_LE: dst ≤ 4095+4095 = 8190 → index up to
  8190*10+8 = 81908 vs 40960 entries in `pbuf` (`ENVY24_PLAY_BUFUNIT *
  ENVY24_SAMPLE_NUM = 163840` bytes, allocated at envy24.c:2244-2255 via
  `bus_dmamem_alloc`).
- Same pattern: `envy24_p16sl` (1447-1457), `envy24_p8u` (1489-1496).
- Record mirror: `envy24_r32sl` (1519-1527) / `envy24_r16sl` (1549-1557) read
  `dmabuf[src * ENVY24_REC_CHNUM + slot{,+1}]` with
  `src = dst/2 + ch->offset` unclamped past `ssize` → reads up to ~2× past
  the 49152-entry `rbuf` into the user's record stream (kernel heap
  disclosure).

## User reachability (unprivileged)

- `/dev/dspN` is created 0666 (sound.c:1134-1139, clone device). Each open
  gets its own hw channel of the 5 play / (2+adcn) rec channels registered
  at attach (envy24.c:2586-2597). Two play opens → two hw play channels →
  the second `PCMTRIG_START` runs the offset branch.
- `src` (readyptr) is exactly the number of PCM bytes the user wrote before
  triggering — attacker-chosen. `ptr` (the running first channel's hw
  position) is observable via `SNDCTL_DSP_GETOPTR`/`GETOSPACE` on the
  running channel, so the resulting write index is computable in advance;
  retriggering varies `offset` across [0, dsize-1].
- The write value is the user's PCM payload: **full 32-bit control with
  S32_LE** (`p32sl` stores `data[src]` verbatim), 24-bit (<<16) with S16_LE,
  8-bit with U8. One OOB write pair per emldma pass, repeatable on every
  subsequent EMLDMAWR/START while `readyptr/2 + offset >= dsize`.

## Reproduction

- Guest: DragonFly 6.5-DEVELOPMENT (env.txt) — `pciconf` shows **0** envy24
  (ICE1712, vendor 0x1412) devices; the snd_envy24 driver cannot attach, so
  no live /dev/dsp trigger is possible on this guest.
- `emldma_harness.c` runs the **verbatim** driver functions (play+rec+the
  trigger offset formula) against a canary arena sized exactly like the
  kernel buffers. Results (run.log; identical across run.2/run.3):
  S16_LE +928 B past pbuf (value 0xa7a70000); S32_LE **+81,968 B** past
  (value 0xb9b9b9b9); U8 +5,048 B; record direction leaked 2/1 samples from
  past rbuf into the user buffer; hard-mode guard page (exact kernel size)
  faults at the first write. The arithmetic is deterministic — no race, no
  hardware state needed beyond a second running channel.

## Exploit chain (real hardware)

open /dev/dsp0.p0 (play) + write + start → open /dev/dsp0.p1, SETFMT
S32_LE, SETFRAGMENT/SETBLKSIZE, fill buffer, observe p0 hwptr via GETOPTR,
start p1 → `ch->offset` computed → first `emldma` writes attacker 32-bit
values at `pbuf + (dst*10+slot)*4` with dst ∈ [4096, 8190] → up to ~82 KB
past the 160 KB `bus_dmamem_alloc` region into adjacent kernel heap; repeat
with varying offsets (each second-channel start of the 5 play channels, or
re-open) → corrupt adjacent objects (on this family of allocations:
other DMA buffers / driver softcs) → forge pointer-bearing object →
uid=0. Record direction gives the complementary heap disclosure for
defeating any layout uncertainty. On the standard no-SMAP/SMEP/KASLR
target this is a credible unpriv→root chain; not demonstrated live for
lack of the PCI device in QEMU.

## Fix

`fix.diff` — wrap the phase-adjusted index before the first access in all
five emldma functions: `dst = (src / 2 + ch->offset) % dsize;`
(play) and `src = (dst / 2 + ch->offset) % ssize;` (rec).
Fix-validated: `emldma_harness_fixed.c` (same verbatim code with the fix)
reports **0** OOB writes, **0** leaked samples, and no guard-page fault
(fix_run.log). The identical pattern exists in
`sys/dev/sound/pci/envy24ht.c:1324-1399` and should be fixed the same way.

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