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)
PoC verification
Evidence pack
findings/poc/DF-4284 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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 forgedlink.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 feedslockmgr(&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
fixedValidated 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
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.
No comments yet.