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)
PoC verification
Evidence pack
findings/poc/DF-2351 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
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:
uvc_drv_init_data_fmt()(production, unmodified, compiled intouvc_rig.kofrom/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).uvc_drv_parse_data()(production) then completed withdata->nfmt == 0, exactly as at attach time.uvc_drv_init_cur_fmt_frm()(production, uvc_drv.c:1457-1463) skipped thefor (i = v->data->nfmt; i > 0; i--)search, leftfmt == NULL, and faulted readingfmt->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 = nfmttruncation 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
fixedPatched 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.
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.
No comments yet.