The branch main has been updated by bapt: URL: https://cgit.FreeBSD.org/src/commit/?id=a5307a57c46f16f5b3c29708f1e528963dea150c
commit a5307a57c46f16f5b3c29708f1e528963dea150c Author: Baptiste Daroussin <[email protected]> AuthorDate: 2026-07-20 13:52:06 +0000 Commit: Baptiste Daroussin <[email protected]> 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); }
