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

Signed/unsigned index confusion in VIDIOC_ENUM_FRAMEINTERVALS: unprivileged OOB read of kernel heap at attacker-controlled negative offset (info leak + fatal-trap DoS)

Summary

The unpriv dispatch passes the user index verbatim; uvc_drv.c:750 assigns the __u32 to a signed int so any index >=0x80000000 becomes negative, the 'index < nintervals' guard is trivially satisfied, and interval[index].val loads 4 bytes up to -8GiB below the interval array (which sits at the tail of the single [fmt|frm|interval] slab) - the value flows through value-preserving uvc_simple_frac and is copyout'd. Proven from uid=1001 on the production 0666 cdev built by the real uvc_v4l2_reg: k=1 exact predicted MATCH, k=7 kernel pointer high half, k=33+ groom markers and freed-slab values across 3 rounds, index=0x80000000 -> Fatal trap 12 at the exact OOB instruction (guest down). Unsigned-index one-line fix validated on patched rebuild (EINVAL, guest survives the panic attempt). Any local user on a machine with a UVC webcam.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5427 Β· 20 files
FileTypeDescriptionSize
uvcharness.c β€” 10.7 KB view raw
trigger.c β€” 5.0 KB view raw
Makefile β€” 151 B ↓ download
build.sh β€” 141 B view raw
run.sh β€” 181 B view raw
run_panic.sh β€” 225 B view raw
build.log β€” 5.8 KB view raw
run.log β€” 5.5 KB view raw
run_user.log β€” 1.7 KB view raw
leak_sample.txt β€” 1.1 KB view raw
dmesg.txt β€” 0 B ↓ download
panic.txt β€” 5.4 KB view raw
env.txt β€” 381 B view raw
fix.diff β€” 1.5 KB view raw
fix_build.log β€” 5.9 KB view raw
fix_boot.log β€” 522 B view raw
fix.log β€” 1.3 KB view raw
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 5.3 KB ↓ raw
verdict.json β€” 4.6 KB view raw

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
VERDICT.md
↓ download raw

DF-5427 β€” VERDICT

Status: reproduced (unprivileged leak + unprivileged panic). Impact: leak (kernel heap disclosure, attacker-controlled negative offset) + panic (local DoS). Confidence: certain.

Reproduction narrative

  1. Static proof. uvc_v4l2.c:562-566 dispatches VIDIOC_ENUM_FRAMEINTERVALS with zero validation of itv->index (this ioctl is not even priority-gated, uvc_v4l2.c:469-490). uvc_drv.c:750 copies the __u32 user index into a signed int; uvc_drv.c:756's index < nintervals accepts every negative value; uvc_drv.c:768 then loads frm->interval[index].val β€” a plain movl (%rax,%r10,4),%eax (confirmed in the panic backtrace) at interval + 4*index, i.e. up to 8 GiB below the interval array. The slab holding the array is the single kmalloc at uvc_drv.c:1726-1731 ([fmt|frm|interval], 144 bytes for the harness camera), so anything from index = -33 on reads other heap objects below the allocation.

  2. Rig. uvcharness.c compiles the production uvc sources into a KLD, parses a benign YUY2/640x480/4-discrete-interval config exactly as a real webcam does (uvc_drv_init_ctrl + uvc_drv_parse_ctrl), and calls the production uvc_v4l2_reg() (uvc_v4l2.c:771-799) which does the real make_dev(..., 0666, "video97"). Before each parse it grooms same-size heap chunks with a per-round marker (0x5A/0xA7/0xC3) so the memory below the slab is identifiable, and dumps the kernel-side ground truth of interval[-i], i=1..60.

  3. Unprivileged leak (run_user.log, uid=1001). * Ground truth (dmesg.txt/panic.txt): interval[-1] = 0x01e00280 (= width | height<<16), [-2] = 0x00096000 (max_buffer_size), [-3] = 0x00051615 (default_interval), markers 5a5a5a5a at [-33].. in the marker rounds, kernel pointers (fffff800 8d6416d0) at [-7]/[-8]. * Userland: index=0x80000000|... β†’ wait, small negatives are (uint32_t)-k; TRIGGER k=1 expected num/den=22203/7058 got 22203/7058 -> MATCH β€” the unpriv process received exactly the CF-transformed value of the 4 bytes at interval[-1], predicted by a userland mirror of uvc_simple_frac(). k=2 β†’ β‰ˆ614406.8 (=0x96000), k=3 β†’ 333333.3, k=4/5 β†’ β‰ˆ3109919.6 (=0x2f7400 dMinBitRate), k=7 β†’ 4290000000 (β‰ˆ0xfffff800, kernel pointer high half), k=8 varies per round (β‰ˆ0x8d641xxx, heap address). * Outside the allocation (k=33+): round 1 disclosed the marker 0x5A5A5A5A (=1515870810, received as 1515870967.7 β‰ˆ CF rounding), round 2 the marker 0xA7A7A7A7 (2812782695 β†’ 2812782503.0), round 3 stale interval values (1000000/666666/400000/333333) of a previously-freed slab β€” kernel heap contents below the allocation, varying with heap state, disclosed to uid=1001 across 3 runs (leak_sample.txt). * The value passes through uvc_simple_frac() (a continued-fraction reduction of val/10^7): deterministic and effectively value-preserving to β‰ˆ10^7/denΒ², so pointer-sized material is recoverable to ~unit precision (k=1 MATCH proves the transform is exactly invertible-predictable).

  4. Unprivileged panic (panic.txt). run_user '/tmp/trig --panic' with index = 0x80000000 (INT_MIN β‡’ read at interval βˆ’ 8 GiB): Fatal user address access from kernel mode from trig Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff7fe4f1e9950 (= 0xfffff8004f1e9950 - 8GiB) Stopped at uvc_drv_enum_v4l2_frameintervals+0x18b: movl (%rax,%r10,4),%eax Guest down (ssh hung, vm.sh status β‡’ down), triggered by the uid=1001 process trig.

  5. Fix validation (fix.log, fix_build.log, fix_boot.log). fix.diff makes index uint32_t and compares in unsigned arithmetic. Applied to the guest's /usr/src, harness rebuilt against the patched tree: * every negative index β†’ EINVAL ("Invalid argument"), nothing copied out β€” also the kernel-side sanity call now returns ret=22 with num=0 den=0; * in-bounds index=0 β†’ still 1/30 (no functional regression); * --panic β†’ EINVAL, guest stays up (vm.sh status: up). Guest was then vm.sh reset with-src back to clean state.

