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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2352 Β· 15 files
FileTypeDescriptionSize
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
README.md readme human-facing repro + expected output
↓ download raw

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
VERDICT.md verdict full narrative, line-by-line trace, fix validation
↓ download raw

DF-2352 β€” VERDICT: REPRODUCED (panic, unprivileged ioctl) β€” FIX VALIDATED

Bottom line

Both halves of the finding were proven in one run:

  1. Parse skip: the kernel console shows the production uvc_drv_parse_data() leaving the interval slots of a bFrameIntervalType == 0 frame at min=0 max=0 step=0 even though the crafted descriptor carried min=2000000 max=10000000 step=3333 β€” the DWORDs are simply never read (uvc_drv.c:1917-1923 only walks interval_type entries, then itv += 3 past the M_ZERO slots).
  2. Divide by zero: an unprivileged user (uid 1001) issued ioctl(VIDIOC_S_PARM) on /dev/video0 (mode 0666, registered by production uvc_v4l2_reg, uvc_v4l2.c:783-785). The production dispatch chain uvc_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_type times
  • 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) then interval = 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_video at 1389), so no live device response is needed at trigger time β€” only that v->cur_frm was set to a continuous frame at attach (which the production uvc_drv_init_cur_fmt_frm does from the device's own default-format answer; attacker-controlled).
  • /dev/videoN is created 0666 by uvc_v4l2_reg (uvc_v4l2.c:783-785); v4l2_not_allowed defaults 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_interval returned 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

fixed
baseline reproduced→ patch + rebuild →patched clean

With 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.
↓ fix.diffper-fix-DF-2352

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.