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

uint16_t to uint8_t truncation of nfmt/nfrm/nitv causes NULL-deref panic on device attach

Summary

uvc_drv_init_data_fmt counts formats/frames/intervals into uint16_t locals but stores into uint8_t struct fields (data->nfmt/nfrm/nitv). With exactly 256 format descriptors data->nfmt silently becomes 0. Buffer sizing uses full uint16_t values (allocation correct) but recorded counts wrong. uvc_drv_init_cur_fmt_frm format-search loop skipped when nfmt==0 local fmt pointer stays NULL fmt->nfrm dereferences NULL kernel panic on device attach. 256 VS_FORMAT_* descriptors each 27 bytes total 6912 bytes fits in 65535 config descriptor. Deterministic local DoS via USB device no user interaction beyond plug-in.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2351 Β· 16 files
FileTypeDescriptionSize
rig/uvc_rig.c rig-source KLD rig: #includes the four UNMODIFIED production uvc sources; fabricates struct usb_device (cdesc/ifaces); actions 2351/2352/2352direct/2353/2353prod via sysctl hw.usb.uvcrig 20.7 KB view raw
rig/Makefile build-file KMOD= uvc_rig 321 B ↓ download
panic.txt panic-proof decisive console: truncation markers + Fatal trap 12 va=0x8 + stopped at uvc_drv_init_cur_fmt_frm.cold.36: movq 0x8,%rax 1.3 KB view raw
run.log run-log same decisive run 1.3 KB view raw
fix_run.log fix-run-log patched rig: data->nfmt=256 stored, init_cur_fmt_frm EINVAL, NO PANIC 677 B view raw
build.log build-log baseline (unpatched) uvc_rig.ko build, gcc 8.3 -Werror clean, rc=0 924 B view raw
fix_build.log fix-build-log patched (fix.diff applied to guest /usr/src) uvc_rig.ko rebuild, -Werror clean 859 B view raw
fix.diff suggested-fix git-apply-able combined fix for DF-2351/2352/2353 (uvc_drv.c + uvc_drv.h) 4.3 KB view raw
env.txt environment guest uname 6.5-DEVELOPMENT, gcc 8.3, kldstat (ehci/xhci/uvc_rig), sysctls 501 B view raw
setup.sh build-script push rig + triggers to guest and build (idempotent after vm.sh reset with-src) 947 B view raw
build.sh build-script wrapper calling setup.sh 65 B view raw
run.sh run-script full end-to-end reproduction procedure 89 B view raw
README.md readme human-facing repro + expected output 2.5 KB ↓ raw
VERDICT.md verdict full narrative, line-by-line trace, fix validation 3.8 KB ↓ raw
verdict.json verdict-json machine verdict (persist_poc.py input) 4.1 KB view raw
boot.log.serial serial-log full guest serial console of the decisive session 14.2 KB ↓ download
README.md readme human-facing repro + expected output
↓ download raw

DF-2351 β€” uint16β†’uint8 truncation of nfmt β†’ NULL-deref panic at device attach

sys/bus/u4b/uvc/uvc_drv.c β€” uvc_drv_init_data_fmt() counts formats/frames/ intervals into uint16_t locals (uvc_drv.c:1674) but stores them into the uint8_t fields data->nfmt/nfrm/nitv (uvc_drv.h:662-665, store at uvc_drv.c:1737-1739). With exactly 256 VS_FORMAT_* descriptors the allocation is sized with the full count (correct, 24 KB) but data->nfmt == 0. The later default-format search in uvc_drv_init_cur_fmt_frm() (for (i = v->data->nfmt; i > 0; i--), uvc_drv.c:1457) never runs, fmt stays NULL, and fmt->nfrm (uvc_drv.c:1463) dereferences NULL.

Threat: a malicious USB webcam whose configuration descriptor contains 256 format descriptors β€” deterministic kernel panic at attach, no user interaction beyond plugging (or USB passthrough) in.

Evidence class

Production-handler KLD dispatch rig: the four unmodified production uvc driver sources are #included into uvc_rig.ko (see rig/uvc_rig.c), so the descriptor parsing exercised below is the exact in-tree code. The rig synthesizes only the USB transport (a fake struct usb_device whose ->cdesc is the crafted config descriptor a malicious webcam reports and whose ->ifaces[1].idesc points at the VideoStreaming interface descriptor) β€” no emulated UVC webcam exists for this QEMU guest.

