strlcpy() from non-NUL-terminated VS_FORMAT_FRAME_BASED bGuidFmt: kernel heap OOB read past the cdesc allocation, disclosed to unprivileged userspace via VIDIOC_ENUM_FMT
Summary
strlcpy walks the raw 16-byte GUID until NUL - past the descriptor and (all-non-NUL to blob end) past the wTotalLength-sized cdesc kmalloc: up to 63 bytes of adjacent kernel heap land in fmt->name and uvc_drv_enum_v4l2_fmt hands 31 of them to ANY local user via VIDIOC_ENUM_FMT (cdev 0666). Guest-verified 3/3 with the real driver sources compiled into a KLD harness: strlen=63 = 27 in-bounds + 36 OOB bytes, groom markers in the user-visible description. memcpy-16+NUL fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5305 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.3 KB | β raw | |
| VERDICT.md | β | 4.6 KB | β raw | |
| uvcharness.c | β | 8.8 KB | view raw | |
| Makefile | β | 119 B | β download | |
| build.sh | β | 161 B | view raw | |
| run.sh | β | 145 B | view raw | |
| build.log | β | 924 B | view raw | |
| run.log | β | 3.5 KB | view raw | |
| run.2.log | β | 2.0 KB | view raw | |
| run_patched.log | β | 2.4 KB | view raw | |
| fix.log | β | 2.4 KB | view raw | |
| leak_sample.txt | β | 1.5 KB | view raw | |
| env.txt | β | 372 B | view raw | |
| fix.diff | β | 2.9 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 3.6 KB | view raw | |
| code_hash.txt | β | 65 B | view raw |
DF-5305 β strlcpy() from non-NUL-terminated device GUID: heap OOB read past the cdesc allocation, disclosed to unprivileged userspace via VIDIOC_ENUM_FMT
Where
sys/bus/u4b/uvc/uvc_drv.c:1888-1890 (uvc_drv_parse_data(), case
UDESCSUB_VS_FORMAT_FRAME_BASED):
} else {
strlcpy(fmt->name,
(const char *)(fbraw->bGuidFmt),
sizeof(fmt->name)); /* <-- bGuidFmt is 16 raw bytes */
fmt->fcc = 0;
}
bGuidFmt is a 16-byte raw GUID inside the class-specific format
descriptor β it is not a C string. strlcpy() walks src until a
NUL byte to compute strlen(), then copies up to 63 bytes into
fmt->name[64]. A device that fills the GUID and the trailing
descriptor bytes with non-zero values makes the walk run off the end of
the descriptor, through the rest of the kmalloc()ed config-descriptor
buffer (udev->cdesc, allocated with exactly wTotalLength bytes,
sys/bus/u4b/usb_request.c:1366), and into adjacent kernel heap β
reading kernel memory until the first NUL.
fmt->name is then handed to unprivileged userspace by
uvc_drv_enum_v4l2_fmt() (uvc_drv.c:833,
strlcpy(vfmt->description, fmt->name, sizeof(vfmt->description)),
31 bytes) β reachable with VIDIOC_ENUM_FMT on /dev/videoN, which
uvc_v4l2_reg() creates mode 0666 (uvc_v4l2.c:783-785).
Attack model
Crafted USB webcam descriptors (evil-USB device; QEMU-emulatable). The
device controls the leak length (non-NUL run) and the leaked window
(placement of the format descriptor near the end of cdesc); any local
user can then read the leaked bytes.
Build / run (as verified on the QEMU guest)
./build.sh # KLD harness that #includes the REAL uvc sources ./run.sh # kldload -> 3-round leak test at load
The harness (uvcharness.c) compiles the actual
uvc_buf.c/uvc_ctrls.c/uvc_v4l2.c/uvc_drv.c from /usr/src (driver
registration neutered) and drives the real uvc_drv_parse_ctrl() /
uvc_drv_parse_data() / uvc_drv_enum_v4l2_fmt() with a crafted
72-byte config blob whose last bytes are non-NUL, so strlcpy() must
cross the allocation boundary to terminate. A groomed marker chunk
(0x43/0x45/0x47 per round) is placed after the blob.
Expected (baseline, unpatched)
HARNESS[1] DF-5305: name strlen=63; in-bounds non-NUL run ... is 27 bytes HARNESS[1] DF-5305: *** OOB READ PAST ALLOCATION CONFIRMED: 36 bytes from outside the blob *** HARNESS[1] DF-5305: uvc_drv_enum_v4l2_fmt ret=0 -> user-visible description=<...ZZZZCCCC>
36 bytes read from beyond the 72-byte cdesc allocation land in
fmt->name; the final CCCC in the user-visible description are those
leaked heap bytes.
After fix.diff (memcpy of exactly 16 GUID bytes + NUL)
HARNESS[1] DF-5305: name strlen=16; ... user-visible description=<16 GUID bytes only>
Files
uvcharness.c harness source (#includes the real driver code) Makefile guest KLD makefile build.sh/run.sh build.log compiler output (successful build) run.log baseline: 3 rounds, OOB confirmed each time run.2.log earlier 68-byte-blob run (walk crossed, first OOB byte was 0) run_patched.log / fix.log post-fix runs: strlen=16, no OOB fix.diff git-apply-able unified diff (git apply -p0) leak_sample.txt extracted fmt->name across the 3 baseline rounds env.txt guest uname / kldstat
DF-5305 VERDICT
Status: reproduced (kernel heap OOB read -> info leak toward unprivileged userspace), impact: leak, confidence: certain.
Root cause (path:line)
sys/bus/u4b/uvc/uvc_drv.c:1888-1890βstrlcpy(fmt->name, (const char *)fbraw->bGuidFmt, sizeof(fmt->name)).uvc_vs_frame_based_format_desc.bGuidFmt(uvc_drv.h:350-364) is 16 raw, non-NUL-terminated descriptor bytes.strlcpy()first walkssrcto find its NUL β the walk leaves the descriptor, traverses the remainder of the config blob, and when every byte from the GUID to the end ofcdescis non-NUL it runs past thekmalloc()ed config-descriptor buffer (allocated with exactlywTotalLengthbytes inusbd_req_get_config_desc_full(),sys/bus/u4b/usb_request.c:1341-1375). Up to 63 bytes are copied intofmt->name[64]; the read itself is unbounded until a NUL.- Disclosure sink:
uvc_drv_enum_v4l2_fmt()sys/bus/u4b/uvc/uvc_drv.c:833copiesfmt->nameintov4l2_fmtdesc.description[32](31 bytes), whichVIDIOC_ENUM_FMT(uvc_v4l2.c:548-554) copies back to the caller. The cdev is created 0666 (uvc_v4l2.c:783-785), so the readback needs no privileges.
Reproduction (QEMU guest, DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 2026)
uvcharness.ko β a KLD that #includes the real
uvc_buf.c/uvc_ctrls.c/uvc_v4l2.c/uvc_drv.c (DRIVER_MODULE neutered) β
builds a 72-byte cdesc blob:
config(9) | VC iface(9) | VC_HEADER(13, bIfaceNums=1) | VS iface(9) |
VS_FORMAT_FRAME_BASED(28, unknown non-NUL GUID, all trailer bytes 0x55) |
4 pad bytes 0x5A β descriptors end at 68, the last 4 bytes belong to
the buffer but to no descriptor. The driver parse functions are then
invoked exactly as attach would: uvc_drv_init_ctrl() ->
uvc_drv_parse_ctrl() -> uvc_drv_parse_data() -> (leak) ->
uvc_drv_enum_v4l2_fmt().
Baseline (run.log, 3 rounds with groom markers 0x43/0x45/0x47):
HARNESS[1] ... name strlen=63; in-bounds non-NUL run ... is 27 bytes *** OOB READ PAST ALLOCATION CONFIRMED: 36 bytes from outside the blob *** user-visible description=<...UUUUUUUZZZZCCCC>
- 27 bytes are the in-bounds run (16 GUID + 7 trailer fields + 4 pad);
the remaining 36 bytes were read from beyond the 72-byte
allocation and are exactly the groomed marker of the adjacent heap
chunk (0x43 round 1, 0x45 round 2, 0x47 round 3 β reproducible and
placement-controlled, see
leak_sample.txt). - An earlier run (
run.2.log) with a 68-byte blob shows the walk crossing the logical cdesc end even when the first out-of-bounds byte happens to be NUL (name = exactly the in-bounds run).
In production the adjacent memory is whatever follows the device's
cdesc allocation β kernel heap contents (free-list pointers, object
data), i.e. a genuine kernel-memory disclosure of up to 31 bytes per
query through the world-writable /dev/videoN.
Fix validation
fix.diff (same diff in all three DF-530x packs) replaces the
strlcpy() with a bounded memcpy() of exactly
sizeof(fbraw->bGuidFmt) (16) bytes plus an explicit NUL. Applied to
the guest's /usr/src/sys/bus/u4b/uvc/uvc_drv.c, the harness rebuilt
against the patched source (run_patched.log / fix.log):
HARNESS[1..3] DF-5305: name strlen=16; ... (no OOB line, no marker bytes) user-visible description=<16 GUID bytes only>
Baseline bad behavior gone; fix confirmed.
Exploit chain (ceiling)
evil-USB descriptor at attach -> strlcpy walks past cdesc allocation
-> up to 63 bytes of adjacent kernel heap copied into fmt->name ->
any local user calls VIDIOC_ENUM_FMT on the 0666 /dev/videoN and
receives up to 31 leaked bytes. With heap grooming from the device
side (descriptor sizes control the cdesc allocation size class) this is
a repeatable kernel-heap info leak useful for defeating heap
randomization / KASLR-style mitigations and for heap-layout
reconnaissance. It is a read primitive only β no write, no direct
escalation.
Notes
- Distinct from DF-2350 (VS frame interval parse OOB read): different
descriptor subtype, different sink (
fmt->namevia ENUM_FMT vs interval array via ENUM_FRAMEINTERVALS), different fix site. usb_desc_foreach()(usb_parse.c) keeps every descriptor walk insidewTotalLength; thestrlcpy()here is the only thing that leaves the buffer.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.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.
run_patched.log, fix.log, leak_sample.txt (post-fix section)
Confirmed kernel references
Detail
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 (decisive lines)
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
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.
Verified recommended fix
Replace the strlcpy() of bGuidFmt with a bounded memcpy() of exactly sizeof(bGuidFmt) bytes plus an explicit NUL terminator (see fix.diff).
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).
No comments yet.