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

feed_mixer_feed FXROUND past the caller's free space (persistent sndbuf ring overflow) and mcnt = count - rcnt unsigned underflow arming a ~4GiB memset at b+rcnt when any child feeder over-returns

Summary

Entry count rounded past the caller's FREE space (children fill past free; sndbuf_acquire ring overflow, rl>bufsize, getfree wraps) - 500/500 harness checks fail. Worse: each child's return is FXROUNDed; with an over-returning child head (proven in-tree: feeder_format/volume) rcnt>count makes mcnt=count-rcnt underflow and the memset executes at ~4GiB length - caught verbatim ('MEMSET-BOMB: memset(b+65556, 0x00, 4294967284)', exit 42). Vchan mixing default (snd_maxautovchans=16): two /dev/dsp opens hit this. Generator fix + clamp-rcnt hardening.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4849 Β· 16 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
verdict.json β€” 4.6 KB view raw
fxshim.h β€” 6.9 KB view raw
fxsrc.c β€” 2.0 KB view raw
ffmix.c β€” 9.2 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-4849 β€” feeder_mixer.c FXROUND ring overflow + mcnt-underflow memset bomb

What this proves

The REAL, UNMODIFIED sys/dev/sound/pcm/feeder_mixer.c:

  • M1 (:299) count = SND_FXROUND(count, sz) misrounds PAST the caller's free space for bad-residue values inside the claimed domain (first bad 8195 for sz=12). Children fill count > free bytes into b and the mixer RETURNS rcnt > free β€” in-kernel sndbuf_acquire(to, tmpbuf, rcnt) then overflows the ring (buffer.c:615-636, KASSERT-only in production; rl inflates past bufsize, sndbuf_getfree = bufsize - rl wraps unsigned). 500/500 M1 checks fail.

  • M2 (:339-364, :362, :368) when a child feeder returns more than asked (proven possible in-tree by DF-4846/DF-4848: feeder_format/feeder_volume return count+delta on the same misrounds), rcnt = SND_FXROUND(ret, sz) exceeds count and mcnt = count - rcnt underflows to ~0xFFFFfffX, then :368 executes memset(b + rcnt, zerodata, ~4GiB) β€” catastrophic heap overwrite. The harness catches the exact call:

MEMSET-BOMB: memset(b + 65556, 0x00, 4294967284) [mixer count window was 65544]

(exit code 42). This is a vchan-mixing default path: snd_maxautovchans = 16 by default β€” any two /dev/dsp opens mix through feed_mixer_feed.

Build / run (guest)

cd /root/fx && sh build.sh && ./ffmix    # exit=42 (memset bomb), M1 500 fails
cp snd_fxdiv_gen_fixed.h snd_fxdiv_gen.h && cc -O1 -g -o ffmix_fixed ffmix.c fxsrc.c
./ffmix_fixed                           # ALL PASS, no bomb

In-kernel churn artifacts

churn_bad.log / churn_dmesg.txt (ES1370, 6ch+7ch vchan children, bad-residue-paced writes): no differential anomaly vs the aligned control (ctlchurn.c) on this no-INVARIANTS production kernel; the observed "play interrupt timeout, channel dead" occurs equally in the control run (ES1370-without-audiodev artifact, not attributable to the defect).

Fix

fix.diff (shared generator fix). A/B: baseline M1 500 fails + bomb β†’ fixed ALL PASS, no bomb. Defensively, :362 should also clamp (if (rcnt > count) rcnt = count;) so no child misbehavior can ever arm the giant memset.

VERDICT.md
↓ download raw

DF-4849 β€” VERDICT

status: reproduced (feeder level, real unmodified code on the audit guest) primitive: (M1) persistent sndbuf ring overflow β€” mixer returns more bytes than the destination has free; (M2) mcnt unsigned underflow arming a ~4GiB memset at b + rcnt β€” caught verbatim by the harness: MEMSET-BOMB: memset(b + 65556, 0x00, 4294967284) confidence: certain (M1 direct on pristine code; M2 mixer logic proven on pristine code with the child-overshoot precondition demonstrated separately on real feeder_format/feeder_volume code β€” DF-4846/DF-4848)

Root cause

feed_mixer_feed (feeder_mixer.c:279-388):

  1. :299 count = SND_FXROUND(count, sz) β€” the caller (sndbuf_feed, buffer.c:708) passes count = min(free, maxfeed); for bad-residue free values (first bad 8195 for sz 12; 15–40% of values β‰₯ first_bad) the generated table misrounds UP (shared snd_fxdiv_gen.awk defect β€” see DF-4846), so count > free. Children are asked for count and fill it into b; the mixer returns rcnt = count > free. Kernel side: sndbuf_acquire(to, tmpbuf, rcnt) with rcnt > free copies past the free region (ring overwrite of live audio) and inflates b->rl past b->bufsize β€” sndbuf_getfree = bufsize - rl then wraps unsigned (buffer.c:615-636; only KASSERT-protected in production).

  2. :339-364 rcnt = SND_FXROUND(FEEDER_FEED(ch->feeder, ...), sz) β€” the FXROUND of a child's return. If the child returns more than asked (count + delta β€” exactly what DF-4846 proves feeder_format does and DF-4848 proves feeder_volume does on the same misrounds), rcnt can exceed count, and :362 mcnt = count - rcnt underflows (e.g. 65544 βˆ’ 65556 β†’ 0xFFFFFFF4 = 4294967284). :368 then executes memset(b + rcnt, sndbuf_zerodata(...), mcnt) β€” a ~4GiB kernel heap memset 12 bytes past the window base β€” total memory destruction (panic at best).

Reproduction (guest, pristine file)

ffmix (mix.log):

