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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4952 Β· 17 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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);
  1. The feeder directly below the REC mixer (feed_matrix here β€” or feed_format; both proven over-returners on real code, DF-4847/DF-4846) returns count+delta for bad-residue requests (entry SND_FXROUND(count, oalign) misround, in-tree generator: FXROUND(16390,12)=16392). count was min(bufsize, maxfeed) ≀ free, so sndbuf_acquire(count+delta) with free == bufsize inflates b->rl past b->bufsize β€” buffer.c:615-635 only KASSERTs, compiled out on the production guest (X86_64_GENERIC, no INVARIANTS).
  2. sndbuf_getfree(b) = bufsize - rl (buffer.c:519-525) now wraps to ~0xFFFFxxxx.
  3. min(cnt, maxfeed) == maxfeed (~65532/65544) is then requested into b->tmpbuf, an allocation of only round_page(bufsize) bytes (buffer.c:180-195). The PLAY caller (chn_wrfeed, channel.c:425-427) clamps its request by sndbuf_getsize(); feed_mixer_rec has no such clamp β€” the loop trusts the wrapped free.
  4. The converter (real feed_matrix_feed) computes src = dst + count - j (~44KB past tmpbuf β€” the root stub, emulating feed_root/sndbuf_dispose, writes where told) and writes the whole converted block at dst: 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-issues maxfeed requests 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/dsp open for record on a device with capture caps becomes a vchan child of a REC parent mixed through feed_mixer_rec (feeder_chain.c:832-833).
  • The first capture child sets the parent format (vchan_sync), e.g. S16_LE 6ch β†’ parent sz/align 12 while the hardware side is 2ch β€” the feed_matrix conversion below the mixer with oalign 12 (non-pow2 β†’ misround windows).
  • bufsoft sizes 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 exactly count (zeropads), REC returns min(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_FXDIV of the child return advancing dst past what was written); fixed by the same generator fix (A/B validated in the DF-4710 pack).
  • feed_rate: structurally cannot over-return β€” returns count - left with left -= 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Three-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
↓ fix.diffper-fix-DF-4952

Confirmed kernel references

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.