⬢ DragonFlyBSD Kernel Audit
DF-4454 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-4454",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "dos",
  "confidence": "certain",
  "verdict": "Reproduced on the QEMU guest (DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, ukbd on UHCI). Driver teardown (ukbd_detach -> evdev_free) frees struct evdev_dev while fds are open and a write(2) is in flight; the in-flight write kept injecting through the dangling pointer (full 64 MB write returned success), and after the freed block was re-allocated by a re-attached keyboard, magic-tagged events written to the DELETED devices fd appeared on the NEW devices reader (19/5/2 hits across rounds) - a demonstrated cross-object kernel write. The 6-cycle detach/attach churn hard-wedged the guest (ssh dead, serial silent, vCPUs spinning at 394% CPU). Trigger requires a root-held fd (evdev_open enforces SYSCAP_RESTRICTEDROOT; nodes are root:input) plus device removal (physical unplug / kldunload), so this is a root/physical-triggered kernel UAF with corruption+DoS impact, not an unpriv->root chain. Fix.diff v3 (ev_refs refcounting + evdev_master_lock serialization of si_drv1 publication/teardown + per-event revoked re-check in evdev_write + unref-before-kfree in the dtor) validated on a rebuilt kernel: writes abort with ENODEV/EBADF at revoke, zero cross-injection across 4 rounds and the identical churn stress, clean stale-fd closes, keystroke delivery unregressed.",
  "exploit_chain": "root fd on /dev/input/event4 (or physical unplug while Xorg holds it) -> ukbd_detach -> evdev_unregister (clients revoked, not drained) -> evdev_free kfrees evdev_dev -> (a) in-flight write(2) continues evdev_inject_event() on freed memory for the whole buffer; (b) re-attach re-allocates the same M_EVDEV block -> stale writer injects attacker-chosen {type,code,value} events into the new device (demonstrated with value=0x1337) and races lockinit on the block -> lockmgr state corruption -> kernel livelock/wedge (demonstrated); (c) close(2) of stale fd -> evdev_dtor EVDEV_LOCK on freed memory. No unprivileged entry path on default config.",
  "evidence": "run.log (assembled guest outputs: UAF-CONFIRMED ... magic_hits=19/5/2, writer_bytes=67108848 writer_err=0 through a deleted device); wedge_serial_tail.txt + serial-full.log (baseline 6-cycle churn wedge); fix_run.log + fix_iter1_panic.txt (patched-kernel validation incl. two documented buggy fix iterations); evdev_uaf.c/build.sh/run.sh (reproduce); fix.diff (validated fix); env.txt",
  "kernel_refs": [
    "sys/dev/misc/evdev/evdev.c:113",
    "sys/dev/misc/evdev/evdev.c:366",
    "sys/dev/misc/evdev/cdev.c:110",
    "sys/dev/misc/evdev/cdev.c:160",
    "sys/dev/misc/evdev/cdev.c:233",
    "sys/dev/misc/evdev/cdev.c:98",
    "sys/bus/u4b/input/ukbd.c:1418",
    "sys/bus/u4b/input/ums.c:776",
    "sys/dev/misc/kbdmux/kbdmux.c:464",
    "sys/dev/misc/kbd/atkbd.c:565",
    "sys/dev/misc/psm/psm.c:2112"
  ],
  "poc_changes": "Fixed the seed PoC concept into a runnable program: rounded the write buffer down to whole input_events (24 B) or the kernel rejects with EINVAL at cdev.c:252; added an event stream that survives sparse filtering ([KEY_Q down/up][SYN_REPORT MAGIC] pairs); final version opens the reader with O_NONBLOCK (baseline runs used a blocking reader since events were flowing); added writer errno/byte accounting and magic detection. Host-side orchestration (device_del/device_add racing the write) driven via the QEMU monitor socket since the guest cannot hot-remove its own USB device.",
  "attempts": 12,
  "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": 11400,
  "guest_dirty": 0,
  "build_cmd": "cc -O2 -Wall -o /root/evdev_uaf /root/evdev_uaf.c -lpthread",
  "run_cmd": "run.sh: /root/evdev_uaf /dev/input/event4 64 & ; QEMU monitor: device_del kbd1; sleep 0.08; device_add usb-kbd,bus=uhci.0,id=kbd1",
  "code_hash": "03cf67a9ae07751438efc86540137554a185f060878f51d7de3a9282c40f3c68",
  "notes": "Known DF-1631 (client leak on evdev_open error paths) NOT re-reported; the fixs open-failure path drops its new reference but still leaks the client struct itself - belongs to DF-1631. Baseline wedge self-recovered after minutes; classify impact dos with corruption primitive documented. Second fix iteration lesson recorded: gcc sinks client->ec_evdev loads past kfree() (UB) - the validated dtor unrefs before freeing the client. Fixed guest was reset to the clean with-src snapshot afterwards.",
  "recommended_fix": "Reference-count evdev_dev (alloc/cdev/per-open refs, final kfree deferred to last unref), serialize si_drv1 publish/teardown behind a global evdev_master_lock so evdev_open cannot lock freed memory, and re-check client->ec_revoked every event in the evdev_write loop (fix.diff)",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_kernel_uname": "DragonFly 6.5-DEVELOPMENT #0 (patched build, /boot/kernel/kernel md5 54c1a462e7f1c6bc303929ac0cd99ac3)",
  "fix_verdict": "Validated on a rebuilt+rebooted guest kernel: the exact PoC that delivered 2-19 magic events per round on baseline delivered ZERO across 4 rounds on the patched kernel; in-flight writes abort with ENODEV (revoked re-check) or EBADF (devfs detached cdev) instead of running to completion through freed memory; the identical 6-cycle detach/attach churn that hard-wedged the baseline left the patched guest fully responsive; stale-fd closes are clean; keystroke delivery is unregressed (sendkey smoke test). Two earlier iterations of the fix itself panicked in evdev_dtor (compiler-sunk load past kfree) and are documented in fix_iter1_panic.txt.",
  "fix_evidence": "fix_run.log (patched-kernel runs fixrun3/4/5/fixstress: UAF-NOT-CONFIRMED, magic_hits=0, guest up; smoke-test hexdump of KEY_A down/up); run.log + wedge_serial_tail.txt (baseline: UAF-CONFIRMED, wedge); fix.diff (v3); fix_iter1_panic.txt"
}