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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5022 Β· 12 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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:

  1. compiles the real, unmodified sys/dev/sound/pcm/feeder_volume.c (Makefile .PATH: /usr/src/sys/dev/sound/pcm β€” the feeder_volume.o linked into the module is byte-compiled from the audit target itself);
  2. contains a verbatim copy of feeder.c's feeder_create() / feeder_destroy() β€” the exact code chn_addfeeder() (feeder.c:236-251) executes. Only substitutions: M_FEEDER (a static MALLOC_DEFINE inside feeder.c, unreferenceable) β†’ module-local M_DF5023; feeder_register() bookkeeping stubbed to capture the FEEDER_DECLARE (feeder_volume.c:306) class pointer at kldload; sndstat_register/unregisterfile stubs; and feeder_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 the FEEDER_DECLARE SYSINIT symbol name).
  3. 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)

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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.