⬢ DragonFlyBSD Kernel Audit
DF-5308 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5308",
  "status": "reproduced",
  "reproduced": true,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "usb_filter_detach() (sys/bus/u4b/usb_dev.c:1239-1242) returns early when usb_ref_device() fails without removing the knote from the fifos selinfo.ki_note, but the caller (knote_detach_and_drop, kern_event.c:1764) frees the knote immediately afterwards - leaving a dangling entry that the next KNOTE() from usb_fifo_close() (usb_dev.c:792) or usb_fifo_wakeup() (usb_dev.c:1784) walks and dispatches kn->kn_fop->f_event through. Reproduced 5 times as an unprivileged user (maxx) on the with-src guest kernel (usb_dev.c md5-identical to the audited tree): with kn_fop forged to 0x4141414141414141 the kernel panics 3x at filter_event+0x4: testb $0x2,(%rax) (the f_flags load at our chosen non-canonical address); with kn_fop forged to &kprintf-24 the trap moves 2x to filter_event+0x1d: ret (after the indirect call executed) - the trap site follows the forged pointer, proving attacker-controlled dispatch. The freed 136-byte knote is reclaimed with fully controlled bytes via AF_UNIX bind() with sa_len=136 (dup_sockaddr kmallocs user bytes verbatim; DragonFly slab zones are shared per-size across malloc types). Primitive: arbitrary kernel function call with rdi = fully attacker-controlled 136-byte heap block; on this no-SMAP/SMEP/KASLR guest the remaining step to uid=0 is a fake-filterops/ROP chain (documented, not completed this session). The saturation markers (ioctl=ENXIO with 254 blocked readers, ENOTTY after drain) prove the usb_ref_device-failure path fired on every run, including the patched ones.",
  "exploit_chain": "1) open /dev/usb/0.2.1 (ugen interrupt-IN, unpriv) -> cpd+rxfifo; 2) register 96 EVFILT_READ knotes via 96 kqueues (fanout, flag_isselect=1); 3) fill per-CPU knote cache (64) so later drops kfree to the slab; 4) fork 254 readers blocked in read() -> f->refcount saturates at USB_FIFO_REF_MAX(0xFF) (verified: any ioctl = ENXIO); 5) close the 96 kqueues -> kqueue_terminate -> usb_filter_detach -> usb_ref_device fails -> EARLY RETURN, knote freed while still linked in the live fifo klist; 6) reclaim the freed knote slots with forged 136-byte knotes via AF_UNIX bind spray (kn_next=NULL, kn_status=0, kn_kq=0x4343.., kn_fop=TARGET); 7) kill readers (refcount drains), close(fd) -> usb_cdevpriv_dtor -> usb_fifo_close -> KNOTE(&f->selinfo.ki_note,0) -> knote() walks the forged note -> filter_event -> kn->kn_fop->f_event(kn,0) = arbitrary call with rdi=controlled block. Step 8 (uid=0): point kn_fop at a known-address fake struct filterops (no KASLR) whose f_event is a stack-pivot gadget, ROP from the controlled block - mechanical, not completed this session.",
  "evidence": "panic.txt/panic.2.txt/panic.fault.txt: Fatal trap 9 ... Stopped at filter_event+0x4: testb $0x2,(%rax) with rax = forged kn_fop 0x4141414141414141 (3 runs); panic.call.txt/panic.call.1.txt: Stopped at filter_event+0x1d: ret with kn_fop=&kprintf-24 (2 runs, trap moved past the f_flags load exactly as forged); run.log/run.2.log: full choreography with saturation markers (ENXIO while 254 readers blocked, ENOTTY after drain) proving the usb_ref_device failure path; diag.c output in VERDICT.md; fix_validation.md + run.patched.log: identical rig on fix.diff-patched kernel survives cleanly.",
  "kernel_refs": [
    "sys/bus/u4b/usb_dev.c:1239",
    "sys/bus/u4b/usb_dev.c:1242",
    "sys/bus/u4b/usb_dev.c:792",
    "sys/bus/u4b/usb_dev.c:1784",
    "sys/bus/u4b/usb_dev.c:251",
    "sys/bus/u4b/usb_dev.c:622",
    "sys/bus/u4b/usb_dev.c:969",
    "sys/kern/kern_event.c:1764",
    "sys/kern/kern_event.c:1810",
    "sys/kern/kern_event.c:2089",
    "sys/kern/uipc_usrreq.c:1165"
  ],
  "poc_changes": "None needed structurally; tuned for determinism: NKQ raised 8->96 so freed knotes overflow the per-CPU knote cache (KNOTE_CACHE_MAX=64) into the reclaimable slab; spray moved before reader-kill to precede allocator churn; spray fanned over 6 forked children to cover per-CPU free lists; saturation marker changed from ioctl(FIODTYPE) (intercepted by the generic devfs ioctl layer - never reached usb_ioctl and falsely reported not saturated) to an unknown ioctl cmd which returns ENXIO while usb_ref_device fails and ENOTTY once refs drain.",
  "attempts": 5,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline, /usr/src/sys/bus/u4b/usb_dev.c md5 8307905e7ec38912ff7e2883279a187b == audited tree)",
  "runtime_sec": 4200,
  "guest_dirty": 0,
  "build_cmd": "cc -O -Wall -o df5308 df5308.c",
  "run_cmd": "./df5308 fault 2   (call mode: DF5308_FOP=<kprintf-24> ./df5308 call 1)",
  "code_hash": "8d785477cef93bdcc4717a1a106944f051fa6d0d2572b87de8caa4eeab66fff2",
  "notes": "Secondary manifestation observed twice: guest WEDGE without panic text (junk-reclaimed slot -> garbage kn_kq pool-token/lock deadlock) - same root cause, DoS flavor. Two clean survived runs occurred before the NKQ=96/cache-overflow tuning (reclaim variance); after tuning every fault-mode run either crashed or wedged. kprintf-console variant of the call proof did not print (call executed; trap at the post-call ret) - the two distinct trap sites across the two rigs are the control experiment. Guest reset to with-src snapshot after validation.",
  "recommended_fix": "On usb_ref_device() failure in usb_filter_detach(), best-effort unlink the knote: re-derive the fifo under usb_ref_lock and knote_remove() it if the slot is still populated (see fix.diff: usb_filter_detach_unlink); validated by kernel rebuild - identical PoC survives cleanly.",
  "fix_status": "fixed",
  "fix_baseline_reproduced": true,
  "fix_patched_reproduced": false,
  "fix_verdict": "fix.diff (usb_filter_detach_unlink on the error path) applied to guest /usr/src, kernel rebuilt (nativekernel BUILD_RC=0, INSTALL_RC=0, #1: Fri Oct 2 13:50:14 UTC 2026), identical PoC re-run: both attempts complete the full choreography (saturation ENXIO marker present - the usb_ref_device failure path is exercised) and close(fd) returns; guest stays up, serial log clean. Baseline on the same rig panicked 3x and trapped 2x.",
  "fix_evidence": "fix_validation.md, run.patched.log (= fix_run.log), fix_build.log, env.txt (patched kernel uname)",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Fri Oct  2 13:50:14 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64"
}