Why not uid0

This is a pure OOB read of 4-byte units at controlled negative offsets with a CF-transformed copyout. It is a high-quality heap-disclosure primitive (offset fully controlled, repeatable, no crash for near offsets) β€” ideal for heap-layout/KASLR-adjacent recon in a chain, but no write primitive exists on this path, so local root is not reachable from this bug alone. (Guest has no KASLR; the leak is still the real primitive.)

Attempts

3 (1: Werror truncation build fix; 2: harness sanity call used 0x80000001 = INT_MIN+2 instead of -1 β†’ accidental kernel-context panic that pre-confirmed the fault address/instruction; 3: corrected indices β†’ full unpriv leak + unpriv panic + fix validation).

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

fix.diff applied to guest /usr/src (patch -p0, both hunks clean); harness rebuilt from patched tree; all negative indices return EINVAL with nothing copied out, in-bounds index=0 still returns 1/30, --panic attempt returns EINVAL and the guest stays up (fix.log, vm.sh status up). Baseline bad behavior gone.

fix.log (unpriv trigger on patched build: 'ioctl failed: Invalid argument' for every negative k, 'UNEXPECTED survival' for --panic with guest up); fix_boot.log ('kernel-side negative call ret=22 ... num=0 den=0'); fix_build.log (clean rebuild).
↓ fix.diffper-fix-DF-5427

Confirmed kernel references

Detail

Exploit chain

open /dev/videoN (0666, no privilege, no priority needed) -> ioctl(VIDIOC_ENUM_FRAMEINTERVALS, {.index=(uint32_t)-k, .pixel_format=, .width/.height=}) -> kernel reads 4 bytes at interval-4k (inside slab: adjacent object fields; beyond slab: prior/adjacent heap chunks; kernel pointer halves observed) -> CF-transformed 4-byte value returned to user. Wild index (0x80000000) -> unmapped load -> fatal trap 12 -> local DoS. Read-only primitive: strong heap-disclosure for chaining, no write, no direct uid0.

Evidence (decisive lines)

run_user.log: 'TRIGGER k=1 ... got num/den=22203/7058 -> MATCH' as uid=1001; k=7 -> 4290000000 (~0xfffff800) and k=8 -> ~0x8d641xxx (kernel heap pointers); LEAKSAMPLE k=33+ markers per round (leak_sample.txt); panic.txt: 'Fatal user address access from kernel mode from trig', fault 0xfffff7fe4f1e9950 = interval-8GiB, 'Stopped at uvc_drv_enum_v4l2_frameintervals+0x18b: movl (%rax,%r10,4),%eax'; fix.log: all negative indices EINVAL + guest survives --panic; dmesg.txt: kernel-side ground truth scan interval[-i].

PoC changes

Seed sketch rewritten: (1) harness registers the PRODUCTION cdev via uvc_v4l2_reg() so the unpriv userland ioctl path (uvc_v4l2_ioctl dispatch) is exercised for real; (2) small negative offsets are (uint32_t)-k, NOT 0x80000000|k (first attempt used the latter for the sanity call and faulted the kernel at k=1's INT_MIN+2 address - kept as accidental kernel-context pre-proof); (3) local v4l2 struct/ioctl definitions + userland mirror of uvc_simple_frac() to predict the copied-out rational; (4) heap groom with same-size marker chunks so below-slab reads are identifiable.

Verified recommended fix

Keep itv->index in a uint32_t and do the bounds check in unsigned arithmetic (see fix.diff: uvc_drv.c uvc_drv_enum_v4l2_frameintervals).

Verdict

Unprivileged VIDIOC_ENUM_FRAMEINTERVALS with index>=0x80000000 turns the signed bounds check at uvc_drv.c:756 into a no-op and reads frm->interval[index].val (uvc_drv.c:768) at an attacker-chosen negative word offset (to -8GiB) below the interval slab; the value is continued-fraction transformed and copied out to any local user via the 0666 /dev/videoN cdev. Verified on the guest: uid=1001 received exactly the predicted interval[-1] value (MATCH 22203/7058), groom markers 0x5A5A5A5A/0xA7A7A7A7 and stale freed-slab contents below the allocation across 3 rounds, kernel pointer halves (0xfffff800...) at k=7/43; index=0x80000000 gave Fatal trap 12 (page fault at interval-8GiB) from the unpriv process, guest down. fix.diff (unsigned index) validated on a rebuilt guest kernel-tree harness: all negative indices -> EINVAL, in-bounds index=0 unchanged, panic attempt harmless, guest up.