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

midi_unload() TAILQ_FOREACH advances through midi_destroy()-freed entries β€” use-after-free read with groom-controlled pointer consumption

Summary

midi_unload iterates midi_devs with TAILQ_FOREACH while the loop body calls midi_destroy which TAILQ_REMOVEs and kfrees the entry. The loop advance evaluates TAILQ_NEXT on freed memory; with slab reuse (groomable M_MIDI churn under root control) the next iteration lockmgrs an attacker-influenced pointer. Root (kldunload) + hardware (>=1 midi device) gated. Model-verified on the guest (reuse mode reads forged tqe_next 0x414141414140 and faults at exactly 0x414141414150; stash-next fixed mode clean). Stash-successor fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4284 Β· 10 files
FileTypeDescriptionSize
midi_unload_model.c β€” 5.8 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 90 B view raw
build.log β€” 25 B view raw
run.log β€” 1001 B view raw
env.txt β€” 373 B view raw
fix.diff β€” 594 B view raw
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
verdict.json β€” 3.1 KB view raw

DF-4284 β€” midi_unload() iterates midi_devs while midi_destroy() frees each entry β†’ UAF read in the loop advance

What

midi_unload() [sys/dev/sound/midi/midi.c:1469-1477] uses TAILQ_FOREACH(m, &midi_devs, link) whose loop body calls midi_destroy(m, 1) [midi.c:1474]. midi_destroy() removes the entry from midi_devs [midi.c:1429] and frees it [midi.c:1436]. TAILQ_FOREACH advances by evaluating TAILQ_NEXT(m, link) after the body β€” i.e. it reads m->link.tqe_next from freed memory. The value read is whatever now occupies the freed M_MIDI slab slot; with β‰₯2 midi devices the next loop iteration then executes lockmgr(&m->lock, LK_EXCLUSIVE) / m->busy on that pointer.

Who can trigger

midi_unload() runs only from module unload (kldunload of the midi/ sound module) β€” root β€” and only destroys something when at least one MIDI device is registered (again, only snd_emu10kx hardware registers one). Impact ceiling: panic / undefined behavior driven by a groom-controlled pointer during a privileged unload; not an unprivileged primitive. Filed as a genuine lifetime defect (Low).

Reproduce (model harness β€” runs on the guest)

cd findings/poc/DF-4284
./build.sh      # cc -O0 -g -Wall -o midi_unload_model midi_unload_model.c
./run.sh

Expected (see run.log): - reuse β€” freed slot immediately reoccupied by a same-size "attacker" chunk with forged link.tqe_next = 0x414141414140; the loop advance reads it and the next iteration derefs m->unit at 0x414141414150 β†’ SIGSEGV at exactly the attacker-chosen address (exit 99). Kernel equivalent: lockmgr(&m->lock) on an attacker-influenced pointer. - stale β€” plain free(); tqe_next happens to survive on this libc, so the loop walks stale-but-plausible pointers (still a UAF read, value merely uncontrolled). - fixed β€” fix.diff semantics (stash TAILQ_NEXT before destroy): all three devices visited and destroyed cleanly, exit 0.

TAILQ macros in the harness are verbatim from sys/sys/queue.h (kernel build configuration; TRACEBUF/diagnostic bits omitted).

Fix

fix.diff β€” replace TAILQ_FOREACH with an explicit loop that stashes TAILQ_NEXT(m, link) before midi_destroy(m, 1) can free m. (The dead exit1 path that would re-release m->lock after a successful destroy is unreachable β€” midi_destroy() always returns 0 β€” and is left untouched by this minimal diff; noted in VERDICT.md.)

VERDICT.md
↓ download raw

VERDICT β€” DF-4284

Status: reproduced (model harness on the audit guest) / UAF read in midi_unload Impact: dos (root-gated kldunload path; panic / groom-controlled pointer use) Confidence: certain

Root cause (source-certain)

midi_unload() [sys/dev/sound/midi/midi.c:1469-1477]:

TAILQ_FOREACH(m, &midi_devs, link) {
        lockmgr(&m->lock, LK_EXCLUSIVE);
        if (m->busy)
                retval = EBUSY;
        else
                retval = midi_destroy(m, 1);
        if (retval)
                goto exit1;
}

midi_destroy() [midi.c:1418-1438] does TAILQ_REMOVE(&midi_devs, m, link) [1429] and kfree(m, M_MIDI) [1436]. TAILQ_FOREACH expands to for ((var) = TAILQ_FIRST(head); (var); (var) = TAILQ_NEXT(var, field)) β€” the increment m->link.tqe_next is read after the body has freed m. With β‰₯2 registered devices the pointer read from freed memory becomes the loop's next m, and the next body iteration runs lockmgr(&m->lock, LK_EXCLUSIVE) [1470] and touches m->busy [1471] through it. With slab reuse between the free and the increment, the value is attacker-groomable (M_MIDI-sized allocation churn), so the "device" operated on is an attacker-chosen address.

