β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5305

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5305 Β· 17 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

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 walks src to 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 of cdesc is non-NUL it runs past the kmalloc()ed config-descriptor buffer (allocated with exactly wTotalLength bytes in usbd_req_get_config_desc_full(), sys/bus/u4b/usb_request.c:1341-1375). Up to 63 bytes are copied into fmt->name[64]; the read itself is unbounded until a NUL.
  • Disclosure sink: uvc_drv_enum_v4l2_fmt() sys/bus/u4b/uvc/uvc_drv.c:833 copies fmt->name into v4l2_fmtdesc.description[32] (31 bytes), which VIDIOC_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->name via ENUM_FMT vs interval array via ENUM_FRAMEINTERVALS), different fix site.
  • usb_desc_foreach() (usb_parse.c) keeps every descriptor walk inside wTotalLength; the strlcpy() 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

fixed
baseline reproduced→ patch + rebuild →patched clean

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.

run_patched.log, fix.log, leak_sample.txt (post-fix section)
↓ fix.diffper-fix-DF-5305

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).