DF-5305 / verdict.json
{ "finding_id": "DF-5305", "status": "reproduced", "reproduced": true, "impact": "leak", "confidence": "certain", "verdict": "Reproduced on the QEMU guest with a KLD harness that compiles the real uvc driver sources and drives the real uvc_drv_parse_ctrl()/uvc_drv_parse_data()/uvc_drv_enum_v4l2_fmt() with a crafted 72-byte config blob. strlcpy() at uvc_drv.c:1888 walks off the non-NUL-terminated 16-byte bGuidFmt, crosses the kmalloc()ed cdesc buffer and copies 36 bytes of adjacent kernel heap into fmt->name in each of 3 runs (groom markers 0x43/0x45/0x47 land exactly as predicted). The real uvc_drv_enum_v4l2_fmt() (uvc_drv.c:833) then places leaked bytes into the VIDIOC_ENUM_FMT description field, and the cdev is 0666 (uvc_v4l2.c:783), so any local user can read them. Read primitive only (no write), severity Medium per the audit rubric (limited kernel-memory info leak; evil-USB precondition within the declared attacker model).", "exploit_chain": "crafted USB webcam descriptor (VS_FORMAT_FRAME_BASED with unknown non-NUL GUID at the end of the config blob) -> strlcpy at uvc_drv.c:1888 runs past the wTotalLength-sized cdesc allocation -> up to 63 bytes of adjacent kernel heap copied into fmt->name[64] -> unprivileged VIDIOC_ENUM_FMT on the 0666 /dev/videoN returns up to 31 leaked bytes per query (uvc_drv.c:833)", "evidence": "run.log (3 baseline rounds: name strlen=63 ... OOB READ PAST ALLOCATION CONFIRMED: 36 bytes, description ends with groom-marker bytes), leak_sample.txt (hexdumps), run_patched.log/fix.log (post-fix strlen=16, no marker bytes), VERDICT.md, panic-free guest", "kernel_refs": [ "sys/bus/u4b/uvc/uvc_drv.c:1888", "sys/bus/u4b/uvc/uvc_drv.c:833", "sys/bus/u4b/uvc/uvc_drv.h:350", "sys/bus/u4b/uvc/uvc_v4l2.c:783", "sys/bus/u4b/usb_request.c:1366" ], "poc_changes": "No finding seed existed; harness written from scratch. It #includes the unmodified guest /usr/src uvc sources (DRIVER_MODULE/MODULE_DEPEND/MODULE_VERSION neutered) and feeds a fake usb_device (cdesc+ifaces) so the real static parse functions run against crafted descriptors at load time.", "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": 2100, "guest_dirty": 0, "build_cmd": "cp uvcharness.c Makefile /root/build/ && cd /root/build && make", "run_cmd": "kldload /root/build/uvcharness.ko; dmesg | tail -40", "code_hash": "9425426536799947ba46b93c33c288a70afeb9064800e79373dab40ec681d9cc", "notes": "Distinct from DF-2350 (frame-interval parse OOB read): different descriptor subtype, different sink and disclosure channel. First run with a 68-byte blob (run.2.log) also crossed the buffer end but the first OOB byte was NUL; shrinking the logical end to a 72-byte exact kmalloc zone with an all-non-NUL tail made the disclosure deterministic. Guest reset to clean with-src snapshot after verification.", "recommended_fix": "Replace the strlcpy() of bGuidFmt with a bounded memcpy() of exactly sizeof(bGuidFmt) bytes plus an explicit NUL terminator (see fix.diff).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to the guest /usr/src copy; harness rebuilt against the patched source and re-run 3x: fmt->name strlen=16 (exactly the 16 GUID bytes + NUL), no OOB read, ENUM_FMT description contains only GUID bytes. Baseline (strlen=63, 36 leaked bytes) vs patched (strlen=16, none) recorded in run.log vs run_patched.log/fix.log.", "fix_evidence": "run_patched.log, fix.log, leak_sample.txt (post-fix section)" } |