Build / run

./build.sh                       # push rig + triggers to guest, build uvc_rig.ko (gcc 8.3, -Werror)
./run.sh                         # reset guest, load rig, sysctl hw.usb.uvcrig=2351

Expected output (decisive lines, see panic.txt)

RIG2351: production uvc_drv_init_data_fmt ret=0
RIG2351: counted nfmt=256 (uint16_t local) -> stored data->nfmt=0 nfrm=0 nitv=0 fmt=0xfffff80118496000  <-- TRUNCATION
RIG2351: production uvc_drv_parse_data ret=0 (data->nfmt=0)
RIG2351: entering production uvc_drv_init_cur_fmt_frm ...
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x8
Stopped at      uvc_drv_init_cur_fmt_frm.cold.36:       movq    0x8,%rax

fault virtual address = 0x8 is exactly offsetof(struct uvc_data_format, nfrm) from a NULL fmt.

Fix validation

fix.diff widens nfmt/nfrm/nitv to uint16_t and adds NULL-fmt guards in uvc_drv_init_cur_fmt_frm() and the uvc_drv_init_video() GET_CUR fallback. Re-running the identical trigger on the patched rig (fix_run.log):

RIG2351: ... stored data->nfmt=256 ... fmt=0xfffff80118660000
No frame desc found for default format.
RIG2351: init_cur_fmt_frm returned 22 -- NO PANIC
VERDICT.md verdict full narrative, line-by-line trace, fix validation
↓ download raw

DF-2351 β€” VERDICT: REPRODUCED (panic, deterministic) β€” FIX VALIDATED

Bottom line

The claimed truncation and the claimed NULL-deref panic are both real and were demonstrated inside the kernel with the production uvc code paths:

  1. uvc_drv_init_data_fmt() (production, unmodified, compiled into uvc_rig.ko from /usr/src/sys/bus/u4b/uvc/uvc_drv.c) counted nfmt = 256 (uint16_t local) and stored data->nfmt = 0 (uint8_t field) while allocating the correctly sized 24 KB buffer (fmt=0xfffff80118496000) β€” the console line proves the truncation exactly as the finding describes (uvc_drv.c:1674 vs 1737).
  2. uvc_drv_parse_data() (production) then completed with data->nfmt == 0, exactly as at attach time.
  3. uvc_drv_init_cur_fmt_frm() (production, uvc_drv.c:1457-1463) skipped the for (i = v->data->nfmt; i > 0; i--) search, left fmt == NULL, and faulted reading fmt->nfrm:
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x8                      <- offsetof(uvc_data_format, nfrm)
Stopped at uvc_drv_init_cur_fmt_frm.cold.36: movq 0x8,%rax

(uvc_drv.c:1463). The guest panicked; ssh died; vm.sh status = down.

Line-by-line trace (all cited in sys/bus/u4b/uvc/)

  • uvc_drv.h:662-665 β€” uint8_t nfmt, nfrm, nitv (the truncating store target)
  • uvc_drv.c:1674 β€” uint16_t nfmt, nfrm, nitv (counters)
  • uvc_drv.c:1690-1698 β€” nfmt++ per VS_FORMAT_* descriptor
  • uvc_drv.c:1726-1731 β€” allocation uses the full uint16 values (correct size)
  • uvc_drv.c:1737-1739 β€” data->nfmt = nfmt truncation store
  • uvc_drv.c:1457-1462 β€” search loop skipped when nfmt == 0
  • uvc_drv.c:1463 β€” if (fmt->nfrm == 0) NULL dereference (panic site)
  • same pattern (unfixed) in the GET_CUR fallback at uvc_drv.c:1528-1535

Production reachability

uvc_drv_attach (uvc_drv.c:2673) β†’ uvc_drv_parse_ctrl (2140) β†’ uvc_drv_parse_data (1776) β†’ uvc_drv_init_data_fmt (1667); then uvc_drv_init_video (1488) reaches uvc_drv_init_cur_fmt_frm (1580) as soon as the device answers GET_DEF or GET_CUR probe control requests β€” which a malicious device does by definition. Config-descriptor budget: 256 Γ— 11-byte VS_FORMAT_MJPEG = 2816 bytes (+ headers = 2869 total), far inside the 65535-byte wTotalLength limit. No privilege, no user interaction: the trigger is device attach.

What the rig replaces (and why)

