git: a5307a57c46f - main - uvideo: fix use-after-free in mmap buffer lifetime management
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 21 Jul 2026 15:55:14 UTC
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);
}