⬢ DragonFlyBSD Kernel Audit
DF-5378 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5378",
  "status": "reproduced",
  "reproduced": true,
  "impact": "leak",
  "confidence": "certain",
  "verdict": "Reproduced deterministically with a KLD harness that compiles the real uvc driver sources. uvc_drv_detach() frees the entire control graph via uvc_drv_destroy_ctrl() (uvc_drv.c:2507 -> 2453-2474, no lock held) while the 0666 /dev/videoN cdev is still live (destroy_dev only at uvc_drv.c:2511), so an unprivileged VIDIOC_QUERYCTRL/VIDIOC_QUERYMENU racing a device disconnect executes uvc_query_v4l2_ctrl()/uvc_query_v4l2_menu() (uvc_ctrls.c:1102/1122) over freed memory. Controlled same-address reclaim of all 11 freed chunks proved (1) the UAF WRITE at uvc_ctrls.c:1065 (ctrl->info.cached_sub_info_idx changed 0xAA->0x01 in a freed chunk), (2) the kernel-heap DISCLOSURE at uvc_ctrls.c:1001 and :1150 (31 attacker-marked freed-chunk bytes copied into user-visible queryctrl/querymenu names), and (3) lockmgr on the freed ctrl chunk (uvc_ctrls.c:1107). The uncontrolled variant panics: Fatal trap 9, Stopped at uvc_search_control+0x20 movl 0x70(%rax),%ecx with rax=0x5a5a5a5a5a5a5a5a (freed ctrl chunks poisoned topo_nodes.stqh_first), guest down (panic.txt). Primary classification is memory corruption (bucket memcorrupt): the write primitive + fully-controlled fake-graph walk dominate; leak recorded as the userspace-visible artifact per the verdict enum. uid=0 not completed on this guest (no UVC hardware for the real /dev/videoN path; harness proves the primitives). Fix validated: with fix.diff applied, the fixed production query handler returns ENODEV after the fixed detach sequence and the live path still works; guest stays up.",
  "exploit_chain": "unprivileged user opens 0666 /dev/videoN (uvc_v4l2.c:783) and loops VIDIOC_QUERYCTRL; evil USB webcam disconnects itself (or is unplugged) -> uvc_drv_detach frees sc->ctrl graph at uvc_drv.c:2507 BEFORE destroy_dev at 2511 -> in-flight ioctl walks the freed graph: attacker grooms the M_UVC size-class zones (same-driver allocations reachable from the 0666 node) so the freed uvc_drv_ctrl/topo_node/controls/sub_info chunks are reclaimed under attacker control -> full fake-graph walk (uvc_ctrls.c:1090-1067), 1-byte write cached_sub_info_idx=j (uvc_ctrls.c:1065) into a freed chunk, 31 bytes of kernel heap per QUERYCTRL/QUERYMENU via strlcpy (uvc_ctrls.c:1001,1150); without grooming the walk panics (trap 9 at 0x5a5a5a5a5a5a5a5a+0x70). On real hardware the fake-graph control plus zone grooming is the escalation route; not driven to uid=0 on this guest (no UVC device).",
  "evidence": "run.log: all 11 freed chunks reclaimed at their original addresses, *** UAF WRITE CONFIRMED ... (was 0xAA) ***, q.name/qm.name hexdumps = 31x 0x5A/0x5B markers; panic.txt: Fatal trap 9 ... Stopped at uvc_search_control.isra.3+0x20: movl 0x70(%rax),%ecx; fix.log: [fix baseline]: ret=0 name=<Tilt (Absolute)>, [fix post-detach]: query ret=19 (ENODEV) ... guest up; leak_sample.txt raw bytes",
  "kernel_refs": [
    "sys/bus/u4b/uvc/uvc_ctrls.c:1065",
    "sys/bus/u4b/uvc/uvc_ctrls.c:1090",
    "sys/bus/u4b/uvc/uvc_ctrls.c:1102",
    "sys/bus/u4b/uvc/uvc_ctrls.c:1107",
    "sys/bus/u4b/uvc/uvc_ctrls.c:1001",
    "sys/bus/u4b/uvc/uvc_ctrls.c:1150",
    "sys/bus/u4b/uvc/uvc_drv.c:2507",
    "sys/bus/u4b/uvc/uvc_drv.c:2453",
    "sys/bus/u4b/uvc/uvc_drv.c:2511",
    "sys/bus/u4b/uvc/uvc_v4l2.c:783"
  ],
  "poc_changes": "Seed was a finding-level sketch only; harness written fresh following the DF-5305/5306 KLD pattern: real driver sources #included, real parse+init+query+destroy functions driven on a crafted VideoControl-only blob; added same-address zone-reclaim engine (LIFO order matching the production free sequence), a second control (AE_MODE) so the QUERYMENU disclosure path (menus[0] non-NULL) is exercised, a wild-pointer panic mode, and a fixed-detach simulation mode for fix validation. Fixed one harness bug found during fix-validation (mode 3 initially re-evaluated &video.ctrl->mtx after NULLing -> harness-side NULL deref, corrected to unlock via saved pointer exactly as fix.diff does).",
  "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": 2400,
  "guest_dirty": 0,
  "build_cmd": "cp /usr/src/sys/bus/u4b/uvc/*.c /usr/src/sys/bus/u4b/uvc/*.h /root/build/ && cd /root/build && make   (KLD harness incl. real uvc sources; see build.sh/build.log)",
  "run_cmd": "kldload /root/build/uvcharness.ko    # mode 1 controlled UAF at load; then: sysctl hw.uvcharness=2   # wild-pointer panic",
  "code_hash": "a97fc12789703186b8d722044c553a54d86cdf162e877d19c95d9a3a1ffbf401",
  "notes": "Distinct victim objects from DF-2353 (sc->data/format graph vs sc->ctrl control graph) with different primitives (write + name disclosure). Known-file findings DF-1046/DF-2349 (uint8 loop wrap in uvc_ctrl_init_dev) not re-reported. Also observed, not filed as a finding: sub_info->uvc_info (uvc_ctrls.c:776) stores a pointer to the initializers dead stack frame (never dereferenced today - latent, Info); uvc_ctrl_initialize_control ignores the return of uvc_ctrl_init_menu leaving a dead-path leak (unreachable: M_WAITOK never returns NULL); uvc_query_v4l2_menu does not validate qm->index (v4l2 semantics only, no memory-safety impact).",
  "recommended_fix": "In uvc_drv_detach() call uvc_v4l2_unreg() (destroy_dev drains in-flight ioctls) and NULL video->ctrl under the ctrl lock BEFORE uvc_drv_destroy_data/destroy_ctrl; add video->ctrl==NULL -> ENODEV guards to uvc_query_v4l2_ctrl()/uvc_query_v4l2_menu(); take c->mtx and lockuninit in uvc_drv_destroy_ctrl().",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_kernel_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 (patched-source harness build)",
  "fix_verdict": "fix.diff applied to guest /usr/src (patch -p0, both hunks clean); harness rebuilt against the patched sources with -Werror (fix_build.log). Baseline re-confirmed on the patched build (query ret=0, name=Tilt (Absolute)). With the fixed detach sequence (video->ctrl NULLed under its lock, then production uvc_drv_destroy_ctrl), the patched production uvc_query_v4l2_ctrl() returns ENODEV(19) with no freed-memory access and the guest stays up (fix.log). The uncontrolled-panic behavior is therefore unreachable through the fixed ordering: after uvc_v4l2_unreg()/destroy_dev no ioctl can be in flight against the freed graph, and the guard rejects any straggler. Full-kernel rebuild not run (harness pattern as DF-5305/5306; patch touches only the two driver files, both compile-checked).",
  "fix_evidence": "fix.log lines: HARNESS DF-5378[fix baseline]: ret=0 name=<Tilt (Absolute)> and HARNESS DF-5378[fix post-detach]: query ret=19 (want ENODEV=19, NO freed-memory access, guest up); fix_build.log: FIXBUILD-OK; fix.diff applies clean (patch -p0 dry-run verified on the read-only tree)"
}