DF-4297 / verdict.json
{ "finding_id": "DF-4297", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "REPRODUCED on the stock clean-source guest. kbd_detach (kbd.c:597-603) frees the cdevs genkbd_softc while a /dev/kbdN reader sleeps on it inside genkbdread (tsleep kbdrea, kbd.c:750): the reader is never woken (stuck across detach, proven via ps), and when signalled its EINTR path executes sc->gkb_flags &= ~KB_ASLEEP (kbd.c:757) on the freed chunk - demonstrated byte-exact by re-allocating the freed 536-byte chunk with 0x55 and observing 0x55555555 -> 0x55555554. When the keyboard is subsequently unregistered (holder close, the same sequence a ukbd unplug takes via ukbd.c:1408 kbd_detach then term->kbd_unregister), KBDIO_UNLOADING is delivered to genkbd_event with the stale kb_callback.kc_arg and KNOTE(&sc->gkb_rkq.ki_note,0) (kbd.c:905) walks a knote list inside the freed memory -> Fatal trap 9 general protection fault, Stopped at knote+0x2c; guest dies. Trigger requires an open /dev/kbdN (root-only: 0600 + SYSCAP_RESTRICTEDROOT, kbd.c:672) plus driver detach (module unload or physical USB unplug), hence Low severity memory-corruption/panic, not an unprivileged escalation.", "exploit_chain": "root opens fresh /dev/kbdN (vkbd minted via /dev/vkbdctl clone + KBRELKBD from kbdmux) -> reader blocks in genkbdread tsleep(kbdrea) with gkb_flags=KB_ASLEEP and kb_callback.kc_arg=sc registered by kbd_allocate -> driver detach path kbd_detach(kbd) frees sc with no busy check/wakeup/note purge -> (a) reader stuck on freed pointer; (b) SIGUSR1 wakes reader: kbd still valid so EINTR path write-after-frees bit0 at freed address into re-owned object; (c) closing the vkbd holder -> kbd_unregister -> KBDIO_UNLOADING -> genkbd_event -> KNOTE on freed klist -> fatal trap 9 kernel panic.", "evidence": "run.log/panic.txt: serial console with si_drv1=... gkb_flags=0x00000001, groomed 256 x 536 chunks (first @ <same address>), snap: 0x55555555, check2: 0x55555554, Fatal trap 9 ... Stopped at knote+0x2c. run.fix.log: patched kernel wakes reader (errno=6), chunk stays 0x55555555, no panic.", "kernel_refs": [ "sys/dev/misc/kbd/kbd.c:581-607", "sys/dev/misc/kbd/kbd.c:744-761", "sys/dev/misc/kbd/kbd.c:756-759", "sys/dev/misc/kbd/kbd.c:267-278", "sys/dev/misc/kbd/kbd.c:898-907", "sys/dev/misc/kbd/kbd.c:402-403", "sys/bus/u4b/input/ukbd.c:1408" ], "poc_changes": "Seed concept fully reworked: (1) all kbdN cdevs are KBD_IS_BUSY (syscons owns kbdmux, mux owns atkbd), so a fresh unclaimed keyboard is minted by holding a /dev/vkbdctl clone open (vkbd_dev_open registers + kbd_attach) and released from the mux via KBRELKBD through /dev/ttyv0; (2) the trigger is a KLD that performs exactly the kbd_detach() call ukbd_detach makes, plus groom(0x55)/snap/check2 detection sysctls; (3) SIGUSR1 with no SA_RESTART forces the EINTR stale-write path instead of a read restart; (4) groom pattern 0x55 (bit0 set) because the stale write is an AND-mask that only clears bit0.", "attempts": 4, "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": 55, "guest_dirty": 0, "build_cmd": "cd /root/kbduaf && sh build.sh (cc x3 + bsd.kmod.mk module + kldload vkbd kbdkill.ko)", "run_cmd": "sh /root/kbduaf/run.sh", "code_hash": "sha256 kbdkill.c=967ca959c0f56dce79e28967a947458a7aab91c5dd4db3eb1c1158f911d36d9c kbd_uaf.c=f7e96c7175883f0cad0508dc0016935fe3e364a5e2fb3cbd00ca63c7e339a12c run.sh=632b5018169e05fd4374a2bbaeb0b43accaed2a2cffc01702d8dc8a5f2ed4e61", "notes": "Guest reset to clean-source snapshot after validation. The knote-list-remains-in-freed-memory residual (NULL si_drv1 deref in genkbdkqfilter/genkbdfiltdetach) is DF-1505 (known, not re-reported); DF-4297 covers the read path + stale callback + missing wakeup/purge in kbd_detach itself.", "recommended_fix": "kbd_detach: if KBD_IS_BUSY release the client callback (kbd_release), KNOTE the sc knote list, wakeup(sc), then kfree; genkbdread: re-fetch dev->si_drv1 after every tsleep wakeup and return ENXIO when NULL (see fix.diff).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to guest /usr/src, kernel rebuilt via make nativekernel KERNCONF=X86_64_GENERIC (kernel #1 Sat Sep 19 06:32:43), rebooted, identical run.sh re-executed: reader now wakes at detach with ENXIO (errno=6) instead of sleeping on the freed pointer, the freed-and-groomed chunk stays 0x55555555 (stale write gone), and the holder-close kbd_unregister path no longer panics (guest alive). All three baseline symptoms eliminated.", "fix_evidence": "run.fix.log (patched run: reader gone at step 3, read(2) returned -1 errno=6, gkb_flags now 0x55555555 ... unchanged, still alive (no panic), uname -v shows kernel #1) versus run.log/panic.txt (baseline: stuck kbdrea, 0x55555554, Fatal trap 9 knote+0x2c)." } |