⬢ DragonFlyBSD Kernel Audit
DF-4283 / verdict.json
← back to finding ↓ download raw
{
  "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"
}