# 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)

* `sys/dev/sound/pcm/feeder_volume.c:165` — `f->data = info;` before validation completes
* `sys/dev/sound/pcm/feeder_volume.c:167-170` — `m == NULL` → `kfree(info)`, return EINVAL, **f->data dangling**
* `sys/dev/sound/pcm/feeder_volume.c:173-175` — `apply_matrix` failure → 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()` → second `kfree`
* `sys/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.