The QEMU guest has no emulated UVC webcam (QEMU ships no such device), so the rig fabricates struct usb_device (->cdesc, ->ifaces, ->ifaces_max) and calls the production parse/attach functions with it. Everything from descriptor walk to the panic is in-tree code. The two usbd_* helpers used (usbd_find_descriptor, usb_desc_foreach) only read udev->cdesc/udev->ifaces β€” both attacker-controlled at attach time in production.

Fix validation (same rig, patched production sources)

After applying fix.diff (uint16_t fields + NULL guards), the identical trigger prints stored data->nfmt=256 and returns EINVAL from uvc_drv_init_cur_fmt_frm β€” no panic (see fix_run.log). Baseline panic vs patched clean return, same binary rig, same crafted descriptors.

Impact

Deterministic kernel NULL-deref panic (local DoS) at USB device attach; requires attacker-controlled USB hardware (or USB passthrough of a crafted device). No memory-disclosure or control-flow effect: fmt is NULL, the read is at address 0x8 β€” pure denial of service. Matches the finding's Medium severity.

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

Patched rig (fix.diff applied to guest /usr/src, module rebuilt -Werror clean) stores data->nfmt=256 and returns EINVAL from uvc_drv_init_cur_fmt_frm: no panic, guest stays up. Baseline vs patched differ only by fix.diff.

fix_run.log: 'stored data->nfmt=256 ... fmt=0xfffff80118660000', 'No frame desc found for default format.', 'init_cur_fmt_frm returned 22 -- NO PANIC'; fix_build.log: patched uvc_rig.ko builds clean under -Werror.
↓ fix.diffper-fix-DF-2351

Confirmed kernel references

Detail

Exploit chain

Malicious USB webcam with 256 VS_FORMAT_MJPEG descriptors (2869-byte config descriptor, fits 65535 limit) -> attach -> uvc_drv_parse_ctrl -> uvc_drv_parse_data -> uvc_drv_init_data_fmt stores truncated nfmt=0 (allocation correct) -> device answers GET_DEF/GET_CUR probe (attacker-controlled) -> uvc_drv_init_video -> uvc_drv_init_cur_fmt_frm NULL fmt -> fmt->nfrm read at address 0x8 -> kernel panic. No privilege or user interaction needed beyond device attach; pure DoS (NULL read), no disclosure/control-flow impact.

Evidence (decisive lines)

panic.txt lines: 'stored data->nfmt=0 ... fmt=0xfffff80118496000 <-- TRUNCATION', 'Fatal trap 12 ... fault virtual address = 0x8', 'Stopped at uvc_drv_init_cur_fmt_frm.cold.36: movq 0x8,%rax'. fix_run.log: 'stored data->nfmt=256', 'init_cur_fmt_frm returned 22 -- NO PANIC'.

PoC changes

No PoC seed existed. Built the KLD rig from scratch: rig/uvc_rig.c #includes the four unmodified production uvc sources and fabricates only struct usb_device (cdesc + ifaces). Two early rig-blob bugs fixed during bring-up (VS_FORMAT_MJPEG descriptors must be 11 bytes; VS INPUT HEADER bLength=13) β€” the panic site reproduced even with the first malformed blob (init_data_fmt EINVAL also leaves nfmt==0), but the final run proves the exact truncation path with init_data_fmt ret=0.

Verified recommended fix

Widen uvc_drv_data.nfmt/nfrm/nitv to uint16_t and add fmt==NULL guards in uvc_drv_init_cur_fmt_frm and the init_video GET_CUR fallback

Verdict

REPRODUCED β€” deterministic kernel NULL-deref panic at device attach, driven through the unmodified production uvc parse/attach functions. Kernel log proves the exact claimed truncation: uvc_drv_init_data_fmt counted nfmt=256 (uint16_t local), allocated the correctly sized 24KB buffer, then stored data->nfmt=0 (uint8_t field). uvc_drv_parse_data completed with data->nfmt==0; uvc_drv_init_cur_fmt_frm skipped its search loop, left fmt==NULL, and faulted reading fmt->nfrm (uvc_drv.c:1463): Fatal trap 12, fault virtual address = 0x8 (offsetof nfrm), stopped at uvc_drv_init_cur_fmt_frm.cold.36: movq 0x8,%rax. FIX VALIDATED on the same rig: with fix.diff (uint16_t fields + NULL guards) data->nfmt stores 256 and init_cur_fmt_frm returns EINVAL β€” no panic.