feed_volume_init() publishes f->data before validation completes and frees it on both error branches without clearing it β feeder_create()'s error unwind double-frees the info block
Summary
f->data=info set before default_channel_map/apply_matrix validation; both failure branches kfree without unpublishing, and the framework error path feeder_destroy->FEEDER_FREE kfrees it AGAIN. LIVE-VERIFIED on guest: KLD compiling the REAL unmodified feeder_volume.c + verbatim feeder_create/destroy driven with a 0-channel S16_LE desc panics stock kernel (memory chunk already free); on non-detecting kernels it double-links the slab freelist (object aliasing). No in-tree 0-channel path today (matrix validation) - Low defense-in-depth; FreeBSD head carries the identical defect. Unpublish-before-free fix validated (same EINVAL, no panic).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5022 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5023_harness.c | β | 10.6 KB | view raw | |
| Makefile | β | 485 B | β download | |
| build.sh | β | 149 B | view raw | |
| run.sh | β | 135 B | view raw | |
| build.log | β | 7.4 KB | view raw | |
| run.log | β | 1.1 KB | view raw | |
| panic.txt | β | 1.1 KB | view raw | |
| fix_build.log | β | 7.4 KB | view raw | |
| fix_run.log | β | 665 B | view raw | |
| env.txt | β | 190 B | view raw | |
| fix.diff | β | 558 B | view raw | |
| VERDICT.md | β | 5.6 KB | β raw |
DF-5022 VERDICT β feed_volume_init() double free
Status: REPRODUCED (panic) Β· confidence certain Β· impact panic (kernel memory-corruption class; stock kernel detects the second free and panics β kernels without slab double-free detection get a duplicated freelist entry instead, i.e. aliased live objects).
How it was reproduced
Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026
(X86_64_GENERIC, no sound(4) compiled in β sysctl hw.snd unknown).
Because the stock kernel cannot instantiate the feeder machinery, the PoC
builds a KLD (df5023_dblfree.ko) that:
- compiles the real, unmodified
sys/dev/sound/pcm/feeder_volume.c(Makefile.PATH: /usr/src/sys/dev/sound/pcmβ thefeeder_volume.olinked into the module is byte-compiled from the audit target itself); - contains a verbatim copy of
feeder.c'sfeeder_create()/feeder_destroy()β the exact codechn_addfeeder()(feeder.c:236-251) executes. Only substitutions:M_FEEDER(a static MALLOC_DEFINE inside feeder.c, unreferenceable) β module-localM_DF5023;feeder_register()bookkeeping stubbed to capture theFEEDER_DECLARE(feeder_volume.c:306) class pointer at kldload;sndstat_register/unregisterfilestubs; andfeeder_matrix_default_channel_map()+ its two static tables copied verbatim from feeder_matrix.c:84-101,573-582 (compiling all of feeder_matrix.c would collide on theFEEDER_DECLARESYSINIT symbol name). - runs a control first (valid S16_LE/2ch β init OK, clean destroy,
distinct probe allocations), then the trigger:
desc.in = AFMT_S16_LE(channel field == 0).
Decisive evidence
panic.txt / run.log (serial console of the baseline run):
df5023: control OK: feeder_volume init'd on 0x00200010 (f->data=0xfffff8004f164338), destroying cleanly df5023: control allocs: 0xfffff8004f164338 != 0xfffff8004f164370 (expected distinct) df5023: TRIGGER: desc.in=0x00000010 AFMT_CHANNEL=0 feeder_init(0xfffff8008d9fc750) on feeder_volume returned 22 panic: memory chunk 0xfffff8004f164370 is already free! chunk_mark_free() at chunk_mark_free+0xae _kfree() at _kfree+0x262 feed_volume_free() at feed_volume_free+0x21 β feeder_volume.c:190 kfree(f->data) feeder_create() at feeder_create+0xf8 β verbatim feeder.c:213 feeder_destroy() df5023_modevent() at df5023_modevent+0x1f1
The control proves the harness is faithful (valid path inits and destroys
cleanly, allocations behave). The trigger proves the second free: the chunk
(0xfffff8004f164370 β the second control allocation, i.e. the same 64-byte
slab zone info lives in) is already on the freelist when
feed_volume_free() frees the dangling f->data.
Root cause (path:line)
sys/dev/sound/pcm/feeder_volume.c:165βf->data = info;before validation completessys/dev/sound/pcm/feeder_volume.c:167-170βm == NULLβkfree(info), return EINVAL, f->data danglingsys/dev/sound/pcm/feeder_volume.c:173-175βapply_matrixfailure β same shape (unreachable today: the default map for channels 1..8 always passes apply_matrix's own validation)sys/dev/sound/pcm/feeder.c:210-216β init failure βfeeder_destroy(f)sys/dev/sound/pcm/feeder.c:178-182βFEEDER_FREE(f)βfeed_volume_free()β secondkfreesys/dev/sound/pcm/feeder_matrix.c:577-579βdefault_channel_map(0)returns NULL (0 < 1)
Reachability honesty (why severity Low, not High)
The 0-channel precondition cannot be produced by an unprivileged user through
in-tree paths today: every format reaching the volume feeder is
SND_FORMAT(..., m->channels >= 1, ...) built through
feeder_matrix_format_map() which has no 0-channel entry
(feeder_chain.c:654-667, channel.c:668). The defect detonates if a
driver-supplied matrix (CHANNEL_GETMATRIX / chn_setmatrix) or a future
chain-builder regression ever presents a 0-channel format to feeder_volume.
FreeBSD head (checked 2026-09) carries the identical code shape β defect is
upstream-inherited, unfixed there too.
So: reproduced=1, impact=panic, but the finding is filed Low because no unprivileged trigger exists in-tree; this is a live memory-corruption landmine one validation regression away, not an escalation path today.
Exploit chain
None developed β precondition is kernel-internal (0-channel feeder format); on this guest the stock slab allocator detects the double free and panics (deterministic DoS). On builds without the check, the duplicated freelist entry aliases two live kernel objects of the 64-byte class β an exploitation primitive (cross-object type confusion) if a trigger is ever added.
Fix validation
fix.diff: NULL out f->data before both kfree(info) in
feed_volume_init() (feeder_volume.c:167-170, 173-175).
- baseline: panic (above).
- patched (
patch -p1 -d /usr/src < fix.diff, module rebuilt from the patched tree β same source file, same cc 8.3 flags as the kernel would use; a full kernel rebuild is impossible on this guest because X86_64_GENERIC has no sound(4)):kldload rc=0, control OK, trigger still returns EINVAL ("feeder_init returned 22") without any panic, guest healthy (fix_run.log). fix_status=fixed (module-level validation, documented).
Guest left clean: module unloaded, /usr/src restored from backup, final
vm.sh reset with-src.
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
fixedfix.diff (f->data = NULL before both kfrees) applied to the guest's /usr/src copy; module rebuilt from the patched file with identical compiler flags; the exact trigger that panicked the baseline (0-channel FEEDER_VOLUME desc) now returns EINVAL cleanly (feeder_init returned 22) with kldload rc=0 and no panic - double free eliminated. Module-level validation: the stock X86_64_GENERIC kernel does not compile sound(4), so a full patched-kernel build was not possible on this guest; the object under test is the same feeder_volume.c source compiled with the kernel's own flags.
fix_run.log (patched dmesg: control OK, trigger returned 22, module loaded, no panic); fix_build.log (clean patched build); baseline panic.txt for contrast
Confirmed kernel references
- sys/dev/sound/pcm/feeder_volume.c:165
- sys/dev/sound/pcm/feeder_volume.c:167
- sys/dev/sound/pcm/feeder_volume.c:173
- sys/dev/sound/pcm/feeder_volume.c:190
- sys/dev/sound/pcm/feeder.c:210
- sys/dev/sound/pcm/feeder.c:213
- sys/dev/sound/pcm/feeder.c:178
- sys/dev/sound/pcm/feeder_matrix.c:577
- sys/dev/sound/pcm/feeder_chain.c:654
Detail
Evidence (decisive lines)
panic.txt / run.log: control init+destroy OK then TRIGGER desc.in=0x00000010 AFMT_CHANNEL=0 -> 'feeder_init returned 22' -> 'panic: memory chunk 0xfffff8004f164370 is already free!' with _kfree <- feed_volume_free <- feeder_create stack; fix_run.log: patched tree -> kldload rc=0, same EINVAL, no panic; VERDICT.md for the full narrative
PoC changes
No seed existed. Wrote the KLD harness from scratch: compiles the real /usr/src/sys/dev/sound/pcm/feeder_volume.c; verbatim feeder.c:184-219 feeder_create() and feeder.c:178-182 feeder_destroy() (M_FEEDER -> module-local M_DF5023 since M_FEEDER is a static MALLOC_DEFINE); feeder_register() stub capturing the FEEDER_DECLARE class; verbatim feeder_matrix_default_channel_map + tables from feeder_matrix.c:84-101,573-582; sndstat_register/unregisterfile stubs; probe size 56 == sizeof(struct feed_volume_info); control run before trigger; fixed a first link failure (missing sndstat symbols) and missing generated headers (feeder_if/device_if/bus_if/channel_if, snd_fxdiv_gen.h)
Verified recommended fix
Set f->data = NULL before kfree(info) on both error paths of feed_volume_init() (feeder_volume.c:167-170, 173-175)
Verdict
feed_volume_init() publishes f->data=info (feeder_volume.c:165) before validation completes; its two error branches kfree(info) without clearing f->data, and the framework's feeder_create() error path calls feeder_destroy() -> FEEDER_FREE() -> feed_volume_free() which kfrees the dangling pointer a second time. Reproduced on the QEMU guest with a KLD that compiles the real unmodified feeder_volume.c and a verbatim copy of feeder.c's feeder_create/feeder_destroy: a FEEDER_VOLUME desc with a 0-channel S16_LE format panics the stock 6.5-DEVELOPMENT kernel ('panic: memory chunk ... is already free!', stack feed_volume_free <- feeder_create). In-tree reachability is currently blocked (all feeder formats are SND_FORMAT(..., ch>=1, ...)), so the finding is filed Low: one validation regression away from kernel memory corruption; FreeBSD head carries the identical defect.
No comments yet.