# DF-5427 — signed/unsigned index confusion in VIDIOC_ENUM_FRAMEINTERVALS → kernel heap OOB read (info leak + panic DoS)

## Where

* Entry (unprivileged, /dev/videoN is 0666, no pri required):
  `sys/bus/u4b/uvc/uvc_v4l2.c:562-566` — `VIDIOC_ENUM_FRAMEINTERVALS`
  dispatch passes the user `struct v4l2_frmivalenum *` straight through.
* Root cause / sink: `sys/bus/u4b/uvc/uvc_drv.c:750`
  (`int index = itv->index;` — `__u32` user value into signed int),
  `:756` (`if (index < nintervals)` — passes trivially when negative),
  `:768` (`frm->interval[index].val` — OOB read at attacker-chosen
  negative word offset, value then copied back out by the `_IOWR` ioctl).
* Allocation walked off: `uvc_drv.c:1726-1731` — one slab
  `[fmt array | frm array | interval slab]`; the read walks backward
  through the slab and past its start into adjacent kernel heap.

## Attack model

Any local user (the cdev is `crw-rw-rw- root video`) on a machine with a
UVC webcam issues `ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, {.index =
0xFFFFFFFF - (k-1), .pixel_format = <known fcc>, .width/w height = <known
frame>})`.  `index >= 0x80000000` turns into a negative `int`, the bounds
check is satisfied, and `frm->interval[index].val` reads 4 bytes of kernel
heap at `interval - 4k`.  The value is transformed by
`uvc_simple_frac()` (continued fraction of val/10^7 — effectively
value-preserving to ~1e7/den²) and copied to userspace.  With
`index = 0x80000000` (−8 GiB) the load hits an unmapped address and
panics the kernel.

## Build / run (as verified)

    ./build.sh                  # KLD harness that #includes the REAL uvc sources
                                # + unpriv trigger
    # on guest:  kldload /root/build/uvcharness.ko   (round 1 + /dev/video97)
    run.sh                      # 3 groom rounds + unpriv trigger (root ctx)
    # as unpriv user:
    /tmp/trig                   # leak proof (k=1 deterministic MATCH, k=33+ heap)
    /tmp/trig --panic           # guest dies: Fatal trap 12

## Expected (baseline, verified 2026-10-04)

* `TRIGGER k=1 ... expected num/den=22203/7058 got num/den=22203/7058 -> MATCH`
  (interval[-1] = frm tail = width|height<<16 = 0x01e00280)
* k=2..8 disclose frame/format fields incl. kernel pointer halves
  (k=7 → ≈0xFFFFF800, k=8 → ≈0x8d641xxx)
* k=33+ (outside the 144-byte slab): groom markers 0x5A5A5A5A /
  0xA7A7A7A7 and stale freed-slab interval values across rounds
* `--panic`: `Fatal user address access from kernel mode from trig`,
  `fault virtual address = 0xfffff7fe4f1e9950` (= interval − 8 GiB),
  `Stopped at uvc_drv_enum_v4l2_frameintervals+0x18b: movl (%rax,%r10,4),%eax`
  (panic.txt; process `trig` runs as uid=1001)

## After fix.diff (applied to guest /usr/src, harness rebuilt)

* every negative index → `EINVAL`, no value copied out (fix.log)
* in-bounds `index=0` still returns `1/30` (no regression)
* `--panic` attempt → `EINVAL`, guest stays up (`vm.sh status: up`)

## Files

    uvcharness.c   KLD harness (real driver sources compiled in, real
                   uvc_v4l2_reg() cdev, groom + ground-truth dump)
    trigger.c      unprivileged userland trigger (self-contained v4l2 defs
                   + mirror of uvc_simple_frac for value prediction)
    build.sh/run.sh/run_panic.sh, Makefile
    build.log run.log run_user.log leak_sample.txt dmesg.txt panic.txt env.txt
    fix.diff       unsigned-index fix (validated)
    fix_build.log fix_boot.log fix.log
