Skip site navigation (1)Skip section navigation (2)
Date:      Tue, 21 Jul 2026 15:55:14 +0000
From:      Baptiste Daroussin <bapt@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Subject:   git: a5307a57c46f - main - uvideo: fix use-after-free in mmap buffer lifetime management
Message-ID:  <6a5f9662.2688f.1c1a6132@gitrepo.freebsd.org>

index | next in thread | raw e-mail

The branch main has been updated by bapt:

URL: https://cgit.FreeBSD.org/src/commit/?id=a5307a57c46f16f5b3c29708f1e528963dea150c

commit a5307a57c46f16f5b3c29708f1e528963dea150c
Author:     Baptiste Daroussin <bapt@FreeBSD.org>
AuthorDate: 2026-07-20 13:52:06 +0000
Commit:     Baptiste Daroussin <bapt@FreeBSD.org>
CommitDate: 2026-07-21 15:53:13 +0000

    uvideo: fix use-after-free in mmap buffer lifetime management
    
    The uvideo driver freed the mmap buffer (contigmalloc'd) in several
    paths (VIDIOC_STREAMOFF, last close, detach) without coordinating
    with the lifetime of existing user-space mmap mappings.  This could
    lead to use-after-free when user-space continued to access the
    mapped memory after the backing pages had been freed.
    
    Fix this by switching from the simple d_mmap callback to d_mmap_single
    with custom cdev_pager_ops, and by attaching the contig buffer to a
    single shared vm_object created at REQBUFS time:
    
    - uvideo_reqbufs() allocates a uvideo_mmap_state (independent of the
      softc) and a shared vm_object via cdev_pager_allocate() that spans
      the whole buffer; the softc holds one reference to it.
    - uvideo_cdev_mmap_single() simply hands out additional references to
      that shared object; the requested offset selects which buffer is
      mapped.  The VM system tracks mapping lifetime through the object
      reference count, so no per-mapping bookkeeping is needed.
    - uvideo_pg_ctor/uvideo_pg_dtor validate the mapping and free the
      contig buffer together with the state when the last reference
      (softc's own or a user mapping) is dropped.
    - uvideo_pg_fault installs a fictitious page for the backing physical
      address, following the canonical device-pager pattern: update the
      passed-in page in place when it is already fictitious, otherwise
      allocate a fake page and vm_page_replace() the busy placeholder,
      so that dev_pager_dealloc() does not deadlock.
    - uvideo_vs_free_frame() drops the softc's reference instead of
      contigfree()'ing directly; if mappings still exist the buffer stays
      alive until the last uvideo_pg_dtor().
    - VIDIOC_STREAMOFF no longer frees the buffer (per V4L2 spec).
    - Last close always releases the buffer (deferred if mappings exist).
    - The mmap_state outlives the softc, so the pager dtor can safely
      free the buffer even after device detach.
    
    Reported by:    章鱼哥 (@aipyapp) (www.aipyaipy.com)
    Reported by:    Chris Jarrett-Davies of the OpenAI Codex Security Team
---
 sys/dev/usb/video/uvideo.c | 176 +++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 162 insertions(+), 14 deletions(-)

diff --git a/sys/dev/usb/video/uvideo.c b/sys/dev/usb/video/uvideo.c
index 910915f9f745..07f74f55b121 100644
--- a/sys/dev/usb/video/uvideo.c
+++ b/sys/dev/usb/video/uvideo.c
@@ -43,9 +43,13 @@
 #include <sys/limits.h>
 #include <sys/sysctl.h>
 #include <sys/uio.h>
+#include <sys/rwlock.h>
 
 #include <vm/vm.h>
 #include <vm/pmap.h>
+#include <vm/vm_page.h>
+#include <vm/vm_object.h>
+#include <vm/vm_pager.h>
 
 #include <dev/usb/usb.h>
 #include <dev/usb/usbdi.h>
@@ -163,7 +167,26 @@ static d_read_t		uvideo_cdev_read;
 static d_ioctl_t	uvideo_cdev_ioctl;
 static d_poll_t		uvideo_cdev_poll;
 static d_kqfilter_t	uvideo_cdev_kqfilter;
-static d_mmap_t		uvideo_cdev_mmap;
+static d_mmap_single_t	uvideo_cdev_mmap_single;
+static int		uvideo_pg_ctor(void *, vm_ooffset_t, vm_prot_t,
+			    vm_ooffset_t, struct ucred *, u_short *);
+static void		uvideo_pg_dtor(void *);
+static int		uvideo_pg_fault(vm_object_t, vm_ooffset_t, int,
+			    vm_page_t *);
+
+/*
+ * Per-buffer state for mmap lifetime management.  Allocated together
+ * with the contig buffer at REQBUFS time and attached to a single
+ * shared vm_object whose reference count tracks the active mappings.
+ * The buffer is freed by cdev_pg_dtor() when the last reference (the
+ * softc's own or a user mapping) is dropped.  Lives independently of
+ * the softc so the pager dtor can safely free the buffer even after
+ * device detach.
+ */
+struct uvideo_mmap_state {
+	uint8_t				*ms_buffer;
+	size_t				ms_buffer_size;
+};
 
 static int	uvideo_querycap(struct uvideo_softc *, struct v4l2_capability *);
 static int	uvideo_enum_fmt(struct uvideo_softc *, struct v4l2_fmtdesc *);
@@ -239,6 +262,7 @@ struct uvideo_softc {
 	q_mmap			sc_mmap_q;
 	int			sc_mmap_count;
 	int			sc_mmap_flag;
+	vm_object_t			sc_mmap_object;
 
 	uint8_t			*sc_tmpbuf;
 	int			sc_tmpbuf_size;
@@ -721,6 +745,12 @@ static const struct usb_config uvideo_bulk_config[1] = {
 	},
 };
 
+static const struct cdev_pager_ops uvideo_pager_ops = {
+	.cdev_pg_ctor = uvideo_pg_ctor,
+	.cdev_pg_dtor = uvideo_pg_dtor,
+	.cdev_pg_fault = uvideo_pg_fault,
+};
+
 /*
  * Character device switch
  */
@@ -732,7 +762,7 @@ static struct cdevsw uvideo_cdevsw = {
 	.d_ioctl = uvideo_cdev_ioctl,
 	.d_poll = uvideo_cdev_poll,
 	.d_kqfilter = uvideo_cdev_kqfilter,
-	.d_mmap = uvideo_cdev_mmap,
+	.d_mmap_single = uvideo_cdev_mmap_single,
 	.d_name = "video",
 };
 
@@ -910,6 +940,7 @@ uvideo_attach(device_t dev)
 	/* Init mmap queue */
 	STAILQ_INIT(&sc->sc_mmap_q);
 	sc->sc_mmap_count = 0;
+	sc->sc_mmap_object = NULL;
 
 	/* Allocate unit number and create character device */
 	make_dev_args_init(&args);
@@ -2265,9 +2296,15 @@ uvideo_vs_free_frame(struct uvideo_softc *sc)
 		fb->buf = NULL;
 	}
 
-	if (sc->sc_mmap_buffer != NULL) {
-		contigfree(sc->sc_mmap_buffer, sc->sc_mmap_buffer_size,
-		    M_USBDEV);
+	if (sc->sc_mmap_object != NULL) {
+		/*
+		 * Drop the softc's reference to the shared mmap object.
+		 * If user-space mappings still exist, the object and its
+		 * backing contig buffer stay alive until the last mapping
+		 * is dropped, at which point uvideo_pg_dtor() frees them.
+		 */
+		vm_object_deallocate(sc->sc_mmap_object);
+		sc->sc_mmap_object = NULL;
 		sc->sc_mmap_buffer = NULL;
 		sc->sc_mmap_buffer_size = 0;
 	}
@@ -2857,9 +2894,15 @@ uvideo_cdev_close(struct cdev *dev, int flags, int fmt, struct thread *td)
 		sc->sc_streaming = 0;
 		mtx_unlock(&sc->sc_mtx);
 		uvideo_vs_close(sc);
-		uvideo_vs_free_frame(sc);
 	}
 
+	/*
+	 * Release mmap buffer.  If there are still active mmap
+	 * mappings, the actual contigfree() is deferred to the
+	 * last uvideo_pg_dtor() invocation.
+	 */
+	uvideo_vs_free_frame(sc);
+
 	if (sc->sc_fbuffer != NULL) {
 		free(sc->sc_fbuffer, M_USBDEV);
 		sc->sc_fbuffer = NULL;
@@ -3083,28 +3126,110 @@ uvideo_cdev_kqfilter(struct cdev *dev, struct knote *kn)
 }
 
 static int
-uvideo_cdev_mmap(struct cdev *dev, vm_ooffset_t offset, vm_paddr_t *paddr,
-    int nprot, vm_memattr_t *memattr)
+uvideo_cdev_mmap_single(struct cdev *dev, vm_ooffset_t *offset,
+    vm_size_t size, struct vm_object **object, int nprot)
 {
 	struct uvideo_softc *sc = dev->si_drv1;
 
 	if (sc == NULL || sc->sc_dying)
 		return (ENXIO);
 
-	if (offset >= sc->sc_mmap_buffer_size)
+	if (sc->sc_mmap_object == NULL)
 		return (EINVAL);
 
-	if (sc->sc_mmap_buffer == NULL)
+	if (*offset >= sc->sc_mmap_buffer_size)
 		return (EINVAL);
 
-	if (!sc->sc_mmap_flag)
-		sc->sc_mmap_flag = 1;
+	if (*offset + size > sc->sc_mmap_buffer_size)
+		size = sc->sc_mmap_buffer_size - *offset;
 
-	*paddr = vtophys(sc->sc_mmap_buffer + offset);
+	/*
+	 * Hand out a reference to the shared mmap object, which spans the
+	 * whole buffer; the requested offset selects which buffer within
+	 * it is mapped.  The VM system tracks mapping lifetime through the
+	 * object reference count, so no per-mapping bookkeeping is needed.
+	 */
+	vm_object_reference(sc->sc_mmap_object);
+	*object = sc->sc_mmap_object;
+
+	sc->sc_mmap_flag = 1;
+
+	return (0);
+}
+
+static int
+uvideo_pg_ctor(void *handle, vm_ooffset_t size, vm_prot_t prot,
+    vm_ooffset_t foff, struct ucred *cred, u_short *color)
+{
+	struct uvideo_mmap_state *ms = handle;
+
+	if (ms->ms_buffer == NULL || foff + size > ms->ms_buffer_size)
+		return (EINVAL);
 
+	*color = 0;
 	return (0);
 }
 
+static void
+uvideo_pg_dtor(void *handle)
+{
+	struct uvideo_mmap_state *ms = handle;
+
+	/*
+	 * The last reference to the shared mmap object is gone (either the
+	 * softc released it, or the final user mapping was dropped).  Free
+	 * the backing contig buffer and the state.
+	 */
+	if (ms->ms_buffer != NULL)
+		contigfree(ms->ms_buffer, ms->ms_buffer_size, M_USBDEV);
+	free(ms, M_USBDEV);
+}
+
+static int
+uvideo_pg_fault(vm_object_t object, vm_ooffset_t offset, int prot,
+    vm_page_t *mres)
+{
+	struct uvideo_mmap_state *ms = object->un_pager.devp.handle;
+	vm_paddr_t paddr;
+	vm_page_t m;
+
+	if (ms->ms_buffer == NULL)
+		return (VM_PAGER_FAIL);
+
+	if (offset >= ms->ms_buffer_size)
+		return (VM_PAGER_FAIL);
+
+	paddr = vtophys(ms->ms_buffer + offset);
+	if (paddr == 0)
+		return (VM_PAGER_FAIL);
+
+	if (((*mres)->flags & PG_FICTITIOUS) != 0) {
+		/*
+		 * The passed-in page is already a fake page: just update
+		 * its physical address in place.
+		 */
+		m = *mres;
+		vm_page_updatefake(m, paddr, object->memattr);
+	} else {
+		/*
+		 * Replace the placeholder page allocated by the generic
+		 * fault path with our own fake page.  vm_page_getfake() can
+		 * allocate and must not be called with the object lock held;
+		 * vm_page_replace() then swaps the new page into the object
+		 * and frees the placeholder.  Without this the busy
+		 * placeholder stays in the object and dev_pager_dealloc()
+		 * dead-locks trying to acquire it.
+		 */
+		VM_OBJECT_WUNLOCK(object);
+		m = vm_page_getfake(paddr, object->memattr);
+		VM_OBJECT_WLOCK(object);
+		vm_page_replace(m, object, (*mres)->pindex, *mres);
+		*mres = m;
+	}
+	m->valid = VM_PAGE_BITS_ALL;
+	return (VM_PAGER_OK);
+}
+
 /* ---------------------------------------------------------------- */
 /*  V4L2 Ioctl Handlers                                             */
 /* ---------------------------------------------------------------- */
@@ -3508,6 +3633,7 @@ static int
 uvideo_reqbufs(struct uvideo_softc *sc, struct v4l2_requestbuffers *rb)
 {
 	int i, buf_size, buf_size_total;
+	struct uvideo_mmap_state *ms;
 
 	DPRINTFN(1, "reqbufs: count=%d\n", rb->count);
 
@@ -3537,6 +3663,29 @@ uvideo_reqbufs(struct uvideo_softc *sc, struct v4l2_requestbuffers *rb)
 	}
 	sc->sc_mmap_buffer_size = buf_size_total;
 
+	/*
+	 * Create a single shared vm_object backing the whole mmap buffer.
+	 * Its reference count (bumped by each mmap() and held by the softc)
+	 * tracks the lifetime of the mappings; the contig buffer is freed
+	 * by uvideo_pg_dtor() when the last reference goes away, which may
+	 * be after device detach.
+	 */
+	ms = malloc(sizeof(*ms), M_USBDEV, M_WAITOK | M_ZERO);
+	ms->ms_buffer = sc->sc_mmap_buffer;
+	ms->ms_buffer_size = sc->sc_mmap_buffer_size;
+	sc->sc_mmap_object = cdev_pager_allocate(ms, OBJT_DEVICE,
+	    &uvideo_pager_ops, sc->sc_mmap_buffer_size, VM_PROT_ALL,
+	    0, curthread->td_ucred);
+	if (sc->sc_mmap_object == NULL) {
+		free(ms, M_USBDEV);
+		contigfree(sc->sc_mmap_buffer, sc->sc_mmap_buffer_size,
+		    M_USBDEV);
+		sc->sc_mmap_buffer = NULL;
+		sc->sc_mmap_buffer_size = 0;
+		sc->sc_mmap_count = 0;
+		return (ENOMEM);
+	}
+
 	DPRINTFN(1, "allocated %d bytes mmap buffer\n", buf_size_total);
 
 	for (i = 0; i < sc->sc_mmap_count; i++) {
@@ -3671,7 +3820,6 @@ uvideo_streamoff(struct uvideo_softc *sc, int type)
 	mtx_unlock(&sc->sc_mtx);
 
 	uvideo_vs_close(sc);
-	uvideo_vs_free_frame(sc);
 
 	return (0);
 }


home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?6a5f9662.2688f.1c1a6132>