⬢ DragonFlyBSD Kernel Audit
DF-5434 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5434",
  "status": "reproduced",
  "verdict": "VIDIOC_REQBUFS is accepted while the UVC buffer queue is live (no streaming check at uvc_v4l2.c:613 or uvc_buf.c:537); uvc_buf_queue_free_bufs_locked() (uvc_buf.c:517-525) then frees bq->mem without draining the product/consumer STAILQs and without resetting the bq->buf[] descriptors. The USB completion engine copies frame data through those stale descriptors: (A) with a shrunk count, descriptors with index >= new count keep mem pointing at freed memory while remaining product head, so uvc_bulkbuf_sell_buf()/uvc_buf_sell_buf() usbd_copy_out() writes 0x800 bytes of controlled payload into freed kernel memory - proven deterministically 3x on the guest (exactly 2048 marker bytes at the predicted offset, no race, all under bq->mtx); (B) with a shrunk frame size, uvc_buf_fill_v4l2() (uvc_buf.c:91) never clears vbuf.bytesused, so maxlen = length - bytesused (uvc_buf.c:265) underflows and the copy lands at mem_new + stale_bytesused - proven by Fatal trap 12, supervisor WRITE, at the exact VA predicted from the stale bytesused (0xfffff8011a7cc000), Stopped at memcpy+0xfa inside usbd_copy_out. Distinct from DF-1065 (userspace mmap of freed pages): the victim here is the kernels own completion path. fix.diff (drain+reset in free_bufs_locked, full descriptor re-init in the req_bufs loop, unsigned count clamp) validated on a force-rebuilt KLD from the patched tree: 0 pattern bytes in all rounds, stale bytesused 0x0, no fault, guest up.",
  "reproduced": true,
  "impact": "panic",
  "confidence": "certain",
  "exploit_chain": "unpriv user + streaming UVC camera (0666 /dev/videoN, v4l2_not_allowed=0 default): open -> S_FMT/REQBUFS (acquires pri) -> STREAMON -> QBUF -> frames flow -> REQBUFS(smaller count) [permitted: uvc_v4l2.c:613 checks only type/memory/pri] -> free_bufs_locked frees the N*frame-size M_UVC block while old-indexed descriptors stay linked as product head -> next USB frame payload is memcpyd into the freed block at a chosen offset (device-controlled content; malicious USB device = fully controlled) -> slab-groom a victim kernel object into the freed block -> overwrite function pointer / cred-adjacent structure -> uid=0. The len-shrink variant (reachable via S_PARM refreshing v->req.dwMaxFrameSize from the device probe response mid-stream, uvc_drv.c:1385-1393, or a malicious device) gives a direct controlled-offset OOB write past the new small allocation. uid0 chain not developed on the guest (no UVC hardware; KLD rig proves the kernel-side primitive with production code).",
  "evidence": "run.modes123.log: UAF-WRITE PROVEN: found 2048 bytes of 0xa5/0x58/0x3c pattern inside freed/reclaimed block x3 with product head=... == buf3? YES and stale descriptor dump; panic.txt: PREDICTED fault VA = 0xfffff8011a7cc000 followed by fault virtual address = 0xfffff8011a7cc000, supervisor write data + Stopped at memcpy+0xfa: repe movsq; run.vuln2.log/run.vuln3.log: two more independent vulnerable builds, guest dies on the dangling state; fix_run.log: buf3 NOT re-inited: mem=0 status=0 bytesused=0x0; product head=0 + no hit: found 0 bytes x3 + bytesused=STALE 0x0 + count clamp 0x8; fix_run_user.log: uid=1001 REQBUFS(0x80000000) -> count=0x8, QUERYBUF/QBUF OOB -> EINVAL; guest up.",
  "kernel_refs": [
    "sys/bus/u4b/uvc/uvc_buf.c:517",
    "sys/bus/u4b/uvc/uvc_buf.c:537",
    "sys/bus/u4b/uvc/uvc_buf.c:554",
    "sys/bus/u4b/uvc/uvc_buf.c:569",
    "sys/bus/u4b/uvc/uvc_buf.c:91",
    "sys/bus/u4b/uvc/uvc_buf.c:265",
    "sys/bus/u4b/uvc/uvc_buf.c:268",
    "sys/bus/u4b/uvc/uvc_buf.c:346",
    "sys/bus/u4b/uvc/uvc_v4l2.c:613",
    "sys/bus/u4b/uvc/uvc_v4l2.c:620",
    "sys/bus/u4b/uvc/uvc_drv.c:1385"
  ],
  "poc_changes": "No seed sketch existed (pass-2 discovery). Harness written fresh on the DF-5427 rig pattern: production uvc sources #included with DRIVER_MODULE neutered, production uvc_v4l2_reg() cdev (0666 /dev/video96), completion path driven via real uvc_bulkbuf_sell_buf() with a fabricated Plain-PIO usb_page_cache (usbd_get_page Case 2). Gotchas fixed during iteration: (1) fake uvc_drv_data with num_altsetting=1 so the production close path (uvc_drv_stop_video) is safe on the rig; (2) per-round reset of v->bulk.skip_payload/header_size/payload_size to mirror a fresh stream; (3) uvc_buf.c is #included and NOT in KLD SRCS - make will not rebuild after touching it; every rebuild must rm -f *.o *.ko (a stale module silently invalidated one fix run, caught by behavior diff); (4) the signed-count candidate (count>=0x80000000 -> uninit mem) was killed by objdump evidence: shipping gcc 8.3 folds the uninitialized read into ENOMEM (172a: movslq/jg; 1688: mov $0xc,%r12d) - documented as Info hardening, not the High.",
  "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": 4200,
  "guest_dirty": 0,
  "build_cmd": "sh build.sh (guest /root/build5434: KLD=bufharness.ko from pristine /usr/src uvc sources + cc trig5434.c; fixed build: patch -p0 < fix.diff in /usr/src, cp uvc_buf.c, rm -f *.o *.ko, make)",
  "run_cmd": "kldload /root/build5434/bufharness.ko; sysctl hw.bufharness=1; =2; =3 (UAF proof); sysctl hw.bufharness=4 (OOB panic, fresh reset only); sysctl hw.bufharness=8 (signed-count probe); fix: same modes + /tmp/trig5434 as uid 1001",
  "code_hash": "e4c04a9c81f75420e5b2fe3885eadaf5a99645cab53d6c9786aa85639795d434 (bufharness.c), 1e3d4fded38f014eb51abbf66e261821794b651c4695fdaa284b9e49485d3d47 (trig5434.c), 8c70b88f67b13fdf8d8a95fb089f866a7ae79624cfd0aaeb5001e8de3c81eb4b (fix.diff)",
  "notes": "Impact recorded as panic (strongest observed); the primary classification is kernel memory corruption (memcorrupt bucket): primitive A is a deterministic freed-memory write, primitive B a controlled-offset heap OOB write - both from device-frame data through production uvc_buf.c code paths. Three independent vulnerable builds reproduced (two deliberate, one accidental-but-caught via the SRCS gotcha, which itself double-served as a reproducibility check). The killed signed-count candidate is filed as a separate Info finding (DF-5435) with the objdump fold evidence; its clamp is included in this packs fix.diff. Guest reset clean (with-src) after validation.",
  "recommended_fix": "Drain both STAILQs and reset all bq->buf[] descriptors in uvc_buf_queue_free_bufs_locked(); fully re-init each descriptor (uvc_buf_queue_init_qbuf) in the req_bufs alloc loop so stale bytesused cannot survive; clamp *count with unsigned arithmetic (see fix.diff).",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "fix.diff applied to guest /usr/src (patch -p0, 5 hunks clean); KLD force-rebuilt from the patched tree (rm -f *.o *.ko); modes 1/2/3: descriptors re-inited (mem=0 status=0 bytesused=0x0), product queue drained (head=0), 0 pattern bytes found in the freed block in all three rounds; mode 4: stale bytesused gone (0x0), write in-bounds, no fault; mode 8 + unpriv trig5434: REQBUFS(0x80000000) clamps to count=8 and every OOB QUERYBUF/QBUF returns EINVAL; guest stays up (vm.sh status up). Baseline bad behavior gone.",
  "fix_evidence": "fix_run.log (no hit: found 0 bytes x3, product head=0, bytesused=STALE 0x0); fix_run_user.log (REQBUFS(0x80000000) ret=0 ... count=0x8, QUERYBUF(8..23) ret=-1 errno=25, QBUF(16) ret=-1 errno=25); fix_build.log (clean -Werror rebuild); env.txt (guest up, kldstat)."
}