feed_mixer_rec: no free-space clamp β one feeder over-return wraps sndbuf_getfree to ~4G and the acquire loop re-arms maxfeed (~64KB) requests into the fixed-size bufsoft->tmpbuf: linear heap OOB write by the real converter (REC-path sink of the FXROUND family, distinct from DF-4849's PLAY path)
Summary
The REC dispatch never clamps against free (unlike the PLAY caller): one over-return from the converter below (matrix/format on multichannel capture - live in-tree via the generator defect) inflates rl past bufsize (KASSERT-only), getfree wraps to ~4G, and the loop issues maxfeed-sized requests into a round_page(bufsize) tmpbuf - measured +45,064 bytes past a 20,480-byte window (44,367 corrupted bytes), repeated every pass. Proven on real unmodified feeder_mixer+feeder_matrix (exit 42); fix validated 3 ways (generator fix kills the arming precondition; the REC clamp kills the sink independently). Unpriv /dev/dsp capture; REC-side sibling of DF-4849. Post-generator-fix mcnt underflow confirmed unreachable (root returns exactly count/min(count,ready); no other over-return source exists).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4952 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ffrec.c | β | 11.4 KB | view raw | |
| fxshim.h | β | 6.9 KB | view raw | |
| fxsrc.c | β | 2.0 KB | view raw | |
| build.sh | β | 85 B | view raw | |
| run.sh | β | 220 B | view raw | |
| snd_fxdiv_gen_fixed.awk | β | 4.0 KB | β download | |
| fix.diff | β | 899 B | view raw | |
| build.log | β | 2.4 KB | view raw | |
| run.log | β | 867 B | view raw | |
| baseline.log | β | 859 B | view raw | |
| fixed.log | β | 286 B | view raw | |
| fixedmixer.log | β | 682 B | view raw | |
| ab_all.log | β | 2.1 KB | view raw | |
| env.txt | β | 157 B | view raw | |
| VERDICT.md | β | 7.4 KB | β raw | |
| verdict.json | β | 6.1 KB | view raw | |
| feeder_mixer_clamped.c | β | 10.8 KB | view raw |
DF-4952 β VERDICT
status: reproduced (feeder level, real unmodified feeder_mixer.c +
feeder_matrix.c + real generated snd_fxdiv_gen.h on the audit guest,
md5-verified against the audit tree)
primitive: linear heap OOB write past bufsoft->tmpbuf β continuous
maxfeed-sized (~64KB) writes from the tmpbuf base while the window is
only round_page(bufsize) (~20KB); rear-guard smash measured to offset
45064 with 44367 corrupted bytes, repeated every loop pass.
confidence: certain
Root cause β a NEW sink, distinct from DF-4849 (which is :299-368, the PLAY path)
feed_mixer_rec (sys/dev/sound/pcm/feeder_mixer.c:177-277), the REC
dispatch of the same feeder DF-4849 filed for the PLAY path:
:194 cnt = sndbuf_getsize(b);
:195 maxfeed = SND_FXROUND(SND_FXDIV_MAX, sndbuf_getalign(b)); /* ~64KB */
:197 do {
:198 cnt = FEEDER_FEED(c->feeder->source, c, b->tmpbuf,
:199 min(cnt, maxfeed), c->bufhard);
:200 if (cnt != 0) {
:201 sndbuf_acquire(b, b->tmpbuf, cnt); /* KASSERT-only guard */
:202 cnt = sndbuf_getfree(b);
:203 }
:204 } while (cnt != 0);
- The feeder directly below the REC mixer (
feed_matrixhere β orfeed_format; both proven over-returners on real code, DF-4847/DF-4846) returnscount+deltafor bad-residue requests (entrySND_FXROUND(count, oalign)misround, in-tree generator:FXROUND(16390,12)=16392).countwasmin(bufsize, maxfeed) β€ free, sosndbuf_acquire(count+delta)withfree == bufsizeinflatesb->rlpastb->bufsizeβbuffer.c:615-635only KASSERTs, compiled out on the production guest (X86_64_GENERIC, no INVARIANTS). sndbuf_getfree(b) = bufsize - rl(buffer.c:519-525) now wraps to ~0xFFFFxxxx.min(cnt, maxfeed) == maxfeed(~65532/65544) is then requested intob->tmpbuf, an allocation of onlyround_page(bufsize)bytes (buffer.c:180-195). The PLAY caller (chn_wrfeed, channel.c:425-427) clamps its request bysndbuf_getsize();feed_mixer_rechas no such clamp β the loop trusts the wrapped free.- The converter (real
feed_matrix_feed) computessrc = dst + count - j(~44KB past tmpbuf β the root stub, emulatingfeed_root/sndbuf_dispose, writes where told) and writes the whole converted block atdst: 65544 bytes from the tmpbuf base into a 20480-byte window β 45064 bytes past the end of the heap allocation, with format-converted capture content. The wrapped free never drains, so the loop re-issuesmaxfeedrequests every pass until the hardware side dries up.
Distinctness from DF-4849: different function (REC vs PLAY), different
sink (linear heap OOB write via maxfeed re-request vs ring-wrap +
mcnt memset bomb), different caller-clamp gap. Same family ROOT (the
snd_fxdiv_gen.awk reciprocal defect, DF-4846-4849) β the generator fix
clears it, but the filed DF-4849 hardening (:362 clamp) does NOT cover
this sink; this finding's fix.diff clamps the REC acquire (and also
carries the :362 clamp).
Reproduction (guest, pristine files, in-tree generator)
./ffrec β exit 42:
bufsoft: bufsize=16390 (bad residue: FXROUND(16390,12)=16392 > 16390) tmpbuf allocsize=20480 maxfeed=65544 ACQUIRE-OVER: count=16392 free=16390 (rl=0 size=16390) -- KASSERT compiled out, rl -> 16392 REAR-GUARD SMASHED: window=20480 first_oob=0 farthest=45064 smashed_bytes=44367
(root max write offset prints a wrapped sentinel β it includes writes to
the matrix's own reservoir kmalloc which sits below the window base;
cosmetic harness noise, not part of the proof. The decisive metric is the
rear-guard smash of the tmpbuf window.)
Kernel reachability (unprivileged)
- REC vchan parents are default:
snd_maxautovchans = 16(sound.c:62) covers capture; any/dev/dspopen for record on a device with capture caps becomes a vchan child of a REC parent mixed throughfeed_mixer_rec(feeder_chain.c:832-833). - The first capture child sets the parent format (
vchan_sync), e.g. S16_LE 6ch β parentsz/align 12 while the hardware side is 2ch β thefeed_matrixconversion below the mixer withoalign 12(non-pow2 β misround windows). bufsoftsizes are latency-profile derived (chn_setlatencyβsndbuf_resize(blkcnt, blksz)) β non-pow2 values like 16390 are formable (blksz 8195 Γ blkcnt 2); landing the FIRST request on a bad residue is the same probability class DF-4846-4849 measured (15-40% of values β₯ first_bad β 8195).- Content: format-CONVERTED capture data (up-mixed 2chβ6ch), attacker influenced through whatever the ADC/backend records; size is attacker-paced (drain rate).
Fix validation (three-way, on the guest)
| build | exit | tmpbuf rear guard | rl > bufsize |
|---|---|---|---|
in-tree generator, unpatched mixer (./ffrec) |
42 | SMASHED to +45064 | yes (16392 > 16390) |
in-tree generator + fix.diff clamp (./ffrec_fixedmixer) |
0 | clean | no (acquire clamped) |
fixed generator, unpatched mixer (snd_fxdiv_gen_fixed.awk) |
0 | clean | precondition gone |
fix.diff (this pack) clamps the REC acquire (feeder_mixer.c:200-203)
so b->rl can never pass b->bufsize β kills the sink independently of
the generator fix β and also carries the DF-4849-recommended :362
rcnt > count clamp so no feeder-contract violation anywhere in the tree
can arm the PLAY-path mcnt memset. fix_status: fixed
(baseline_reproduced=1, patched_reproduced=0).
The pass-2 follow-up question (orchestrator): post-generator-fix, is DF-4849's mcnt underflow reachable via any OTHER over-return source?
No. Exhaustive walk of every feeder that can sit at a vchan child chain head (or below the REC mixer), all against the fixed generator:
feed_root(feeder.c:456-508): PLAY returns exactlycount(zeropads), REC returnsmin(count, ready)β never over-returns, generator-independent.feed_format(DF-4846),feed_matrix(DF-4847),feed_volume(DF-4848): over-return was solely the entry/exit FXROUND misround; fixed-generator A/B in the family packs = 0 failures across all (count, align) combos.feed_eq(DF-4710): same generator root (SND_FXDIVof the child return advancingdstpast what was written); fixed by the same generator fix (A/B validated in the DF-4710 pack).feed_rate: structurally cannot over-return β returnscount - leftwithleft -= feed,feed β€ min(maxfeed, left)(feeder_rate.c:1669-1688); pass-2 negative evidence soaked it live.- bypass paths in volume/eq/matrix (
return FEEDER_FEED(f->source, ...)) are bounded by the chain below them, inductively root.
With every feeder contract-abiding, rcnt = SND_FXROUND(ret, sz) β€ count
always, so mcnt = count - rcnt cannot underflow and :368's memset
stays in-window. The :362 clamp in this pack's fix.diff is pure
defense-in-depth against future contract violations. Likewise
post-fix, feed_mixer_rec cannot wrap (this pack's fixed-generator run
finds zero bad-residue buf sizes in [8194..65534] β the arming
precondition does not exist).
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
fixedThree-way A/B on the guest: in-tree generator + unpatched mixer = exit 42 with 45064-byte rear-guard smash; in-tree generator + fix.diff clamp = exit 0 clean (acquire-over count=0); fixed generator + unpatched mixer = exit 0. The mixer-local clamp kills this sink even while the generator defect is still in the tree. Kernel rebuild A/B not performed (harness-level standard, consistent with the DF-4846..4849 family; the no-INVARIANTS guest cannot produce an in-kernel differential for this rig - documented limitation).
fixedmixer.log (in-tree generator + clamped mixer: exit 0, clean guards); baseline.log (unpatched: exit 42 + smash); fixed.log (fixed generator: exit 0); fix.diff
Confirmed kernel references
- sys/dev/sound/pcm/feeder_mixer.c:194-204
- sys/dev/sound/pcm/feeder_mixer.c:198-199
- sys/dev/sound/pcm/feeder_mixer.c:201
- sys/dev/sound/pcm/buffer.c:180-195
- sys/dev/sound/pcm/buffer.c:519-525
- sys/dev/sound/pcm/buffer.c:615-635
- sys/dev/sound/pcm/channel.c:425-430
- sys/dev/sound/pcm/feeder_chain.c:832-833
- sys/dev/sound/pcm/sound.c:62
- sys/dev/sound/pcm/feeder_matrix.c:479-509
Detail
Exploit chain
unprivileged: open /dev/dsp for capture on a vchan-capable device -> REC vchan parent formats to first child's fmt (e.g. S16_LE 6ch, align 12) while hw side is 2ch -> feed_matrix below the mixer over-returns on a bad-residue first request (bufsize ~16390) -> rl > bufsize, getfree wraps to ~4G -> loop issues maxfeed (~64KB) requests into round_page(bufsize) tmpbuf -> linear heap OOB write of ~45KB past a ~20KB heap object with up-mixed capture content, repeated per pass - kernel heap corruption (would be fatal/corrupting in-kernel; no in-kernel differential attempted on this rig for the same ES1370-without-backend limitation documented in DF-4849).
Evidence (decisive lines)
baseline.log / run.log (exit 42: 'ACQUIRE-OVER: count=16392 free=16390 ... rl -> 16392' + 'REAR-GUARD SMASHED: window=20480 farthest=45064 smashed_bytes=44367'); fixed.log (fixed generator: exit 0, 'no bad-residue bufsize exists'); fixedmixer.log (in-tree generator + fix.diff clamp: exit 0, acquire-over count=0); ab_all.log (three-way); VERDICT.md; fix.diff
PoC changes
New REC-path harness on the DF-4846..4849 fxshim infrastructure: faithful sndbuf_acquire/getfree/getready (buffer.c semantics WITHOUT KASSERTs, matching the no-INVARIANTS production kernel), ring buffer + kernel-sized round_page tmpbuf window with guards, real feed_matrix as c->feeder->source below the real mixer, only feed_root stubbed (bounded hw budget, writes where told - exactly what real feed_root/sndbuf_dispose does); bad-residue bufsize auto-scan picks a kernel-formable blkcnt*blksz value inside the generator misround window for divisor 12.
Verified recommended fix
Clamp the REC acquire in feed_mixer_rec (cnt = min(cnt, sndbuf_getfree(b)) before sndbuf_acquire) so b->rl can never pass b->bufsize; independently clamp rcnt<=count at :362 (carried in fix.diff); root fix remains the snd_fxdiv_gen.awk generator (DF-4846 family).
Verdict
Reproduced at feeder level on the audit guest against real, unmodified sys/dev/sound/pcm/feeder_mixer.c + feeder_matrix.c + the real generated snd_fxdiv_gen.h (md5-verified vs the tree): feed_mixer_rec (:194-204) has no getsize/free clamp (unlike the PLAY caller chn_wrfeed channel.c:425-427), so when the feeder directly below the REC mixer over-returns (in-tree generator FXROUND misround, proven class DF-4846/DF-4847: FXROUND(16390,12)=16392), sndbuf_acquire(count+delta) with free==bufsize inflates b->rl past b->bufsize (buffer.c:615-635 KASSERT-only, compiled out on the production guest), sndbuf_getfree() wraps to ~4G, and the loop's min(cnt, maxfeed) passes maxfeed (~65544) requests into b->tmpbuf, an allocation of only round_page(bufsize) (~20KB): the real feed_matrix code then writes 65544 bytes from the tmpbuf base - rear-guard smash measured to +45064 bytes past the 20480-byte window, 44367 corrupted bytes, repeated every pass while the wrapped free never drains. Harness exit 42. REC vchan parents are default-config (snd_maxautovchans=16), reachable by an unprivileged user opening /dev/dsp for capture with a multichannel format; content is attacker-paced format-converted capture data. Distinct sink from DF-4849 (which filed the PLAY path :299-368 ring-wrap + mcnt memset bomb); same generator family root. Three-way fix validation: (in-tree generator, unpatched)=exit 42 + smash; (in-tree generator, fix.diff clamp)=exit 0 clean; (fixed generator, unpatched)=exit 0 - the clamp kills the sink independently of the generator fix.
No comments yet.