# 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 `#include`s 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.