FAIL[M1 ring overflow] count_in(free)=8195 FXROUND=8196 rcnt=8196  (+1 bytes past free)   [500/500]
M2: child0 (feeder_format-style) returns count+8; FXROUND(65544+8, 12) = 65556 > count 65544
MEMSET-BOMB: memset(b + 65556, 0x00, 4294967284) [mixer count window was 65544 => mcnt underflow]

exit code 42. The M2 child stub returns count+8 β€” a delta class the fxfmt/ffvol harnesses produce from REAL feeder code (e.g. feeder_format count=65548 β†’ 65556).

Kernel reachability

  • Vchan mixing is default: snd_maxautovchans = 16 (sound.c:62); every pair of /dev/dsp opens becomes children of one parent mixed by feed_mixer_feed (feeder_mixer.c:314-380). The parent format follows the first child (vchan sync), giving non-pow2 sz (12/14) via unprivileged channel ioctls.
  • M1 needs only: free landing on a bad residue β‰₯ 8195 (user-paced drain) β€” a large fraction of feeds on 8K–64K buffers.
  • M2 needs the additional conjunction of an overshooting child head feeder (DF-4846/DF-4848-proven) and β‰₯ 2 busy children β€” all unprivileged, default-config choices.

In-kernel attempt (honest)

ES1370 + 6ch/7ch vchan children + bad-residue pacing (churn_bad.log, churn_dmesg.txt): the "channel dead" events occur equally in the ALIGNED CONTROL run (ctlchurn.c) β€” an ES1370-without-audiodev emulation artifact, not differential, so no in-kernel anomaly is attributable to the defect on this no-INVARIANTS rig β€” the expected outcome for ring-level overflow (silent) and the memset (would be instantly fatal if its composite conjunction fired; it did not on this rig within the bounded churn).

Fix validation

fix.diff (generator): baseline = M1 500/500 fails + memset bomb (exit 42); fixed = 0 failures, no bomb (mix.log vs mix_fixed.log). fix_status: fixed. Recommended hardening independent of the generator: clamp rcnt to count at :362 (if (rcnt > count) rcnt = count;) so no feeder-contract violation anywhere in the tree can ever arm the giant memset.

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: baseline = 500/500 M1 ring-overflow fails + memset bomb (exit 42); fixed = 0 failures, no bomb (mix.log vs mix_fixed.log). Kernel rebuild A/B not performed (null differential in-kernel baseline on this rig).

mix_fixed.log (0 failures, no bomb); fix.diff; snd_fxdiv_gen_fixed.awk
↓ fix.diffper-fix-DF-4849

Confirmed kernel references

Detail

Exploit chain

unprivileged: two /dev/dsp opens (two vchan children on one default-mixed parent) -> multichannel formats give non-pow2 parent sz (e.g. 6ch S16 = 12) -> user-paced drains land free on bad residues >= 8195 -> persistent ring overflow (audio corruption + getfree wrap); with an overshooting child head feeder (feeder_format/feeder_volume per DF-4846/4848) the mcnt underflow arms a ~4GiB kernel memset 12 bytes past the destination scratch - total kernel memory destruction.

Evidence (decisive lines)

mix.log (M1 500/500 'rcnt=8196 (+1 bytes past free)' + 'MEMSET-BOMB: memset(b + 65556, 0x00, 4294967284)', exit 42); mix_fixed.log (fixed: 0 failures, no bomb); churn_bad.log + churn_dmesg.txt (in-kernel attempt, non-differential); ffmix.c/fxshim.h/fxsrc.c; VERDICT.md

PoC changes

Fresh harness (DF-4846 infrastructure) extended with fake struct pcm_channel/snd_dbuf machinery (children.busy list, CHN_FOREACH/CHN_LOCK/CHN_STOPPED, per-child feed control, sndbuf_* shims) and a memset interceptor defined before including the file under test so the ~4GiB call is recorded (exit 42) instead of executed; M2 child overshoot (+8) is the delta class fxfmt produces from real feeder_format code (count 65548 -> 65556).

Verified recommended fix

Fix snd_fxdiv_gen.awk (domain-exact reciprocals + 64-bit multiply, fix.diff); independently harden :362 with 'if (rcnt > count) rcnt = count;' so no feeder-contract violation can arm the giant memset.

Verdict

Reproduced at feeder level on the audit guest against the real, unmodified sys/dev/sound/pcm/feeder_mixer.c: (M1) :299 count = SND_FXROUND(count, sz) misrounds PAST the caller's free space for bad-residue values inside the claimed domain (first bad 8195 for sz=12; 15-40% of values above first_bad), children fill count > free bytes into b and the mixer returns rcnt > free - in-kernel sndbuf_acquire() (buffer.c:615-636, KASSERT-only) then overflows the ring and inflates rl past bufsize so sndbuf_getfree = bufsize - rl wraps unsigned. 500/500 M1 checks fail. (M2) :339-364 FXROUNDs each child's return; with a child feeder that returns more than asked (proven in-tree on real code by DF-4846 feeder_format and DF-4848 feeder_volume), rcnt > count makes :362 mcnt = count - rcnt UNDERFLOW and :368 executes memset(b + rcnt, zerodata, ~4GiB) - harness caught it verbatim: 'MEMSET-BOMB: memset(b + 65556, 0x00, 4294967284)' (exit 42). Vchan mixing is default (snd_maxautovchans=16, sound.c:62) - any two /dev/dsp opens mix through this path with parent sz set by unprivileged channel ioctls. In-kernel churn (ES1370, 6ch+7ch children, bad-residue pacing) showed no differential anomaly vs the aligned control ('channel dead' occurs in both - ES1370-without-audiodev artifact), so no in-kernel observation on this no-INVARIANTS rig; the memset would be instantly fatal if its composite conjunction fired.