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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4848 Β· 16 files
FileTypeDescriptionSize
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 -= j at :286 underflows to ~4GiB β‡’ the loop re-arms and asks the source for a wild window at dst+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.

VERDICT.md
↓ download raw

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:

  • :282 apply(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-286 j *= 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 writes min(4GiB, ready) bytes at dst+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->tmpbuf or 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 = 16 default, 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 ready at 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

fixed
baseline reproduced→ patch + rebuild →patched clean

A/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
↓ fix.diffper-fix-DF-4848

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).