Division by zero in uvc_drv_try_frame_interval for continuous-interval frames (bFrameIntervalType==0)
Summary
For VS frame descriptors with bFrameIntervalType==0 (continuous intervals) uvc_drv_parse_data skips 3 interval slots (itv+=3) WITHOUT reading dFrameInterval[0..2] min/max/step DWORDs. Slots stay zero (M_ZERO allocation). uvc_drv_try_frame_interval continuous branch divides by step==frame->interval[2].val==0 kernel panic. Any local user triggers via VIDIOC_TRY_FMT/S_FMT/S_PARM on /dev/video0 (mode 0666). Two coupled bugs: parse skips reading DWORDs + try_frame_interval no zero-step guard. Deterministic local DoS.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2352 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger_sparm.c | trigger-source | unprivileged VIDIOC_S_PARM trigger; mirrors the kernel's contrib/v4l/videodev2.h encoding (VIDIOC_S_PARM = _IOWR('V',22,...)) | 2.1 KB | view raw |
| panic.txt | panic-proof | uvc_v4l2_set_streamparm markers + Fatal trap 18 integer divide fault + stopped at uvc_drv_try_frame_interval.isra.2+0x60: divl %r8d,%eax | 1.2 KB | view raw |
| run.log | run-log | same decisive run | 1.2 KB | view raw |
| fix_run.log | fix-run-log | patched rig: step slot=3333, direct call returns 3333200, full unpriv path returns 0, no panic | 1.1 KB | 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.8 KB | β raw |
| VERDICT.md | verdict | full narrative, line-by-line trace, fix validation | 4.3 KB | β raw |
| verdict.json | verdict-json | machine verdict (persist_poc.py input) | 4.5 KB | view raw |
| boot.log.serial | serial-log | full guest serial console of the decisive session | 14.0 KB | β download |
DF-2352 β division by zero in uvc_drv_try_frame_interval (continuous intervals)
sys/bus/u4b/uvc/uvc_drv.c β for VS frame descriptors with
bFrameIntervalType == 0 (continuous interval frames),
uvc_drv_parse_data() reserves 3 interval slots (nitv += 3 at
uvc_drv.c:1705-1706) and skips them during the fill loop
(if (frm->interval_type == 0) itv += 3; uvc_drv.c:1922-1923) without
reading the dFrameInterval[] min/max/step DWORDs that the UVC spec
requires the descriptor to carry. The slots stay zero (M_ZERO
allocation). uvc_drv_try_frame_interval()'s continuous branch then
computes (interval - min + step/2) / step with step == 0
(uvc_drv.c:499-504) β integer divide fault.
Trigger: any local user, ioctl(VIDIOC_S_PARM) on /dev/video0 (mode
0666, created by uvc_v4l2_reg), on a device whose descriptors contain a
continuous-interval frame. The division happens before any USB traffic
in uvc_drv_set_streampar() (uvc_drv.c:1386-1387), so the trigger is a
single unprivileged ioctl.
Evidence class
Production-handler KLD dispatch rig (rig/uvc_rig.c includes the
unmodified production uvc sources). Root fabricates the malicious
device state (descriptors parsed by production code; /dev/video0
registered by production uvc_v4l2_reg()); the fatal ioctl is issued by
an unprivileged user (trigger_sparm.c, uid 1001) through the
production uvc_v4l2_ioctl β uvc_drv_set_streampar β
uvc_drv_try_frame_interval chain.
Build / run
./build.sh # push rig + triggers, build uvc_rig.ko and trigger_sparm ./run.sh # arm via sysctl hw.usb.uvcrig=2352, then run_user /tmp/trigger_sparm
Expected output (see panic.txt)
RIG2352: frame0 interval_type=0, slots after production uvc_drv_parse_data: min=0 max=0 step=0 RIG2352: (descriptor carried min=2000000 max=10000000 step=3333 -- all-zero slots prove the parse skip at uvc_drv.c:1922) uvc_v4l2_set_streamparm Setting frame interval to 1/30 (333333). Fatal trap 18: integer divide fault while in kernel mode Stopped at uvc_drv_try_frame_interval.isra.2+0x60: divl %r8d,%eax
trap 18 is x86 #DE (divide error); the faulting instruction is the
/step division at uvc_drv.c:504.
Fix validation
fix.diff (1) reads the min/max/step DWORDs for continuous frames in
uvc_drv_parse_data() (when bLength >= 38, else safe defaults of 1),
and (2) guards step == 0 (and clamps) in uvc_drv_try_frame_interval().
On the patched rig (fix_run.log):
RIG2352direct: step slot=3333 ... returned 3333200 -- NO DIV-BY-ZERO RIG2352: ... slots after production uvc_drv_parse_data: min=2000000 max=10000000 step=3333 trigger_sparm: ioctl returned 0 errno=0 <- full unprivileged path, no panic uvc_drv_do_request-1-USB_ERR_NOT_CONFIGURED <- rig's fake transport, graceful
DF-2352 β VERDICT: REPRODUCED (panic, unprivileged ioctl) β FIX VALIDATED
Bottom line
Both halves of the finding were proven in one run:
- Parse skip: the kernel console shows the production
uvc_drv_parse_data()leaving the interval slots of abFrameIntervalType == 0frame atmin=0 max=0 step=0even though the crafted descriptor carriedmin=2000000 max=10000000 step=3333β the DWORDs are simply never read (uvc_drv.c:1917-1923 only walksinterval_typeentries, thenitv += 3past the M_ZERO slots). - Divide by zero: an unprivileged user (uid 1001) issued
ioctl(VIDIOC_S_PARM)on/dev/video0(mode 0666, registered by productionuvc_v4l2_reg, uvc_v4l2.c:783-785). The production dispatch chainuvc_v4l2_ioctl(uvc_v4l2.c:503-508) βuvc_drv_set_streampar(uvc_drv.c:1370-1387) βuvc_drv_try_frame_interval(uvc_drv.c:480) executed the continuous branch:
uvc_v4l2_set_streamparm <- production kprintf, uvc_drv.c:1377 Setting frame interval to 1/30 (333333). <- production kprintf, uvc_drv.c:1382 Fatal trap 18: integer divide fault while in kernel mode Stopped at uvc_drv_try_frame_interval.isra.2+0x60: divl %r8d,%eax
Trap 18 = x86 #DE; the faulting instruction is the / step at
uvc_drv.c:504. Guest panicked (ssh died, vm.sh status = down).
Line-by-line trace
- uvc_drv.c:1705-1706 β
nitv += (bFrameIntervalType > 0) ? ... : 3(slots reserved for continuous frames) - uvc_drv.c:1917-1921 β fill loop runs only
interval_typetimes - uvc_drv.c:1922-1923 β
if (frm->interval_type == 0) itv += 3;(dFrameInterval[0..2] min/max/step NEVER read β slots stay 0) - uvc_drv.c:500-504 β
step = frame->interval[2].val;(=0) theninterval = min + (interval - min + step/2) / step * step;β #DE - Reachability: the S_PARM path divides BEFORE any USB control request
(uvc_drv.c:1386-1387 precedes
uvc_drv_probe_videoat 1389), so no live device response is needed at trigger time β only thatv->cur_frmwas set to a continuous frame at attach (which the productionuvc_drv_init_cur_fmt_frmdoes from the device's own default-format answer; attacker-controlled). /dev/videoNis created 0666 byuvc_v4l2_reg(uvc_v4l2.c:783-785);v4l2_not_alloweddefaults to 0 (uvc_v4l2.c:66-72) β any local user can open it. (The finding said "mode 0666" β verified.)
Rig-synthesized parts
The malicious descriptor blob and the answered GET_DEF/GET_CUR probe
state (fabricated by calling production uvc_drv_init_cur_fmt_frm
directly) β in production both come from the device. The ioctl path from
/dev/video0 down to the division is 100 % production code. The guest
has no emulated UVC webcam, which is the only reason the hardware layer
is synthesized.
Note on the trigger source: DragonFly's VIDIOC_S_PARM is
_IOWR('V', 22, ...) (sys/contrib/v4l/videodev2.h:1830), not Linux's 75
β trigger_sparm.c mirrors the kernel header exactly.
Fix validation
fix.diff: read min/max/step for continuous frames when
bLength >= 26+12 (usb_desc_foreach guarantees those bytes are inside
the validated descriptor), else safe fallback 1/1/1; plus a step == 0
guard and min/max clamps in uvc_drv_try_frame_interval(). On the
patched rig (fix_build.log / fix_run.log):
- direct call:
step slot=3333,uvc_drv_try_frame_intervalreturned 3333200 β no #DE; - full unprivileged
VIDIOC_S_PARM:ioctl returned 0, no panic; the follow-on probe request fails gracefully against the rig's fake transport (USB_ERR_NOT_CONFIGUREDβ a rig artifact; a real device would answer).
Impact
Deterministic kernel panic (local DoS) from a single unprivileged ioctl on any UVC device exposing a continuous-interval frame β a descriptor shape fully controlled by a malicious/faulty webcam. Matches the finding's Medium severity (attacker-controlled-hardware precondition for the descriptor half; the trigger itself needs no privilege).
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
fixedWith fix.diff applied (guest /usr/src, module rebuilt -Werror clean): direct call prints step slot=3333 and uvc_drv_try_frame_interval returns 3333200 without #DE; the full unprivileged VIDIOC_S_PARM path returns 0 and the guest stays up (probe fails gracefully against the rig's fake transport).
fix_run.log: 'RIG2352direct: step slot=3333 ... returned 3333200 -- NO DIV-BY-ZERO'; 'slots after production uvc_drv_parse_data: min=2000000 max=10000000 step=3333'; 'trigger_sparm: ioctl returned 0 errno=0'; fix_build.log: clean patched build.
Confirmed kernel references
Detail
Exploit chain
Malicious webcam descriptor with continuous-interval frame -> attach parse leaves step==0 (M_ZERO slots) -> cur_frm = that frame via production uvc_drv_init_cur_fmt_frm (device answers GET_DEF/GET_CUR: attacker-controlled) -> any local user opens /dev/video0 (0666) -> VIDIOC_S_PARM -> production dispatch divides by step==0 -> x86 #DE -> kernel panic. Trigger needs no privilege and no device interaction at trigger time.
Evidence (decisive lines)
panic.txt: 'slots after production uvc_drv_parse_data: min=0 max=0 step=0' + '(descriptor carried min=2000000 max=10000000 step=3333)', 'uvc_v4l2_set_streamparm', 'Setting frame interval to 1/30 (333333).', 'Fatal trap 18: integer divide fault while in kernel mode', 'Stopped at uvc_drv_try_frame_interval.isra.2+0x60: divl %r8d,%eax'. fix_run.log: 'step slot=3333', 'returned 3333200 -- NO DIV-BY-ZERO', 'trigger_sparm: ioctl returned 0 errno=0'.
PoC changes
No PoC seed existed. Built from scratch. trigger_sparm.c mirrors the kernel's own sys/contrib/v4l/videodev2.h ioctl encoding β DragonFly's VIDIOC_S_PARM is _IOWR('V', 22, ...) (NOT Linux's 75); structs mirrored byte-exact. The rig needed the descriptor-blob corrections noted in DF-2351 (shared blob builder).
Verified recommended fix
Read dFrameInterval min/max/step DWORDs for bFrameIntervalType==0 frames in uvc_drv_parse_data (when bLength permits, else 1/1/1) and guard step==0 (plus min/max clamps) in uvc_drv_try_frame_interval
Verdict
REPRODUCED β deterministic divide-by-zero kernel panic triggered by an UNPRIVILEGED local user through the full production ioctl dispatch. Kernel log first proves the parse half: production uvc_drv_parse_data left a bFrameIntervalType==0 frame's interval slots at min=0 max=0 step=0 although the descriptor carried 2000000/10000000/3333 (the driver never reads dFrameInterval[] for continuous frames, uvc_drv.c:1922). Then uid-1001 ioctl(VIDIOC_S_PARM) on /dev/video0 (0666, production uvc_v4l2_reg) executed uvc_v4l2_ioctl -> uvc_drv_set_streampar -> uvc_drv_try_frame_interval continuous branch: Fatal trap 18 (integer divide fault), stopped at uvc_drv_try_frame_interval.isra.2+0x60: divl %r8d,%eax β the /step at uvc_drv.c:504. The division precedes any USB traffic (uvc_drv.c:1386-1387), so a single unprivileged ioctl suffices. FIX VALIDATED: patched parser reads min/max/step (slots printed 2000000/10000000/3333), step!=0 guard present; full unprivileged S_PARM path returns 0 with no panic.
No comments yet.