# DF-3998 — VERDICT

**Finding:** envy24ht(4) carries the identical DF-3996 emldma defect: every
DMA-emulator function forms its ring index as `dst = src/2 + ch->offset`
(play) / `src = dst/2 + ch->offset` (rec) and performs the FIRST loop access
BEFORE the `%= dsize`/`%= ssize` wrap, so whenever `src/2 + offset >= dsize`
(rec: `>= ssize`) the access lands past the shared DMA buffer. The phase
offset comes from `envy24htchan_trigger`'s second-same-direction-channel
START branch (envy24ht.c:1720-1726), same formula as envy24.c:1810.

**Status: reproduced (verbatim-code harness on the guest; live /dev/dsp
trigger hardware-gated — no ICE1724 PCI device in QEMU).**
Confidence: certain. Impact class on real hardware: kernel heap disclosure
(record direction) — the record direction is directly reachable
unprivileged; the play-direction OOB write is present in the code but gated
behind a second PLAY channel, which the driver does not register (see
below), so on the driver as shipped it is latent.

## Root cause (path:line)

- `envy24htchan_trigger` PCMTRIG_START else-branch,
  `sys/dev/sound/pci/envy24ht.c:1721-1723`:
  `ch->offset = ((ptr / ch->blk + 1) * ch->blk % (ch->size / 4)) * 4 / ch->unit;`
  executed when `sc->run[slot] != 1` after `sc->run[slot]++` (1714) — i.e.
  for every same-direction channel after the first.
- `envy24ht_r16sl` envy24ht.c:1459 `src = dst / 2 + ch->offset;` then
  1465-1466 read `dmabuf[src * 2 + slot{,+1}]` BEFORE `src %= ssize` (1470).
- `envy24ht_r32sl` 1429 / 1435-1436 / 1440 — same.
- `envy24ht_p32sl` 1324 / 1330-1331 / 1333, `envy24ht_p16sl` 1357 /
  1366-1367 / 1375, `envy24ht_p8u` 1399 / 1405-1406 / 1408 — play twins.
- Buffers: `sc->rbuf` = ENVY24HT_REC_BUFUNIT(8)*4096 = 32768 B = 8192
  sample32_t entries, `sc->pbuf` = 131072 B = 32768 entries
  (envy24ht.c:2160-2161, allocated 2171-2186 via bus_dmamem_alloc).

## Reachability analysis (what is and is NOT reachable as shipped)

- The driver registers **1 play + (2+adcn) rec** channels
  (envy24ht.c:2494-2505; adcn = ((scfg & 0x0c)>>2)+1, standard boards
  scfg=0x0b → adcn=3 → 5 rec channels). The offset branch requires
  `sc->run[slot] != 1` — for PLAY that needs 2 running play channels;
  only 1 exists, and `chn_start()` refuses to re-START a running channel
  (sys/dev/sound/pcm/channel.c:697-701 `CHN_STARTED(c)` guard) → the
  play-direction OOB **write** (p8u/p16sl/p32sl) is unreachable on the
  driver as currently registered, i.e. LATENT — it goes live the moment the
  commented-out 5-play-channel registration (envy24ht.c:2493,2498) is
  restored, which the code comments show was the design.
- The **record** direction has 3-6 channels: the SECOND rec channel's
  PCMTRIG_START runs the offset branch unconditionally. r16sl with
  S16_LE gives ssize = ch->size/8 = 2048 while the offset formula scales
  with ch->size/4 → attacker-chosen ptr (observable via SNDCTL_DSP_GETIPTR
  on the running first rec channel) + user blocksize (SNDCTL_DSP_SETFRAGMENT)
  yields offset up to ~4088, and freeptr (how many bytes the user has
  consumed) sets dst/2 — `src = dst/2 + offset` reaches 8183 vs ssize 2048
  → first access reads `rbuf[16360..16361]`, 32672 bytes PAST the 32 KB
  record DMA buffer, into the user's record stream (`read()` on /dev/dsp).
  One OOB read pair per EMLDMARD/START; leaks the low 16 bits (S16_LE) or
  the full 32 bits (S32_LE) of each adjacent kernel word.
- Data-side edge: with freeptr at maximum, `data[dst+1]` writes one element
  (2-4 bytes) past the kmalloc'd `ch->data` (M_ENVY24HT, ch->size bytes) —
  observed in the harness as `data16[8192]` write.

## Reproduction

- Guest: DragonFly 6.5-DEVELOPMENT (env.txt), no 1412:1724 PCI device
  (`envy24-pci-devs=0`) → live trigger impossible on this VM.
- `emldmaht_harness.c` runs the five emldma functions, the trigger offset
  formula and gethwptr bounds VERBATIM against canary arenas sized exactly
  like pbuf/rbuf. Results (run.log, deterministic across run.2.log):
  - [C] second rec channel (num=2), ptr=4087, blk=8 (user blocksize 16),
    freeptr max → offset 4088 → 1 sample pair read from +32672 B past
    rbuf end into the user stream (0xFEE9 = low half of the kernel-heap
    marker 0xDEADFEE9); 12 additional before-rbuf samples from the periodic
    `src %= ssize` wrap landing on src<3 with slot −6 (that part is the
    DF-3999 slot defect, amplified by the offset); data-side 1-element
    write past ch->data with the leaked kernel value.
  - [D] play twin: offset 4064 → first write at +130024 B past the 128 KB
    pbuf with the user's PCM payload 0xB9B90000 (S16_LE) — the write
    primitive, latent as described above.
  - hard mode (run.hard.log): rbuf sized exactly against a PROT_NONE guard
    page → SIGSEGV on the FIRST OOB read — the arithmetic is exactly what
    the kernel would run against its bus_dmamem allocation.
- Fixed variant (fix.diff semantics applied): 0 leaked samples, 0 OOB
  writes, no fault (fix_run.log).

## Exploit chain (real hardware)

open /dev/dsp0.r0 (first rec channel), start recording → open .r1,
SETFMT S16_LE/S32_LE, SETFRAGMENT for the chosen blk, consume bytes to set
freeptr, observe r0 hwptr via SNDCTL_DSP_GETIPTR, trigger r1 START →
`ch->offset` computed (up to 4088/2047 by format) → each EMLDMARD leaks a
32-bit kernel word from up to ~32 KB past the 32 KB record DMA buffer into
the user's record stream; retrigger/re-open varies the offset → repeatable
kernel heap disclosure from an unprivileged user (0666 /dev/dsp clones).
The complementary play-direction primitive (attacker-controlled 32-bit
writes up to ~130 KB past the 128 KB play DMA buffer with S32_LE) is
currently dormant — it activates for any build/patch that restores the
designed 5-play-channel registration, at which point this becomes the
DF-3996 privesc-grade chain verbatim. Fix both now.

## Fix

`fix.diff` — wrap the phase-adjusted index before the first access in all
five emldma functions (play: `dst = (src/2 + ch->offset) % (ch->size/4|8)`;
rec: `src = (dst/2 + ch->offset) % (ch->size/8)`), plus even-alignment of
the data-side index so the (src, src+1)/(dst, dst+1) pair can never
straddle the end of `ch->data`. Validated at harness level: baseline
leaks + hard-mode fault vs 0/0/no-fault after the fix.

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