DF-4283 / verdict.json
{ "finding_id": "DF-4283", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "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.", "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": "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)", "kernel_refs": [ "sys/dev/sound/midi/midi.c:932-941", "sys/dev/sound/midi/midi.c:901-930", "sys/dev/sound/midi/midi.c:364-366", "sys/kern/kern_event.c:1902-1909", "sys/kern/kern_event.c:706-708", "sys/sys/queue.h:208-219" ], "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)", "attempts": 3, "guest_uname": "DragonFly 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", "runtime_sec": 150, "guest_dirty": 1, "build_cmd": "sh 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_cmd": "sh run.sh", "code_hash": "967ea2e3fbdb11cf866afd7ecbd907eac3efe586bf44c279cdaebd6b97ea640c", "notes": "Model-level reproduction: the harness runs on the guest and executes the exact kernel macro sequence, but the in-kernel panic itself cannot fire there (no MPU-capable audio hardware: /dev/midiN.M only via snd_emu401/emu10kx -> mpu401_init -> midi_init; verified no EMU PCI device and no /dev/midi* nodes). tty.c:1308-1340 shows the correct per-list detach idiom. Fix mirrors that idiom; validated in harness fixed mode.", "recommended_fix": "split midi_filter_detach into per-filter callbacks that knote_remove() only from the owning klist (fix.diff)", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Validated 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.", "fix_evidence": "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" } |