Adjacent latent defect (not fixed by the minimal diff, documented only): the exit1 path [1488-1489] does lockmgr(&m->lock, LK_RELEASE) after midi_destroy() already released and freed m->lock β€” currently dead code because midi_destroy() unconditionally returns 0, but a tripwire if that ever changes.

Reachability

MOD_UNLOAD of the midi module only (midi_modevent [midi.c:1515-1516] ← kldunload sound), i.e. root, and it only does work when at least one MIDI device is registered β€” in-tree only on snd_emu10kx hardware (emu10kx-midi.c:201 β†’ mpu401.c:185 β†’ midi_init). Not triggerable live on the audit guest (no EMU audio, verified: pciconf shows no 1102 device, no /dev/midi* nodes after kldload sound).

Verification performed (this run)

Model harness midi_unload_model on the guest (DragonFly 6.5-DEVELOPMENT, gcc 8.3), TAILQ macros verbatim from sys/sys/queue.h:

  • mode reuse: after each destroy the freed slot is re-groomed by a same-size chunk with forged link.tqe_next = 0x414141414140. The loop advance reads the forged pointer and the next iteration faults at 0x414141414150 (= forged + offsetof(unit)) β†’ SIGSEGV, exit 99. run.log: "[groom] freed slot reoccupied; forged link.tqe_next = 0x414141414140" then "[reuse] signal 11 at 0x414141414150". This is the controlled-UAF-read demonstration: in-kernel the equivalent read feeds lockmgr(&m->lock) on an attacker-influenced address.
  • mode stale: plain free(); this libc preserves tqe_next so the loop happens to walk the stale chain β€” an uncontrolled UAF read (undefined value, real allocator reuse makes it arbitrary).
  • mode fixed (fix.diff semantics β€” stash TAILQ_NEXT before destroy): all three devices visited and destroyed cleanly, exit 0.

Exploit-chain assessment

Requires root (kldunload) already, so no privilege-boundary crossing; worst case is kernel panic / memory-unsafety during a privileged operation with groom-controlled pointers (root β†’ kernel-exec adjacent, but root can load modules anyway). Severity Low; filed for correctness/lifetime hygiene.

Fix validation

Model-level: fixed mode runs the same scenario with the patched loop shape and completes cleanly (run.log "[fixed] clean: all devices destroyed, no UAF"). Kernel-level patched build not exercisable on this guest (needs registered midi devices, i.e. emu10kx hw).

Bottom line

Real use-after-free read (with groom-controlled pointer consumption) in the midi module unload path β€” certain at source level, demonstrated by a verbatim-semantics harness on the guest; root- and hardware-gated, hence Low severity.

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

Validated at harness level: with the stash-next loop shape (fix.diff) all devices are visited and destroyed cleanly (exit 0) while the baseline faults through the freed-slot pointer (exit 99). Kernel-level patched build not exercisable on this guest (no midi devices can register).

run.log: '[fixed] fixed midi_unload(): stash next before destroy' + '[fixed] clean: all devices destroyed, no UAF' exit=0 vs '[reuse] signal 11 at 0x414141414150' exit=99
↓ fix.diffper-fix-DF-4284

Confirmed kernel references

Detail

Exploit chain

root: kldunload sound with >=1 midi device -> midi_unload TAILQ_FOREACH -> midi_destroy(m) kfrees m -> loop advance reads m->link.tqe_next from freed M_MIDI slot (groomable via same-zone kmalloc churn) -> next iteration lockmgr(&m->lock) on attacker-influenced pointer -> panic/arbitrary kernel-memory access during privileged unload

Evidence (decisive lines)

run.log: '[groom] freed slot reoccupied; forged link.tqe_next = 0x414141414140' then '[reuse] signal 11 at 0x414141414150' (exit 99); '[fixed] clean: all devices destroyed, no UAF' (exit 0)

PoC changes

harness written from scratch; iteration 1 missed TAILQ_INIT define (compile fix); iteration 2 used a non-canonical forged pointer (SIGBUS before handler) -> switched to canonical 0x414141414140 and added SIGBUS handling + fflush

Verified recommended fix

stash TAILQ_NEXT(m, link) before midi_destroy(m, 1) in the midi_unload loop (fix.diff)

Verdict

midi_unload() iterates midi_devs with TAILQ_FOREACH whose body calls midi_destroy(), which TAILQ_REMOVEs and kfrees the current entry; the loop advance reads TAILQ_NEXT(m) from freed memory (UAF read) and, with slab reuse between free and advance, operates on a groom-controlled pointer (lockmgr(&m->lock)/m->busy). Demonstrated by a verbatim-TAILQ harness on the guest: reuse mode reads forged tqe_next 0x414141414140 from the freed slot and faults at exactly 0x414141414150; fixed semantics (stash next before destroy) traverse all devices cleanly. Root-gated (kldunload) and hardware-gated (needs >=1 registered midi device, i.e. snd_emu10kx) so no live kernel trigger on this guest.