feed_volume_feed divides the USER-PACED source return with inexact SND_FXDIV: frame overcount writes past the window and count -= j*align underflows to ~4GiB, re-arming the loop to pull a directed, user-length-controlled write past the window end
Summary
FXDIV applied to the source return - exactly the bytes the unpriv user has write()n - bad-residue returns give floor+1 frames (apply reads AND writes a frame past the fed data), and count -= j*align underflows when j>count, re-arming the loop with ~4GiB at dst+overrun: directed length (pending ready bytes) and content (user audio) past the destination scratch. feeder_volume is default for every vchan child. HARNESS-VERIFIED: 2810/21604 checks fail (3ch align=6 count=32771 -> write at 32772 + underflow re-arm); fixed generator: 0. Strongest primitive of the family.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4848 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| verdict.json | β | 4.1 KB | view raw | |
| fxshim.h | β | 6.9 KB | view raw | |
| fxsrc.c | β | 2.0 KB | view raw | |
| ffvol.c | β | 5.5 KB | view raw | |
| fxfmt.c | β | 6.6 KB | view raw | |
| ffmat.c | β | 5.3 KB | view raw | |
| ffvol.c | β | 5.5 KB | view raw | |
| ffmix.c | β | 9.2 KB | view raw | |
| dspchurn.c | β | 2.1 KB | view raw | |
| ctlchurn.c | β | 991 B | view raw | |
| build.sh | β | 119 B | view raw | |
| run.sh | β | 140 B | view raw | |
| fix.diff | β | 1.0 KB | view raw | |
| snd_fxdiv_gen_fixed.awk | β | 4.0 KB | β download |
DF-4848 β feeder_volume.c FXDIV frame-overcount + count underflow (evidence pack)
What this proves
The REAL, UNMODIFIED sys/dev/sound/pcm/feeder_volume.c divides its source
feeder's RETURN VALUE with the inexact generated SND_FXDIV
(feeder_volume.c:277). The return value is user-paced (it is the amount of
data actually flowing β feed_root returns min(count, sndbuf_getready),
and ready is exactly what the unprivileged user write()s). For bad-
residue returns inside the claimed [1,65536] domain (first bad: 32768 for
align 6, 16384 for align 10, 8195 for 12, 5473 for 14), FXDIV returns
floor+1 frames, so:
- T1
apply()reads AND writes a frame past the fed data β past the destination window when the return fills the window (ret == count). - T2
j*align > countβcount -= jat :286 underflows to ~4GiB β the loop re-arms and asks the source for a wild window atdst+overrun; with more data ready (user wrote more), the source writes up to bufsoft-ready bytes PAST the window end β a directed heap overflow with user-controlled audio bytes.
feeder_volume is inserted by default for every vchan child (feeder_chain.c:713-716, SD_F_VPC with snd_maxautovchans=16 default).
Build / run (guest)
cd /root/fx && sh build.sh && ./ffvol # baseline: FAIL (2810/21604) cp snd_fxdiv_gen_fixed.h snd_fxdiv_gen.h && cc -O1 -g -o ffvol_fixed ffvol.c fxsrc.c ./ffvol_fixed # ALL PASS
Expected output
vol.log: ffvol: checks=21604 failures=2810 -> FAIL, decisive lines:
FAIL[T1 write] ch=3 align=6 count=32771 ret=32772 REAR guard (write past
window) maxoff=32772 β the 3-channel (align 6) case at the first bad
residue region.
Fix
fix.diff (shared generator fix). A/B: baseline 2810 failures β fixed 0.
DF-4848 β VERDICT
status: reproduced (feeder level, real unmodified code on the audit guest)
primitive: OOB read+write of up to alignβ1 bytes past the destination
window; then count underflow to ~4GiB and a wild second-pass source fill
that can write up to bufsoft-ready (tens of KB) of user-controlled bytes past
the window end
confidence: certain (harness-proven on pristine code; underflow re-arm
observed directly)
Root cause
feeder_volume.c:277:
j = SND_FXDIV(FEEDER_FEED(f->source, c, dst, count, source), info->align);
The dividend is the SOURCE RETURN β not a kernel-chosen aligned value but the
byte count actually delivered, which for the root feeder is
min(count, sndbuf_getready(src)) (feeder.c:466) β i.e. exactly what the
unprivileged user has write()n into the channel. The generated SND_FXDIV
misrounds UP (floor+1) for bad-residue dividends within the claimed domain
(first bad x: align 6 β 32768, align 10 β 16384, align 12 β 8195, align 14 β
5473; see DF-4846 for the exhaustive enumeration). Then:
:282apply(vol, matrix, ch, dst, j)processes (ret/align + 1) frames β reading and writing one frame past the fed data; when ret == count the extra frame is past the destination window (canary REAR hit at exactly count+1 in the harness).:284-286j *= align; count -= j;with j = ret+delta > count β count wraps to ~4294967295 βwhile (count != 0)re-arms βFEEDER_FEED(f->source, c, dst+overrun, ~4GiB, ...)β the source (root) then writesmin(4GiB, ready)bytes atdst+overrun, i.e. up to the bufsoft-ready amount of user-audio bytes PAST the window end. In-kernel the window is the caller's scratch (to->tmpbufor the format feeder's in-place tail): a directed heap overflow whose length the attacker paces via write() sizes and whose content is the attacker's audio samples.
Reproduction (guest, pristine file)
ffvol (vol.log): checks=21604 failures=2810, decisive:
FAIL[T1 write] ch=3 align=6 count=32771 ret=32772 REAR guard (write past window) maxoff=32772
T2 (underflow re-arm) is detected via the source-ask audit (wild pointer /
maxask > window); the T1 lines above are the direct window overruns.
Kernel reachability
- feeder_volume is inserted for every vchan child by default:
feeder_chain.c:713-716(SD_F_VPC+snd_maxautovchans = 16default, sound.c:62), and for soft-pcm-volume devices. - Channel counts 3/5/6/7 (align 6/10/12/14) are unprivileged SNDCTL_DSP_CHANNELS choices (dsp.c clamps β€ 8 β 3,5,6,7 all legal).
- The dividend is user-paced: write sizes choose
readyat feed time. - The underflow second pass writes past the window into adjacent kernel heap; the harness bounds it to the guarded arena to log instead of corrupting the guest process.
In-kernel churn (bad-residue pacing on 6ch/7ch vchans, ES1370): no differential anomaly vs aligned control on this no-INVARIANTS kernel (logs in the DF-4849 pack) β consistent with DF-4710's experience; the primitive's observability requires a detector (INVARIANTS/KASAN) the production guest lacks.
Fix validation
fix.diff (generator): baseline 2810 failures β fixed 0 (ALL PASS)
(vol.log vs vol_fixed.log). fix_status: fixed.
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
fixedA/B at harness level, only the generated header swapped: 2810 failures vs 0 (vol.log vs vol_fixed.log). Kernel rebuild A/B not performed (null in-kernel baseline observable on this no-INVARIANTS rig, same standard as DF-4710).
vol_fixed.log (failures=0); fix.diff; snd_fxdiv_gen_fixed.awk
Confirmed kernel references
Detail
Exploit chain
unprivileged: open /dev/dsp (vchan) -> SNDCTL_DSP_CHANNELS=6 (align 12) -> pace write() sizes so the feed return lands on a bad residue (>=8195, ~29% of values) with more data pending -> volume feeder miscounts a frame, writes past the window, and on a full window underflows count -> second-pass source fill writes attacker-paced user audio bytes past the destination scratch (to->tmpbuf / format feeder tail) into adjacent kernel heap.
Evidence (decisive lines)
vol.log (baseline: checks=21604 failures=2810, 'FAIL[T1 write] ch=3 align=6 count=32771 ret=32772 REAR guard (write past window) maxoff=32772' + T2 underflow re-arm audit lines); vol_fixed.log (fixed: 0); ffvol.c/fxshim.h/fxsrc.c; VERDICT.md
PoC changes
Fresh harness (DF-4846 infrastructure) plus a complete fake struct pcm_channel (c->volume[][]) with non-flat volumes so the bypass paths do not short-circuit; desc->type must be FEEDER_VOLUME for feeder_volume_apply_matrix.
Verified recommended fix
Fix snd_fxdiv_gen.awk (domain-exact reciprocals + 64-bit multiply, fix.diff); defensively, clamp j*align <= count in feed_volume_feed before the loop decrement.
Verdict
Reproduced at feeder level on the audit guest against the real, unmodified sys/dev/sound/pcm/feeder_volume.c: feed_volume_feed():277 divides the SOURCE FEEDER'S RETURN with the inexact generated SND_FXDIV - and that dividend is user-paced (feed_root returns min(count, sndbuf_getready), i.e. exactly what the unprivileged user write()s). For bad-residue returns inside the claimed [1,65536] domain (first bad: 32768@align6, 16384@align10, 8195@align12, 5473@align14) FXDIV returns floor+1 frames, so apply() reads AND writes a frame past the fed data (past the destination window when ret==count - harness: count=32771 align=6 ret=32772, REAR guard hit), and :284-286 count -= j*align UNDERFLOWS to ~4GiB when j>count, re-arming the loop to ask the source for a wild window at dst+overrun - with more data ready the source then writes up to bufsoft-ready bytes of user-controlled audio PAST the window end (directed heap overflow; bounded only by the user's pending write data). feeder_volume is default for every vchan child (feeder_chain.c:713-716, SD_F_VPC, snd_maxautovchans=16 default) and 3/5/6/7-channel formats are unprivileged SNDCTL_DSP_CHANNELS choices. Harness: 2810/21604 checks fail baseline; 0 with the fixed generator. In-kernel churn showed no differential anomaly on this no-INVARIANTS kernel (logs in DF-4849 pack).
No comments yet.