ADC DMA-ring completion feed uses the raw APU CURPTR register as its only bound β desync/power-state race yields unbounded kernel OOB write
Summary
aggch_feed_adc_stereo/mono compute cur = wp_rdapu(APUREG_CURPTR) - base_offset with no clamp against the ring size; if the register desyncs the unsigned subtraction underflows toward 2^32 and interleave()/mixdown() write int16 pairs per iteration past the DMA allocation into adjacent kernel memory, with attacker-recorded audio as the write contents. The driver itself creates incoherent windows: agg_intr gates on a lockless curpwr read while power/suspend power the register file down under agg_lock, stop_adc races an interrupt already past its active-bit test, and wp_rdapu returns raw garbage on timeout with no error propagation. Unprivileged via /dev/dspN 0666 O_RDONLY + suspend churn; ceiling corruption primitive (uid=0), floor panic. Hardware-gated (no ESS on guest). Clamp-the-index fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4157 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 2.9 KB | β raw | |
| maestro_probe.c | β | 1.5 KB | view raw | |
| build.sh | β | 111 B | view raw | |
| run.sh | β | 152 B | view raw | |
| build.log | β | 325 B | view raw | |
| run.log | β | 325 B | view raw | |
| env.txt | β | 370 B | view raw | |
| fix.diff | β | 1012 B | view raw | |
| verdict.json | β | 3.3 KB | view raw |
DF-4157 β ADC DMA-ring feed trusts the APU CURPTR register as its only bound
File: sys/dev/sound/pci/maestro.c (pass 2, GLM 5.3)
What
The record-channel completion walk derives its ring index exclusively from a device register:
cur = wp_rdapu(ch->parent, 0, APUREG_CURPTR); /* 16-bit hw register */
cur -= 0xffff & ((ch->srcphys - ch->base) >> 1); /* unclamped */
...
interleave(ch->src + last, src2 + last, ch->sink + 2*last, cur - last);
(maestro.c:1090-1104, mono variant maestro.c:1128-1139). cur is
unsigned; if the register read returns a value below the base offset the
subtraction underflows toward 2^32 and interleave()/mixdown() then write
2 int16 per iteration β an unbounded OOB write past the 3*bufsz stat
busdma allocation into adjacent kernel memory.
In nominal operation the APU wraps at LOOPLEN so cur β [0, buflen/2) and
everything is consistent β the software math is only as good as the
register. Desync windows that the driver itself creates:
- Power-state TOCTOU:
agg_intr()testsess->curpwr <= D1(maestro.c:1653,1675) withoutagg_lock, whileagg_suspend()/agg_power(..., D3)(maestro.c:821-832, 1985-1995) powers the part down under the lock. An interrupt that already passed the check feeds from a dead register file afterpci_set_powerstate(D3). - stop/start race:
aggch_stop_adc()clears theactivebit and sets the APUs INACTIVE under the lock (maestro.c:1046-1053) whileagg_intr()may already be past itsess->active & mtest and about tochn_intr(ess->rch.channel)βPCMTRIG_EMLDMARDβ feed.
The author's own comment ("This function expects n not overwrapping the
buffer boundary", maestro.c:1066-1070) documents the trust.
Impact ceiling
Kernel OOB write with attacker-influenced contents (the L/R sample data being interleaved is the recorded audio) and huge length β a corruption primitive, not just a panic, on any machine with a Maestro card when the race hits. Hardware-gated (needs the card + suspend/resume or a stop/rstorm race), hence Medium/likely rather than High.
Guest result
Audit guest has no ESS Maestro PCI function and no /dev/dsp (run.log) β
the driver never attaches. untested on this guest; defect is
source-certain. wp_rdapu itself even prints "indexing timed out" when
the register won't echo (maestro.c:455-456) and still returns whatever
PORT_DSP_DATA holds β there is no error propagation either.
Fix
fix.diff: clamp the register-derived index to the ring size in both feed
functions (treat desync as "wrapped"). Applies cleanly with git apply.
DF-4157 VERDICT
status: untested (hardware-gated) | impact ceiling: kernel OOB write (corruption primitive)
Reproduced?
No. The audit guest has no Maestro PCI audio function (pciconf -lv),
no /dev/dsp (run.log) β the driver never attaches, so the record path
cannot be exercised. Source-level certainty for the defect:
maestro.c:1090-1092(aggch_feed_adc_stereo) andmaestro.c:1129-1131(aggch_feed_adc_mono):cur= 16-bitAPUREG_CURPTRregister value minus the 0xffff-masked base offset β no clamp against the ring size before it becomes the sample count ofinterleave()/mixdown()(maestro.c:1073-1081,1115-1121), which write 2 int16 / 1 int16 per iteration directly into the3*ess->bufszstatbusdma buffer (maestro.c:1829).- Underflow path: register < base offset β
cur β 2^32 - kβcur > lastbranch βinterleave(..., cur - last)with n β 4e9 β writes march off the end of the DMA allocation into adjacent kernel memory with recorded-audio contents. - Software consistency (why it needs desync): LOOPLEN programmed as
ch->buflen >> ch->stereo(maestro.c:997,1003) equals the ring sample capacity, so a live, coherent APU wraps in-range. The driver itself creates incoherent windows: agg_intr()readsess->curpwrlocklessly (maestro.c:1653) and dispatcheschn_intr(rch.channel)βPCMTRIG_EMLDMARDβ feed, whileagg_suspend()/agg_power(D3)power the part down underagg_lock(maestro.c:821-832,1989-1992) β post-D3 register reads are garbage.aggch_stop_adc()clearsactiveand sets APUs INACTIVE under the lock (maestro.c:1049-1053) racing an interrupt already past itsess->active & mtest (maestro.c:1664-1676).wp_rdapu()has no error propagation either β on timeout it prints and still returns rawPORT_DSP_DATA(maestro.c:451-458).
Exploit chain (if hardware present)
open /dev/dspN O_RDONLY (0666 via devfs clone) β set AFMT_S16_LE/stereo β
read() loop keeps the ADC feeding β concurrently force the power-state
race (suspend/resume storm, or stop/start churn of the record channel) β
when a desynced CURPTR lands, interleave() smashes adjacent kernel heap
with recorded-audio (attacker-chosen int16 pairs) at attacker-influenced
length β groom victim objects adjacent to the stat buffer β uid=0.
Not demonstrable on this guest; recorded as ceiling.
Fix validation
Not possible on this guest (no hardware). fix.diff clamps the
register-derived index in both feed functions; git apply --check passes.
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
not_testableNo Maestro hardware on the audit guest, so neither baseline corruption nor patched behaviour can be observed. fix.diff applies cleanly (git apply --check) and bounds cur to [0, ring) in both aggch_feed_adc_stereo and aggch_feed_adc_mono.
findings/poc/DF-4157/fix.diff (git apply --check clean)
Confirmed kernel references
Detail
Exploit chain
open /dev/dspN (0666) O_RDONLY -> AFMT_S16_LE stereo -> read loop drives PCMTRIG_EMLDMARD feeds -> concurrent suspend/resume or record stop/start churn hits the curpwr/CURPTR desync -> interleave() smashes kernel memory adjacent to the stat DMA allocation with attacker-recorded int16 pairs -> slab-groom a victim object adjacent -> uid=0 (ceiling; not demonstrated, no hardware)
Evidence (decisive lines)
maestro.c:1090-1104 (stereo feed, unclamped cur), maestro.c:1128-1139 (mono feed), maestro.c:1073-1081 (interleave writes 2 int16/iter), maestro.c:1653+1674-1676 (lockless curpwr gate then chn_intr dispatch), maestro.c:821-832 (D3 power-down under lock), maestro.c:1049-1053 (stop_adc clears active under lock), maestro.c:451-458 (wp_rdapu returns garbage on timeout, no error propagation), maestro.c:1829 (3*bufsz stat allocation); run.log (no maestro on guest)
PoC changes
authored fresh probe (no seed); would-be trigger sketch documented in probe header
Verified recommended fix
Clamp the register-derived cur to the ring bounds in both feed functions (treat desync as wrapped)
Verdict
aggch_feed_adc_stereo/mono derive their copy length solely from the APU CURPTR hardware register minus the base offset with no clamp; a desynced register (power-state TOCTOU: lockless ess->curpwr check in agg_intr vs agg_power(D3) under lock; or stop_adc racing a stale interrupt) underflows cur toward 2^32 and interleave()/mixdown() then write unboundedly past the 3*bufsz stat busdma buffer with recorded-audio contents. Source-certain absence of bounds; exploitability gated on Maestro hardware and hitting the desync window, hence likely/Medium. Guest has no Maestro device so untested.
No comments yet.