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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4157 Β· 10 files
FileTypeDescriptionSize
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:

  1. Power-state TOCTOU: agg_intr() tests ess->curpwr <= D1 (maestro.c:1653,1675) without agg_lock, while agg_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 after pci_set_powerstate(D3).
  2. stop/start race: aggch_stop_adc() clears the active bit and sets the APUs INACTIVE under the lock (maestro.c:1046-1053) while agg_intr() may already be past its ess->active & m test and about to chn_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.

VERDICT.md
↓ download raw

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) and maestro.c:1129-1131 (aggch_feed_adc_mono): cur = 16-bit APUREG_CURPTR register value minus the 0xffff-masked base offset β€” no clamp against the ring size before it becomes the sample count of interleave()/mixdown() (maestro.c:1073-1081, 1115-1121), which write 2 int16 / 1 int16 per iteration directly into the 3*ess->bufsz stat busdma buffer (maestro.c:1829).
  • Underflow path: register < base offset β†’ cur β‰ˆ 2^32 - k β†’ cur > last branch β†’ 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() reads ess->curpwr locklessly (maestro.c:1653) and dispatches chn_intr(rch.channel) β†’ PCMTRIG_EMLDMARD β†’ feed, while agg_suspend()/agg_power(D3) power the part down under agg_lock (maestro.c:821-832, 1989-1992) β€” post-D3 register reads are garbage.
  • aggch_stop_adc() clears active and sets APUs INACTIVE under the lock (maestro.c:1049-1053) racing an interrupt already past its ess->active & m test (maestro.c:1664-1676).
  • wp_rdapu() has no error propagation either β€” on timeout it prints and still returns raw PORT_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_testable
baseline no→ patch + rebuild →patched clean

No 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)
↓ fix.diffper-fix-DF-4157

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.