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)
PoC verification
Evidence pack
findings/poc/DF-5427 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_FRAMEINTERVALSdispatch passes the userstruct v4l2_frmivalenum *straight through. - Root cause / sink:
sys/bus/u4b/uvc/uvc_drv.c:750(int index = itv->index;β__u32user 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_IOWRioctl). - 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; processtrigruns 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=0still returns1/30(no regression) --panicattempt β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
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
-
Static proof.
uvc_v4l2.c:562-566dispatchesVIDIOC_ENUM_FRAMEINTERVALSwith zero validation ofitv->index(this ioctl is not even priority-gated,uvc_v4l2.c:469-490).uvc_drv.c:750copies the__u32user index into a signedint;uvc_drv.c:756'sindex < nintervalsaccepts every negative value;uvc_drv.c:768then loadsfrm->interval[index].valβ a plainmovl (%rax,%r10,4),%eax(confirmed in the panic backtrace) atinterval + 4*index, i.e. up to 8 GiB below the interval array. The slab holding the array is the singlekmallocatuvc_drv.c:1726-1731([fmt|frm|interval], 144 bytes for the harness camera), so anything fromindex = -33on reads other heap objects below the allocation. -
Rig.
uvcharness.ccompiles 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 productionuvc_v4l2_reg()(uvc_v4l2.c:771-799) which does the realmake_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 ofinterval[-i], i=1..60. -
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), markers5a5a5a5aat [-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 atinterval[-1], predicted by a userland mirror ofuvc_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 throughuvc_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). -
Unprivileged panic (panic.txt).
run_user '/tmp/trig --panic'withindex = 0x80000000(INT_MIN β read atinterval β 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),%eaxGuest down (ssh hung,vm.sh statusβ down), triggered by the uid=1001 processtrig. -
Fix validation (fix.log, fix_build.log, fix_boot.log).
fix.diffmakesindexuint32_tand 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 returnsret=22withnum=0 den=0; * in-boundsindex=0β still1/30(no functional regression); *--panicβEINVAL, guest stays up (vm.sh status: up). Guest was thenvm.sh reset with-srcback 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
fixedfix.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).
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=
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.
No comments yet.