midi(4) kqueue filter detach removes knote from BOTH klists β unguarded SLIST_REMOVE walks off the foreign list and dereferences NULL β deterministic kernel panic
Summary
midi_kqfilter inserts each knote into exactly one of rkq/wkq, but the shared midi_filter_detach calls knote_remove on BOTH lists. knote_remove is a raw SLIST_REMOVE with no membership guard: for a knote not in the list it walks to NULL and dereferences curelm->sle_next - page fault. Triggered by ANY knote detach: EV_DELETE, kqueue close, or simply process exit after one EV_ADD. /dev/midiN.M is 0666, so any local user on a host with a MIDI device (SB Live!/Audigy via snd_emu10kx) panics the kernel with a 3-syscall program. tty(4) shows the correct per-list idiom. Model-verified on the guest with a line-verbatim harness (SIGSEGV at 0x0 both modes; fix clean); live trigger impossible only because QEMU has no emu10k1. Per-filter-detach fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4283 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| midi_kq_model.c | β | 7.0 KB | view raw | |
| kq_trigger.c | β | 1.7 KB | view raw | |
| build.sh | β | 66 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 202 B | view raw | |
| run.log | β | 1.3 KB | view raw | |
| env.txt | β | 373 B | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| README.md | β | 3.1 KB | β raw | |
| VERDICT.md | β | 5.1 KB | β raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-4283 β midi(4) kqueue filter detach removes knote from BOTH klists β NULL-deref panic
What
midi_kqfilter() (sys/dev/sound/midi/midi.c:901-930) inserts each knote into
exactly ONE of m->rkq.ki_note (EVFILT_READ) or m->wkq.ki_note
(EVFILT_WRITE), but the shared detach callback midi_filter_detach()
(midi.c:932-941) removes the knote from both lists. knote_remove()
(sys/kern/kern_event.c:1902-1909) is a plain SLIST_REMOVE
(sys/sys/queue.h:208-219) with no membership guard: for a knote that is
not in the list it walks curelm = SLIST_NEXT(curelm, ...) until curelm
becomes NULL and dereferences it β page fault on the unmapped low page β
kernel panic. (Empty foreign list: immediate NULL deref; foreign list
holding a different knote: walk to the end, then NULL deref β both modes
demonstrated.)
Correct in-kernel idiom for comparison: sys/kern/tty.c:1308-1314 / 1334-1340 use one detach callback per list.
Trigger (unprivileged, when a MIDI device exists)
/dev/midiN.M is created with mode 0666 (midi.c:364-366). Any local user:
fd = open("/dev/midi0.1", O_RDWR);
kq = kqueue();
EV_SET(&kev, fd, EVFILT_READ, EV_ADD, ...); kevent(kq, &kev, 1, ...);
/* any of: EV_DELETE, close(kq), process exit -> filter detach -> panic */
Detach fires via knote_detach_and_drop() on EV_DELETE
(sys/kern/kern_event.c:1365-1368) and on kqueue teardown
(sys/kern/kern_event.c:706-708). It is enough to run kevent(2) once with
EV_ADD and exit the process.
Hardware gating (why no live kernel trigger on the audit guest)
/dev/midiN.M exists only when some driver calls midi_init(). The only
in-tree caller chain is snd_emu10kx β emu_midi_attach β mpu401_init()
(sys/dev/sound/pci/emu10kx-midi.c:201 β sys/dev/sound/midi/mpu401.c:185 β
midi_init). QEMU cannot emulate an EMU10K1/EMU10K2 PCI audio card, so the
raw-MIDI node cannot appear on the audit guest. On real hardware with a
SoundBlaster Live!/Audigy (driver auto-loads), the panic is deterministic.
Reproduce (model harness β runs on the guest)
cd findings/poc/DF-4283
./build.sh # cc -O0 -g -Wall -o midi_kq_model midi_kq_model.c
# cc -O0 -g -Wall -o kq_trigger kq_trigger.c
./run.sh
Expected (see run.log):
- empty β SIGSEGV at faulting address 0x0, exit 99 (kernel: panic)
- peer β SIGSEGV at faulting address 0x0, exit 99 (kernel: panic)
- fixed β clean removal, exit 0 (fix.diff semantics)
- controlβ clean, exit 0
- kq_trigger /dev/midistat β live kevent add/delete mechanics exercised
on the real guest syscalls, guest stays alive (midistat has no kqfilter,
ENODEV on EV_ADD β proving only the missing MIDI hardware blocks the
live trigger).
The harness copies the queue macros and knote_remove() verbatim from
sys/sys/queue.h:191-219 and sys/kern/kern_event.c:1902-1909 (LWKT pool
token omitted β an SMP exclusivity primitive irrelevant to single-threaded
list semantics) and midi_filter_detach() verbatim from midi.c:932-941.
Fix
fix.diff β split the detach callback per filter (mirrors the tty(4)
idiom). midi_kq_model fixed mode models the patched behavior: clean.
VERDICT β DF-4283
Status: reproduced (model harness on the audit guest) / kernel panic, local DoS Impact: dos β deterministic NULL-pointer-dereference kernel panic Confidence: certain
Root cause (source-certain)
midi_kqfilter()[sys/dev/sound/midi/midi.c:901-930] selects exactly one klist per knote:EVFILT_READ β &m->rkq.ki_note,EVFILT_WRITE β &m->wkq.ki_note, thenknote_insert(klist, kn).- The shared detach callback
midi_filter_detach()[midi.c:932-941] does:c knote_remove(rklist, kn); knote_remove(wklist, kn);i.e. it also removes the knote from the list it was never in. knote_remove()[sys/kern/kern_event.c:1902-1909] callsSLIST_REMOVE, which [sys/sys/queue.h:208-219] walks the list to find the element with no membership check:c struct type *curelm = SLIST_FIRST((head)); while (SLIST_NEXT(curelm, field) != (elm)) curelm = SLIST_NEXT(curelm, field);When the element is absent,curelmwalks off the end (NULL) andSLIST_NEXT(curelm, ...)dereferences NULL.- Detach runs whenever the knote goes away: EV_DELETE
[kern_event.c:1365-1368] and kqueue teardown / process exit
[kern_event.c:706-708] both call
knote_detach_and_drop()βf_detach(kn).
Net: an EV_ADD followed by any detach of a midi fd knote = page fault on the kernel unmapped low page = panic. tty(4) shows the correct per-list idiom [sys/kern/tty.c:1308-1314, 1334-1340].
Reachability
/dev/midiN.Mis created mode 0666 [midi.c:364-366] β no privileges needed once the node exists.- The node exists only when a driver calls
midi_init(); the sole in-tree caller chain issnd_emu10kxβmpu401_init()[emu10kx-midi.c:201 β mpu401.c:185 β midi_init], i.e. SoundBlaster Live!/Audigy PCI audio. Verified on the audit guest:kldload soundsucceeds,/dev/midistat(0666) appears, no/dev/midi*nodes are possible (no EMU PCI device under QEMU). So the live kernel trigger is hardware-gated on this guest but deterministic on any host with an emu10kx card and the standard module autoload.
Verification performed (this run)
- Model harness
midi_kq_model(built + run on the guest, gcc 8.3, DragonFly 6.5-DEVELOPMENT): queue macros copied verbatim from queue.h:191-219,knote_remove()verbatim from kern_event.c:1902-1909 (pool-token omitted),midi_filter_detach()verbatim from midi.c:932-941. - modeempty(foreign list empty): SIGSEGV at address 0x0 β the immediateSLIST_NEXT(NULL)deref. exit 99. - modepeer(foreign list holds a different knote): SIGSEGV at address 0x0 β walk-to-end then NULL deref. exit 99. - modefixed(fix.diff split-detach semantics): clean, exit 0. - modecontrol(membership holds): clean β proves the macro itself is sound when used correctly. See run.log lines: "[empty] SIGSEGV at faulting address 0x0", "[peer] SIGSEGV at faulting address 0x0". - Live syscall control
kq_trigger /dev/midistaton the guest: realkevent(2)EV_ADD/EV_DELETE + kqueue-teardown-with-attached-knote against the only live midi-family node. EV_ADD β ENODEV (midistat has no d_kqfilter), no crash, guest alive β the trigger mechanics are real; only the absent MPU hardware (i.e. the midi fd) blocks the live panic.
Why not a live kernel panic
QEMU has no EMU10K1/EMU10K2 model; snd_emu401/ISA sound does not exist in
this tree (sys/dev/sound/isa absent); no other driver calls midi_init.
The kernel path is therefore unreachable on this guest β a QEMU
limitation, not a property of the bug (any SB Live!/Audigy host panics on
first kqueue use of its midi device by any local user).
Exploit-chain assessment
Primitive is a NULL-dereference read (curelm->sle_next with
curelm == NULL); SLIST_REMOVE never writes through the NULL cursor before
faulting (SLIST_REMOVE_AFTER(curelm,...) runs only after a successful
walk). No controlled write, no info leak β impact ceiling is
denial of service (panic), but it is instant, repeatable, and
unprivileged on affected hardware.
Fix validation
fix.diff splits midi_filter_detach into per-filter callbacks
(mirroring tty.c). Model mode fixed re-runs the identical scenario with
patched semantics: clean removal, no foreign-list walk, exit 0
(run.log "[fixed] clean: knote removed from its owning list"). A
kernel-level patched-kernel build cannot be exercised on this guest for the
same hardware reason as above.
Bottom line
Real, deterministic, unprivileged-kernel-panic bug in the midi(4) kqueue path, certain at source level and demonstrated by a line-faithful harness on the audit guest; live kernel triggering requires emu10kx-class audio hardware (not emulatable in QEMU).
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: identical scenario with the split-detach semantics (fix.diff) removes the knote cleanly with no foreign-list walk (run.log '[fixed] clean', exit 0) vs baseline NULL-deref (exit 99). A patched-kernel build cannot exercise this path on this guest for the same hardware reason the baseline cannot (no emu10kx -> no /dev/midiN.M); fix is the standard tty(4)-style idiom, low regression risk.
run.log lines: '[fixed] calling fixed detach_read()...' / '[fixed] clean: knote removed from its owning list, no foreign-list walk' / exit=0; baseline: '[empty]/[peer] SIGSEGV at faulting address 0x0' exit=99
Confirmed kernel references
Detail
Exploit chain
open(/dev/midiN.M) [0666] -> kqueue -> EV_ADD EVFILT_READ -> EV_DELETE/close/exit -> knote_detach_and_drop -> midi_filter_detach -> knote_remove(foreign klist) -> SLIST_REMOVE walks curelm to NULL -> SLIST_NEXT(NULL) deref -> page fault on unmapped low page -> kernel panic (NULL-deref read only; no write primitive; DoS ceiling)
Evidence (decisive lines)
run.log: '[empty] SIGSEGV at faulting address 0x0' and '[peer] SIGSEGV at faulting address 0x0' (exit 99); '[fixed] clean: knote removed from its owning list' (exit 0); kq_trigger /dev/midistat 'TRIGGER-MECHANICS-OK ... (guest still alive)'; env.txt: no /dev/midi* after kldload sound (hw gating proof)
PoC changes
harness written from scratch (no seed): queue macros + knote_remove copied verbatim from queue.h:191-219 / kern_event.c:1902-1909 (LWKT pool token omitted), midi_filter_detach verbatim from midi.c:932-941; iteration 1 missed SLIST_INSERT_HEAD/SLIST_EMPTY defines (compile fix), iteration 2 run.sh aborted on expected nonzero exit (set -e removed)
Verified recommended fix
split midi_filter_detach into per-filter callbacks that knote_remove() only from the owning klist (fix.diff)
Verdict
midi_filter_detach() removes each knote from BOTH m->rkq and m->wkq while midi_kqfilter() inserted it into exactly one; knote_remove()/SLIST_REMOVE has no membership guard, so the foreign-list removal walks off the end and dereferences NULL -> kernel panic on any knote detach (EV_DELETE, kqueue close, process exit) against a 0666 /dev/midiN.M node. Demonstrated by a line-verbatim userland harness on the audit guest (SIGSEGV at address 0x0 in both empty- and peer-foreign-list modes; fixed semantics clean; real kevent add/delete mechanics exercised live against /dev/midistat as control). Live kernel trigger impossible on this guest only because /dev/midiN.M exists solely on snd_emu10kx hardware (sole in-tree midi_init caller chain, QEMU has no emu10k1 model); on affected hosts the panic is deterministic and unprivileged.
No comments yet.