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

Unclamped phase offset in the envy24 DMA emulators: second-channel start writes/reads up to ~2x past the shared play/record DMA buffers β€” controlled 32-bit kernel heap write (S32_LE) and kernel heap disclosure to userspace

Summary

When a second same-direction channel is started, envy24chan_trigger computes a phase offset that can reach dsize-1. Every DMA-emulator function forms its ring index as dst = src/2 + ch->offset and performs the FIRST loop access BEFORE the dst %= dsize wrap, so whenever readyptr/2 + offset >= dsize the first write lands past the end of the shared pbuf (163,840 bytes) β€” with S32_LE the written value is the raw 32-bit user sample; measured worst case 81,968 bytes past the allocation, repeatable per EMLDMAWR. Record direction copies kernel heap past rbuf into the user's audio stream. Index deterministic from the observable hw pointer (GETOPTR). /dev/dspN is 0666 β€” unprivileged. Verified with a verbatim-code harness (S32_LE +81968B, S16_LE +928B, U8 +5048B, record leak 2/1 samples, guard-page hard mode, deterministic 3 runs); fix-validated (0 OOB, 0 leaks). Live chain hardware-gated (no ICE1712 in guest). Sibling lead: identical pattern in envy24ht.c:1324-1399.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3996 Β· 12 files
FileTypeDescriptionSize
emldma_harness.c β€” 14.0 KB view raw
emldma_harness_fixed.c β€” 14.1 KB view raw
build.sh β€” 107 B view raw
run.sh β€” 100 B view raw
build.log β€” 9 B view raw
run.log β€” 1.8 KB view raw
run.2.log β€” 263 B view raw
run.3.log β€” 263 B view raw
fix_run.log β€” 695 B view raw
env.txt β€” 204 B view raw
fix.diff β€” 1.8 KB view raw
VERDICT.md β€” 5.3 KB ↓ raw
VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff semantics applied to the verbatim harness functions: baseline 3 OOB-write cases + 2 record leaks + guard-page fault; patched run reports 0 OOB writes, 0 leaks, no fault (fix_run.log). No kernel build performed for the sound driver (no hardware to attach it); the diff is a 5-line arithmetic wrap, compile-trivial.

findings/poc/DF-3996/fix_run.log; findings/poc/DF-3996/emldma_harness_fixed.c
↓ fix.diffper-fix-DF-3996

Confirmed kernel references

Detail

Exploit chain

open two 0666 /dev/dsp clone channels of the envy24 card -> start ch1 -> SETFMT S32_LE + fill buffer on ch2, observe ch1 hwptr via SNDCTL_DSP_GETOPTR -> trigger ch2 start (offset branch, envy24.c:1810) -> emldma writes user 32-bit PCM at pbuf+(dst10+slot)4 with dst up to 8190 (~82KB past the bus_dmamem region into adjacent kernel heap) -> repeat across the 5 play channels / retrigger to place multiple writes -> corrupt adjacent kernel object -> uid=0 on the no-hardening target; record direction provides the complementary heap disclosure. Live chain not executed: guest lacks the PCI device (hardware-gated).

Evidence (decisive lines)

run.log (S32 case: first_entry=61452 (+81968 bytes past pbuf), value 0xb9b9b9b9; S16 +928B 0xa7a70000; U8 +5048B; rec leaks 2/1 samples), run.2.log/run.3.log (deterministic), fix_run.log (patched: all zeros, no fault), VERDICT.md full line-by-line trace

PoC changes

Seed sketch replaced entirely: PoC is a faithful harness embedding the five emldma functions and the trigger offset formula VERBATIM from envy24.c with snd_dbuf readyptr/freeptr stubs and canary/guard arenas sized exactly like pbuf/rbuf; realistic [0,size) readyptr values (harness v1 overflowed its own user-side array with readyptr==size - fixed).

Verified recommended fix

Wrap the phase-adjusted index before the first loop access in all five emldma functions: dst=(src/2+ch->offset)%dsize (play), src=(dst/2+ch->offset)%ssize (rec) - see fix.diff

Verdict

Reproduced via verbatim-code harness on the guest (no ICE1712 hardware present, so live /dev/dsp trigger impossible on this VM): with the exact kernel constants, a second-channel PCMTRIG_START offset plus attacker-chosen readyptr makes the first emldma loop iteration write up to 81968 bytes past the 163840-byte play DMA buffer (full 32-bit user-controlled value with S32_LE; +928B S16_LE; +5048B U8) and the record direction copy samples from past the 196608-byte record DMA buffer into the user stream (kernel heap disclosure). Arithmetic is deterministic - no race required. Fix validated at harness level: wrapping dst/src by dsize/ssize eliminates all OOB writes, leaks and the guard-page fault.