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)
PoC verification
Evidence pack
findings/poc/DF-3996 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_triggerPCMTRIG_STARTelse-branch,sys/dev/sound/pci/envy24.c:1810-1812:ch->offset = ((ptr / ch->blk + 1) * ch->blk % (ch->size / 4)) * 4 / ch->unit;executed whensc->run[slot] != 1β i.e. for every channel after the first of the same direction. offset can be as large asdsize-1(S32_LE:8191/2 = 4095, dsize = 4096).envy24_p32slenvy24.c:1414-1415:src = readyptr/4; dst = src/2 + ch->offset;then envy24.c:1421-1422 writesdmabuf[dst * ENVY24_PLAY_CHNUM + slot{,+1}]beforedst %= 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 inpbuf(ENVY24_PLAY_BUFUNIT * ENVY24_SAMPLE_NUM = 163840bytes, allocated at envy24.c:2244-2255 viabus_dmamem_alloc).- Same pattern:
envy24_p16sl(1447-1457),envy24_p8u(1489-1496). - Record mirror:
envy24_r32sl(1519-1527) /envy24_r16sl(1549-1557) readdmabuf[src * ENVY24_REC_CHNUM + slot{,+1}]withsrc = dst/2 + ch->offsetunclamped pastssizeβ reads up to ~2Γ past the 49152-entryrbufinto the user's record stream (kernel heap disclosure).
User reachability (unprivileged)
/dev/dspNis 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 secondPCMTRIG_STARTruns 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 viaSNDCTL_DSP_GETOPTR/GETOSPACEon the running channel, so the resulting write index is computable in advance; retriggering variesoffsetacross [0, dsize-1].- The write value is the user's PCM payload: full 32-bit control with
S32_LE (
p32slstoresdata[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 whilereadyptr/2 + offset >= dsize.
Reproduction
- Guest: DragonFly 6.5-DEVELOPMENT (env.txt) β
pciconfshows 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.cruns 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
fixedfix.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
Confirmed kernel references
- sys/dev/sound/pci/envy24.c:1810
- sys/dev/sound/pci/envy24.c:1414
- sys/dev/sound/pci/envy24.c:1421
- sys/dev/sound/pci/envy24.c:1447
- sys/dev/sound/pci/envy24.c:1457
- sys/dev/sound/pci/envy24.c:1489
- sys/dev/sound/pci/envy24.c:1496
- sys/dev/sound/pci/envy24.c:1519
- sys/dev/sound/pci/envy24.c:1526
- sys/dev/sound/pci/envy24.c:1549
- sys/dev/sound/pci/envy24.c:1556
- sys/dev/sound/pci/envy24.c:2244
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.
No comments yet.