* [PATCH v10 0/5] media: add virtio-media driver
@ 2026-10-29 15:30 Brian Daniels
2026-10-29 15:30 ` [PATCH v10 1/5] media: virtio: Add core driver and session management Brian Daniels
` (4 more replies)
0 siblings, 5 replies; 11+ messages in thread
From: Brian Daniels @ 2026-10-29 15:30 UTC (permalink / raw)
To: Mauro Carvalho Chehab
Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
gurchetansingh, hverkuil, linux-kernel, linux-media, mst,
nicolas.dufresne, virtualization, xuanzhuo, dbassey,
laurent.pinchart, Brian Daniels
From: Alexandre Courbot <gnurou@gmail.com>
Add the first version of the virtio-media driver.
This driver acts roughly as a V4L2 relay between user-space and the
virtio virtual device on the host, so it is relatively simple, yet
unconventional. It doesn't use VB2 or other frameworks typically used in
a V4L2 driver, and most of its complexity resides in correctly and
efficiently building the virtio descriptor chain to pass to the host,
avoiding copies whenever possible. This is done by
scatterlist_builder.[ch].
This version supports MMAP buffers. DMABUF support is still pending.
---
Guest Setup
Tests were ran on a Debian 12 guest running with crosvm. The guest image
was created with:
$ virt-builder debian-12 --root-password password:""
Build crosvm and launch the guest starting at the "Crosvm" section on
this page: https://github.com/chromeos/virtio-media/blob/main/TRY_IT_OUT.md#crosvm
NOTE: Before running v4l2-compliance in the guest, you need to install
v4l-utils and ffmpeg:
$ apt update && apt install v4l-utils ffmpeg
---
Compliance Testing
This was tested using v4l2-compliance. Since virtio-media serves as
a proxy to host devices for the guest VMs, we expect the guest
compliance test to essentially match the host compliance test for the
same device.
NOTE: v4l2-compliance changes its test behavior depending on the driver
name. In the guest, the driver name for virtio-media proxied-devices is
always "virtio-media", even if the actual host device has a driver name
of e.g. "uvcvideo". To ensure the test is consistent between the host
and the guest, I created a patch for the v4l2-compliance tool that
allows you to override the driver name. All test results that follow use
this patch:
https://lore.kernel.org/all/20261029152030.3764241-1-briandaniels@google.com/
All tests used a Logitech USB Webcam C925e.
As tested on the host:
$ v4l2-compliance -d1 -s
v4l2-compliance 1.33.0-5471, 64 bits, 64-bit time_t
v4l2-compliance SHA: 9f2d3ea879ff 2026-05-28 14:45:11
Compliance test for uvcvideo device /dev/video1:
Driver Info:
Driver name : uvcvideo
Card type : Logitech Webcam C925e
Bus info : usb-0000:04:00.1-3
Driver version : 6.18.14
Capabilities : 0x84a00001
Video Capture
Metadata Capture
Streaming
Extended Pix Format
Device Capabilities
Device Caps : 0x04200001
Video Capture
Streaming
Extended Pix Format
Media Driver Info:
Driver name : uvcvideo
Model : Logitech Webcam C925e
Serial : 686F371F
Bus info : usb-0000:04:00.1-3
Media version : 6.18.14
Hardware revision: 0x00000016 (22)
Driver version : 6.18.14
Interface Info:
ID : 0x03000002
Type : V4L Video
Entity Info:
ID : 0x00000001 (1)
Name : Logitech Webcam C925e
Function : V4L2 I/O
Flags : default
Pad 0x01000007 : 0: Sink
Link 0x0200001f: from remote pad 0x100000a of entity 'Processing 3' (Video Pixel Formatter): Data, Enabled, Immutable
Required ioctls:
test MC information (see 'Media Driver Info' above): OK
test VIDIOC_QUERYCAP: OK
test invalid ioctls: OK
Allow for multiple opens:
test second /dev/video1 open: OK
test VIDIOC_QUERYCAP: OK
test VIDIOC_G/S_PRIORITY: OK
test for unlimited opens: OK
Debug ioctls:
test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported)
test VIDIOC_LOG_STATUS: OK (Not Supported)
Input ioctls:
test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
test VIDIOC_ENUMAUDIO: OK (Not Supported)
test VIDIOC_G/S/ENUMINPUT: OK
test VIDIOC_G/S_AUDIO: OK (Not Supported)
Inputs: 1 Audio Inputs: 0 Tuners: 0
Output ioctls:
test VIDIOC_G/S_MODULATOR: OK (Not Supported)
test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
test VIDIOC_ENUMAUDOUT: OK (Not Supported)
test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
test VIDIOC_G/S_AUDOUT: OK (Not Supported)
Outputs: 0 Audio Outputs: 0 Modulators: 0
Input/Output configuration ioctls:
test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK (Not Supported)
test VIDIOC_DV_TIMINGS_CAP: OK (Not Supported)
test VIDIOC_G/S_EDID: OK (Not Supported)
Control ioctls (Input 0):
test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
test VIDIOC_QUERYCTRL: OK
test VIDIOC_G/S_CTRL: OK
fail: v4l2-test-controls.cpp(983): ret != EINVAL (got 13)
test VIDIOC_G/S/TRY_EXT_CTRLS: FAIL
test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
Standard Controls: 19 Private Controls: 0
Format ioctls (Input 0):
test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK
test VIDIOC_G/S_PARM: OK
test VIDIOC_G_FBUF: OK (Not Supported)
test VIDIOC_G_FMT: OK
test VIDIOC_TRY_FMT: OK
test VIDIOC_S_FMT: OK
test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
test Cropping: OK (Not Supported)
test Composing: OK (Not Supported)
test Scaling: OK (Not Supported)
Codec ioctls (Input 0):
test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
test VIDIOC_G_ENC_INDEX: OK (Not Supported)
test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)
Buffer ioctls (Input 0):
test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK
test CREATE_BUFS maximum buffers: OK
test VIDIOC_REMOVE_BUFS: OK
test VIDIOC_EXPBUF: OK
test Requests: OK (Not Supported)
test blocking wait: OK
Test input 0:
Streaming ioctls:
test read/write: OK (Not Supported)
Video Capture: Frame #000
Video Capture: Frame #001
Video Capture: Frame #002
Video Capture: Frame #003
Video Capture: Frame #004
Video Capture: Frame #005
Video Capture: Frame #006
Video Capture: Frame #007
Video Capture: Frame #008
Video Capture: Frame #009
Video Capture: Frame #010
Video Capture: Frame #011
Video Capture: Frame #012
Video Capture: Frame #013
Video Capture: Frame #014
Video Capture: Frame #015
Video Capture: Frame #016
Video Capture: Frame #017
Video Capture: Frame #018
Video Capture: Frame #019
Video Capture: Frame #020
Video Capture: Frame #021
Video Capture: Frame #022
Video Capture: Frame #023
Video Capture: Frame #024
Video Capture: Frame #025
Video Capture: Frame #026
Video Capture: Frame #027
Video Capture: Frame #028
Video Capture: Frame #029
Video Capture: Frame #030
Video Capture: Frame #031
Video Capture: Frame #032
Video Capture: Frame #033
Video Capture: Frame #034
Video Capture: Frame #035
Video Capture: Frame #036
Video Capture: Frame #037
Video Capture: Frame #038
Video Capture: Frame #039
Video Capture: Frame #040
Video Capture: Frame #041
Video Capture: Frame #042
Video Capture: Frame #043
Video Capture: Frame #044
Video Capture: Frame #045
Video Capture: Frame #046
Video Capture: Frame #047
Video Capture: Frame #048
Video Capture: Frame #049
Video Capture: Frame #050
Video Capture: Frame #051
Video Capture: Frame #052
Video Capture: Frame #053
Video Capture: Frame #054
Video Capture: Frame #055
Video Capture: Frame #056
Video Capture: Frame #057
Video Capture: Frame #058
Video Capture: Frame #059
test MMAP (no poll, REQBUFS): OK
Video Capture: Frame #000 (select)
Video Capture: Frame #001 (select)
Video Capture: Frame #002 (select)
Video Capture: Frame #003 (select)
Video Capture: Frame #004 (select)
Video Capture: Frame #005 (select)
Video Capture: Frame #006 (select)
Video Capture: Frame #007 (select)
Video Capture: Frame #008 (select)
Video Capture: Frame #009 (select)
Video Capture: Frame #010 (select)
Video Capture: Frame #011 (select)
Video Capture: Frame #012 (select)
Video Capture: Frame #013 (select)
Video Capture: Frame #014 (select)
Video Capture: Frame #015 (select)
Video Capture: Frame #016 (select)
Video Capture: Frame #017 (select)
Video Capture: Frame #018 (select)
Video Capture: Frame #019 (select)
Video Capture: Frame #020 (select)
Video Capture: Frame #021 (select)
Video Capture: Frame #022 (select)
Video Capture: Frame #023 (select)
Video Capture: Frame #024 (select)
Video Capture: Frame #025 (select)
Video Capture: Frame #026 (select)
Video Capture: Frame #027 (select)
Video Capture: Frame #028 (select)
Video Capture: Frame #029 (select)
Video Capture: Frame #030 (select)
Video Capture: Frame #031 (select)
Video Capture: Frame #032 (select)
Video Capture: Frame #033 (select)
Video Capture: Frame #034 (select)
Video Capture: Frame #035 (select)
Video Capture: Frame #036 (select)
Video Capture: Frame #037 (select)
Video Capture: Frame #038 (select)
Video Capture: Frame #039 (select)
Video Capture: Frame #040 (select)
Video Capture: Frame #041 (select)
Video Capture: Frame #042 (select)
Video Capture: Frame #043 (select)
Video Capture: Frame #044 (select)
Video Capture: Frame #045 (select)
Video Capture: Frame #046 (select)
Video Capture: Frame #047 (select)
Video Capture: Frame #048 (select)
Video Capture: Frame #049 (select)
Video Capture: Frame #050 (select)
Video Capture: Frame #051 (select)
Video Capture: Frame #052 (select)
Video Capture: Frame #053 (select)
Video Capture: Frame #054 (select)
Video Capture: Frame #055 (select)
Video Capture: Frame #056 (select)
Video Capture: Frame #057 (select)
Video Capture: Frame #058 (select)
Video Capture: Frame #059 (select)
test MMAP (select, REQBUFS): OK
Video Capture: Frame #000 (epoll)
Video Capture: Frame #001 (epoll)
Video Capture: Frame #002 (epoll)
Video Capture: Frame #003 (epoll)
Video Capture: Frame #004 (epoll)
Video Capture: Frame #005 (epoll)
Video Capture: Frame #006 (epoll)
Video Capture: Frame #007 (epoll)
Video Capture: Frame #008 (epoll)
Video Capture: Frame #009 (epoll)
Video Capture: Frame #010 (epoll)
Video Capture: Frame #011 (epoll)
Video Capture: Frame #012 (epoll)
Video Capture: Frame #013 (epoll)
Video Capture: Frame #014 (epoll)
Video Capture: Frame #015 (epoll)
Video Capture: Frame #016 (epoll)
Video Capture: Frame #017 (epoll)
Video Capture: Frame #018 (epoll)
Video Capture: Frame #019 (epoll)
Video Capture: Frame #020 (epoll)
Video Capture: Frame #021 (epoll)
Video Capture: Frame #022 (epoll)
Video Capture: Frame #023 (epoll)
Video Capture: Frame #024 (epoll)
Video Capture: Frame #025 (epoll)
Video Capture: Frame #026 (epoll)
Video Capture: Frame #027 (epoll)
Video Capture: Frame #028 (epoll)
Video Capture: Frame #029 (epoll)
Video Capture: Frame #030 (epoll)
Video Capture: Frame #031 (epoll)
Video Capture: Frame #032 (epoll)
Video Capture: Frame #033 (epoll)
Video Capture: Frame #034 (epoll)
Video Capture: Frame #035 (epoll)
Video Capture: Frame #036 (epoll)
Video Capture: Frame #037 (epoll)
Video Capture: Frame #038 (epoll)
Video Capture: Frame #039 (epoll)
Video Capture: Frame #040 (epoll)
Video Capture: Frame #041 (epoll)
Video Capture: Frame #042 (epoll)
Video Capture: Frame #043 (epoll)
Video Capture: Frame #044 (epoll)
Video Capture: Frame #045 (epoll)
Video Capture: Frame #046 (epoll)
Video Capture: Frame #047 (epoll)
Video Capture: Frame #048 (epoll)
Video Capture: Frame #049 (epoll)
Video Capture: Frame #050 (epoll)
Video Capture: Frame #051 (epoll)
Video Capture: Frame #052 (epoll)
Video Capture: Frame #053 (epoll)
Video Capture: Frame #054 (epoll)
Video Capture: Frame #055 (epoll)
Video Capture: Frame #056 (epoll)
Video Capture: Frame #057 (epoll)
Video Capture: Frame #058 (epoll)
Video Capture: Frame #059 (epoll)
test MMAP (epoll, REQBUFS): OK
Video Capture: Frame #000
Video Capture: Frame #001
Video Capture: Frame #002
Video Capture: Frame #003
Video Capture: Frame #004
Video Capture: Frame #005
Video Capture: Frame #006
Video Capture: Frame #007
Video Capture: Frame #008
Video Capture: Frame #009
Video Capture: Frame #010
Video Capture: Frame #011
Video Capture: Frame #012
Video Capture: Frame #013
Video Capture: Frame #014
Video Capture: Frame #015
Video Capture: Frame #016
Video Capture: Frame #017
Video Capture: Frame #018
Video Capture: Frame #019
Video Capture: Frame #020
Video Capture: Frame #021
Video Capture: Frame #022
Video Capture: Frame #023
Video Capture: Frame #024
Video Capture: Frame #025
Video Capture: Frame #026
Video Capture: Frame #027
Video Capture: Frame #028
Video Capture: Frame #029
Video Capture: Frame #030
Video Capture: Frame #031
Video Capture: Frame #032
Video Capture: Frame #033
Video Capture: Frame #034
Video Capture: Frame #035
Video Capture: Frame #036
Video Capture: Frame #037
Video Capture: Frame #038
Video Capture: Frame #039
Video Capture: Frame #040
Video Capture: Frame #041
Video Capture: Frame #042
Video Capture: Frame #043
Video Capture: Frame #044
Video Capture: Frame #045
Video Capture: Frame #046
Video Capture: Frame #047
Video Capture: Frame #048
Video Capture: Frame #049
Video Capture: Frame #050
Video Capture: Frame #051
Video Capture: Frame #052
Video Capture: Frame #053
Video Capture: Frame #054
Video Capture: Frame #055
Video Capture: Frame #056
Video Capture: Frame #057
Video Capture: Frame #058
Video Capture: Frame #059
test MMAP (no poll, CREATE_BUFS): OK
Video Capture: Frame #000 (select)
Video Capture: Frame #001 (select)
Video Capture: Frame #002 (select)
Video Capture: Frame #003 (select)
Video Capture: Frame #004 (select)
Video Capture: Frame #005 (select)
Video Capture: Frame #006 (select)
Video Capture: Frame #007 (select)
Video Capture: Frame #008 (select)
Video Capture: Frame #009 (select)
Video Capture: Frame #010 (select)
Video Capture: Frame #011 (select)
Video Capture: Frame #012 (select)
Video Capture: Frame #013 (select)
Video Capture: Frame #014 (select)
Video Capture: Frame #015 (select)
Video Capture: Frame #016 (select)
Video Capture: Frame #017 (select)
Video Capture: Frame #018 (select)
Video Capture: Frame #019 (select)
Video Capture: Frame #020 (select)
Video Capture: Frame #021 (select)
Video Capture: Frame #022 (select)
Video Capture: Frame #023 (select)
Video Capture: Frame #024 (select)
Video Capture: Frame #025 (select)
Video Capture: Frame #026 (select)
Video Capture: Frame #027 (select)
Video Capture: Frame #028 (select)
Video Capture: Frame #029 (select)
Video Capture: Frame #030 (select)
Video Capture: Frame #031 (select)
Video Capture: Frame #032 (select)
Video Capture: Frame #033 (select)
Video Capture: Frame #034 (select)
Video Capture: Frame #035 (select)
Video Capture: Frame #036 (select)
Video Capture: Frame #037 (select)
Video Capture: Frame #038 (select)
Video Capture: Frame #039 (select)
Video Capture: Frame #040 (select)
Video Capture: Frame #041 (select)
Video Capture: Frame #042 (select)
Video Capture: Frame #043 (select)
Video Capture: Frame #044 (select)
Video Capture: Frame #045 (select)
Video Capture: Frame #046 (select)
Video Capture: Frame #047 (select)
Video Capture: Frame #048 (select)
Video Capture: Frame #049 (select)
Video Capture: Frame #050 (select)
Video Capture: Frame #051 (select)
Video Capture: Frame #052 (select)
Video Capture: Frame #053 (select)
Video Capture: Frame #054 (select)
Video Capture: Frame #055 (select)
Video Capture: Frame #056 (select)
Video Capture: Frame #057 (select)
Video Capture: Frame #058 (select)
Video Capture: Frame #059 (select)
test MMAP (select, CREATE_BUFS): OK
Video Capture: Frame #000 (epoll)
Video Capture: Frame #001 (epoll)
Video Capture: Frame #002 (epoll)
Video Capture: Frame #003 (epoll)
Video Capture: Frame #004 (epoll)
Video Capture: Frame #005 (epoll)
Video Capture: Frame #006 (epoll)
Video Capture: Frame #007 (epoll)
Video Capture: Frame #008 (epoll)
Video Capture: Frame #009 (epoll)
Video Capture: Frame #010 (epoll)
Video Capture: Frame #011 (epoll)
Video Capture: Frame #012 (epoll)
Video Capture: Frame #013 (epoll)
Video Capture: Frame #014 (epoll)
Video Capture: Frame #015 (epoll)
Video Capture: Frame #016 (epoll)
Video Capture: Frame #017 (epoll)
Video Capture: Frame #018 (epoll)
Video Capture: Frame #019 (epoll)
Video Capture: Frame #020 (epoll)
Video Capture: Frame #021 (epoll)
Video Capture: Frame #022 (epoll)
Video Capture: Frame #023 (epoll)
Video Capture: Frame #024 (epoll)
Video Capture: Frame #025 (epoll)
Video Capture: Frame #026 (epoll)
Video Capture: Frame #027 (epoll)
Video Capture: Frame #028 (epoll)
Video Capture: Frame #029 (epoll)
Video Capture: Frame #030 (epoll)
Video Capture: Frame #031 (epoll)
Video Capture: Frame #032 (epoll)
Video Capture: Frame #033 (epoll)
Video Capture: Frame #034 (epoll)
Video Capture: Frame #035 (epoll)
Video Capture: Frame #036 (epoll)
Video Capture: Frame #037 (epoll)
Video Capture: Frame #038 (epoll)
Video Capture: Frame #039 (epoll)
Video Capture: Frame #040 (epoll)
Video Capture: Frame #041 (epoll)
Video Capture: Frame #042 (epoll)
Video Capture: Frame #043 (epoll)
Video Capture: Frame #044 (epoll)
Video Capture: Frame #045 (epoll)
Video Capture: Frame #046 (epoll)
Video Capture: Frame #047 (epoll)
Video Capture: Frame #048 (epoll)
Video Capture: Frame #049 (epoll)
Video Capture: Frame #050 (epoll)
Video Capture: Frame #051 (epoll)
Video Capture: Frame #052 (epoll)
Video Capture: Frame #053 (epoll)
Video Capture: Frame #054 (epoll)
Video Capture: Frame #055 (epoll)
Video Capture: Frame #056 (epoll)
Video Capture: Frame #057 (epoll)
Video Capture: Frame #058 (epoll)
Video Capture: Frame #059 (epoll)
test MMAP (epoll, CREATE_BUFS): OK
Video Capture: Frame #000
Video Capture: Frame #001
Video Capture: Frame #002
Video Capture: Frame #003
Video Capture: Frame #004
Video Capture: Frame #005
Video Capture: Frame #006
Video Capture: Frame #007
Video Capture: Frame #008
Video Capture: Frame #009
Video Capture: Frame #010
Video Capture: Frame #011
Video Capture: Frame #012
Video Capture: Frame #013
Video Capture: Frame #014
Video Capture: Frame #015
Video Capture: Frame #016
Video Capture: Frame #017
Video Capture: Frame #018
Video Capture: Frame #019
Video Capture: Frame #020
Video Capture: Frame #021
Video Capture: Frame #022
Video Capture: Frame #023
Video Capture: Frame #024
Video Capture: Frame #025
Video Capture: Frame #026
Video Capture: Frame #027
Video Capture: Frame #028
Video Capture: Frame #029
Video Capture: Frame #030
Video Capture: Frame #031
Video Capture: Frame #032
Video Capture: Frame #033
Video Capture: Frame #034
Video Capture: Frame #035
Video Capture: Frame #036
Video Capture: Frame #037
Video Capture: Frame #038
Video Capture: Frame #039
Video Capture: Frame #040
Video Capture: Frame #041
Video Capture: Frame #042
Video Capture: Frame #043
Video Capture: Frame #044
Video Capture: Frame #045
Video Capture: Frame #046
Video Capture: Frame #047
Video Capture: Frame #048
Video Capture: Frame #049
Video Capture: Frame #050
Video Capture: Frame #051
Video Capture: Frame #052
Video Capture: Frame #053
Video Capture: Frame #054
Video Capture: Frame #055
Video Capture: Frame #056
Video Capture: Frame #057
Video Capture: Frame #058
Video Capture: Frame #059
test USERPTR (no poll): OK
Video Capture: Frame #000 (select)
Video Capture: Frame #001 (select)
Video Capture: Frame #002 (select)
Video Capture: Frame #003 (select)
Video Capture: Frame #004 (select)
Video Capture: Frame #005 (select)
Video Capture: Frame #006 (select)
Video Capture: Frame #007 (select)
Video Capture: Frame #008 (select)
Video Capture: Frame #009 (select)
Video Capture: Frame #010 (select)
Video Capture: Frame #011 (select)
Video Capture: Frame #012 (select)
Video Capture: Frame #013 (select)
Video Capture: Frame #014 (select)
Video Capture: Frame #015 (select)
Video Capture: Frame #016 (select)
Video Capture: Frame #017 (select)
Video Capture: Frame #018 (select)
Video Capture: Frame #019 (select)
Video Capture: Frame #020 (select)
Video Capture: Frame #021 (select)
Video Capture: Frame #022 (select)
Video Capture: Frame #023 (select)
Video Capture: Frame #024 (select)
Video Capture: Frame #025 (select)
Video Capture: Frame #026 (select)
Video Capture: Frame #027 (select)
Video Capture: Frame #028 (select)
Video Capture: Frame #029 (select)
Video Capture: Frame #030 (select)
Video Capture: Frame #031 (select)
Video Capture: Frame #032 (select)
Video Capture: Frame #033 (select)
Video Capture: Frame #034 (select)
Video Capture: Frame #035 (select)
Video Capture: Frame #036 (select)
Video Capture: Frame #037 (select)
Video Capture: Frame #038 (select)
Video Capture: Frame #039 (select)
Video Capture: Frame #040 (select)
Video Capture: Frame #041 (select)
Video Capture: Frame #042 (select)
Video Capture: Frame #043 (select)
Video Capture: Frame #044 (select)
Video Capture: Frame #045 (select)
Video Capture: Frame #046 (select)
Video Capture: Frame #047 (select)
Video Capture: Frame #048 (select)
Video Capture: Frame #049 (select)
Video Capture: Frame #050 (select)
Video Capture: Frame #051 (select)
Video Capture: Frame #052 (select)
Video Capture: Frame #053 (select)
Video Capture: Frame #054 (select)
Video Capture: Frame #055 (select)
Video Capture: Frame #056 (select)
Video Capture: Frame #057 (select)
Video Capture: Frame #058 (select)
Video Capture: Frame #059 (select)
test USERPTR (select): OK
test DMABUF: Cannot test, specify --expbuf-device
Total for uvcvideo device /dev/video1: 58, Succeeded: 57, Failed: 1, Warnings: 0
As tested on the guest:
$ v4l2-compliance -d0 -s --driver-name uvcvideo
v4l2-compliance 1.33.0-5457, 64 bits, 64-bit time_t
v4l2-compliance SHA: e7e240f546f3 2026-05-28 17:06:12
Compliance test for uvcvideo device (overridden from virtio-media) /dev/video0:
Driver Info:
Driver name : uvcvideo
Card type : Logitech Webcam C925e
Bus info : platform:virtio-media
Driver version : 7.1.0
Capabilities : 0x84200001
Video Capture
Streaming
Extended Pix Format
Device Capabilities
Device Caps : 0x04200001
Video Capture
Streaming
Extended Pix Format
Required ioctls:
test VIDIOC_QUERYCAP: OK
test invalid ioctls: OK
Allow for multiple opens:
test second /dev/video0 open: OK
test VIDIOC_QUERYCAP: OK
test VIDIOC_G/S_PRIORITY: OK
test for unlimited opens: OK
Debug ioctls:
test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported)
test VIDIOC_LOG_STATUS: OK (Not Supported)
Input ioctls:
test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
test VIDIOC_S_HW_FREQ_SEEK: OK
test VIDIOC_ENUMAUDIO: OK (Not Supported)
test VIDIOC_G/S/ENUMINPUT: OK
test VIDIOC_G/S_AUDIO: OK (Not Supported)
Inputs: 1 Audio Inputs: 0 Tuners: 0
Output ioctls:
test VIDIOC_G/S_MODULATOR: OK (Not Supported)
test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
test VIDIOC_ENUMAUDOUT: OK (Not Supported)
test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
test VIDIOC_G/S_AUDOUT: OK (Not Supported)
Outputs: 0 Audio Outputs: 0 Modulators: 0
Input/Output configuration ioctls:
test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK (Not Supported)
test VIDIOC_DV_TIMINGS_CAP: OK (Not Supported)
test VIDIOC_G/S_EDID: OK (Not Supported)
Control ioctls (Input 0):
test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
test VIDIOC_QUERYCTRL: OK
test VIDIOC_G/S_CTRL: OK
fail: v4l2-test-controls.cpp(981): ret (got 22)
test VIDIOC_G/S/TRY_EXT_CTRLS: FAIL
test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
Standard Controls: 19 Private Controls: 0
Format ioctls (Input 0):
test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK
test VIDIOC_G/S_PARM: OK
test VIDIOC_G_FBUF: OK (Not Supported)
test VIDIOC_G_FMT: OK
test VIDIOC_TRY_FMT: OK
test VIDIOC_S_FMT: OK
test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
test Cropping: OK (Not Supported)
test Composing: OK (Not Supported)
test Scaling: OK (Not Supported)
Codec ioctls (Input 0):
test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
test VIDIOC_G_ENC_INDEX: OK (Not Supported)
test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)
Buffer ioctls (Input 0):
test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK
test CREATE_BUFS maximum buffers: OK
test VIDIOC_REMOVE_BUFS: OK
test VIDIOC_EXPBUF: OK (Not Supported)
test Requests: OK (Not Supported)
test blocking wait: OK
Test input 0:
Streaming ioctls:
test read/write: OK (Not Supported)
Video Capture: Frame #000
Video Capture: Frame #001
Video Capture: Frame #002
Video Capture: Frame #003
Video Capture: Frame #004
Video Capture: Frame #005
Video Capture: Frame #006
Video Capture: Frame #007
Video Capture: Frame #008
Video Capture: Frame #009
Video Capture: Frame #010
Video Capture: Frame #011
Video Capture: Frame #012
Video Capture: Frame #013
Video Capture: Frame #014
Video Capture: Frame #015
Video Capture: Frame #016
Video Capture: Frame #017
Video Capture: Frame #018
Video Capture: Frame #019
Video Capture: Frame #020
Video Capture: Frame #021
Video Capture: Frame #022
Video Capture: Frame #023
Video Capture: Frame #024
Video Capture: Frame #025
Video Capture: Frame #026
Video Capture: Frame #027
Video Capture: Frame #028
Video Capture: Frame #029
Video Capture: Frame #030
Video Capture: Frame #031
Video Capture: Frame #032
Video Capture: Frame #033
Video Capture: Frame #034
Video Capture: Frame #035
Video Capture: Frame #036
Video Capture: Frame #037
Video Capture: Frame #038
Video Capture: Frame #039
Video Capture: Frame #040
Video Capture: Frame #041
Video Capture: Frame #042
Video Capture: Frame #043
Video Capture: Frame #044
Video Capture: Frame #045
Video Capture: Frame #046
Video Capture: Frame #047
Video Capture: Frame #048
Video Capture: Frame #049
Video Capture: Frame #050
Video Capture: Frame #051
Video Capture: Frame #052
Video Capture: Frame #053
Video Capture: Frame #054
Video Capture: Frame #055
Video Capture: Frame #056
Video Capture: Frame #057
Video Capture: Frame #058
Video Capture: Frame #059
test MMAP (no poll, REQBUFS): OK
Video Capture: Frame #000 (select)
Video Capture: Frame #001 (select)
Video Capture: Frame #002 (select)
Video Capture: Frame #003 (select)
Video Capture: Frame #004 (select)
Video Capture: Frame #005 (select)
Video Capture: Frame #006 (select)
Video Capture: Frame #007 (select)
Video Capture: Frame #008 (select)
Video Capture: Frame #009 (select)
Video Capture: Frame #010 (select)
Video Capture: Frame #011 (select)
Video Capture: Frame #012 (select)
Video Capture: Frame #013 (select)
Video Capture: Frame #014 (select)
Video Capture: Frame #015 (select)
Video Capture: Frame #016 (select)
Video Capture: Frame #017 (select)
Video Capture: Frame #018 (select)
Video Capture: Frame #019 (select)
Video Capture: Frame #020 (select)
Video Capture: Frame #021 (select)
Video Capture: Frame #022 (select)
Video Capture: Frame #023 (select)
Video Capture: Frame #024 (select)
Video Capture: Frame #025 (select)
Video Capture: Frame #026 (select)
Video Capture: Frame #027 (select)
Video Capture: Frame #028 (select)
Video Capture: Frame #029 (select)
Video Capture: Frame #030 (select)
Video Capture: Frame #031 (select)
Video Capture: Frame #032 (select)
Video Capture: Frame #033 (select)
Video Capture: Frame #034 (select)
Video Capture: Frame #035 (select)
Video Capture: Frame #036 (select)
Video Capture: Frame #037 (select)
Video Capture: Frame #038 (select)
Video Capture: Frame #039 (select)
Video Capture: Frame #040 (select)
Video Capture: Frame #041 (select)
Video Capture: Frame #042 (select)
Video Capture: Frame #043 (select)
Video Capture: Frame #044 (select)
Video Capture: Frame #045 (select)
Video Capture: Frame #046 (select)
Video Capture: Frame #047 (select)
Video Capture: Frame #048 (select)
Video Capture: Frame #049 (select)
Video Capture: Frame #050 (select)
Video Capture: Frame #051 (select)
Video Capture: Frame #052 (select)
Video Capture: Frame #053 (select)
Video Capture: Frame #054 (select)
Video Capture: Frame #055 (select)
Video Capture: Frame #056 (select)
Video Capture: Frame #057 (select)
Video Capture: Frame #058 (select)
Video Capture: Frame #059 (select)
test MMAP (select, REQBUFS): OK
Video Capture: Frame #000 (epoll)
Video Capture: Frame #001 (epoll)
Video Capture: Frame #002 (epoll)
Video Capture: Frame #003 (epoll)
Video Capture: Frame #004 (epoll)
Video Capture: Frame #005 (epoll)
Video Capture: Frame #006 (epoll)
Video Capture: Frame #007 (epoll)
Video Capture: Frame #008 (epoll)
Video Capture: Frame #009 (epoll)
Video Capture: Frame #010 (epoll)
Video Capture: Frame #011 (epoll)
Video Capture: Frame #012 (epoll)
Video Capture: Frame #013 (epoll)
Video Capture: Frame #014 (epoll)
Video Capture: Frame #015 (epoll)
Video Capture: Frame #016 (epoll)
Video Capture: Frame #017 (epoll)
Video Capture: Frame #018 (epoll)
Video Capture: Frame #019 (epoll)
Video Capture: Frame #020 (epoll)
Video Capture: Frame #021 (epoll)
Video Capture: Frame #022 (epoll)
Video Capture: Frame #023 (epoll)
Video Capture: Frame #024 (epoll)
Video Capture: Frame #025 (epoll)
Video Capture: Frame #026 (epoll)
Video Capture: Frame #027 (epoll)
Video Capture: Frame #028 (epoll)
Video Capture: Frame #029 (epoll)
Video Capture: Frame #030 (epoll)
Video Capture: Frame #031 (epoll)
Video Capture: Frame #032 (epoll)
Video Capture: Frame #033 (epoll)
Video Capture: Frame #034 (epoll)
Video Capture: Frame #035 (epoll)
Video Capture: Frame #036 (epoll)
Video Capture: Frame #037 (epoll)
Video Capture: Frame #038 (epoll)
Video Capture: Frame #039 (epoll)
Video Capture: Frame #040 (epoll)
Video Capture: Frame #041 (epoll)
Video Capture: Frame #042 (epoll)
Video Capture: Frame #043 (epoll)
Video Capture: Frame #044 (epoll)
Video Capture: Frame #045 (epoll)
Video Capture: Frame #046 (epoll)
Video Capture: Frame #047 (epoll)
Video Capture: Frame #048 (epoll)
Video Capture: Frame #049 (epoll)
Video Capture: Frame #050 (epoll)
Video Capture: Frame #051 (epoll)
Video Capture: Frame #052 (epoll)
Video Capture: Frame #053 (epoll)
Video Capture: Frame #054 (epoll)
Video Capture: Frame #055 (epoll)
Video Capture: Frame #056 (epoll)
Video Capture: Frame #057 (epoll)
Video Capture: Frame #058 (epoll)
Video Capture: Frame #059 (epoll)
test MMAP (epoll, REQBUFS): OK
Video Capture: Frame #000
Video Capture: Frame #001
Video Capture: Frame #002
Video Capture: Frame #003
Video Capture: Frame #004
Video Capture: Frame #005
Video Capture: Frame #006
Video Capture: Frame #007
Video Capture: Frame #008
Video Capture: Frame #009
Video Capture: Frame #010
Video Capture: Frame #011
Video Capture: Frame #012
Video Capture: Frame #013
Video Capture: Frame #014
Video Capture: Frame #015
Video Capture: Frame #016
Video Capture: Frame #017
Video Capture: Frame #018
Video Capture: Frame #019
Video Capture: Frame #020
Video Capture: Frame #021
Video Capture: Frame #022
Video Capture: Frame #023
Video Capture: Frame #024
Video Capture: Frame #025
Video Capture: Frame #026
Video Capture: Frame #027
Video Capture: Frame #028
Video Capture: Frame #029
Video Capture: Frame #030
Video Capture: Frame #031
Video Capture: Frame #032
Video Capture: Frame #033
Video Capture: Frame #034
Video Capture: Frame #035
Video Capture: Frame #036
Video Capture: Frame #037
Video Capture: Frame #038
Video Capture: Frame #039
Video Capture: Frame #040
Video Capture: Frame #041
Video Capture: Frame #042
Video Capture: Frame #043
Video Capture: Frame #044
Video Capture: Frame #045
Video Capture: Frame #046
Video Capture: Frame #047
Video Capture: Frame #048
Video Capture: Frame #049
Video Capture: Frame #050
Video Capture: Frame #051
Video Capture: Frame #052
Video Capture: Frame #053
Video Capture: Frame #054
Video Capture: Frame #055
Video Capture: Frame #056
Video Capture: Frame #057
Video Capture: Frame #058
Video Capture: Frame #059
test MMAP (no poll, CREATE_BUFS): OK
Video Capture: Frame #000 (select)
Video Capture: Frame #001 (select)
Video Capture: Frame #002 (select)
Video Capture: Frame #003 (select)
Video Capture: Frame #004 (select)
Video Capture: Frame #005 (select)
Video Capture: Frame #006 (select)
Video Capture: Frame #007 (select)
Video Capture: Frame #008 (select)
Video Capture: Frame #009 (select)
Video Capture: Frame #010 (select)
Video Capture: Frame #011 (select)
Video Capture: Frame #012 (select)
Video Capture: Frame #013 (select)
Video Capture: Frame #014 (select)
Video Capture: Frame #015 (select)
Video Capture: Frame #016 (select)
Video Capture: Frame #017 (select)
Video Capture: Frame #018 (select)
Video Capture: Frame #019 (select)
Video Capture: Frame #020 (select)
Video Capture: Frame #021 (select)
Video Capture: Frame #022 (select)
Video Capture: Frame #023 (select)
Video Capture: Frame #024 (select)
Video Capture: Frame #025 (select)
Video Capture: Frame #026 (select)
Video Capture: Frame #027 (select)
Video Capture: Frame #028 (select)
Video Capture: Frame #029 (select)
Video Capture: Frame #030 (select)
Video Capture: Frame #031 (select)
Video Capture: Frame #032 (select)
Video Capture: Frame #033 (select)
Video Capture: Frame #034 (select)
Video Capture: Frame #035 (select)
Video Capture: Frame #036 (select)
Video Capture: Frame #037 (select)
Video Capture: Frame #038 (select)
Video Capture: Frame #039 (select)
Video Capture: Frame #040 (select)
Video Capture: Frame #041 (select)
Video Capture: Frame #042 (select)
Video Capture: Frame #043 (select)
Video Capture: Frame #044 (select)
Video Capture: Frame #045 (select)
Video Capture: Frame #046 (select)
Video Capture: Frame #047 (select)
Video Capture: Frame #048 (select)
Video Capture: Frame #049 (select)
Video Capture: Frame #050 (select)
Video Capture: Frame #051 (select)
Video Capture: Frame #052 (select)
Video Capture: Frame #053 (select)
Video Capture: Frame #054 (select)
Video Capture: Frame #055 (select)
Video Capture: Frame #056 (select)
Video Capture: Frame #057 (select)
Video Capture: Frame #058 (select)
Video Capture: Frame #059 (select)
test MMAP (select, CREATE_BUFS): OK
Video Capture: Frame #000 (epoll)
Video Capture: Frame #001 (epoll)
Video Capture: Frame #002 (epoll)
Video Capture: Frame #003 (epoll)
Video Capture: Frame #004 (epoll)
Video Capture: Frame #005 (epoll)
Video Capture: Frame #006 (epoll)
Video Capture: Frame #007 (epoll)
Video Capture: Frame #008 (epoll)
Video Capture: Frame #009 (epoll)
Video Capture: Frame #010 (epoll)
Video Capture: Frame #011 (epoll)
Video Capture: Frame #012 (epoll)
Video Capture: Frame #013 (epoll)
Video Capture: Frame #014 (epoll)
Video Capture: Frame #015 (epoll)
Video Capture: Frame #016 (epoll)
Video Capture: Frame #017 (epoll)
Video Capture: Frame #018 (epoll)
Video Capture: Frame #019 (epoll)
Video Capture: Frame #020 (epoll)
Video Capture: Frame #021 (epoll)
Video Capture: Frame #022 (epoll)
Video Capture: Frame #023 (epoll)
Video Capture: Frame #024 (epoll)
Video Capture: Frame #025 (epoll)
Video Capture: Frame #026 (epoll)
Video Capture: Frame #027 (epoll)
Video Capture: Frame #028 (epoll)
Video Capture: Frame #029 (epoll)
Video Capture: Frame #030 (epoll)
Video Capture: Frame #031 (epoll)
Video Capture: Frame #032 (epoll)
Video Capture: Frame #033 (epoll)
Video Capture: Frame #034 (epoll)
Video Capture: Frame #035 (epoll)
Video Capture: Frame #036 (epoll)
Video Capture: Frame #037 (epoll)
Video Capture: Frame #038 (epoll)
Video Capture: Frame #039 (epoll)
Video Capture: Frame #040 (epoll)
Video Capture: Frame #041 (epoll)
Video Capture: Frame #042 (epoll)
Video Capture: Frame #043 (epoll)
Video Capture: Frame #044 (epoll)
Video Capture: Frame #045 (epoll)
Video Capture: Frame #046 (epoll)
Video Capture: Frame #047 (epoll)
Video Capture: Frame #048 (epoll)
Video Capture: Frame #049 (epoll)
Video Capture: Frame #050 (epoll)
Video Capture: Frame #051 (epoll)
Video Capture: Frame #052 (epoll)
Video Capture: Frame #053 (epoll)
Video Capture: Frame #054 (epoll)
Video Capture: Frame #055 (epoll)
Video Capture: Frame #056 (epoll)
Video Capture: Frame #057 (epoll)
Video Capture: Frame #058 (epoll)
Video Capture: Frame #059 (epoll)
test MMAP (epoll, CREATE_BUFS): OK
test USERPTR (no poll): OK (Not Supported)
test USERPTR (select): OK (Not Supported)
test DMABUF (no poll): OK (Not Supported)
test DMABUF (select): OK (Not Supported)
Total for uvcvideo device /dev/video0: 59, Succeeded: 58, Failed: 1, Warnings: 0
---
Changes in v10:
- Rebased on top of v7.3-rc1
- Add missing padding in ``struct virtio_media_cmd_mmap``
- Add ``virtio_media_config`` to uapi header and use proper ``cread`` during init
- Remove redundant assignment of ``vfl_type``
- Fix tear down order of the video device and resources in
``virtio_media_remove()``
- Call ``virtio_device_ready()`` before starting device
- Move from ``devm`` to manual memory management to prevent potential use after
free
- Detach unused buffers from the virtqueues during device remove
- Remove wait timeout during kick command
- Fix use after free in case of transport-level error
- Add additional validity check for scatterlist data buffer
- Ensure PFNs are not truncated
- Use dynamic ``shadow_buffer_size`` in ``scatterlist_builder`` instead of the
``VIRTIO_SHADOW_BUF_SIZE`` constant to guard against future modifications
- Fix case where exact fit into a shadow buffer was wrongly rejected
- Fail early if ``virtio_get_shm_region()`` fails
- Move guest allocation before host call in ``reqbufs`` and ``createbufs``
- Clean up host-side mmap if ``io_remap_pfn_range()`` fails
- Ensure command failure returns in ``ext_controls`` ioctls
- Replace array ``vmalloc`` allocations with ``vcalloc`` for overflow protection
- Add check in ``dqbuf`` ioctl if no buffer is in the queue in case multiple
threads call ``VIDIOC_DQBUF``
- Change ``copy_from_user()``/``copy_to_user()`` failure return value from
``EINVAL`` to ``EFAULT``
- Ensure reserved fields are initialized to zero
- Ensure ``device_type`` from config area is valid
- Set ``done`` flag after ``resp_len`` to address a race in the condition check
fast path
- Do not leave host session open if guest allocations fail
- Rename ``errno`` to ``error`` in ``virtio_media`` UAPI to prevent
compilation errors in userspaces apps that include ``errno.h``
- Keep user pages pinned during ``ext_ctrls`` request
Changes in v9:
- Removed note from cover letter about the dependency on the VIRTIO_ID_MEDIA
patch since this was merged.
Changes in v8:
- Fix usage of integer types in uapi
- Add uapi header test exemption due to depenency on currently exempt videodev2.h
Changes in v7:
- Removed USERPTR feature
- Updated Kconfig dependency to include little endian and 64bit platforms (not
only ARM and x86)
- Fixed incorrect constant in vfl_dir detection
- Clarified that error codes returned by the host are positive integers
Changes in v6:
- Added missing "WITH Linux-syscall-note" for SPDX-License-Identifier in
``include/uapi/linux/virtio_media.h``
- Rebased on top of v7.2-rc1
Changes in v5:
- Add DMA buffer alignment and padding for commands, responses, and the event
buffer
- Corrected styling of kernel-doc comments
- Removed the configurable ``driver_name`` module parameter
- Moved the protocol to ``include/uapi/linux/virtio_media.h``
- Link to v4: https://lore.kernel.org/all/20260622204343.1994418-1-briandaniels@google.com/
Changes in v4:
- Rebased on top of v7.1-rc1
- Replace usages of filep->private_data with file_to_v4l2_fh()
throughout the driver
- Link to v3: https://lore.kernel.org/r/20250412-virtio-media-v3-1-97dc94c18398@gmail.com
Changes in v3:
- Rebased on top of v6.15-rc1 and removes obsolete control callbacks.
- Link to v2: https://lore.kernel.org/r/20250201-virtio-media-v2-1-ac840681452d@gmail.com
Changes in v2:
- Fixed kernel test robot and media CI warnings (ignored a few false
positives).
- Changed in-driver email address to personal one since my Google one
will soon become invalid.
- Link to v1: https://lore.kernel.org/r/20250123-virtio-media-v1-1-81e2549b86b9@gmail.com
Brian Daniels (5):
media: virtio: Add core driver and session management
media: virtio: Add V4L2 ioctl forwarding infrastructure
media: virtio: Add extended controls support
media: virtio: Add buffer streaming, event handling, and poll
media: virtio: Add MMAP buffer mapping support
MAINTAINERS | 8 +
drivers/media/Kconfig | 13 +
drivers/media/Makefile | 2 +
drivers/media/virtio/Makefile | 7 +
drivers/media/virtio/scatterlist_builder.c | 538 ++++++++
drivers/media/virtio/scatterlist_builder.h | 113 ++
drivers/media/virtio/session.h | 135 ++
drivers/media/virtio/virtio_media.h | 95 ++
drivers/media/virtio/virtio_media_driver.c | 1045 +++++++++++++++
drivers/media/virtio/virtio_media_ioctls.c | 1377 ++++++++++++++++++++
include/uapi/linux/virtio_media.h | 309 +++++
usr/include/Makefile | 1 +
12 files changed, 3643 insertions(+)
create mode 100644 drivers/media/virtio/Makefile
create mode 100644 drivers/media/virtio/scatterlist_builder.c
create mode 100644 drivers/media/virtio/scatterlist_builder.h
create mode 100644 drivers/media/virtio/session.h
create mode 100644 drivers/media/virtio/virtio_media.h
create mode 100644 drivers/media/virtio/virtio_media_driver.c
create mode 100644 drivers/media/virtio/virtio_media_ioctls.c
create mode 100644 include/uapi/linux/virtio_media.h
base-commit: d7e7bca7169873b46e857c025c10f3336c1258d8
--
2.56.0.317.g454f8302e1-goog
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v10 1/5] media: virtio: Add core driver and session management
2026-10-29 15:30 [PATCH v10 0/5] media: add virtio-media driver Brian Daniels
@ 2026-10-29 15:30 ` Brian Daniels
2026-10-29 15:49 ` sashiko-bot
2026-10-29 15:30 ` [PATCH v10 2/5] media: virtio: Add V4L2 ioctl forwarding infrastructure Brian Daniels
` (3 subsequent siblings)
4 siblings, 1 reply; 11+ messages in thread
From: Brian Daniels @ 2026-10-29 15:30 UTC (permalink / raw)
To: Mauro Carvalho Chehab
Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
gurchetansingh, hverkuil, linux-kernel, linux-media, mst,
nicolas.dufresne, virtualization, xuanzhuo, dbassey,
laurent.pinchart, Brian Daniels
From: Alexandre Courbot <gnurou@gmail.com>
Add the initial virtio-media driver core, build system integration,
MAINTAINERS entry, and V4L2 session lifecycle management.
The driver binds to VIRTIO_ID_MEDIA devices, reads device capabilities
and device node type from the virtio configuration space, sets up the
command virtqueue for synchronous host requests, and registers the V4L2
and video devices. Opening and closing the video device creates and
destroys a host-backed session via VIRTIO_MEDIA_CMD_OPEN and
VIRTIO_MEDIA_CMD_CLOSE.
Protocol structures in the UAPI header include explicit __reserved
fields solely to guarantee 64-bit alignment rather than for future
protocol expansion. Because the UAPI header includes linux/videodev2.h,
which is currently excluded from standalone UAPI header compilation
checks (CONFIG_UAPI_HEADER_TEST=y), add linux/virtio_media.h to the same
exception list in usr/include/Makefile.
Signed-off-by: Alexandre Courbot <gnurou@gmail.com>
Assisted-by: LLM
Co-developed-by: Brian Daniels <briandaniels@google.com>
Signed-off-by: Brian Daniels <briandaniels@google.com>
---
MAINTAINERS | 8 +
drivers/media/Kconfig | 13 +
drivers/media/Makefile | 2 +
drivers/media/virtio/Makefile | 7 +
drivers/media/virtio/session.h | 58 +++
drivers/media/virtio/virtio_media.h | 70 +++
drivers/media/virtio/virtio_media_driver.c | 503 +++++++++++++++++++++
include/uapi/linux/virtio_media.h | 118 +++++
usr/include/Makefile | 1 +
9 files changed, 780 insertions(+)
create mode 100644 drivers/media/virtio/Makefile
create mode 100644 drivers/media/virtio/session.h
create mode 100644 drivers/media/virtio/virtio_media.h
create mode 100644 drivers/media/virtio/virtio_media_driver.c
create mode 100644 include/uapi/linux/virtio_media.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 760791e36..30c565537 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -28973,6 +28973,7 @@ F: Documentation/devicetree/bindings/virtio/
F: Documentation/driver-api/virtio/
F: drivers/block/virtio_blk.c
F: drivers/crypto/virtio/
+F: drivers/media/virtio/
F: drivers/vdpa/
F: drivers/virtio/
F: include/linux/vdpa.h
@@ -29085,6 +29086,13 @@ S: Maintained
F: drivers/iommu/virtio-iommu.c
F: include/uapi/linux/virtio_iommu.h
+VIRTIO MEDIA DRIVER
+M: Brian Daniels <briandaniels@google.com>
+L: linux-media@vger.kernel.org
+S: Maintained
+F: drivers/media/virtio/
+F: include/uapi/linux/virtio_media.h
+
VIRTIO MEM DRIVER
M: David Hildenbrand <david@kernel.org>
L: virtualization@lists.linux.dev
diff --git a/drivers/media/Kconfig b/drivers/media/Kconfig
index 6abc9302c..ce7d088e7 100644
--- a/drivers/media/Kconfig
+++ b/drivers/media/Kconfig
@@ -136,6 +136,19 @@ config MEDIA_PLATFORM_SUPPORT
Say Y when you want to be able to see such devices.
+config MEDIA_VIRTIO
+ tristate "Virtio-media Driver"
+ depends on VIRTIO && VIDEO_DEV && 64BIT && (X86 || CPU_LITTLE_ENDIAN)
+ select VIDEOBUF2_CORE
+ select VIDEOBUF2_MEMOPS
+ help
+ Enables the virtio-media driver.
+
+ This driver is used to virtualize media devices such as cameras or
+ decoders from a host into a guest using the V4L2 protocol.
+
+ If unsure, say N.
+
config MEDIA_TEST_SUPPORT
bool
prompt "Test drivers" if MEDIA_SUPPORT_FILTER
diff --git a/drivers/media/Makefile b/drivers/media/Makefile
index 20fac24e4..357e786cc 100644
--- a/drivers/media/Makefile
+++ b/drivers/media/Makefile
@@ -23,6 +23,8 @@ obj-$(CONFIG_DVB_CORE) += dvb-core/
# There are both core and drivers at RC subtree - merge before drivers
obj-y += rc/
+obj-$(CONFIG_MEDIA_VIRTIO) += virtio/
+
obj-$(CONFIG_CEC_CORE) += cec/
#
diff --git a/drivers/media/virtio/Makefile b/drivers/media/virtio/Makefile
new file mode 100644
index 000000000..09d9834da
--- /dev/null
+++ b/drivers/media/virtio/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Makefile for the virtio-media device driver.
+
+virtio-media-objs := virtio_media_driver.o
+
+obj-$(CONFIG_MEDIA_VIRTIO) += virtio-media.o
diff --git a/drivers/media/virtio/session.h b/drivers/media/virtio/session.h
new file mode 100644
index 000000000..4ade7fb9c
--- /dev/null
+++ b/drivers/media/virtio/session.h
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+ */
+
+/*
+ * Definitions of virtio-media session related structures.
+ *
+ * Copyright (c) 2024-2026 Google LLC.
+ */
+
+#ifndef __VIRTIO_MEDIA_SESSION_H
+#define __VIRTIO_MEDIA_SESSION_H
+
+#include <media/v4l2-fh.h>
+
+#include "uapi/linux/virtio_media.h"
+
+/**
+ * struct virtio_media_session - A session on a virtio_media device.
+ * @fh: file handler for the session.
+ * @file: file pointer associated with the session's file handler.
+ * @id: session ID used to communicate with the device.
+ * @cmd: union of session-level commands. A session can have one command
+ * currently running.
+ * @list: link into the list of sessions for the device.
+ */
+struct virtio_media_session {
+ struct v4l2_fh fh;
+ struct file *file;
+ u32 id;
+
+ __dma_from_device_group_begin();
+ union {
+ struct virtio_media_cmd_close close;
+ } cmd;
+ __dma_from_device_group_end();
+
+ struct list_head list;
+};
+
+static inline struct virtio_media_session *fh_to_session(struct v4l2_fh *fh)
+{
+ return container_of(fh, struct virtio_media_session, fh);
+}
+
+static inline void
+virtio_media_session_fh_add(struct virtio_media_session *session,
+ struct file *file)
+{
+ v4l2_fh_add(&session->fh, file);
+ session->file = file;
+}
+
+static inline void
+virtio_media_session_fh_del(struct virtio_media_session *session)
+{
+ v4l2_fh_del(&session->fh, session->file);
+}
+
+#endif // __VIRTIO_MEDIA_SESSION_H
diff --git a/drivers/media/virtio/virtio_media.h b/drivers/media/virtio/virtio_media.h
new file mode 100644
index 000000000..68299377a
--- /dev/null
+++ b/drivers/media/virtio/virtio_media.h
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+ */
+
+/*
+ * Virtio-media structures & functions declarations.
+ *
+ * Copyright (c) 2024-2026 Google LLC.
+ */
+
+#ifndef __VIRTIO_MEDIA_H
+#define __VIRTIO_MEDIA_H
+
+#include <linux/virtio_config.h>
+#include <media/v4l2-device.h>
+
+#include "uapi/linux/virtio_media.h"
+
+#define VIRTIO_MEDIA_DEFAULT_DRIVER_NAME "virtio-media"
+
+/**
+ * struct virtio_media - Virtio-media device.
+ * @v4l2_dev: v4l2_device for the media device.
+ * @video_dev: video_device for the media device.
+ * @virtio_dev: virtio device for the media device.
+ * @commandq: virtio command queue.
+ * @sessions: list of active sessions on the device.
+ * @sessions_lock: protects @sessions and &struct virtio_media_session.list.
+ * @cmd: union of device-level commands. Session commands are handled by
+ * &struct virtio_media_session.
+ * @resp: union of responses to device-level commands. Session responses are
+ * handled by &struct virtio_media_session.
+ * @vlock: serializes access to the command queue.
+ * @wq: waitqueue for host responses on the command queue.
+ */
+struct virtio_media {
+ struct v4l2_device v4l2_dev;
+ struct video_device video_dev;
+
+ struct virtio_device *virtio_dev;
+ struct virtqueue *commandq;
+
+ struct list_head sessions;
+ struct mutex sessions_lock; /* protects sessions list */
+
+ __dma_from_device_group_begin();
+ union {
+ struct virtio_media_cmd_open open;
+ } cmd;
+
+ union {
+ struct virtio_media_resp_open open;
+ } resp;
+ __dma_from_device_group_end();
+
+ struct mutex vlock; /* serializes command queue access */
+ wait_queue_head_t wq;
+};
+
+static inline struct virtio_media *
+to_virtio_media(struct video_device *video_dev)
+{
+ return container_of(video_dev, struct virtio_media, video_dev);
+}
+
+/* virtio_media_driver.c */
+
+int virtio_media_send_command(struct virtio_media *vv, struct scatterlist **sgs,
+ const size_t out_sgs, const size_t in_sgs,
+ size_t minimum_resp_len, size_t *resp_len);
+#endif // __VIRTIO_MEDIA_H
+
diff --git a/drivers/media/virtio/virtio_media_driver.c b/drivers/media/virtio/virtio_media_driver.c
new file mode 100644
index 000000000..10073d863
--- /dev/null
+++ b/drivers/media/virtio/virtio_media_driver.c
@@ -0,0 +1,503 @@
+// SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+
+
+/*
+ * Virtio-media driver.
+ *
+ * Copyright (c) 2024-2026 Google LLC.
+ */
+
+#include <linux/device.h>
+#include <linux/dev_printk.h>
+#include <linux/mutex.h>
+#include <linux/types.h>
+#include <linux/module.h>
+#include <linux/virtio.h>
+#include <linux/virtio_config.h>
+#include <linux/virtio_ids.h>
+#include <linux/slab.h>
+#include <linux/scatterlist.h>
+
+#include <media/v4l2-dev.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-fh.h>
+
+#include "uapi/linux/virtio_media.h"
+#include "session.h"
+#include "virtio_media.h"
+
+/**
+ * virtio_media_session_alloc() - Allocate a new session.
+ * @vv: virtio-media device the session belongs to.
+ * @file: file associated with the session.
+ *
+ * The caller must initialize the ``id`` field before calling
+ * virtio_media_session_add().
+ */
+static struct virtio_media_session *
+virtio_media_session_alloc(struct virtio_media *vv, struct file *file)
+{
+ struct virtio_media_session *session;
+
+ session = kzalloc_obj(*session, GFP_KERNEL);
+ if (!session)
+ return ERR_PTR(-ENOMEM);
+
+ // The caller is expected to set the ID once the host assigns a value.
+ session->id = 0;
+
+ INIT_LIST_HEAD(&session->list);
+ v4l2_fh_init(&session->fh, &vv->video_dev);
+ virtio_media_session_fh_add(session, file);
+
+ return session;
+}
+
+/**
+ * virtio_media_session_add() - Add a session to a device.
+ * @vv: virtio-media device the session belongs to.
+ * @session: session to add to the device.
+ */
+static void virtio_media_session_add(struct virtio_media *vv,
+ struct virtio_media_session *session)
+{
+ mutex_lock(&vv->sessions_lock);
+ list_add_tail(&session->list, &vv->sessions);
+ mutex_unlock(&vv->sessions_lock);
+}
+
+/**
+ * virtio_media_session_free() - Free all resources of a session.
+ * @session: session to destroy.
+ *
+ * All the resources of @session, as well as the backing memory of @session
+ * itself, are freed.
+ */
+static void virtio_media_session_free(struct virtio_media_session *session)
+{
+ virtio_media_session_fh_del(session);
+ v4l2_fh_exit(&session->fh);
+
+ kfree(session);
+}
+
+/**
+ * virtio_media_session_remove() - Remove a session from the device.
+ * @vv: virtio-media device the session belongs to.
+ * @session: session to remove.
+ *
+ * This only removes the session from the device. To free it, call
+ * virtio_media_session_free().
+ */
+static void virtio_media_session_remove(struct virtio_media *vv,
+ struct virtio_media_session *session)
+{
+ mutex_lock(&vv->sessions_lock);
+ list_del(&session->list);
+ mutex_unlock(&vv->sessions_lock);
+}
+
+/**
+ * virtio_media_session_close() - Close a session.
+ * @vv: virtio-media device the session belongs to.
+ * @session: session to close.
+ *
+ * This sends the ``VIRTIO_MEDIA_CMD_CLOSE`` command to the device. If @session
+ * is no longer used, it should be first removed by calling
+ * virtio_media_session_remove(), then freed by calling
+ * virtio_media_session_free().
+ */
+static int virtio_media_session_close(struct virtio_media *vv,
+ struct virtio_media_session *session)
+{
+ struct virtio_media_cmd_close *cmd_close = &session->cmd.close;
+ struct scatterlist cmd_sg = {};
+ struct scatterlist *sgs[1] = { &cmd_sg };
+ int ret;
+
+ mutex_lock(&vv->vlock);
+
+ cmd_close->hdr.cmd = VIRTIO_MEDIA_CMD_CLOSE;
+ cmd_close->hdr.__reserved = 0;
+ cmd_close->session_id = session->id;
+ cmd_close->__reserved = 0;
+
+ sg_set_buf(&cmd_sg, cmd_close, sizeof(*cmd_close));
+ sg_mark_end(&cmd_sg);
+
+ ret = virtio_media_send_command(vv, sgs, 1, 0, 0, NULL);
+ mutex_unlock(&vv->vlock);
+ if (ret < 0)
+ return ret;
+
+ return 0;
+}
+
+/**
+ * struct virtio_media_cmd_callback_param - Callback parameters to the virtio
+ * command queue.
+ * @vv: virtio-media device in use.
+ * @done: flag to be switched once the command is completed.
+ * @resp_len: length of the received response from the command. Only valid
+ * after @done flag has switched to %true.
+ */
+struct virtio_media_cmd_callback_param {
+ struct virtio_media *vv;
+ bool done;
+ size_t resp_len;
+};
+
+/**
+ * commandq_callback() - Callback for the command queue.
+ * @queue: command virtqueue.
+ *
+ * This just wakes up the thread that was waiting on the command to complete.
+ */
+static void commandq_callback(struct virtqueue *queue)
+{
+ unsigned int len;
+ struct virtio_media_cmd_callback_param *param;
+
+process_bufs:
+ while ((param = virtqueue_get_buf(queue, &len))) {
+ param->resp_len = len;
+ param->done = true;
+ wake_up(¶m->vv->wq);
+ }
+
+ if (!virtqueue_enable_cb(queue)) {
+ virtqueue_disable_cb(queue);
+ goto process_bufs;
+ }
+}
+
+/**
+ * virtio_media_kick_command() - send a command to the commandq.
+ * @vv: virtio-media device in use.
+ * @sgs: descriptor chain to send.
+ * @out_sgs: number of device-readable descriptors in @sgs.
+ * @in_sgs: number of device-writable descriptors in @sgs.
+ * @resp_len: output parameter. Upon success, contains the size of the response
+ * in bytes.
+ */
+static int virtio_media_kick_command(struct virtio_media *vv,
+ struct scatterlist **sgs,
+ const size_t out_sgs, const size_t in_sgs,
+ size_t *resp_len)
+{
+ struct virtio_media_cmd_callback_param cb_param = {
+ .vv = vv,
+ .done = false,
+ .resp_len = 0,
+ };
+ struct virtio_media_resp_header *resp_header;
+ int ret = virtqueue_add_sgs(vv->commandq, sgs, out_sgs, in_sgs,
+ &cb_param, GFP_ATOMIC);
+ if (ret) {
+ v4l2_err(&vv->v4l2_dev,
+ "failed to add sgs to command virtqueue\n");
+ return ret;
+ }
+
+ if (!virtqueue_kick(vv->commandq)) {
+ v4l2_err(&vv->v4l2_dev, "failed to kick command virtqueue\n");
+ return -EINVAL;
+ }
+
+ /* Wait for the response. */
+ wait_event(vv->wq, cb_param.done);
+
+ if (resp_len)
+ *resp_len = cb_param.resp_len;
+
+ if (in_sgs > 0) {
+ /*
+ * If we expect a response, make sure we have at least a
+ * response header - anything shorter is invalid.
+ */
+ if (cb_param.resp_len < sizeof(*resp_header)) {
+ v4l2_err(&vv->v4l2_dev,
+ "received response header is too short\n");
+ return -EINVAL;
+ }
+
+ resp_header = sg_virt(sgs[out_sgs]);
+ if (resp_header->status)
+ /* Host returns a positive error code. */
+ return -resp_header->status;
+ }
+
+ return 0;
+}
+
+/**
+ * virtio_media_send_command() - Send a command to the device and wait for its
+ * response.
+ * @vv: virtio-media device in use.
+ * @sgs: descriptor chain to send.
+ * @out_sgs: number of device-readable descriptors in @sgs.
+ * @in_sgs: number of device-writable descriptors in @sgs.
+ * @minimum_resp_len: minimum length of the response expected by the caller
+ * when the command is successful. Anything shorter than
+ * that will result in %-EINVAL being returned.
+ * @resp_len: output parameter. Upon success, contains the size of the response
+ * in bytes.
+ */
+int virtio_media_send_command(struct virtio_media *vv, struct scatterlist **sgs,
+ const size_t out_sgs, const size_t in_sgs,
+ size_t minimum_resp_len, size_t *resp_len)
+{
+ size_t local_resp_len = resp_len ? *resp_len : 0;
+ int ret = virtio_media_kick_command(vv, sgs, out_sgs, in_sgs,
+ &local_resp_len);
+ if (resp_len)
+ *resp_len = local_resp_len;
+
+ /*
+ * If the host could not process the command, there is no valid
+ * response.
+ */
+ if (ret < 0)
+ return ret;
+
+ /* Make sure the host wrote a complete reply. */
+ if (local_resp_len < minimum_resp_len) {
+ v4l2_err(&vv->v4l2_dev,
+ "received response is too short: received %zu, expected at least %zu\n",
+ local_resp_len, minimum_resp_len);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+/**
+ * virtio_media_device_open() - Create a new session from an opened file.
+ * @file: opened file for the session.
+ */
+static int virtio_media_device_open(struct file *file)
+{
+ struct video_device *video_dev = video_devdata(file);
+ struct virtio_media *vv = to_virtio_media(video_dev);
+ struct virtio_media_cmd_open *cmd_open = &vv->cmd.open;
+ struct virtio_media_resp_open *resp_open = &vv->resp.open;
+ struct scatterlist cmd_sg = {}, resp_sg = {};
+ struct scatterlist *sgs[2] = { &cmd_sg, &resp_sg };
+ struct virtio_media_session *session;
+ int ret;
+
+ session = virtio_media_session_alloc(vv, file);
+ if (IS_ERR(session))
+ return PTR_ERR(session);
+
+ mutex_lock(&vv->vlock);
+
+ sg_set_buf(&cmd_sg, cmd_open, sizeof(*cmd_open));
+ sg_mark_end(&cmd_sg);
+
+ sg_set_buf(&resp_sg, resp_open, sizeof(*resp_open));
+ sg_mark_end(&resp_sg);
+
+ cmd_open->hdr.cmd = VIRTIO_MEDIA_CMD_OPEN;
+ cmd_open->hdr.__reserved = 0;
+ ret = virtio_media_send_command(vv, sgs, 1, 1, sizeof(*resp_open),
+ NULL);
+ session->id = resp_open->session_id;
+
+ mutex_unlock(&vv->vlock);
+
+ if (ret < 0) {
+ virtio_media_session_free(session);
+ return ret;
+ }
+
+ virtio_media_session_add(vv, session);
+ file->private_data = &session->fh;
+
+ return 0;
+}
+
+/**
+ * virtio_media_device_close() - Close a previously opened session.
+ * @file: file of the session to close.
+ *
+ * This sends the ``VIRTIO_MEDIA_CMD_CLOSE`` command to the device, closes the
+ * session on the driver side, and frees the session.
+ */
+static int virtio_media_device_close(struct file *file)
+{
+ struct video_device *video_dev = video_devdata(file);
+ struct virtio_media *vv = to_virtio_media(video_dev);
+ struct virtio_media_session *session =
+ fh_to_session(file->private_data);
+
+ int ret = virtio_media_session_close(vv, session);
+
+ virtio_media_session_remove(vv, session);
+ virtio_media_session_free(session);
+
+ return ret;
+}
+
+static const struct v4l2_file_operations virtio_media_fops = {
+ .owner = THIS_MODULE,
+ .open = virtio_media_device_open,
+ .release = virtio_media_device_close,
+};
+
+static void virtio_media_release(struct v4l2_device *v4l2_dev)
+{
+ struct virtio_media *vv =
+ container_of(v4l2_dev, struct virtio_media, v4l2_dev);
+
+ v4l2_device_unregister(&vv->v4l2_dev);
+ kfree(vv);
+}
+
+static int virtio_media_probe(struct virtio_device *virtio_dev)
+{
+ struct device *dev = &virtio_dev->dev;
+ struct virtqueue *vqs[1];
+ static struct virtqueue_info vq_info[1] = {
+ {
+ .name = "command",
+ .callback = commandq_callback,
+ },
+ };
+ struct virtio_media *vv;
+ struct video_device *vd;
+ int ret;
+
+ vv = kzalloc_obj(struct virtio_media);
+ if (!vv)
+ return -ENOMEM;
+
+ INIT_LIST_HEAD(&vv->sessions);
+ mutex_init(&vv->sessions_lock);
+ mutex_init(&vv->vlock);
+
+ vv->virtio_dev = virtio_dev;
+ virtio_dev->priv = vv;
+
+ init_waitqueue_head(&vv->wq);
+
+ ret = v4l2_device_register(dev, &vv->v4l2_dev);
+ if (ret)
+ goto err_free_virtio_media;
+
+ vv->v4l2_dev.release = virtio_media_release;
+
+ ret = virtio_find_vqs(virtio_dev, 1, vqs, vq_info, NULL);
+ if (ret)
+ goto err_v4l2_device_unregister;
+
+ vv->commandq = vqs[0];
+
+ vd = &vv->video_dev;
+ vd->v4l2_dev = &vv->v4l2_dev;
+ vd->fops = &virtio_media_fops;
+ vd->release = video_device_release_empty;
+ strscpy(vd->name, "virtio-media", sizeof(vd->name));
+
+ video_set_drvdata(vd, vv);
+
+ virtio_cread_le(virtio_dev, struct virtio_media_config, device_caps,
+ &vd->device_caps);
+ if (vd->device_caps & (V4L2_CAP_VIDEO_M2M | V4L2_CAP_VIDEO_M2M_MPLANE))
+ vd->vfl_dir = VFL_DIR_M2M;
+ else if (vd->device_caps &
+ (V4L2_CAP_VIDEO_OUTPUT | V4L2_CAP_VIDEO_OUTPUT_MPLANE))
+ vd->vfl_dir = VFL_DIR_TX;
+ else
+ vd->vfl_dir = VFL_DIR_RX;
+
+ virtio_device_ready(virtio_dev);
+
+ __u32 device_type;
+
+ virtio_cread_le(virtio_dev, struct virtio_media_config, device_type,
+ &device_type);
+ if (device_type >= VFL_TYPE_MAX) {
+ dev_err(&virtio_dev->dev, "Invalid device_type: %u",
+ device_type);
+ ret = -EINVAL;
+ goto err_del_vqs;
+ }
+
+ ret = video_register_device(vd, device_type, -1);
+ if (ret)
+ goto err_del_vqs;
+
+ return 0;
+
+err_del_vqs:
+ virtio_dev->config->del_vqs(virtio_dev);
+err_v4l2_device_unregister:
+ v4l2_device_unregister(&vv->v4l2_dev);
+err_free_virtio_media:
+ kfree(vv);
+ return ret;
+}
+
+static void virtio_media_remove(struct virtio_device *virtio_dev)
+{
+ struct virtio_media *vv = virtio_dev->priv;
+ struct list_head *p, *n;
+
+ /* Remove the video device to stop accessing the commandq virtqueue */
+ video_unregister_device(&vv->video_dev);
+
+ /*
+ * Acquire vlock to ensure no concurrent accesses to the commandq
+ * virtqueue
+ */
+ mutex_lock(&vv->vlock);
+
+ /* Stop all virtqueue callbacks */
+ virtio_reset_device(virtio_dev);
+
+ /*
+ * Only detach the unused buffers to allow internal virtqueue clean up.
+ * The actual buffers are freed in virtio_media_release()
+ */
+ while (virtqueue_detach_unused_buf(vv->commandq))
+ ;
+
+ /* Delete the virtqueues */
+ virtio_dev->config->del_vqs(virtio_dev);
+
+ mutex_unlock(&vv->vlock);
+
+ list_for_each_safe(p, n, &vv->sessions) {
+ struct virtio_media_session *s =
+ list_entry(p, struct virtio_media_session, list);
+
+ virtio_media_session_remove(vv, s);
+ virtio_media_session_free(s);
+ }
+
+ v4l2_device_put(&vv->v4l2_dev);
+}
+
+static struct virtio_device_id id_table[] = {
+ { VIRTIO_ID_MEDIA, VIRTIO_DEV_ANY_ID },
+ { 0 },
+};
+
+static unsigned int features[] = {};
+
+static struct virtio_driver virtio_media_driver = {
+ .feature_table = features,
+ .feature_table_size = ARRAY_SIZE(features),
+ .driver.name = VIRTIO_MEDIA_DEFAULT_DRIVER_NAME,
+ .driver.owner = THIS_MODULE,
+ .id_table = id_table,
+ .probe = virtio_media_probe,
+ .remove = virtio_media_remove,
+};
+
+module_virtio_driver(virtio_media_driver);
+
+MODULE_DEVICE_TABLE(virtio, id_table);
+MODULE_DESCRIPTION("virtio media driver");
+MODULE_AUTHOR("Alexandre Courbot <gnurou@gmail.com>");
+MODULE_LICENSE("Dual BSD/GPL");
diff --git a/include/uapi/linux/virtio_media.h b/include/uapi/linux/virtio_media.h
new file mode 100644
index 000000000..ad05d7421
--- /dev/null
+++ b/include/uapi/linux/virtio_media.h
@@ -0,0 +1,118 @@
+/* SPDX-License-Identifier: ((GPL-2.0+ WITH Linux-syscall-note) OR BSD-3-Clause) */
+
+/*
+ * Definitions of virtio-media protocol structures.
+ *
+ * Copyright (c) 2024-2026 Google LLC.
+ */
+
+#ifndef _UAPI__LINUX_VIRTIO_MEDIA_H
+#define _UAPI__LINUX_VIRTIO_MEDIA_H
+
+#include <linux/types.h>
+#include <linux/videodev2.h>
+
+/*
+ * Virtio protocol definition.
+ */
+
+/**
+ * struct virtio_media_config - Video device configuration space.
+ * @device_caps: flags representing the device capabilities as used in &struct
+ * v4l2_capability. It corresponds with the device_caps field in
+ * the &struct video_device
+ * @ device_type: informs the driver of the type of the video device. It
+ * corresponds to with the ``vfl_devnod_type`` field of the
+ * device.
+ * @card: name of the device, a NUL-terminated UTF-8 string. It corresponds
+ * with the card field of the struct v4l2_capability. If all the
+ * characters of the field are used, it does not need to be
+ * NUL-terminated.
+ */
+struct virtio_media_config {
+ __le32 device_caps;
+ __le32 device_type;
+ __u8 card[32];
+};
+
+/**
+ * struct virtio_media_cmd_header - Header for all virtio-media commands.
+ * @cmd: one of VIRTIO_MEDIA_CMD_*.
+ * @__reserved: must be set to zero by the driver.
+ *
+ * This header starts all commands from the driver to the device on the
+ * commandq.
+ */
+struct virtio_media_cmd_header {
+ __le32 cmd;
+ __le32 __reserved;
+};
+
+/**
+ * struct virtio_media_resp_header - Header for all virtio-media responses.
+ * @status: 0 if the command was successful, or one of the standard Linux error
+ * codes as a positive integer.
+ * @__reserved: must be set to zero by the device.
+ *
+ * This header starts all responses from the device to the driver on the
+ * commandq.
+ */
+struct virtio_media_resp_header {
+ __le32 status;
+ __le32 __reserved;
+};
+
+/**
+ * VIRTIO_MEDIA_CMD_OPEN - Command for creating a new session.
+ *
+ * This is the equivalent of calling ``open`` on a V4L2 device node. Upon
+ * success, a session id is returned which can be used to perform other
+ * commands on the session, notably ioctls.
+ */
+#define VIRTIO_MEDIA_CMD_OPEN 1
+
+/**
+ * struct virtio_media_cmd_open - Driver command for VIRTIO_MEDIA_CMD_OPEN.
+ * @hdr: header with cmd member set to VIRTIO_MEDIA_CMD_OPEN.
+ */
+struct virtio_media_cmd_open {
+ struct virtio_media_cmd_header hdr;
+};
+
+/**
+ * struct virtio_media_resp_open - Device response for VIRTIO_MEDIA_CMD_OPEN.
+ * @hdr: header containing the status of the command.
+ * @session_id: if &struct virtio_media_resp_header.status == 0, contains the
+ * id of the newly created session.
+ * @__reserved: must be set to zero by the device.
+ */
+struct virtio_media_resp_open {
+ struct virtio_media_resp_header hdr;
+ __le32 session_id;
+ __le32 __reserved;
+};
+
+/**
+ * VIRTIO_MEDIA_CMD_CLOSE - Command for closing an active session.
+ *
+ * This is the equivalent of calling ``close`` on a previously opened V4L2
+ * session. All resources associated with this session will be freed and the
+ * session ID shall not be used again after queueing this command.
+ *
+ * This command does not require a response from the device.
+ */
+#define VIRTIO_MEDIA_CMD_CLOSE 2
+
+/**
+ * struct virtio_media_cmd_close - Driver command for VIRTIO_MEDIA_CMD_CLOSE.
+ * @hdr: header with cmd member set to VIRTIO_MEDIA_CMD_CLOSE.
+ * @session_id: id of the session to close.
+ * @__reserved: must be set to zero by the driver.
+ */
+struct virtio_media_cmd_close {
+ struct virtio_media_cmd_header hdr;
+ __le32 session_id;
+ __le32 __reserved;
+};
+
+#endif // _UAPI__LINUX_VIRTIO_MEDIA_H
diff --git a/usr/include/Makefile b/usr/include/Makefile
index ee69dd9d9..df106b5d9 100644
--- a/usr/include/Makefile
+++ b/usr/include/Makefile
@@ -37,6 +37,7 @@ no-header-test += linux/usb/audio.h
no-header-test += linux/v4l2-mediabus.h
no-header-test += linux/v4l2-subdev.h
no-header-test += linux/videodev2.h
+no-header-test += linux/virtio_media.h
no-header-test += linux/vm_sockets.h
no-header-test += sound/asequencer.h
no-header-test += sound/asoc.h
--
2.56.0.317.g454f8302e1-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v10 2/5] media: virtio: Add V4L2 ioctl forwarding infrastructure
2026-10-29 15:30 [PATCH v10 0/5] media: add virtio-media driver Brian Daniels
2026-10-29 15:30 ` [PATCH v10 1/5] media: virtio: Add core driver and session management Brian Daniels
@ 2026-10-29 15:30 ` Brian Daniels
2026-10-29 16:15 ` sashiko-bot
2026-10-29 15:30 ` [PATCH v10 3/5] media: virtio: Add extended controls support Brian Daniels
` (2 subsequent siblings)
4 siblings, 1 reply; 11+ messages in thread
From: Brian Daniels @ 2026-10-29 15:30 UTC (permalink / raw)
To: Mauro Carvalho Chehab
Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
gurchetansingh, hverkuil, linux-kernel, linux-media, mst,
nicolas.dufresne, virtualization, xuanzhuo, dbassey,
laurent.pinchart, Brian Daniels
From: Alexandre Courbot <gnurou@gmail.com>
Introduce the scatterlist builder and synchronous V4L2 ioctl forwarding
via VIRTIO_MEDIA_CMD_IOCTL.
The scatterlist builder constructs virtio descriptor chains for ioctl
commands and responses, mapping 1:1 kernel memory and vmalloc buffers
directly into scatterlists when possible while falling back to a
per-session shadow buffer otherwise.
Use this infrastructure to implement VIDIOC_QUERYCAP from the virtio
configuration space along with fixed-size read, write, and read-write
V4L2 ioctls covering format negotiation, frame sizes and intervals,
control and menu queries, inputs and outputs, audio, selection, video
standards, tuners, and DV timings.
Signed-off-by: Alexandre Courbot <gnurou@gmail.com>
Assisted-by: LLM
Co-developed-by: Brian Daniels <briandaniels@google.com>
Signed-off-by: Brian Daniels <briandaniels@google.com>
---
drivers/media/virtio/Makefile | 2 +-
drivers/media/virtio/scatterlist_builder.c | 222 ++++++++
drivers/media/virtio/scatterlist_builder.h | 90 +++
drivers/media/virtio/session.h | 28 +-
drivers/media/virtio/virtio_media.h | 9 +
drivers/media/virtio/virtio_media_driver.c | 24 +-
drivers/media/virtio/virtio_media_ioctls.c | 606 +++++++++++++++++++++
include/uapi/linux/virtio_media.h | 43 ++
8 files changed, 1020 insertions(+), 4 deletions(-)
create mode 100644 drivers/media/virtio/scatterlist_builder.c
create mode 100644 drivers/media/virtio/scatterlist_builder.h
create mode 100644 drivers/media/virtio/virtio_media_ioctls.c
diff --git a/drivers/media/virtio/Makefile b/drivers/media/virtio/Makefile
index 09d9834da..f1bc8a3ce 100644
--- a/drivers/media/virtio/Makefile
+++ b/drivers/media/virtio/Makefile
@@ -2,6 +2,6 @@
#
# Makefile for the virtio-media device driver.
-virtio-media-objs := virtio_media_driver.o
+virtio-media-objs := scatterlist_builder.o virtio_media_ioctls.o virtio_media_driver.o
obj-$(CONFIG_MEDIA_VIRTIO) += virtio-media.o
diff --git a/drivers/media/virtio/scatterlist_builder.c b/drivers/media/virtio/scatterlist_builder.c
new file mode 100644
index 000000000..ae8ffee82
--- /dev/null
+++ b/drivers/media/virtio/scatterlist_builder.c
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+
+
+/*
+ * Scatterlist builder helpers for virtio-media.
+ *
+ * Copyright (c) 2024-2026 Google LLC.
+ */
+
+#include <linux/moduleparam.h>
+#include <linux/scatterlist.h>
+#include <linux/videodev2.h>
+#include <media/videobuf2-memops.h>
+
+#include "uapi/linux/virtio_media.h"
+#include "scatterlist_builder.h"
+#include "session.h"
+
+/*
+ * If set to %true, then the driver will always copy the data passed to the
+ * host into the shadow buffer (instead of trying to map the source memory into
+ * the SG table directly when possible).
+ */
+static bool always_use_shadow_buffer;
+module_param(always_use_shadow_buffer, bool, 0660);
+
+/* Convert a V4L2 IOCTL into the IOCTL code we can give to the host */
+#define VIRTIO_MEDIA_IOCTL_CODE(IOCTL) (((IOCTL) >> _IOC_NRSHIFT) & _IOC_NRMASK)
+
+/**
+ * scatterlist_builder_add_descriptor() - Add a descriptor to the chain.
+ * @builder: builder to use.
+ * @desc_index: index of the descriptor to add.
+ *
+ * Returns %-ENOSPC if @builder->sgs is already full.
+ */
+int scatterlist_builder_add_descriptor(struct scatterlist_builder *builder,
+ size_t desc_index)
+{
+ if (builder->cur_sg >= builder->num_sgs)
+ return -ENOSPC;
+ builder->sgs[builder->cur_sg++] = &builder->descs[desc_index];
+
+ return 0;
+}
+
+/**
+ * scatterlist_builder_add_data() - Append arbitrary data to the descriptor
+ * chain.
+ * @builder: builder to use.
+ * @data: pointer to the data to add to the descriptor chain.
+ * @len: length of the data to add.
+ *
+ * @data will either be directly referenced, or copied into the shadow buffer
+ * to be referenced from there.
+ */
+int scatterlist_builder_add_data(struct scatterlist_builder *builder,
+ void *data, size_t len)
+{
+ const size_t cur_desc = builder->cur_desc;
+
+ if (len == 0)
+ return 0;
+
+ if (builder->cur_desc >= builder->num_descs)
+ return -ENOSPC;
+
+ if (!always_use_shadow_buffer && virt_addr_valid(data) &&
+ virt_addr_valid(data + len)) {
+ /*
+ * If "data" is in the 1:1 physical memory mapping then we can
+ * use a single SG entry and avoid copying.
+ */
+ struct page *page = virt_to_page(data);
+ size_t offset = (((size_t)data) & ~PAGE_MASK);
+ struct scatterlist *next_desc =
+ &builder->descs[builder->cur_desc];
+
+ memset(next_desc, 0, sizeof(*next_desc));
+ sg_set_page(next_desc, page, len, offset);
+ builder->cur_desc++;
+ } else if (!always_use_shadow_buffer && is_vmalloc_addr(data)) {
+ bool prev_pfn_valid = false;
+ unsigned long prev_pfn = 0;
+
+ /*
+ * If "data" has been vmalloc'ed, we need at most one entry per
+ * memory page but can avoid copying.
+ */
+ while (len > 0) {
+ struct page *page = vmalloc_to_page(data);
+ unsigned long cur_pfn = page_to_pfn(page);
+ /* All pages but the first will start at offset 0. */
+ unsigned long offset =
+ (((unsigned long)data) & ~PAGE_MASK);
+ size_t len_in_page = min(PAGE_SIZE - offset, len);
+ struct scatterlist *next_desc =
+ &builder->descs[builder->cur_desc];
+
+ if (builder->cur_desc >= builder->num_descs)
+ return -ENOSPC;
+
+ /* Optimize contiguous pages */
+ if (prev_pfn_valid && (cur_pfn == prev_pfn + 1)) {
+ (next_desc - 1)->length += len_in_page;
+ } else {
+ memset(next_desc, 0, sizeof(*next_desc));
+ sg_set_page(next_desc, page, len_in_page,
+ offset);
+ builder->cur_desc++;
+ }
+ data += len_in_page;
+ len -= len_in_page;
+ prev_pfn = cur_pfn;
+ prev_pfn_valid = true;
+ }
+ } else {
+ /*
+ * As a last resort, copy into the shadow buffer and reference
+ * it with a single SG entry. Calling
+ * scatterlist_builder_retrieve_data() will be necessary to copy
+ * the data written by the device back into @data.
+ */
+ void *shadow_buffer =
+ builder->shadow_buffer + builder->shadow_buffer_pos;
+ struct page *page = virt_to_page(shadow_buffer);
+ unsigned long offset =
+ (((unsigned long)shadow_buffer) & ~PAGE_MASK);
+ struct scatterlist *next_desc =
+ &builder->descs[builder->cur_desc];
+
+ if (len >
+ builder->shadow_buffer_size - builder->shadow_buffer_pos)
+ return -ENOSPC;
+
+ memcpy(shadow_buffer, data, len);
+ memset(next_desc, 0, sizeof(*next_desc));
+ sg_set_page(next_desc, page, len, offset);
+ builder->cur_desc++;
+ builder->shadow_buffer_pos += len;
+ }
+
+ sg_mark_end(&builder->descs[builder->cur_desc - 1]);
+ return scatterlist_builder_add_descriptor(builder, cur_desc);
+}
+
+/**
+ * scatterlist_builder_retrieve_data() - Retrieve a response written by the
+ * device on the shadow buffer.
+ * @builder: builder to use.
+ * @sg_index: index of the descriptor to read from.
+ * @data: destination for the shadowed data.
+ *
+ * If the shadow buffer is pointed to by the descriptor at index @sg_index of
+ * the chain, then ``sg->length`` bytes are copied back from it into @data.
+ * Otherwise nothing is done since the device has written into @data directly.
+ *
+ * @data must have originally been added by scatterlist_builder_add_data() as
+ * the same size as passed to scatterlist_builder_add_data() will be copied
+ * back.
+ */
+int scatterlist_builder_retrieve_data(struct scatterlist_builder *builder,
+ size_t sg_index, void *data)
+{
+ void *shadow_buf = builder->shadow_buffer;
+ struct scatterlist *sg;
+ void *kaddr;
+
+ /* We can only retrieve from the range of sgs currently set. */
+ if (sg_index >= builder->cur_sg)
+ return -ERANGE;
+
+ sg = builder->sgs[sg_index];
+ kaddr = pfn_to_kaddr(page_to_pfn(sg_page(sg))) + sg->offset;
+
+ if (kaddr >= shadow_buf &&
+ kaddr < shadow_buf + builder->shadow_buffer_size) {
+ if (kaddr + sg->length >
+ shadow_buf + builder->shadow_buffer_size)
+ return -EINVAL;
+
+ memcpy(data, kaddr, sg->length);
+ }
+
+ return 0;
+}
+
+/**
+ * scatterlist_builder_add_ioctl_cmd() - Add an ioctl command to the descriptor
+ * chain.
+ * @builder: builder to use.
+ * @session: session on behalf of which the ioctl command is added.
+ * @ioctl_code: code of the ioctl to add (i.e. ``VIDIOC_*``).
+ */
+int scatterlist_builder_add_ioctl_cmd(struct scatterlist_builder *builder,
+ struct virtio_media_session *session,
+ u32 ioctl_code)
+{
+ struct virtio_media_cmd_ioctl *cmd_ioctl = &session->cmd.ioctl;
+
+ cmd_ioctl->hdr.cmd = VIRTIO_MEDIA_CMD_IOCTL;
+ cmd_ioctl->hdr.__reserved = 0;
+ cmd_ioctl->session_id = session->id;
+ cmd_ioctl->code = VIRTIO_MEDIA_IOCTL_CODE(ioctl_code);
+
+ return scatterlist_builder_add_data(builder, cmd_ioctl,
+ sizeof(*cmd_ioctl));
+}
+
+/**
+ * scatterlist_builder_add_ioctl_resp() - Add storage to receive an ioctl
+ * response to the descriptor chain.
+ * @builder: builder to use.
+ * @session: session on behalf of which the ioctl response is added.
+ */
+int scatterlist_builder_add_ioctl_resp(struct scatterlist_builder *builder,
+ struct virtio_media_session *session)
+{
+ struct virtio_media_resp_ioctl *resp_ioctl = &session->resp.ioctl;
+
+ return scatterlist_builder_add_data(builder, resp_ioctl,
+ sizeof(*resp_ioctl));
+}
diff --git a/drivers/media/virtio/scatterlist_builder.h b/drivers/media/virtio/scatterlist_builder.h
new file mode 100644
index 000000000..044ea409a
--- /dev/null
+++ b/drivers/media/virtio/scatterlist_builder.h
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+ */
+
+/*
+ * Scatterlist builder helpers for virtio-media.
+ *
+ * Copyright (c) 2024-2026 Google LLC.
+ */
+
+#ifndef __VIRTIO_MEDIA_SCATTERLIST_BUILDER_H
+#define __VIRTIO_MEDIA_SCATTERLIST_BUILDER_H
+
+#include <linux/scatterlist.h>
+
+#include "session.h"
+
+/**
+ * struct scatterlist_builder - helper to build a scatterlist from data.
+ * @descs: pool of descriptors to use.
+ * @num_descs: number of entries in descs.
+ * @cur_desc: next descriptor to be used in @descs.
+ * @shadow_buffer: pointer to a shadow buffer where elements that cannot be
+ * mapped directly into the scatterlist get copied.
+ * @shadow_buffer_size: size of @shadow_buffer.
+ * @shadow_buffer_pos: current position in @shadow_buffer.
+ * @sgs: descriptor chain to eventually pass to virtio functions.
+ * @num_sgs: total number of entries in @sgs.
+ * @cur_sg: next entry in @sgs to be used.
+ *
+ * Virtio passes data from the driver to the device (through e.g.
+ * virtqueue_add_sgs()) via a scatterlist that the device interprets as a
+ * linear view over scattered driver memory.
+ *
+ * In virtio-media, the payload of ioctls from user-space can for the most part
+ * be passed as-is, or after slight modification, which makes it tempting to
+ * just forward the ioctl payload received from user-space as-is instead of
+ * doing another copy into a dedicated buffer. This structure helps with this.
+ *
+ * virtio-media descriptor chains are typically made of the following parts:
+ *
+ * Device-readable:
+ * - A command structure, i.e. ``virtio_media_cmd_*``,
+ * - An ioctl payload (one of the regular ioctl parameters),
+ * - (optionally) arrays of &struct virtio_media_sg_entry describing the
+ * content of buffers in guest memory.
+ *
+ * Device-writable:
+ * - A response structure, i.e. ``virtio_media_resp_*``,
+ * - An ioctl payload, that the device will write to.
+ *
+ * This structure helps laying out the descriptor chain into its @sgs member in
+ * an optimal way, by building a scatterlist adapted to the originating memory
+ * of the data we want to pass to the device while avoiding copies when
+ * possible.
+ *
+ * It is made of a pool of &struct scatterlist (@descs) that is used to
+ * build the final descriptor chain @sgs, and a @shadow_buffer where data that
+ * cannot (or should not) be mapped directly by the host can be temporarily
+ * copied.
+ */
+struct scatterlist_builder {
+ struct scatterlist *descs;
+ size_t num_descs;
+ size_t cur_desc;
+
+ void *shadow_buffer;
+ size_t shadow_buffer_size;
+ size_t shadow_buffer_pos;
+
+ struct scatterlist **sgs;
+ size_t num_sgs;
+ size_t cur_sg;
+};
+
+int scatterlist_builder_add_descriptor(struct scatterlist_builder *builder,
+ size_t desc_index);
+
+int scatterlist_builder_add_data(struct scatterlist_builder *builder,
+ void *data, size_t len);
+
+int scatterlist_builder_retrieve_data(struct scatterlist_builder *builder,
+ size_t sg_index, void *data);
+
+int scatterlist_builder_add_ioctl_cmd(struct scatterlist_builder *builder,
+ struct virtio_media_session *session,
+ u32 ioctl_code);
+
+int scatterlist_builder_add_ioctl_resp(struct scatterlist_builder *builder,
+ struct virtio_media_session *session);
+
+#endif // __VIRTIO_MEDIA_SCATTERLIST_BUILDER_H
diff --git a/drivers/media/virtio/session.h b/drivers/media/virtio/session.h
index 4ade7fb9c..6fe7ff5ae 100644
--- a/drivers/media/virtio/session.h
+++ b/drivers/media/virtio/session.h
@@ -9,17 +9,32 @@
#ifndef __VIRTIO_MEDIA_SESSION_H
#define __VIRTIO_MEDIA_SESSION_H
+#include <linux/scatterlist.h>
#include <media/v4l2-fh.h>
#include "uapi/linux/virtio_media.h"
+/*
+ * Size of the per-session virtio shadow and event buffers. 16K should be
+ * enough to contain everything we need.
+ */
+#define VIRTIO_SHADOW_BUF_SIZE 0x4000
+
/**
* struct virtio_media_session - A session on a virtio_media device.
* @fh: file handler for the session.
* @file: file pointer associated with the session's file handler.
* @id: session ID used to communicate with the device.
- * @cmd: union of session-level commands. A session can have one command
- * currently running.
+ * @cmd: union of session commands ``close`` and ``ioctl``. A session can have
+ * one command currently running. The rest of the commands are handled by
+ * &struct virtio_media.
+ * @resp: union of responses to session commands. A session can wait on one
+ * command only. The rest of the responses are handled by
+ * &struct virtio_media.
+ * @shadow_buf: shadow buffer where data to be added to the descriptor chain can
+ * be staged before being sent to the device.
+ * @command_sgs: SG table gathering descriptors for a given command and its
+ * response.
* @list: link into the list of sessions for the device.
*/
struct virtio_media_session {
@@ -30,9 +45,18 @@ struct virtio_media_session {
__dma_from_device_group_begin();
union {
struct virtio_media_cmd_close close;
+ struct virtio_media_cmd_ioctl ioctl;
} cmd;
+
+ union {
+ struct virtio_media_resp_ioctl ioctl;
+ } resp;
__dma_from_device_group_end();
+ void *shadow_buf;
+
+ struct sg_table command_sgs;
+
struct list_head list;
};
diff --git a/drivers/media/virtio/virtio_media.h b/drivers/media/virtio/virtio_media.h
index 68299377a..a120c1014 100644
--- a/drivers/media/virtio/virtio_media.h
+++ b/drivers/media/virtio/virtio_media.h
@@ -14,6 +14,8 @@
#include "uapi/linux/virtio_media.h"
+#define DESC_CHAIN_MAX_LEN SG_MAX_SINGLE_ALLOC
+
#define VIRTIO_MEDIA_DEFAULT_DRIVER_NAME "virtio-media"
/**
@@ -66,5 +68,12 @@ to_virtio_media(struct video_device *video_dev)
int virtio_media_send_command(struct virtio_media *vv, struct scatterlist **sgs,
const size_t out_sgs, const size_t in_sgs,
size_t minimum_resp_len, size_t *resp_len);
+
+/* virtio_media_ioctls.c */
+
+long virtio_media_device_ioctl(struct file *file, unsigned int cmd,
+ unsigned long arg);
+extern const struct v4l2_ioctl_ops virtio_media_ioctl_ops;
+
#endif // __VIRTIO_MEDIA_H
diff --git a/drivers/media/virtio/virtio_media_driver.c b/drivers/media/virtio/virtio_media_driver.c
index 10073d863..2f2e95077 100644
--- a/drivers/media/virtio/virtio_media_driver.c
+++ b/drivers/media/virtio/virtio_media_driver.c
@@ -37,10 +37,20 @@ static struct virtio_media_session *
virtio_media_session_alloc(struct virtio_media *vv, struct file *file)
{
struct virtio_media_session *session;
+ int ret;
session = kzalloc_obj(*session, GFP_KERNEL);
if (!session)
- return ERR_PTR(-ENOMEM);
+ goto err_session;
+
+ session->shadow_buf = kzalloc(VIRTIO_SHADOW_BUF_SIZE, GFP_KERNEL);
+ if (!session->shadow_buf)
+ goto err_shadow_buf;
+
+ ret = sg_alloc_table(&session->command_sgs, DESC_CHAIN_MAX_LEN,
+ GFP_KERNEL);
+ if (ret)
+ goto err_payload_sgs;
// The caller is expected to set the ID once the host assigns a value.
session->id = 0;
@@ -50,6 +60,13 @@ virtio_media_session_alloc(struct virtio_media *vv, struct file *file)
virtio_media_session_fh_add(session, file);
return session;
+
+err_payload_sgs:
+ kfree(session->shadow_buf);
+err_shadow_buf:
+ kfree(session);
+err_session:
+ return ERR_PTR(-ENOMEM);
}
/**
@@ -77,6 +94,9 @@ static void virtio_media_session_free(struct virtio_media_session *session)
virtio_media_session_fh_del(session);
v4l2_fh_exit(&session->fh);
+ sg_free_table(&session->command_sgs);
+
+ kfree(session->shadow_buf);
kfree(session);
}
@@ -342,6 +362,7 @@ static const struct v4l2_file_operations virtio_media_fops = {
.owner = THIS_MODULE,
.open = virtio_media_device_open,
.release = virtio_media_device_close,
+ .unlocked_ioctl = virtio_media_device_ioctl,
};
static void virtio_media_release(struct v4l2_device *v4l2_dev)
@@ -394,6 +415,7 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
vd = &vv->video_dev;
vd->v4l2_dev = &vv->v4l2_dev;
+ vd->ioctl_ops = &virtio_media_ioctl_ops;
vd->fops = &virtio_media_fops;
vd->release = video_device_release_empty;
strscpy(vd->name, "virtio-media", sizeof(vd->name));
diff --git a/drivers/media/virtio/virtio_media_ioctls.c b/drivers/media/virtio/virtio_media_ioctls.c
new file mode 100644
index 000000000..656ecea8a
--- /dev/null
+++ b/drivers/media/virtio/virtio_media_ioctls.c
@@ -0,0 +1,606 @@
+// SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+
+
+/*
+ * Ioctl implementations for the virtio-media driver.
+ *
+ * Copyright (c) 2024-2026 Google LLC.
+ */
+
+#include <linux/mutex.h>
+#include <linux/videodev2.h>
+#include <linux/virtio_config.h>
+#include <media/v4l2-ioctl.h>
+
+#include "scatterlist_builder.h"
+#include "virtio_media.h"
+
+/**
+ * virtio_media_send_r_ioctl() - Send a read-only ioctl to the device.
+ * @fh: file handler of the session doing the ioctl.
+ * @ioctl: ``VIDIOC_*`` ioctl code.
+ * @ioctl_data: pointer to the ioctl payload.
+ * @ioctl_data_len: length in bytes of the ioctl payload.
+ *
+ * Send an ioctl that has no driver payload, but expects a response from the
+ * host (i.e. an ioctl specified with ``_IOR``).
+ */
+static int virtio_media_send_r_ioctl(struct v4l2_fh *fh, u32 ioctl,
+ void *ioctl_data, size_t ioctl_data_len)
+{
+ struct video_device *video_dev = fh->vdev;
+ struct virtio_media *vv = to_virtio_media(video_dev);
+ struct virtio_media_session *session = fh_to_session(fh);
+ struct scatterlist *sgs[3];
+ struct scatterlist_builder builder = {
+ .descs = session->command_sgs.sgl,
+ .num_descs = DESC_CHAIN_MAX_LEN,
+ .cur_desc = 0,
+ .shadow_buffer = session->shadow_buf,
+ .shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
+ .shadow_buffer_pos = 0,
+ .sgs = sgs,
+ .num_sgs = ARRAY_SIZE(sgs),
+ .cur_sg = 0,
+ };
+
+ /* Command descriptor */
+ int ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
+
+ if (ret)
+ return ret;
+
+ /* Response descriptor */
+ ret = scatterlist_builder_add_ioctl_resp(&builder, session);
+ if (ret)
+ return ret;
+
+ /* Response payload */
+ ret = scatterlist_builder_add_data(&builder, ioctl_data,
+ ioctl_data_len);
+ if (ret) {
+ v4l2_err(&vv->v4l2_dev,
+ "failed to prepare command descriptor chain\n");
+ return ret;
+ }
+
+ ret = virtio_media_send_command(vv, sgs, 1, 2,
+ sizeof(struct virtio_media_resp_ioctl) +
+ ioctl_data_len, NULL);
+ if (ret < 0)
+ return ret;
+
+ ret = scatterlist_builder_retrieve_data(&builder, 2, ioctl_data);
+ if (ret) {
+ v4l2_err(&vv->v4l2_dev,
+ "failed to retrieve response descriptor chain\n");
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * virtio_media_send_w_ioctl() - Send a write-only ioctl to the device.
+ * @fh: file handler of the session doing the ioctl.
+ * @ioctl: ``VIDIOC_*`` ioctl code.
+ * @ioctl_data: pointer to the ioctl payload.
+ * @ioctl_data_len: length in bytes of the ioctl payload.
+ *
+ * Send an ioctl that does not expect a reply beyond an error status (i.e. an
+ * ioctl specified with ``_IOW``) to the host.
+ */
+static int virtio_media_send_w_ioctl(struct v4l2_fh *fh, u32 ioctl,
+ const void *ioctl_data,
+ size_t ioctl_data_len)
+{
+ struct video_device *video_dev = fh->vdev;
+ struct virtio_media *vv = to_virtio_media(video_dev);
+ struct virtio_media_session *session = fh_to_session(fh);
+ struct scatterlist *sgs[3];
+ struct scatterlist_builder builder = {
+ .descs = session->command_sgs.sgl,
+ .num_descs = DESC_CHAIN_MAX_LEN,
+ .cur_desc = 0,
+ .shadow_buffer = session->shadow_buf,
+ .shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
+ .shadow_buffer_pos = 0,
+ .sgs = sgs,
+ .num_sgs = ARRAY_SIZE(sgs),
+ .cur_sg = 0,
+ };
+
+ /* Command descriptor */
+ int ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
+
+ if (ret)
+ return ret;
+
+ /* Command payload */
+ ret = scatterlist_builder_add_data(&builder, (void *)ioctl_data,
+ ioctl_data_len);
+ if (ret) {
+ v4l2_err(&vv->v4l2_dev,
+ "failed to prepare command descriptor chain\n");
+ return ret;
+ }
+
+ /* Response descriptor */
+ ret = scatterlist_builder_add_ioctl_resp(&builder, session);
+ if (ret)
+ return ret;
+
+ ret = virtio_media_send_command(vv, sgs, 2, 1,
+ sizeof(struct virtio_media_resp_ioctl),
+ NULL);
+ if (ret < 0)
+ return ret;
+
+ return 0;
+}
+
+/**
+ * virtio_media_send_wr_ioctl() - Send a read-write ioctl to the device.
+ * @fh: file handler of the session doing the ioctl.
+ * @ioctl: ``VIDIOC_*`` ioctl code.
+ * @ioctl_data: pointer to the ioctl payload.
+ * @ioctl_data_len: length in bytes of the ioctl payload.
+ * @minimum_resp_payload: minimum expected length of the response's payload.
+ *
+ * Sends an ioctl that expects a response of exactly the same size as the
+ * input (i.e. an ioctl specified with ``_IOWR``) to the host.
+ *
+ * This corresponds to what most V4L2 ioctls do. For instance
+ * ``VIDIOC_ENUM_FMT`` takes a partially-initialized &struct v4l2_fmtdesc
+ * and returns its filled version.
+ */
+static int virtio_media_send_wr_ioctl(struct v4l2_fh *fh, u32 ioctl,
+ void *ioctl_data, size_t ioctl_data_len,
+ size_t minimum_resp_payload)
+{
+ struct video_device *video_dev = fh->vdev;
+ struct virtio_media *vv = to_virtio_media(video_dev);
+ struct virtio_media_session *session = fh_to_session(fh);
+ struct scatterlist *sgs[4];
+ struct scatterlist_builder builder = {
+ .descs = session->command_sgs.sgl,
+ .num_descs = DESC_CHAIN_MAX_LEN,
+ .cur_desc = 0,
+ .shadow_buffer = session->shadow_buf,
+ .shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
+ .shadow_buffer_pos = 0,
+ .sgs = sgs,
+ .num_sgs = ARRAY_SIZE(sgs),
+ .cur_sg = 0,
+ };
+
+ /* Command descriptor */
+ int ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
+
+ if (ret)
+ return ret;
+
+ /* Command payload */
+ ret = scatterlist_builder_add_data(&builder, ioctl_data,
+ ioctl_data_len);
+ if (ret) {
+ v4l2_err(&vv->v4l2_dev,
+ "failed to prepare command descriptor chain\n");
+ return ret;
+ }
+
+ /* Response descriptor */
+ ret = scatterlist_builder_add_ioctl_resp(&builder, session);
+ if (ret)
+ return ret;
+
+ /* Response payload, same as command */
+ ret = scatterlist_builder_add_descriptor(&builder, 1);
+ if (ret)
+ return ret;
+
+ ret = virtio_media_send_command(vv, sgs, 2, 2,
+ sizeof(struct virtio_media_resp_ioctl) +
+ minimum_resp_payload,
+ NULL);
+ if (ret < 0)
+ return ret;
+
+ ret = scatterlist_builder_retrieve_data(&builder, 3, ioctl_data);
+ if (ret) {
+ v4l2_err(&vv->v4l2_dev,
+ "failed to retrieve response descriptor chain\n");
+ return ret;
+ }
+
+ return 0;
+}
+
+/*
+ * Macros suitable for defining ioctls with a constant size payload.
+ */
+
+#define SIMPLE_WR_IOCTL(name, ioctl, payload_t) \
+ static int virtio_media_##name(struct file *file, void *fh, \
+ payload_t *payload) \
+ { \
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file); \
+ return virtio_media_send_wr_ioctl(vfh, ioctl, payload,\
+ sizeof(*payload), \
+ sizeof(*payload)); \
+ }
+#define SIMPLE_R_IOCTL(name, ioctl, payload_t) \
+ static int virtio_media_##name(struct file *file, void *fh, \
+ payload_t *payload) \
+ { \
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file); \
+ return virtio_media_send_r_ioctl(vfh, ioctl, payload,\
+ sizeof(*payload)); \
+ }
+#define SIMPLE_W_IOCTL(name, ioctl, payload_t) \
+ static int virtio_media_##name(struct file *file, void *fh, \
+ payload_t *payload) \
+ { \
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file); \
+ return virtio_media_send_w_ioctl(vfh, ioctl, payload,\
+ sizeof(*payload)); \
+ }
+
+/*
+ * V4L2 ioctl handlers.
+ *
+ * Most of these functions just forward the ioctl to the host, for these we can
+ * use one of the SIMPLE_*_IOCTL macros. Exceptions that have their own
+ * standalone function follow.
+ */
+
+SIMPLE_WR_IOCTL(enum_fmt, VIDIOC_ENUM_FMT, struct v4l2_fmtdesc)
+SIMPLE_WR_IOCTL(g_fmt, VIDIOC_G_FMT, struct v4l2_format)
+SIMPLE_WR_IOCTL(s_fmt, VIDIOC_S_FMT, struct v4l2_format)
+SIMPLE_WR_IOCTL(try_fmt, VIDIOC_TRY_FMT, struct v4l2_format)
+SIMPLE_WR_IOCTL(enum_framesizes, VIDIOC_ENUM_FRAMESIZES,
+ struct v4l2_frmsizeenum)
+SIMPLE_WR_IOCTL(enum_frameintervals, VIDIOC_ENUM_FRAMEINTERVALS,
+ struct v4l2_frmivalenum)
+SIMPLE_WR_IOCTL(query_ext_ctrl, VIDIOC_QUERY_EXT_CTRL,
+ struct v4l2_query_ext_ctrl)
+SIMPLE_WR_IOCTL(s_dv_timings, VIDIOC_S_DV_TIMINGS, struct v4l2_dv_timings)
+SIMPLE_WR_IOCTL(g_dv_timings, VIDIOC_G_DV_TIMINGS, struct v4l2_dv_timings)
+SIMPLE_R_IOCTL(query_dv_timings, VIDIOC_QUERY_DV_TIMINGS,
+ struct v4l2_dv_timings)
+SIMPLE_WR_IOCTL(enum_dv_timings, VIDIOC_ENUM_DV_TIMINGS,
+ struct v4l2_enum_dv_timings)
+SIMPLE_WR_IOCTL(dv_timings_cap, VIDIOC_DV_TIMINGS_CAP,
+ struct v4l2_dv_timings_cap)
+SIMPLE_WR_IOCTL(enuminput, VIDIOC_ENUMINPUT, struct v4l2_input)
+SIMPLE_WR_IOCTL(querymenu, VIDIOC_QUERYMENU, struct v4l2_querymenu)
+SIMPLE_WR_IOCTL(enumoutput, VIDIOC_ENUMOUTPUT, struct v4l2_output)
+SIMPLE_WR_IOCTL(enumaudio, VIDIOC_ENUMAUDIO, struct v4l2_audio)
+SIMPLE_R_IOCTL(g_audio, VIDIOC_G_AUDIO, struct v4l2_audio)
+SIMPLE_W_IOCTL(s_audio, VIDIOC_S_AUDIO, const struct v4l2_audio)
+SIMPLE_WR_IOCTL(enumaudout, VIDIOC_ENUMAUDOUT, struct v4l2_audioout)
+SIMPLE_R_IOCTL(g_audout, VIDIOC_G_AUDOUT, struct v4l2_audioout)
+SIMPLE_W_IOCTL(s_audout, VIDIOC_S_AUDOUT, const struct v4l2_audioout)
+SIMPLE_WR_IOCTL(g_modulator, VIDIOC_G_MODULATOR, struct v4l2_modulator)
+SIMPLE_W_IOCTL(s_modulator, VIDIOC_S_MODULATOR, const struct v4l2_modulator)
+SIMPLE_WR_IOCTL(g_selection, VIDIOC_G_SELECTION, struct v4l2_selection)
+SIMPLE_WR_IOCTL(s_selection, VIDIOC_S_SELECTION, struct v4l2_selection)
+SIMPLE_R_IOCTL(g_enc_index, VIDIOC_G_ENC_INDEX, struct v4l2_enc_idx)
+SIMPLE_WR_IOCTL(encoder_cmd, VIDIOC_ENCODER_CMD, struct v4l2_encoder_cmd)
+SIMPLE_WR_IOCTL(try_encoder_cmd, VIDIOC_TRY_ENCODER_CMD,
+ struct v4l2_encoder_cmd)
+SIMPLE_WR_IOCTL(try_decoder_cmd, VIDIOC_TRY_DECODER_CMD,
+ struct v4l2_decoder_cmd)
+SIMPLE_WR_IOCTL(g_parm, VIDIOC_G_PARM, struct v4l2_streamparm)
+SIMPLE_WR_IOCTL(s_parm, VIDIOC_S_PARM, struct v4l2_streamparm)
+SIMPLE_R_IOCTL(g_std, VIDIOC_G_STD, v4l2_std_id)
+SIMPLE_R_IOCTL(querystd, VIDIOC_QUERYSTD, v4l2_std_id)
+SIMPLE_WR_IOCTL(enumstd, VIDIOC_ENUMSTD, struct v4l2_standard)
+SIMPLE_WR_IOCTL(g_tuner, VIDIOC_G_TUNER, struct v4l2_tuner)
+SIMPLE_W_IOCTL(s_tuner, VIDIOC_S_TUNER, const struct v4l2_tuner)
+SIMPLE_WR_IOCTL(g_frequency, VIDIOC_G_FREQUENCY, struct v4l2_frequency)
+SIMPLE_W_IOCTL(s_frequency, VIDIOC_S_FREQUENCY, const struct v4l2_frequency)
+SIMPLE_WR_IOCTL(enum_freq_bands, VIDIOC_ENUM_FREQ_BANDS,
+ struct v4l2_frequency_band)
+SIMPLE_WR_IOCTL(g_sliced_vbi_cap, VIDIOC_G_SLICED_VBI_CAP,
+ struct v4l2_sliced_vbi_cap)
+SIMPLE_W_IOCTL(s_hw_freq_seek, VIDIOC_S_HW_FREQ_SEEK,
+ const struct v4l2_hw_freq_seek)
+
+/*
+ * QUERYCAP is handled by reading the configuration area.
+ */
+
+static int virtio_media_querycap(struct file *file, void *fh,
+ struct v4l2_capability *cap)
+{
+ struct video_device *video_dev = video_devdata(file);
+ struct virtio_media *vv = to_virtio_media(video_dev);
+
+ strscpy(cap->bus_info, "platform:virtio-media");
+ strscpy(cap->driver, VIRTIO_MEDIA_DEFAULT_DRIVER_NAME);
+
+ virtio_cread_bytes(vv->virtio_dev, 8, cap->card, sizeof(cap->card));
+
+ cap->capabilities = video_dev->device_caps | V4L2_CAP_DEVICE_CAPS;
+ cap->device_caps = video_dev->device_caps;
+
+ return 0;
+}
+
+/*
+ * s/g_input/output work with an unsigned int - recast this to a u32 so the
+ * size is unambiguous.
+ */
+
+static int virtio_media_g_input(struct file *file, void *fh, unsigned int *i)
+{
+ u32 input;
+
+ int ret = virtio_media_send_wr_ioctl(file_to_v4l2_fh(file),
+ VIDIOC_G_INPUT, &input,
+ sizeof(input), sizeof(input));
+ if (ret)
+ return ret;
+
+ *i = input;
+
+ return 0;
+}
+
+static int virtio_media_s_input(struct file *file, void *fh, unsigned int i)
+{
+ u32 input = i;
+
+ return virtio_media_send_wr_ioctl(file_to_v4l2_fh(file),
+ VIDIOC_S_INPUT, &input,
+ sizeof(input), sizeof(input));
+}
+
+static int virtio_media_g_output(struct file *file, void *fh, unsigned int *o)
+{
+ u32 output;
+
+ int ret = virtio_media_send_wr_ioctl(file_to_v4l2_fh(file),
+ VIDIOC_G_OUTPUT, &output,
+ sizeof(output), sizeof(output));
+ if (ret)
+ return ret;
+
+ *o = output;
+
+ return 0;
+}
+
+static int virtio_media_s_output(struct file *file, void *fh, unsigned int o)
+{
+ u32 output = o;
+
+ return virtio_media_send_wr_ioctl(file_to_v4l2_fh(file),
+ VIDIOC_S_OUTPUT, &output,
+ sizeof(output), sizeof(output));
+}
+
+/*
+ * s_std doesn't work with a pointer, so we cannot use SIMPLE_W_IOCTL.
+ */
+
+static int virtio_media_s_std(struct file *file, void *fh, v4l2_std_id s)
+{
+ int ret = virtio_media_send_w_ioctl(file_to_v4l2_fh(file), VIDIOC_S_STD,
+ &s, sizeof(s));
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+const struct v4l2_ioctl_ops virtio_media_ioctl_ops = {
+ /* VIDIOC_QUERYCAP handler */
+ .vidioc_querycap = virtio_media_querycap,
+
+ /* VIDIOC_ENUM_FMT handlers */
+ .vidioc_enum_fmt_vid_cap = virtio_media_enum_fmt,
+ .vidioc_enum_fmt_vid_overlay = virtio_media_enum_fmt,
+ .vidioc_enum_fmt_vid_out = virtio_media_enum_fmt,
+ .vidioc_enum_fmt_sdr_cap = virtio_media_enum_fmt,
+ .vidioc_enum_fmt_sdr_out = virtio_media_enum_fmt,
+ .vidioc_enum_fmt_meta_cap = virtio_media_enum_fmt,
+ .vidioc_enum_fmt_meta_out = virtio_media_enum_fmt,
+
+ /* VIDIOC_G_FMT handlers */
+ .vidioc_g_fmt_vid_cap = virtio_media_g_fmt,
+ .vidioc_g_fmt_vid_overlay = virtio_media_g_fmt,
+ .vidioc_g_fmt_vid_out = virtio_media_g_fmt,
+ .vidioc_g_fmt_vid_out_overlay = virtio_media_g_fmt,
+ .vidioc_g_fmt_vbi_cap = virtio_media_g_fmt,
+ .vidioc_g_fmt_vbi_out = virtio_media_g_fmt,
+ .vidioc_g_fmt_sliced_vbi_cap = virtio_media_g_fmt,
+ .vidioc_g_fmt_sliced_vbi_out = virtio_media_g_fmt,
+ .vidioc_g_fmt_vid_cap_mplane = virtio_media_g_fmt,
+ .vidioc_g_fmt_vid_out_mplane = virtio_media_g_fmt,
+ .vidioc_g_fmt_sdr_cap = virtio_media_g_fmt,
+ .vidioc_g_fmt_sdr_out = virtio_media_g_fmt,
+ .vidioc_g_fmt_meta_cap = virtio_media_g_fmt,
+ .vidioc_g_fmt_meta_out = virtio_media_g_fmt,
+
+ /* VIDIOC_S_FMT handlers */
+ .vidioc_s_fmt_vid_cap = virtio_media_s_fmt,
+ .vidioc_s_fmt_vid_overlay = virtio_media_s_fmt,
+ .vidioc_s_fmt_vid_out = virtio_media_s_fmt,
+ .vidioc_s_fmt_vid_out_overlay = virtio_media_s_fmt,
+ .vidioc_s_fmt_vbi_cap = virtio_media_s_fmt,
+ .vidioc_s_fmt_vbi_out = virtio_media_s_fmt,
+ .vidioc_s_fmt_sliced_vbi_cap = virtio_media_s_fmt,
+ .vidioc_s_fmt_sliced_vbi_out = virtio_media_s_fmt,
+ .vidioc_s_fmt_vid_cap_mplane = virtio_media_s_fmt,
+ .vidioc_s_fmt_vid_out_mplane = virtio_media_s_fmt,
+ .vidioc_s_fmt_sdr_cap = virtio_media_s_fmt,
+ .vidioc_s_fmt_sdr_out = virtio_media_s_fmt,
+ .vidioc_s_fmt_meta_cap = virtio_media_s_fmt,
+ .vidioc_s_fmt_meta_out = virtio_media_s_fmt,
+
+ /* VIDIOC_TRY_FMT handlers */
+ .vidioc_try_fmt_vid_cap = virtio_media_try_fmt,
+ .vidioc_try_fmt_vid_overlay = virtio_media_try_fmt,
+ .vidioc_try_fmt_vid_out = virtio_media_try_fmt,
+ .vidioc_try_fmt_vid_out_overlay = virtio_media_try_fmt,
+ .vidioc_try_fmt_vbi_cap = virtio_media_try_fmt,
+ .vidioc_try_fmt_vbi_out = virtio_media_try_fmt,
+ .vidioc_try_fmt_sliced_vbi_cap = virtio_media_try_fmt,
+ .vidioc_try_fmt_sliced_vbi_out = virtio_media_try_fmt,
+ .vidioc_try_fmt_vid_cap_mplane = virtio_media_try_fmt,
+ .vidioc_try_fmt_vid_out_mplane = virtio_media_try_fmt,
+ .vidioc_try_fmt_sdr_cap = virtio_media_try_fmt,
+ .vidioc_try_fmt_sdr_out = virtio_media_try_fmt,
+ .vidioc_try_fmt_meta_cap = virtio_media_try_fmt,
+ .vidioc_try_fmt_meta_out = virtio_media_try_fmt,
+
+ /* Standard handling */
+ .vidioc_g_std = virtio_media_g_std,
+ .vidioc_s_std = virtio_media_s_std,
+ .vidioc_querystd = virtio_media_querystd,
+
+ /* Input handling */
+ .vidioc_enum_input = virtio_media_enuminput,
+ .vidioc_g_input = virtio_media_g_input,
+ .vidioc_s_input = virtio_media_s_input,
+
+ /* Output handling */
+ .vidioc_enum_output = virtio_media_enumoutput,
+ .vidioc_g_output = virtio_media_g_output,
+ .vidioc_s_output = virtio_media_s_output,
+
+ /* Control handling */
+ .vidioc_query_ext_ctrl = virtio_media_query_ext_ctrl,
+ .vidioc_querymenu = virtio_media_querymenu,
+
+ /* Audio ioctls */
+ .vidioc_enumaudio = virtio_media_enumaudio,
+ .vidioc_g_audio = virtio_media_g_audio,
+ .vidioc_s_audio = virtio_media_s_audio,
+
+ /* Audio out ioctls */
+ .vidioc_enumaudout = virtio_media_enumaudout,
+ .vidioc_g_audout = virtio_media_g_audout,
+ .vidioc_s_audout = virtio_media_s_audout,
+ .vidioc_g_modulator = virtio_media_g_modulator,
+ .vidioc_s_modulator = virtio_media_s_modulator,
+
+ /* Crop ioctls */
+ /*
+ * Not directly an ioctl (part of VIDIOC_CROPCAP), so no need to
+ * implement.
+ */
+ .vidioc_g_pixelaspect = NULL,
+ .vidioc_g_selection = virtio_media_g_selection,
+ .vidioc_s_selection = virtio_media_s_selection,
+
+ /* Compression ioctls */
+ /* Deprecated in V4L2. */
+ .vidioc_g_jpegcomp = NULL,
+ /* Deprecated in V4L2. */
+ .vidioc_s_jpegcomp = NULL,
+ .vidioc_g_enc_index = virtio_media_g_enc_index,
+ .vidioc_encoder_cmd = virtio_media_encoder_cmd,
+ .vidioc_try_encoder_cmd = virtio_media_try_encoder_cmd,
+ .vidioc_try_decoder_cmd = virtio_media_try_decoder_cmd,
+
+ /* Stream type-dependent parameter ioctls */
+ .vidioc_g_parm = virtio_media_g_parm,
+ .vidioc_s_parm = virtio_media_s_parm,
+
+ /* Tuner ioctls */
+ .vidioc_g_tuner = virtio_media_g_tuner,
+ .vidioc_s_tuner = virtio_media_s_tuner,
+ .vidioc_g_frequency = virtio_media_g_frequency,
+ .vidioc_s_frequency = virtio_media_s_frequency,
+ .vidioc_enum_freq_bands = virtio_media_enum_freq_bands,
+
+ /* Sliced VBI cap */
+ .vidioc_g_sliced_vbi_cap = virtio_media_g_sliced_vbi_cap,
+
+ /* Log status ioctl */
+ /* Guest-only operation */
+ .vidioc_log_status = NULL,
+
+ .vidioc_s_hw_freq_seek = virtio_media_s_hw_freq_seek,
+
+ .vidioc_enum_framesizes = virtio_media_enum_framesizes,
+ .vidioc_enum_frameintervals = virtio_media_enum_frameintervals,
+
+ /* DV Timings IOCTLs */
+ .vidioc_s_dv_timings = virtio_media_s_dv_timings,
+ .vidioc_g_dv_timings = virtio_media_g_dv_timings,
+ .vidioc_query_dv_timings = virtio_media_query_dv_timings,
+ .vidioc_enum_dv_timings = virtio_media_enum_dv_timings,
+ .vidioc_dv_timings_cap = virtio_media_dv_timings_cap,
+ .vidioc_g_edid = NULL,
+ .vidioc_s_edid = NULL,
+
+ /* For other private ioctls */
+ .vidioc_default = NULL,
+};
+
+long virtio_media_device_ioctl(struct file *file, unsigned int cmd,
+ unsigned long arg)
+{
+ struct video_device *video_dev = video_devdata(file);
+ struct virtio_media *vv = to_virtio_media(video_dev);
+ struct v4l2_fh *vfh = NULL;
+ struct v4l2_standard standard;
+ v4l2_std_id std_id = 0;
+ int ret;
+
+ if (test_bit(V4L2_FL_USES_V4L2_FH, &video_dev->flags))
+ vfh = file_to_v4l2_fh(file);
+
+ mutex_lock(&vv->vlock);
+
+ /*
+ * We need to handle a few ioctls manually because their results
+ * rely on vfd->tvnorms, which is normally updated by the driver
+ * as S_INPUT is called. Since we want to just pass these ioctls
+ * through, we have to hijack them from here.
+ */
+ switch (cmd) {
+ case VIDIOC_S_STD:
+ ret = copy_from_user(&std_id, (void __user *)arg,
+ sizeof(std_id));
+ if (ret) {
+ ret = -EFAULT;
+ break;
+ }
+ ret = virtio_media_s_std(file, vfh, std_id);
+ break;
+ case VIDIOC_ENUMSTD:
+ ret = copy_from_user(&standard, (void __user *)arg,
+ sizeof(standard));
+ if (ret) {
+ ret = -EFAULT;
+ break;
+ }
+ ret = virtio_media_enumstd(file, vfh, &standard);
+ if (ret)
+ break;
+ ret = copy_to_user((void __user *)arg, &standard,
+ sizeof(standard));
+ if (ret)
+ ret = -EFAULT;
+ break;
+ case VIDIOC_QUERYSTD:
+ ret = virtio_media_querystd(file, vfh, &std_id);
+ if (ret)
+ break;
+ ret = copy_to_user((void __user *)arg, &std_id, sizeof(std_id));
+ if (ret)
+ ret = -EFAULT;
+ break;
+ default:
+ ret = video_ioctl2(file, cmd, arg);
+ break;
+ }
+
+ mutex_unlock(&vv->vlock);
+
+ return ret;
+}
diff --git a/include/uapi/linux/virtio_media.h b/include/uapi/linux/virtio_media.h
index ad05d7421..c1e68d47c 100644
--- a/include/uapi/linux/virtio_media.h
+++ b/include/uapi/linux/virtio_media.h
@@ -115,4 +115,47 @@ struct virtio_media_cmd_close {
__le32 __reserved;
};
+/**
+ * VIRTIO_MEDIA_CMD_IOCTL - Driver command for executing an ioctl.
+ *
+ * This command asks the device to run one of the ``VIDIOC_*`` ioctls on the
+ * active session.
+ *
+ * The code of the ioctl is extracted from the VIDIOC_* definitions in
+ * ``videodev2.h``, and consists of the second argument of the ``_IO*`` macro.
+ *
+ * Each ioctl has a payload, which is defined by the third argument of the
+ * ``_IO*`` macro defining it. It can be writable by the driver (``_IOW``), the
+ * device (``_IOR``), or both (``_IOWR``).
+ *
+ * If an ioctl is writable by the driver, it must be followed by a
+ * driver-writable descriptor containing the payload.
+ *
+ * If an ioctl is writable by the device, it must be followed by a
+ * device-writable descriptor of the size of the payload that the device will
+ * write into.
+ *
+ */
+#define VIRTIO_MEDIA_CMD_IOCTL 3
+
+/**
+ * struct virtio_media_cmd_ioctl - Driver command for VIRTIO_MEDIA_CMD_IOCTL.
+ * @hdr: header with cmd member set to VIRTIO_MEDIA_CMD_IOCTL.
+ * @session_id: id of the session to run the ioctl on.
+ * @code: code of the ioctl to run.
+ */
+struct virtio_media_cmd_ioctl {
+ struct virtio_media_cmd_header hdr;
+ __le32 session_id;
+ __le32 code;
+};
+
+/**
+ * struct virtio_media_resp_ioctl - Device response for VIRTIO_MEDIA_CMD_IOCTL.
+ * @hdr: header containing the status of the ioctl.
+ */
+struct virtio_media_resp_ioctl {
+ struct virtio_media_resp_header hdr;
+};
+
#endif // _UAPI__LINUX_VIRTIO_MEDIA_H
--
2.56.0.317.g454f8302e1-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v10 3/5] media: virtio: Add extended controls support
2026-10-29 15:30 [PATCH v10 0/5] media: add virtio-media driver Brian Daniels
2026-10-29 15:30 ` [PATCH v10 1/5] media: virtio: Add core driver and session management Brian Daniels
2026-10-29 15:30 ` [PATCH v10 2/5] media: virtio: Add V4L2 ioctl forwarding infrastructure Brian Daniels
@ 2026-10-29 15:30 ` Brian Daniels
2026-10-29 16:37 ` sashiko-bot
2026-10-29 15:30 ` [PATCH v10 4/5] media: virtio: Add buffer streaming, event handling, and poll Brian Daniels
2026-10-29 15:30 ` [PATCH v10 5/5] media: virtio: Add MMAP buffer mapping support Brian Daniels
4 siblings, 1 reply; 11+ messages in thread
From: Brian Daniels @ 2026-10-29 15:30 UTC (permalink / raw)
To: Mauro Carvalho Chehab
Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
gurchetansingh, hverkuil, linux-kernel, linux-media, mst,
nicolas.dufresne, virtualization, xuanzhuo, dbassey,
laurent.pinchart, Brian Daniels
From: Alexandre Courbot <gnurou@gmail.com>
Add support for VIDIOC_G_EXT_CTRLS, VIDIOC_S_EXT_CTRLS, and
VIDIOC_TRY_EXT_CTRLS.
Extended controls carry an auxiliary array of struct v4l2_ext_control
entries and may reference userspace payload buffers. Pin userspace
control payloads using vb2_create_framevec() for the duration of the
virtio command and pass their physical memory segments to the host as an
array of struct virtio_media_sg_entry descriptors in the session shadow
buffer.
Signed-off-by: Alexandre Courbot <gnurou@gmail.com>
Assisted-by: LLM
Co-developed-by: Brian Daniels <briandaniels@google.com>
Signed-off-by: Brian Daniels <briandaniels@google.com>
---
drivers/media/virtio/scatterlist_builder.c | 244 +++++++++++++++++++++
drivers/media/virtio/scatterlist_builder.h | 15 ++
drivers/media/virtio/virtio_media_ioctls.c | 155 +++++++++++++
include/uapi/linux/virtio_media.h | 13 ++
4 files changed, 427 insertions(+)
diff --git a/drivers/media/virtio/scatterlist_builder.c b/drivers/media/virtio/scatterlist_builder.c
index ae8ffee82..3615f45e1 100644
--- a/drivers/media/virtio/scatterlist_builder.c
+++ b/drivers/media/virtio/scatterlist_builder.c
@@ -220,3 +220,247 @@ int scatterlist_builder_add_ioctl_resp(struct scatterlist_builder *builder,
return scatterlist_builder_add_data(builder, resp_ioctl,
sizeof(*resp_ioctl));
}
+
+/**
+ * __scatterlist_builder_add_userptr() - Add user pages to @builder.
+ * @builder: builder to use.
+ * @userptr: pointer to userspace memory that we want to add.
+ * @length: length of the data to add.
+ * @sg_list: output parameter. Upon success, points to the area of the shadow
+ * buffer containing the array of SG entries to be added to the
+ * descriptor chain.
+ * @nents: output parameter. Upon success, contains the number of entries
+ * pointed to by @sg_list.
+ *
+ * Data referenced by userspace pointers can be potentially large and very
+ * scattered, which could overwhelm the descriptor chain if added as-is. For
+ * these, we instead build an array of &struct virtio_media_sg_entry in the
+ * shadow buffer and reference it using a single descriptor.
+ *
+ * This function is a helper to perform that. Callers should then add the
+ * descriptor to the chain properly.
+ *
+ * Returns %-EFAULT if @userptr is not a valid user address, which is a case the
+ * driver should consider as "normal" operation. All other failures signal a
+ * problem with the driver.
+ */
+static int
+__scatterlist_builder_add_userptr(struct scatterlist_builder *builder,
+ unsigned long userptr, unsigned long length,
+ struct virtio_media_sg_entry **sg_list,
+ int *nents)
+{
+ struct sg_table sg_table = {};
+ struct frame_vector *framevec;
+ struct scatterlist *sg_iter;
+ struct page **pages;
+ const unsigned int offset = userptr & ~PAGE_MASK;
+ unsigned int pages_count;
+ size_t entries_size;
+ int i;
+ int ret;
+
+ if (builder->framevec) {
+ pr_warn("previous framevec found!");
+ vb2_destroy_framevec(builder->framevec);
+ builder->framevec = NULL;
+ }
+
+ framevec = vb2_create_framevec(userptr, length, true);
+ if (IS_ERR(framevec)) {
+ if (PTR_ERR(framevec) != -EFAULT) {
+ pr_warn("error %ld creating frame vector for userptr 0x%lx, length 0x%lx\n",
+ PTR_ERR(framevec), userptr, length);
+ } else {
+ /* -EINVAL is expected in case of invalid userptr. */
+ framevec = ERR_PTR(-EINVAL);
+ }
+ return PTR_ERR(framevec);
+ }
+
+ pages = frame_vector_pages(framevec);
+ if (IS_ERR(pages)) {
+ pr_warn("error getting vector pages\n");
+ ret = PTR_ERR(pages);
+ goto free_framevec;
+ }
+ pages_count = frame_vector_count(framevec);
+ ret = sg_alloc_table_from_pages(&sg_table, pages, pages_count, offset,
+ length, 0);
+ if (ret) {
+ pr_warn("error creating sg table\n");
+ goto free_framevec;
+ }
+
+ /* Allocate our actual SG in the shadow buffer. */
+ *nents = sg_nents(sg_table.sgl);
+ entries_size = sizeof(**sg_list) * *nents;
+ if (builder->shadow_buffer_pos + entries_size >
+ builder->shadow_buffer_size) {
+ ret = -ENOMEM;
+ goto free_sg;
+ }
+
+ *sg_list = builder->shadow_buffer + builder->shadow_buffer_pos;
+ builder->shadow_buffer_pos += entries_size;
+
+ for_each_sgtable_sg(&sg_table, sg_iter, i) {
+ struct virtio_media_sg_entry *sg_entry = &(*sg_list)[i];
+
+ sg_entry->start = sg_phys(sg_iter);
+ sg_entry->len = sg_iter->length;
+ sg_entry->__reserved = 0;
+ }
+
+ builder->framevec = framevec;
+
+free_sg:
+ sg_free_table(&sg_table);
+free_framevec:
+ // Only destroy the framevec if we didn't save it
+ if (!builder->framevec && framevec)
+ vb2_destroy_framevec(framevec);
+ return ret;
+}
+
+/**
+ * scatterlist_builder_add_userptr() - Add a user-memory buffer using an array
+ * of &struct virtio_media_sg_entry.
+ * @builder: builder to use.
+ * @userptr: pointer to userspace memory that we want to add.
+ * @length: length of the data to add.
+ *
+ * Upon success, an array of &struct virtio_media_sg_entry referencing
+ * @userptr has been built into the shadow buffer, and that array added to the
+ * descriptor chain.
+ */
+static int scatterlist_builder_add_userptr(struct scatterlist_builder *builder,
+ unsigned long userptr,
+ unsigned long length)
+{
+ int ret;
+ int nents;
+ struct virtio_media_sg_entry *sg_list;
+
+ ret = __scatterlist_builder_add_userptr(builder, userptr, length,
+ &sg_list, &nents);
+ if (ret)
+ return ret;
+
+ ret = scatterlist_builder_add_data(builder, sg_list,
+ sizeof(*sg_list) * nents);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+/**
+ * scatterlist_builder_add_ext_ctrls() - Add a &struct v4l2_ext_controls and its
+ * controls to @builder.
+ * @builder: builder to use.
+ * @ctrls: &struct v4l2_ext_controls to add.
+ *
+ * Add @ctrls and its array of &struct v4l2_ext_control to the descriptor
+ * chain.
+ */
+int scatterlist_builder_add_ext_ctrls(struct scatterlist_builder *builder,
+ struct v4l2_ext_controls *ctrls)
+{
+ int ret;
+
+ /* v4l2_ext_controls */
+ ret = scatterlist_builder_add_data(builder, ctrls, sizeof(*ctrls));
+ if (ret)
+ return ret;
+
+ if (ctrls->count > 0) {
+ /* array of v4l2_controls */
+ ret = scatterlist_builder_add_data(builder, ctrls->controls,
+ sizeof(ctrls->controls[0]) *
+ ctrls->count);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * scatterlist_builder_add_ext_ctrls_userptrs() - Add the userspace payloads of
+ * a &struct v4l2_ext_controls
+ * to the descriptor chain.
+ * @builder: builder to use.
+ * @ctrls: &struct v4l2_ext_controls from which we want to add the
+ * userspace payload.
+ *
+ * Add the userspace payloads of @ctrls to the descriptor chain. This is split
+ * out of scatterlist_builder_add_ext_ctrls() because we only want to add
+ * these to the device-readable part of the descriptor chain.
+ */
+int
+scatterlist_builder_add_ext_ctrls_userptrs(struct scatterlist_builder *builder,
+ struct v4l2_ext_controls *ctrls)
+{
+ int i;
+ int ret;
+
+ /* Pointers to user memory in individual controls */
+ for (i = 0; i < ctrls->count; i++) {
+ struct v4l2_ext_control *ctrl = &ctrls->controls[i];
+
+ if (ctrl->size > 0) {
+ unsigned long uptr = (unsigned long)ctrl->ptr;
+
+ ret = scatterlist_builder_add_userptr(builder, uptr,
+ ctrl->size);
+ if (ret)
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+/**
+ * scatterlist_builder_retrieve_ext_ctrls() - Retrieve controls written by the
+ * device on the shadow buffer,
+ * if needed.
+ * @builder: builder to use.
+ * @sg_index: index of the first SG entry of the controls in the builder's
+ * descriptor chain.
+ * @ctrls: &struct v4l2_ext_controls to copy shadow buffer data into.
+ *
+ * If the shadow buffer is pointed to by @sg_index, copy its content back into
+ * @ctrls.
+ */
+int scatterlist_builder_retrieve_ext_ctrls(struct scatterlist_builder *builder,
+ size_t sg_index,
+ struct v4l2_ext_controls *ctrls)
+{
+ struct v4l2_ext_control *controls_backup = ctrls->controls;
+ int ret;
+
+ ret = scatterlist_builder_retrieve_data(builder, sg_index++, ctrls);
+
+ if (ret)
+ goto cleanup;
+
+ ctrls->controls = controls_backup;
+
+ if (ctrls->count > 0 && ctrls->controls) {
+ ret = scatterlist_builder_retrieve_data(builder, sg_index++,
+ ctrls->controls);
+ if (ret)
+ goto cleanup;
+ }
+
+cleanup:
+ // Clean up the framevec created in __scatterlist_builder_add_userptr()
+ if (builder->framevec) {
+ vb2_destroy_framevec(builder->framevec);
+ builder->framevec = NULL;
+ }
+
+ return ret;
+}
diff --git a/drivers/media/virtio/scatterlist_builder.h b/drivers/media/virtio/scatterlist_builder.h
index 044ea409a..79d53f602 100644
--- a/drivers/media/virtio/scatterlist_builder.h
+++ b/drivers/media/virtio/scatterlist_builder.h
@@ -25,6 +25,8 @@
* @sgs: descriptor chain to eventually pass to virtio functions.
* @num_sgs: total number of entries in @sgs.
* @cur_sg: next entry in @sgs to be used.
+ * @framevec: If a struct frame_vector was used to pin user pages, hold it here
+ * until after the response is received so it can later be freed.
*
* Virtio passes data from the driver to the device (through e.g.
* virtqueue_add_sgs()) via a scatterlist that the device interprets as a
@@ -69,6 +71,8 @@ struct scatterlist_builder {
struct scatterlist **sgs;
size_t num_sgs;
size_t cur_sg;
+
+ struct frame_vector *framevec;
};
int scatterlist_builder_add_descriptor(struct scatterlist_builder *builder,
@@ -87,4 +91,15 @@ int scatterlist_builder_add_ioctl_cmd(struct scatterlist_builder *builder,
int scatterlist_builder_add_ioctl_resp(struct scatterlist_builder *builder,
struct virtio_media_session *session);
+int scatterlist_builder_add_ext_ctrls(struct scatterlist_builder *builder,
+ struct v4l2_ext_controls *ctrls);
+
+int
+scatterlist_builder_add_ext_ctrls_userptrs(struct scatterlist_builder *builder,
+ struct v4l2_ext_controls *ctrls);
+
+int scatterlist_builder_retrieve_ext_ctrls(struct scatterlist_builder *builder,
+ size_t sg_index,
+ struct v4l2_ext_controls *ctrls);
+
#endif // __VIRTIO_MEDIA_SCATTERLIST_BUILDER_H
diff --git a/drivers/media/virtio/virtio_media_ioctls.c b/drivers/media/virtio/virtio_media_ioctls.c
index 656ecea8a..51f46b334 100644
--- a/drivers/media/virtio/virtio_media_ioctls.c
+++ b/drivers/media/virtio/virtio_media_ioctls.c
@@ -41,6 +41,7 @@ static int virtio_media_send_r_ioctl(struct v4l2_fh *fh, u32 ioctl,
.sgs = sgs,
.num_sgs = ARRAY_SIZE(sgs),
.cur_sg = 0,
+ .framevec = NULL,
};
/* Command descriptor */
@@ -107,6 +108,7 @@ static int virtio_media_send_w_ioctl(struct v4l2_fh *fh, u32 ioctl,
.sgs = sgs,
.num_sgs = ARRAY_SIZE(sgs),
.cur_sg = 0,
+ .framevec = NULL,
};
/* Command descriptor */
@@ -171,6 +173,7 @@ static int virtio_media_send_wr_ioctl(struct v4l2_fh *fh, u32 ioctl,
.sgs = sgs,
.num_sgs = ARRAY_SIZE(sgs),
.cur_sg = 0,
+ .framevec = NULL,
};
/* Command descriptor */
@@ -215,6 +218,124 @@ static int virtio_media_send_wr_ioctl(struct v4l2_fh *fh, u32 ioctl,
return 0;
}
+/**
+ * virtio_media_send_ext_controls_ioctl() - Send an ioctl taking extended
+ * controls as parameters to the device.
+ * @fh: file handler of the session doing the ioctl.
+ * @ioctl: ``VIDIOC_*`` ioctl code.
+ * @ctrls: &struct v4l2_ext_controls to be sent as the ioctl payload.
+ *
+ * Queues an ioctl that sends a &struct v4l2_ext_controls to the host and
+ * receives an updated version.
+ *
+ * &struct v4l2_ext_controls has a pointer to an array of
+ * &struct v4l2_ext_control, and also potentially pointers to user-space memory
+ * that we need to map properly, hence the dedicated function.
+ */
+static int virtio_media_send_ext_controls_ioctl(struct v4l2_fh *fh, u32 ioctl,
+ struct v4l2_ext_controls *ctrls)
+{
+ struct video_device *video_dev = fh->vdev;
+ struct virtio_media *vv = to_virtio_media(video_dev);
+ struct virtio_media_session *session = fh_to_session(fh);
+ size_t num_cmd_sgs;
+ size_t end_ctrls_sg;
+ struct v4l2_ext_control *controls_backup = ctrls->controls;
+ const u32 num_ctrls = ctrls->count;
+ struct scatterlist *sgs[64];
+ struct scatterlist_builder builder = {
+ .descs = session->command_sgs.sgl,
+ .num_descs = DESC_CHAIN_MAX_LEN,
+ .cur_desc = 0,
+ .shadow_buffer = session->shadow_buf,
+ .shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
+ .shadow_buffer_pos = 0,
+ .sgs = sgs,
+ .num_sgs = ARRAY_SIZE(sgs),
+ .cur_sg = 0,
+ .framevec = NULL,
+ };
+ size_t resp_len = 0;
+ int i;
+
+ /* Command descriptor */
+ int ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
+
+ if (ret)
+ return ret;
+
+ /* v4l2_controls */
+ ret = scatterlist_builder_add_ext_ctrls(&builder, ctrls);
+ if (ret)
+ return ret;
+
+ end_ctrls_sg = builder.cur_sg;
+
+ ret = scatterlist_builder_add_ext_ctrls_userptrs(&builder, ctrls);
+ if (ret)
+ return ret;
+
+ num_cmd_sgs = builder.cur_sg;
+
+ /* Response descriptor */
+ ret = scatterlist_builder_add_ioctl_resp(&builder, session);
+ if (ret)
+ return ret;
+
+ /* Response payload (same as input but without userptrs) */
+ for (i = 1; i < end_ctrls_sg; i++) {
+ ret = scatterlist_builder_add_descriptor(&builder, i);
+ if (ret < 0)
+ return ret;
+ }
+
+ ret = virtio_media_send_command(vv, builder.sgs, num_cmd_sgs,
+ builder.cur_sg - num_cmd_sgs,
+ sizeof(struct virtio_media_resp_ioctl) +
+ sizeof(*ctrls),
+ &resp_len);
+
+ /* Just in case the host touched these. */
+ ctrls->controls = controls_backup;
+ if (ctrls->count != num_ctrls) {
+ v4l2_err(&vv->v4l2_dev,
+ "device returned a number of controls different than the one submitted\n");
+ }
+ if (ctrls->count > num_ctrls)
+ return -ENOSPC;
+ if (ret < 0) {
+ /*
+ * Even if we have received an error, we may need to read our
+ * payloadback.
+ */
+ if (resp_len >= sizeof(struct virtio_media_resp_ioctl) +
+ sizeof(*ctrls)) {
+ /*
+ * Deliberately ignore the error here as we want to
+ * return the previous one.
+ */
+ scatterlist_builder_retrieve_ext_ctrls(&builder,
+ num_cmd_sgs + 1,
+ ctrls);
+ }
+
+ return ret;
+ }
+
+ resp_len -= sizeof(struct virtio_media_resp_ioctl);
+
+ /* Make sure that the reply's length covers our v4l2_ext_controls */
+ if (resp_len < sizeof(*ctrls))
+ return -EINVAL;
+
+ ret = scatterlist_builder_retrieve_ext_ctrls(&builder, num_cmd_sgs + 1,
+ ctrls);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
/*
* Macros suitable for defining ioctls with a constant size payload.
*/
@@ -327,6 +448,37 @@ static int virtio_media_querycap(struct file *file, void *fh,
return 0;
}
+/*
+ * Extended control ioctls are handled mostly identically.
+ */
+
+static int virtio_media_g_ext_ctrls(struct file *file, void *fh,
+ struct v4l2_ext_controls *ctrls)
+{
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file);
+
+ return virtio_media_send_ext_controls_ioctl(vfh, VIDIOC_G_EXT_CTRLS,
+ ctrls);
+}
+
+static int virtio_media_s_ext_ctrls(struct file *file, void *fh,
+ struct v4l2_ext_controls *ctrls)
+{
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file);
+
+ return virtio_media_send_ext_controls_ioctl(vfh, VIDIOC_S_EXT_CTRLS,
+ ctrls);
+}
+
+static int virtio_media_try_ext_ctrls(struct file *file, void *fh,
+ struct v4l2_ext_controls *ctrls)
+{
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file);
+
+ return virtio_media_send_ext_controls_ioctl(vfh, VIDIOC_TRY_EXT_CTRLS,
+ ctrls);
+}
+
/*
* s/g_input/output work with an unsigned int - recast this to a u32 so the
* size is unambiguous.
@@ -472,6 +624,9 @@ const struct v4l2_ioctl_ops virtio_media_ioctl_ops = {
/* Control handling */
.vidioc_query_ext_ctrl = virtio_media_query_ext_ctrl,
+ .vidioc_g_ext_ctrls = virtio_media_g_ext_ctrls,
+ .vidioc_s_ext_ctrls = virtio_media_s_ext_ctrls,
+ .vidioc_try_ext_ctrls = virtio_media_try_ext_ctrls,
.vidioc_querymenu = virtio_media_querymenu,
/* Audio ioctls */
diff --git a/include/uapi/linux/virtio_media.h b/include/uapi/linux/virtio_media.h
index c1e68d47c..f117b97fc 100644
--- a/include/uapi/linux/virtio_media.h
+++ b/include/uapi/linux/virtio_media.h
@@ -158,4 +158,17 @@ struct virtio_media_resp_ioctl {
struct virtio_media_resp_header hdr;
};
+/**
+ * struct virtio_media_sg_entry - Description of part of a scattered guest
+ * memory.
+ * @start: start guest address of the memory segment.
+ * @len: length of this memory segment.
+ * @__reserved: must be set to zero by the driver.
+ */
+struct virtio_media_sg_entry {
+ __le64 start;
+ __le32 len;
+ __le32 __reserved;
+};
+
#endif // _UAPI__LINUX_VIRTIO_MEDIA_H
--
2.56.0.317.g454f8302e1-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v10 4/5] media: virtio: Add buffer streaming, event handling, and poll
2026-10-29 15:30 [PATCH v10 0/5] media: add virtio-media driver Brian Daniels
` (2 preceding siblings ...)
2026-10-29 15:30 ` [PATCH v10 3/5] media: virtio: Add extended controls support Brian Daniels
@ 2026-10-29 15:30 ` Brian Daniels
2026-10-29 17:00 ` sashiko-bot
2026-10-29 15:30 ` [PATCH v10 5/5] media: virtio: Add MMAP buffer mapping support Brian Daniels
4 siblings, 1 reply; 11+ messages in thread
From: Brian Daniels @ 2026-10-29 15:30 UTC (permalink / raw)
To: Mauro Carvalho Chehab
Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
gurchetansingh, hverkuil, linux-kernel, linux-media, mst,
nicolas.dufresne, virtualization, xuanzhuo, dbassey,
laurent.pinchart, Brian Daniels
From: Alexandre Courbot <gnurou@gmail.com>
Add V4L2 buffer queue state tracking, streaming ioctls, asynchronous
event virtqueue processing, V4L2 event subscription, and poll support.
Set up the event virtqueue to receive VIRTIO_MEDIA_EVT_DQBUF,
VIRTIO_MEDIA_EVT_EVENT, and VIRTIO_MEDIA_EVT_ERROR notifications from
the host, dispatching dequeued buffers, V4L2 events, and unrecoverable
session errors to their respective sessions.
Implement buffer and streaming ioctls (VIDIOC_REQBUFS,
VIDIOC_CREATE_BUFS, VIDIOC_QUERYBUF, VIDIOC_PREPARE_BUF, VIDIOC_QBUF,
VIDIOC_DQBUF, VIDIOC_STREAMON, VIDIOC_STREAMOFF, and VIDIOC_DECODER_CMD)
as well as VIDIOC_SUBSCRIBE_EVENT, VIDIOC_UNSUBSCRIBE_EVENT, and the
poll file operation.
Signed-off-by: Alexandre Courbot <gnurou@gmail.com>
Assisted-by: LLM
Co-developed-by: Brian Daniels <briandaniels@google.com>
Signed-off-by: Brian Daniels <briandaniels@google.com>
---
drivers/media/virtio/scatterlist_builder.c | 72 +++
drivers/media/virtio/scatterlist_builder.h | 8 +
drivers/media/virtio/session.h | 51 ++
drivers/media/virtio/virtio_media.h | 11 +
drivers/media/virtio/virtio_media_driver.c | 376 ++++++++++++-
drivers/media/virtio/virtio_media_ioctls.c | 618 ++++++++++++++++++++-
include/uapi/linux/virtio_media.h | 68 +++
7 files changed, 1199 insertions(+), 5 deletions(-)
diff --git a/drivers/media/virtio/scatterlist_builder.c b/drivers/media/virtio/scatterlist_builder.c
index 3615f45e1..9be968ed8 100644
--- a/drivers/media/virtio/scatterlist_builder.c
+++ b/drivers/media/virtio/scatterlist_builder.c
@@ -355,6 +355,78 @@ static int scatterlist_builder_add_userptr(struct scatterlist_builder *builder,
return 0;
}
+/**
+ * scatterlist_builder_add_buffer() - Add a &struct v4l2_buffer and its planes
+ * to the descriptor chain.
+ * @builder: builder to use.
+ * @b: &struct v4l2_buffer to add.
+ */
+int scatterlist_builder_add_buffer(struct scatterlist_builder *builder,
+ struct v4l2_buffer *b)
+{
+ int ret;
+
+ /* v4l2_buffer */
+ ret = scatterlist_builder_add_data(builder, b, sizeof(*b));
+ if (ret)
+ return ret;
+
+ if (V4L2_TYPE_IS_MULTIPLANAR(b->type) && b->length > 0) {
+ /* Array of v4l2_planes */
+ ret = scatterlist_builder_add_data(builder, b->m.planes,
+ sizeof(struct v4l2_plane) *
+ b->length);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * scatterlist_builder_retrieve_buffer() - Retrieve a &struct v4l2_buffer
+ * written by the device on the shadow
+ * buffer, if needed.
+ * @builder: builder to use.
+ * @sg_index: index of the first SG entry of the buffer in the builder's
+ * descriptor chain.
+ * @b: &struct v4l2_buffer to copy shadow buffer data into.
+ * @orig_planes: the original ``planes`` pointer, to be restored if the buffer
+ * is multi-planar.
+ *
+ * If the &struct v4l2_buffer pointed to by @sg_index was copied into the
+ * shadow buffer, then its updated content is copied back into @b.
+ * Otherwise nothing is done as the device has written into @b directly.
+ *
+ * @orig_planes is used to restore the original ``planes`` pointer in case it
+ * gets modified by the host. The specification stipulates that the host should
+ * not modify it, but we enforce this for additional safety.
+ */
+int scatterlist_builder_retrieve_buffer(struct scatterlist_builder *builder,
+ size_t sg_index, struct v4l2_buffer *b,
+ struct v4l2_plane *orig_planes)
+{
+ int ret;
+
+ ret = scatterlist_builder_retrieve_data(builder, sg_index++, b);
+ if (ret)
+ return ret;
+
+ if (V4L2_TYPE_IS_MULTIPLANAR(b->type)) {
+ b->m.planes = orig_planes;
+
+ if (orig_planes) {
+ ret = scatterlist_builder_retrieve_data(builder,
+ sg_index++,
+ b->m.planes);
+ if (ret)
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
/**
* scatterlist_builder_add_ext_ctrls() - Add a &struct v4l2_ext_controls and its
* controls to @builder.
diff --git a/drivers/media/virtio/scatterlist_builder.h b/drivers/media/virtio/scatterlist_builder.h
index 79d53f602..6a3c7ce44 100644
--- a/drivers/media/virtio/scatterlist_builder.h
+++ b/drivers/media/virtio/scatterlist_builder.h
@@ -91,6 +91,14 @@ int scatterlist_builder_add_ioctl_cmd(struct scatterlist_builder *builder,
int scatterlist_builder_add_ioctl_resp(struct scatterlist_builder *builder,
struct virtio_media_session *session);
+int scatterlist_builder_add_buffer(struct scatterlist_builder *builder,
+ struct v4l2_buffer *buffer);
+
+int scatterlist_builder_retrieve_buffer(struct scatterlist_builder *builder,
+ size_t sg_index,
+ struct v4l2_buffer *buffer,
+ struct v4l2_plane *orig_planes);
+
int scatterlist_builder_add_ext_ctrls(struct scatterlist_builder *builder,
struct v4l2_ext_controls *ctrls);
diff --git a/drivers/media/virtio/session.h b/drivers/media/virtio/session.h
index 6fe7ff5ae..16bb86bf4 100644
--- a/drivers/media/virtio/session.h
+++ b/drivers/media/virtio/session.h
@@ -14,17 +14,55 @@
#include "uapi/linux/virtio_media.h"
+#define VIRTIO_MEDIA_LAST_QUEUE (V4L2_BUF_TYPE_META_OUTPUT)
+
/*
* Size of the per-session virtio shadow and event buffers. 16K should be
* enough to contain everything we need.
*/
#define VIRTIO_SHADOW_BUF_SIZE 0x4000
+/**
+ * struct virtio_media_buffer - Current state of a buffer.
+ * @buffer: &struct v4l2_buffer with current information about the buffer.
+ * @planes: backing planes array for @buffer.
+ * @list: link into the list of buffers pending dequeue.
+ */
+struct virtio_media_buffer {
+ struct v4l2_buffer buffer;
+ struct v4l2_plane planes[VIDEO_MAX_PLANES];
+ struct list_head list;
+};
+
+/**
+ * struct virtio_media_queue_state - Represents the state of a V4L2 queue.
+ * @streaming: Whether the queue is currently streaming.
+ * @allocated_bufs: How many buffers are currently allocated.
+ * @is_capture_last: set to true when the last buffer has been received on a
+ * capture queue, so we can return %-EPIPE on subsequent
+ * DQBUF requests.
+ * @buffers: Buffer state array of size @allocated_bufs.
+ * @queued_bufs: How many buffers are currently queued on the device.
+ * @pending_dqbufs: Buffers that are available for being dequeued.
+ */
+struct virtio_media_queue_state {
+ bool streaming;
+ size_t allocated_bufs;
+ bool is_capture_last;
+
+ struct virtio_media_buffer *buffers;
+ size_t queued_bufs;
+ struct list_head pending_dqbufs;
+};
+
/**
* struct virtio_media_session - A session on a virtio_media device.
* @fh: file handler for the session.
* @file: file pointer associated with the session's file handler.
* @id: session ID used to communicate with the device.
+ * @nonblocking_dequeue: whether dequeue should block or not (nonblocking if
+ * file opened with O_NONBLOCK).
+ * @uses_mplane: whether the queues for this session use the MPLANE API or not.
* @cmd: union of session commands ``close`` and ``ioctl``. A session can have
* one command currently running. The rest of the commands are handled by
* &struct virtio_media.
@@ -35,12 +73,19 @@
* be staged before being sent to the device.
* @command_sgs: SG table gathering descriptors for a given command and its
* response.
+ * @queues: state of all the queues for this session.
+ * @queues_lock: protects all members for the queues for this session.
+ * @dqbuf_wait: waitqueue for dequeued buffers, if ``VIDIOC_DQBUF`` needs to
+ * block or when polling.
+ * @error: true if the session has encountered an unrecoverable session error.
* @list: link into the list of sessions for the device.
*/
struct virtio_media_session {
struct v4l2_fh fh;
struct file *file;
u32 id;
+ bool nonblocking_dequeue;
+ bool uses_mplane;
__dma_from_device_group_begin();
union {
@@ -57,6 +102,12 @@ struct virtio_media_session {
struct sg_table command_sgs;
+ struct virtio_media_queue_state queues[VIRTIO_MEDIA_LAST_QUEUE + 1];
+ struct mutex queues_lock; /* protects queues array and states */
+ wait_queue_head_t dqbuf_wait;
+ bool error;
+ struct mutex error_lock; /* protects error */
+
struct list_head list;
};
diff --git a/drivers/media/virtio/virtio_media.h b/drivers/media/virtio/virtio_media.h
index a120c1014..8b21c1941 100644
--- a/drivers/media/virtio/virtio_media.h
+++ b/drivers/media/virtio/virtio_media.h
@@ -24,8 +24,12 @@
* @video_dev: video_device for the media device.
* @virtio_dev: virtio device for the media device.
* @commandq: virtio command queue.
+ * @eventq: virtio event queue.
+ * @eventq_work: work to run when events are received on @eventq.
+ * @event_buffer: buffer for event descriptors.
* @sessions: list of active sessions on the device.
* @sessions_lock: protects @sessions and &struct virtio_media_session.list.
+ * @events_lock: prevents concurrent processing of events.
* @cmd: union of device-level commands. Session commands are handled by
* &struct virtio_media_session.
* @resp: union of responses to device-level commands. Session responses are
@@ -39,10 +43,16 @@ struct virtio_media {
struct virtio_device *virtio_dev;
struct virtqueue *commandq;
+ struct virtqueue *eventq;
+ struct work_struct eventq_work;
+
+ void *event_buffer;
struct list_head sessions;
struct mutex sessions_lock; /* protects sessions list */
+ struct mutex events_lock; /* prevents concurrent event processing */
+
__dma_from_device_group_begin();
union {
struct virtio_media_cmd_open open;
@@ -68,6 +78,7 @@ to_virtio_media(struct video_device *video_dev)
int virtio_media_send_command(struct virtio_media *vv, struct scatterlist **sgs,
const size_t out_sgs, const size_t in_sgs,
size_t minimum_resp_len, size_t *resp_len);
+void virtio_media_process_events(struct virtio_media *vv);
/* virtio_media_ioctls.c */
diff --git a/drivers/media/virtio/virtio_media_driver.c b/drivers/media/virtio/virtio_media_driver.c
index 2f2e95077..877ac154b 100644
--- a/drivers/media/virtio/virtio_media_driver.c
+++ b/drivers/media/virtio/virtio_media_driver.c
@@ -16,15 +16,22 @@
#include <linux/virtio_ids.h>
#include <linux/slab.h>
#include <linux/scatterlist.h>
+#include <linux/vmalloc.h>
+#include <linux/workqueue.h>
+#include <linux/dma-mapping.h>
+#include <linux/poll.h>
#include <media/v4l2-dev.h>
#include <media/v4l2-device.h>
#include <media/v4l2-fh.h>
+#include <media/v4l2-event.h>
#include "uapi/linux/virtio_media.h"
#include "session.h"
#include "virtio_media.h"
+#define VIRTIO_MEDIA_NUM_EVENT_BUFS 16
+
/**
* virtio_media_session_alloc() - Allocate a new session.
* @vv: virtio-media device the session belongs to.
@@ -37,6 +44,7 @@ static struct virtio_media_session *
virtio_media_session_alloc(struct virtio_media *vv, struct file *file)
{
struct virtio_media_session *session;
+ int i;
int ret;
session = kzalloc_obj(*session, GFP_KERNEL);
@@ -54,11 +62,20 @@ virtio_media_session_alloc(struct virtio_media *vv, struct file *file)
// The caller is expected to set the ID once the host assigns a value.
session->id = 0;
+ session->nonblocking_dequeue = file->f_flags & O_NONBLOCK;
INIT_LIST_HEAD(&session->list);
v4l2_fh_init(&session->fh, &vv->video_dev);
virtio_media_session_fh_add(session, file);
+ for (i = 0; i <= VIRTIO_MEDIA_LAST_QUEUE; i++)
+ INIT_LIST_HEAD(&session->queues[i].pending_dqbufs);
+ mutex_init(&session->queues_lock);
+
+ init_waitqueue_head(&session->dqbuf_wait);
+
+ mutex_init(&session->error_lock);
+
return session;
err_payload_sgs:
@@ -91,11 +108,16 @@ static void virtio_media_session_add(struct virtio_media *vv,
*/
static void virtio_media_session_free(struct virtio_media_session *session)
{
+ int i;
+
virtio_media_session_fh_del(session);
v4l2_fh_exit(&session->fh);
sg_free_table(&session->command_sgs);
+ for (i = 0; i <= VIRTIO_MEDIA_LAST_QUEUE; i++)
+ vfree(session->queues[i].buffers);
+
kfree(session->shadow_buf);
kfree(session);
}
@@ -152,6 +174,31 @@ static int virtio_media_session_close(struct virtio_media *vv,
return 0;
}
+/**
+ * virtio_media_find_session() - Look up a session with a given ID.
+ * @vv: virtio-media device to lookup the session from.
+ * @id: ID of the session to lookup.
+ */
+static struct virtio_media_session *
+virtio_media_find_session(struct virtio_media *vv, u32 id)
+{
+ struct list_head *p;
+ struct virtio_media_session *session = NULL;
+
+ mutex_lock(&vv->sessions_lock);
+ list_for_each(p, &vv->sessions) {
+ struct virtio_media_session *s =
+ list_entry(p, struct virtio_media_session, list);
+ if (s->id == id) {
+ session = s;
+ break;
+ }
+ }
+ mutex_unlock(&vv->sessions_lock);
+
+ return session;
+}
+
/**
* struct virtio_media_cmd_callback_param - Callback parameters to the virtio
* command queue.
@@ -290,6 +337,234 @@ int virtio_media_send_command(struct virtio_media *vv, struct scatterlist **sgs,
return 0;
}
+/**
+ * virtio_media_send_event_buffer() - Sends an event buffer to the host so it
+ * can return it with an event.
+ * @vv: virtio-media device in use.
+ * @event_buffer: pointer to the event buffer to send to the device.
+ */
+static int virtio_media_send_event_buffer(struct virtio_media *vv,
+ void *event_buffer)
+{
+ struct scatterlist *sgs[1], vresp;
+ int ret;
+
+ sg_init_one(&vresp, event_buffer, VIRTIO_MEDIA_EVENT_MAX_SIZE);
+ sgs[0] = &vresp;
+
+ ret = virtqueue_add_sgs(vv->eventq, sgs, 0, 1, event_buffer,
+ GFP_ATOMIC);
+ if (ret) {
+ v4l2_err(&vv->v4l2_dev,
+ "failed to add sgs to event virtqueue\n");
+ return ret;
+ }
+
+ if (!virtqueue_kick(vv->eventq)) {
+ v4l2_err(&vv->v4l2_dev, "failed to kick event virtqueue\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+/**
+ * eventq_callback() - Callback for the event queue.
+ * @queue: event virtqueue.
+ *
+ * This just schedules for event work to be run.
+ */
+static void eventq_callback(struct virtqueue *queue)
+{
+ struct virtio_media *vv = queue->vdev->priv;
+
+ schedule_work(&vv->eventq_work);
+}
+
+/**
+ * virtio_media_process_dqbuf_event() - Process a dequeued event for a session.
+ * @vv: virtio-media device in use.
+ * @session: session the event is addressed to.
+ * @dqbuf_evt: the dequeued event to process.
+ *
+ * Invalid events are ignored with an error log.
+ */
+static void
+virtio_media_process_dqbuf_event(struct virtio_media *vv,
+ struct virtio_media_session *session,
+ struct virtio_media_event_dqbuf *dqbuf_evt)
+{
+ struct virtio_media_buffer *dqbuf;
+ const enum v4l2_buf_type queue_type = dqbuf_evt->buffer.type;
+ struct virtio_media_queue_state *queue;
+ typeof(dqbuf->buffer.m) buffer_m;
+ typeof(dqbuf->buffer.m.planes[0].m) plane_m;
+ int i;
+
+ if (queue_type >= ARRAY_SIZE(session->queues)) {
+ v4l2_err(&vv->v4l2_dev,
+ "unmanaged queue %d passed to dqbuf event",
+ dqbuf_evt->buffer.type);
+ return;
+ }
+ queue = &session->queues[queue_type];
+
+ if (dqbuf_evt->buffer.index >= queue->allocated_bufs) {
+ v4l2_err(&vv->v4l2_dev,
+ "invalid buffer ID %d for queue %d in dqbuf event",
+ dqbuf_evt->buffer.index, dqbuf_evt->buffer.type);
+ return;
+ }
+
+ dqbuf = &queue->buffers[dqbuf_evt->buffer.index];
+
+ /*
+ * Preserve the 'm' union that was passed to us during QBUF so userspace
+ * gets back the information it submitted.
+ */
+ buffer_m = dqbuf->buffer.m;
+ memcpy(&dqbuf->buffer, &dqbuf_evt->buffer, sizeof(dqbuf->buffer));
+ dqbuf->buffer.m = buffer_m;
+ if (V4L2_TYPE_IS_MULTIPLANAR(dqbuf->buffer.type)) {
+ if (dqbuf->buffer.length > VIDEO_MAX_PLANES) {
+ v4l2_err(&vv->v4l2_dev,
+ "invalid number of planes received from host for a multiplanar buffer\n");
+ return;
+ }
+ for (i = 0; i < dqbuf->buffer.length; i++) {
+ plane_m = dqbuf->planes[i].m;
+ memcpy(&dqbuf->planes[i], &dqbuf_evt->planes[i],
+ sizeof(struct v4l2_plane));
+ dqbuf->planes[i].m = plane_m;
+ }
+ }
+
+ /* Set the DONE flag as the buffer is waiting to be dequeued. */
+ dqbuf->buffer.flags |= V4L2_BUF_FLAG_DONE;
+
+ mutex_lock(&session->queues_lock);
+ list_add_tail(&dqbuf->list, &queue->pending_dqbufs);
+ queue->queued_bufs -= 1;
+ mutex_unlock(&session->queues_lock);
+
+ wake_up(&session->dqbuf_wait);
+}
+
+/**
+ * virtio_media_process_events() - Process all pending events on a device.
+ * @vv: device whose pending events we want to process.
+ *
+ * Retrieves all pending events on @vv's event queue and dispatches them to
+ * their corresponding session.
+ *
+ * Invalid events are ignored with an error log.
+ */
+void virtio_media_process_events(struct virtio_media *vv)
+{
+ struct virtio_media_event_error *error_evt;
+ struct virtio_media_event_dqbuf *dqbuf_evt;
+ struct virtio_media_event_event *event_evt;
+ struct virtio_media_session *session;
+ struct virtio_media_event_header *evt;
+ unsigned int len;
+
+ mutex_lock(&vv->events_lock);
+
+process_bufs:
+ while ((evt = virtqueue_get_buf(vv->eventq, &len))) {
+ /* Make sure we received enough data */
+ if (len < sizeof(*evt)) {
+ v4l2_err(&vv->v4l2_dev,
+ "event is too short: got %u, expected at least %zu\n",
+ len, sizeof(*evt));
+ goto end_of_event;
+ }
+
+ session = virtio_media_find_session(vv, evt->session_id);
+ if (!session) {
+ v4l2_err(&vv->v4l2_dev, "cannot find session %d\n",
+ evt->session_id);
+ goto end_of_event;
+ }
+
+ switch (evt->event) {
+ case VIRTIO_MEDIA_EVT_ERROR:
+ if (len < sizeof(*error_evt)) {
+ v4l2_err(&vv->v4l2_dev,
+ "error event is too short: got %u, expected %zu\n",
+ len, sizeof(*error_evt));
+ break;
+ }
+ error_evt = (struct virtio_media_event_error *)evt;
+ v4l2_err(&vv->v4l2_dev,
+ "received error %d for session %d",
+ error_evt->error, error_evt->hdr.session_id);
+
+ mutex_lock(&session->error_lock);
+ session->error = true;
+ mutex_unlock(&session->error_lock);
+
+ virtio_media_session_close(vv, session);
+
+ /* Wake up any waiting dqbuf requests */
+ wake_up(&session->dqbuf_wait);
+
+ break;
+
+ /*
+ * Dequeued buffer: put it into the right queue so user-space
+ * can dequeue it.
+ */
+ case VIRTIO_MEDIA_EVT_DQBUF:
+ if (len < sizeof(*dqbuf_evt)) {
+ v4l2_err(&vv->v4l2_dev,
+ "dqbuf event is too short: got %u, expected %zu\n",
+ len, sizeof(*dqbuf_evt));
+ break;
+ }
+ dqbuf_evt = (struct virtio_media_event_dqbuf *)evt;
+ virtio_media_process_dqbuf_event(vv, session,
+ dqbuf_evt);
+ break;
+
+ case VIRTIO_MEDIA_EVT_EVENT:
+ if (len < sizeof(*event_evt)) {
+ v4l2_err(&vv->v4l2_dev,
+ "session event is too short: got %u expected %zu\n",
+ len, sizeof(*event_evt));
+ break;
+ }
+
+ event_evt = (struct virtio_media_event_event *)evt;
+ v4l2_event_queue_fh(&session->fh, &event_evt->event);
+ break;
+
+ default:
+ v4l2_err(&vv->v4l2_dev, "unknown event type %d\n",
+ evt->event);
+ break;
+ }
+
+end_of_event:
+ virtio_media_send_event_buffer(vv, evt);
+ }
+
+ if (!virtqueue_enable_cb(vv->eventq)) {
+ virtqueue_disable_cb(vv->eventq);
+ goto process_bufs;
+ }
+
+ mutex_unlock(&vv->events_lock);
+}
+
+static void virtio_media_event_work(struct work_struct *work)
+{
+ struct virtio_media *vv =
+ container_of(work, struct virtio_media, eventq_work);
+
+ virtio_media_process_events(vv);
+}
+
/**
* virtio_media_device_open() - Create a new session from an opened file.
* @file: opened file for the session.
@@ -358,10 +633,65 @@ static int virtio_media_device_close(struct file *file)
return ret;
}
+/**
+ * virtio_media_device_poll() - Poll logic for a virtio-media device.
+ * @file: file of the session to poll.
+ * @wait: poll table to wait on.
+ */
+static __poll_t virtio_media_device_poll(struct file *file, poll_table *wait)
+{
+ struct virtio_media_session *session =
+ fh_to_session(file->private_data);
+ enum v4l2_buf_type capture_type =
+ session->uses_mplane ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE :
+ V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ enum v4l2_buf_type output_type =
+ session->uses_mplane ? V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE :
+ V4L2_BUF_TYPE_VIDEO_OUTPUT;
+ struct virtio_media_queue_state *capture_queue =
+ &session->queues[capture_type];
+ struct virtio_media_queue_state *output_queue =
+ &session->queues[output_type];
+ __poll_t req_events = poll_requested_events(wait);
+ __poll_t rc = 0;
+
+ poll_wait(file, &session->dqbuf_wait, wait);
+ poll_wait(file, &session->fh.wait, wait);
+
+ mutex_lock(&session->error_lock);
+ if (session->error)
+ rc |= EPOLLERR;
+ mutex_unlock(&session->error_lock);
+
+ mutex_lock(&session->queues_lock);
+ if (req_events & (EPOLLIN | EPOLLRDNORM)) {
+ if (!capture_queue->streaming ||
+ (capture_queue->queued_bufs == 0 &&
+ list_empty(&capture_queue->pending_dqbufs)))
+ rc |= EPOLLERR;
+ else if (!list_empty(&capture_queue->pending_dqbufs))
+ rc |= EPOLLIN | EPOLLRDNORM;
+ }
+ if (req_events & (EPOLLOUT | EPOLLWRNORM)) {
+ if (!output_queue->streaming)
+ rc |= EPOLLERR;
+ else if (output_queue->queued_bufs <
+ output_queue->allocated_bufs)
+ rc |= EPOLLOUT | EPOLLWRNORM;
+ }
+ mutex_unlock(&session->queues_lock);
+
+ if (v4l2_event_pending(&session->fh))
+ rc |= EPOLLPRI;
+
+ return rc;
+}
+
static const struct v4l2_file_operations virtio_media_fops = {
.owner = THIS_MODULE,
.open = virtio_media_device_open,
.release = virtio_media_device_close,
+ .poll = virtio_media_device_poll,
.unlocked_ioctl = virtio_media_device_ioctl,
};
@@ -371,29 +701,47 @@ static void virtio_media_release(struct v4l2_device *v4l2_dev)
container_of(v4l2_dev, struct virtio_media, v4l2_dev);
v4l2_device_unregister(&vv->v4l2_dev);
+ kfree(vv->event_buffer);
kfree(vv);
}
static int virtio_media_probe(struct virtio_device *virtio_dev)
{
struct device *dev = &virtio_dev->dev;
- struct virtqueue *vqs[1];
- static struct virtqueue_info vq_info[1] = {
+ struct virtqueue *vqs[2];
+ static struct virtqueue_info vq_info[2] = {
{
.name = "command",
.callback = commandq_callback,
},
+ {
+ .name = "event",
+ .callback = eventq_callback,
+ },
};
struct virtio_media *vv;
struct video_device *vd;
+ int i;
int ret;
vv = kzalloc_obj(struct virtio_media);
if (!vv)
return -ENOMEM;
+ const size_t virtio_media_event_aligned_size =
+ ALIGN(VIRTIO_MEDIA_EVENT_MAX_SIZE, dma_get_cache_alignment());
+
+ vv->event_buffer = kzalloc(virtio_media_event_aligned_size *
+ VIRTIO_MEDIA_NUM_EVENT_BUFS,
+ GFP_KERNEL);
+ if (!vv->event_buffer) {
+ ret = -ENOMEM;
+ goto err_free_virtio_media;
+ }
+
INIT_LIST_HEAD(&vv->sessions);
mutex_init(&vv->sessions_lock);
+ mutex_init(&vv->events_lock);
mutex_init(&vv->vlock);
vv->virtio_dev = virtio_dev;
@@ -403,15 +751,17 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
ret = v4l2_device_register(dev, &vv->v4l2_dev);
if (ret)
- goto err_free_virtio_media;
+ goto err_free_event_buffer;
vv->v4l2_dev.release = virtio_media_release;
- ret = virtio_find_vqs(virtio_dev, 1, vqs, vq_info, NULL);
+ ret = virtio_find_vqs(virtio_dev, 2, vqs, vq_info, NULL);
if (ret)
goto err_v4l2_device_unregister;
vv->commandq = vqs[0];
+ vv->eventq = vqs[1];
+ INIT_WORK(&vv->eventq_work, virtio_media_event_work);
vd = &vv->video_dev;
vd->v4l2_dev = &vv->v4l2_dev;
@@ -449,12 +799,25 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
if (ret)
goto err_del_vqs;
+ for (i = 0; i < VIRTIO_MEDIA_NUM_EVENT_BUFS; i++) {
+ void *ebuf = vv->event_buffer +
+ virtio_media_event_aligned_size * i;
+
+ ret = virtio_media_send_event_buffer(vv, ebuf);
+ if (ret)
+ goto err_video_unregister_device;
+ }
+
return 0;
+err_video_unregister_device:
+ video_unregister_device(&vv->video_dev);
err_del_vqs:
virtio_dev->config->del_vqs(virtio_dev);
err_v4l2_device_unregister:
v4l2_device_unregister(&vv->v4l2_dev);
+err_free_event_buffer:
+ kfree(vv->event_buffer);
err_free_virtio_media:
kfree(vv);
return ret;
@@ -468,6 +831,9 @@ static void virtio_media_remove(struct virtio_device *virtio_dev)
/* Remove the video device to stop accessing the commandq virtqueue */
video_unregister_device(&vv->video_dev);
+ /* Stop event handling work queue from accessing eventq virtqueue */
+ cancel_work_sync(&vv->eventq_work);
+
/*
* Acquire vlock to ensure no concurrent accesses to the commandq
* virtqueue
@@ -483,6 +849,8 @@ static void virtio_media_remove(struct virtio_device *virtio_dev)
*/
while (virtqueue_detach_unused_buf(vv->commandq))
;
+ while (virtqueue_detach_unused_buf(vv->eventq))
+ ;
/* Delete the virtqueues */
virtio_dev->config->del_vqs(virtio_dev);
diff --git a/drivers/media/virtio/virtio_media_ioctls.c b/drivers/media/virtio/virtio_media_ioctls.c
index 51f46b334..9bf283469 100644
--- a/drivers/media/virtio/virtio_media_ioctls.c
+++ b/drivers/media/virtio/virtio_media_ioctls.c
@@ -9,6 +9,8 @@
#include <linux/mutex.h>
#include <linux/videodev2.h>
#include <linux/virtio_config.h>
+#include <linux/vmalloc.h>
+#include <media/v4l2-event.h>
#include <media/v4l2-ioctl.h>
#include "scatterlist_builder.h"
@@ -218,6 +220,101 @@ static int virtio_media_send_wr_ioctl(struct v4l2_fh *fh, u32 ioctl,
return 0;
}
+/**
+ * virtio_media_send_buffer_ioctl() - Send an ioctl taking a buffer as
+ * parameter to the device.
+ * @fh: file handler of the session doing the ioctl.
+ * @ioctl: ``VIDIOC_*`` ioctl code.
+ * @b: &struct v4l2_buffer to be sent as the ioctl payload.
+ *
+ * Buffers can require an additional descriptor to send their planes array, and
+ * can have pointers to userspace memory hence this dedicated function.
+ */
+static int virtio_media_send_buffer_ioctl(struct v4l2_fh *fh, u32 ioctl,
+ struct v4l2_buffer *b)
+{
+ struct video_device *video_dev = fh->vdev;
+ struct virtio_media *vv = to_virtio_media(video_dev);
+ struct virtio_media_session *session = fh_to_session(fh);
+ struct v4l2_plane *orig_planes = NULL;
+ struct scatterlist *sgs[64];
+ /*
+ * End of the device-readable buffer SGs, to reuse in device-writable
+ * section.
+ */
+ size_t num_cmd_sgs;
+ size_t end_buf_sg;
+ struct scatterlist_builder builder = {
+ .descs = session->command_sgs.sgl,
+ .num_descs = DESC_CHAIN_MAX_LEN,
+ .cur_desc = 0,
+ .shadow_buffer = session->shadow_buf,
+ .shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
+ .shadow_buffer_pos = 0,
+ .sgs = sgs,
+ .num_sgs = ARRAY_SIZE(sgs),
+ .cur_sg = 0,
+ .framevec = NULL,
+ };
+ size_t resp_len;
+ int ret;
+ int i;
+
+ if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
+ return -EINVAL;
+
+ if (V4L2_TYPE_IS_MULTIPLANAR(b->type))
+ orig_planes = b->m.planes;
+
+ /* Command descriptor */
+ ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
+ if (ret)
+ return ret;
+
+ /* Command payload (struct v4l2_buffer) */
+ ret = scatterlist_builder_add_buffer(&builder, b);
+ if (ret < 0)
+ return ret;
+
+ end_buf_sg = builder.cur_sg;
+ num_cmd_sgs = builder.cur_sg;
+
+ /* Response descriptor */
+ ret = scatterlist_builder_add_ioctl_resp(&builder, session);
+ if (ret)
+ return ret;
+
+ /* Response payload (same as input, but no userptr mapping) */
+ for (i = 1; i < end_buf_sg; i++) {
+ ret = scatterlist_builder_add_descriptor(&builder, i);
+ if (ret < 0)
+ return ret;
+ }
+
+ ret = virtio_media_send_command(vv, builder.sgs, num_cmd_sgs,
+ builder.cur_sg - num_cmd_sgs,
+ sizeof(struct virtio_media_resp_ioctl) +
+ sizeof(*b), &resp_len);
+ if (ret < 0)
+ return ret;
+
+ resp_len -= sizeof(struct virtio_media_resp_ioctl);
+
+ /* Make sure that the reply length covers our v4l2_buffer */
+ if (resp_len < sizeof(*b))
+ return -EINVAL;
+
+ ret = scatterlist_builder_retrieve_buffer(&builder, num_cmd_sgs + 1, b,
+ orig_planes);
+ if (ret) {
+ v4l2_err(&vv->v4l2_dev,
+ "failed to retrieve response descriptor chain\n");
+ return ret;
+ }
+
+ return 0;
+}
+
/**
* virtio_media_send_ext_controls_ioctl() - Send an ioctl taking extended
* controls as parameters to the device.
@@ -336,6 +433,41 @@ static int virtio_media_send_ext_controls_ioctl(struct v4l2_fh *fh, u32 ioctl,
return 0;
}
+/**
+ * virtio_media_clear_queue() - clear all pending buffers on a streamed-off
+ * queue.
+ * @session: session which the queue to clear belongs to.
+ * @queue: state of the queue to clear.
+ *
+ * Helper function to clear the list of buffers waiting to be dequeued on a
+ * queue that has just been streamed off.
+ */
+static void virtio_media_clear_queue(struct virtio_media_session *session,
+ struct virtio_media_queue_state *queue)
+{
+ struct list_head *p, *n;
+ int i;
+
+ mutex_lock(&session->queues_lock);
+
+ list_for_each_safe(p, n, &queue->pending_dqbufs) {
+ struct virtio_media_buffer *dqbuf =
+ list_entry(p, struct virtio_media_buffer, list);
+
+ list_del(&dqbuf->list);
+ }
+
+ /* All buffers are now dequeued. */
+ for (i = 0; i < queue->allocated_bufs; i++)
+ queue->buffers[i].buffer.flags = 0;
+
+ queue->queued_bufs = 0;
+ queue->streaming = false;
+ queue->is_capture_last = false;
+
+ mutex_unlock(&session->queues_lock);
+}
+
/*
* Macros suitable for defining ioctls with a constant size payload.
*/
@@ -479,6 +611,431 @@ static int virtio_media_try_ext_ctrls(struct file *file, void *fh,
ctrls);
}
+/*
+ * Subscribe/unsubscribe from an event.
+ */
+
+static int
+virtio_media_subscribe_event(struct v4l2_fh *fh,
+ const struct v4l2_event_subscription *sub)
+{
+ struct video_device *video_dev = fh->vdev;
+ struct virtio_media *vv = to_virtio_media(video_dev);
+ int ret;
+
+ /* First subscribe to the event in the guest. */
+ switch (sub->type) {
+ case V4L2_EVENT_SOURCE_CHANGE:
+ ret = v4l2_src_change_event_subscribe(fh, sub);
+ break;
+ default:
+ ret = v4l2_event_subscribe(fh, sub, 1, NULL);
+ break;
+ }
+ if (ret)
+ return ret;
+
+ /* Then ask the host to signal us these events. */
+ ret = virtio_media_send_w_ioctl(fh, VIDIOC_SUBSCRIBE_EVENT, sub,
+ sizeof(*sub));
+ if (ret < 0) {
+ v4l2_event_unsubscribe(fh, sub);
+ return ret;
+ }
+
+ /*
+ * Subscribing to an event may result in that event being signaled
+ * immediately. Process all pending events to make sure we don't
+ * miss it.
+ */
+ if (sub->flags & V4L2_EVENT_SUB_FL_SEND_INITIAL)
+ virtio_media_process_events(vv);
+
+ return 0;
+}
+
+static int
+virtio_media_unsubscribe_event(struct v4l2_fh *fh,
+ const struct v4l2_event_subscription *sub)
+{
+ int ret = virtio_media_send_w_ioctl(fh, VIDIOC_UNSUBSCRIBE_EVENT, sub,
+ sizeof(*sub));
+ if (ret < 0)
+ return ret;
+
+ ret = v4l2_event_unsubscribe(fh, sub);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+/*
+ * Streamon/off affect the local queue state.
+ */
+
+static int virtio_media_streamon(struct file *file, void *fh,
+ enum v4l2_buf_type i)
+{
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file);
+ struct virtio_media_session *session = fh_to_session(vfh);
+ int ret;
+
+ if (i > VIRTIO_MEDIA_LAST_QUEUE)
+ return -EINVAL;
+
+ ret = virtio_media_send_w_ioctl(vfh, VIDIOC_STREAMON, &i, sizeof(i));
+ if (ret < 0)
+ return ret;
+
+ session->queues[i].streaming = true;
+
+ return 0;
+}
+
+static int virtio_media_streamoff(struct file *file, void *fh,
+ enum v4l2_buf_type i)
+{
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file);
+ struct virtio_media_session *session = fh_to_session(vfh);
+ int ret;
+
+ if (i > VIRTIO_MEDIA_LAST_QUEUE)
+ return -EINVAL;
+
+ ret = virtio_media_send_w_ioctl(vfh, VIDIOC_STREAMOFF, &i, sizeof(i));
+ if (ret < 0)
+ return ret;
+
+ virtio_media_clear_queue(session, &session->queues[i]);
+
+ return 0;
+}
+
+/*
+ * Buffer creation/queuing functions deal with the local driver state.
+ */
+
+static int virtio_media_reqbufs(struct file *file, void *fh,
+ struct v4l2_requestbuffers *b)
+{
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file);
+ struct virtio_media_session *session = fh_to_session(vfh);
+ struct virtio_media_buffer *buffers = NULL;
+ struct virtio_media_queue_state *queue;
+ int ret;
+
+ if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
+ return -EINVAL;
+
+ if (b->memory == V4L2_MEMORY_USERPTR)
+ return -EINVAL;
+
+ if (b->count > 0) {
+ buffers =
+ vcalloc(b->count, sizeof(struct virtio_media_buffer));
+ if (!buffers)
+ return -ENOMEM;
+ }
+
+ ret = virtio_media_send_wr_ioctl(vfh, VIDIOC_REQBUFS, b, sizeof(*b),
+ sizeof(*b));
+ if (ret) {
+ vfree(buffers);
+ return ret;
+ }
+
+ queue = &session->queues[b->type];
+
+ /* REQBUFS(0) is an implicit STREAMOFF. */
+ if (b->count == 0)
+ virtio_media_clear_queue(session, queue);
+
+ vfree(queue->buffers);
+ queue->buffers = buffers;
+
+ queue->allocated_bufs = b->count;
+
+ /*
+ * If a multiplanar queue is successfully used here, this means
+ * we are using the multiplanar interface.
+ */
+ if (V4L2_TYPE_IS_MULTIPLANAR(b->type))
+ session->uses_mplane = true;
+
+ b->capabilities &= ~V4L2_BUF_CAP_SUPPORTS_USERPTR;
+
+ /* We do not support DMABUF yet. */
+ b->capabilities &= ~V4L2_BUF_CAP_SUPPORTS_DMABUF;
+
+ return 0;
+}
+
+static int virtio_media_querybuf(struct file *file, void *fh,
+ struct v4l2_buffer *b)
+{
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file);
+ struct virtio_media_session *session = fh_to_session(vfh);
+ struct virtio_media_queue_state *queue;
+ struct virtio_media_buffer *buffer;
+
+ int ret = virtio_media_send_buffer_ioctl(vfh, VIDIOC_QUERYBUF, b);
+
+ if (ret)
+ return ret;
+
+ if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
+ return -EINVAL;
+
+ queue = &session->queues[b->type];
+ if (b->index >= queue->allocated_bufs)
+ return -EINVAL;
+
+ buffer = &queue->buffers[b->index];
+ /*
+ * Set the DONE flag if the buffer is waiting in our own dequeue
+ * queue.
+ */
+ b->flags |= (buffer->buffer.flags & V4L2_BUF_FLAG_DONE);
+
+ return 0;
+}
+
+static int virtio_media_create_bufs(struct file *file, void *fh,
+ struct v4l2_create_buffers *b)
+{
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file);
+ struct virtio_media_session *session = fh_to_session(vfh);
+ struct virtio_media_queue_state *queue;
+ struct virtio_media_buffer *buffers = NULL;
+ u32 type = b->format.type;
+ int ret;
+
+ if (type > VIRTIO_MEDIA_LAST_QUEUE)
+ return -EINVAL;
+
+ queue = &session->queues[type];
+
+ if (b->count > 0) {
+ /*
+ * This allocates a new buffer large enough for the existing
+ * buffers (``queue->allocated_bufs``) as well as the newly
+ * requested buffers (``b->count``). The host may allocate
+ * fewer buffers than requested, as indicated by the final
+ * value of ``b->count``. In this case, we do not re-allocate
+ * to avoid the case where this new allocation fails and the
+ * host buffers need to be freed. Instead we simply use the
+ * larger buffer.
+ */
+ buffers = vcalloc(queue->allocated_bufs + b->count,
+ sizeof(*queue->buffers));
+ if (!buffers)
+ return -ENOMEM;
+ }
+
+ ret = virtio_media_send_wr_ioctl(vfh, VIDIOC_CREATE_BUFS, b, sizeof(*b),
+ sizeof(*b));
+ if (ret) {
+ if (buffers)
+ vfree(buffers);
+
+ return ret;
+ }
+
+ /* If count is zero, we were just checking for format. */
+ if (b->count == 0)
+ return 0;
+
+ memcpy(buffers, queue->buffers,
+ sizeof(*queue->buffers) * queue->allocated_bufs);
+ vfree(queue->buffers);
+ queue->buffers = buffers;
+
+ queue->allocated_bufs = b->index + b->count;
+
+ return 0;
+}
+
+static int virtio_media_prepare_buf(struct file *file, void *fh,
+ struct v4l2_buffer *b)
+{
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file);
+ struct virtio_media_session *session = fh_to_session(vfh);
+ struct virtio_media_queue_state *queue;
+ struct virtio_media_buffer *buffer;
+ int i, ret;
+
+ if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
+ return -EINVAL;
+ queue = &session->queues[b->type];
+ if (b->index >= queue->allocated_bufs)
+ return -EINVAL;
+ buffer = &queue->buffers[b->index];
+
+ buffer->buffer.m = b->m;
+ if (V4L2_TYPE_IS_MULTIPLANAR(b->type)) {
+ if (b->length > VIDEO_MAX_PLANES)
+ return -EINVAL;
+ for (i = 0; i < b->length; i++)
+ buffer->planes[i].m = b->m.planes[i].m;
+ }
+
+ ret = virtio_media_send_buffer_ioctl(vfh, VIDIOC_PREPARE_BUF, b);
+ if (ret)
+ return ret;
+
+ buffer->buffer.flags = V4L2_BUF_FLAG_PREPARED;
+
+ return 0;
+}
+
+static int virtio_media_qbuf(struct file *file, void *fh, struct v4l2_buffer *b)
+{
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file);
+ struct virtio_media_session *session = fh_to_session(vfh);
+ struct virtio_media_queue_state *queue;
+ struct virtio_media_buffer *buffer;
+ bool prepared;
+ u32 old_flags;
+ int i, ret;
+
+ if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
+ return -EINVAL;
+ queue = &session->queues[b->type];
+ if (b->index >= queue->allocated_bufs)
+ return -EINVAL;
+ buffer = &queue->buffers[b->index];
+ prepared = buffer->buffer.flags & V4L2_BUF_FLAG_PREPARED;
+
+ /*
+ * Store the buffer and plane `m` information so we can retrieve
+ * it again when DQBUF occurs.
+ */
+ if (!prepared) {
+ buffer->buffer.m = b->m;
+ if (V4L2_TYPE_IS_MULTIPLANAR(b->type)) {
+ if (b->length > VIDEO_MAX_PLANES)
+ return -EINVAL;
+ for (i = 0; i < b->length; i++)
+ buffer->planes[i].m = b->m.planes[i].m;
+ }
+ }
+ old_flags = buffer->buffer.flags;
+ buffer->buffer.flags = V4L2_BUF_FLAG_QUEUED;
+
+ ret = virtio_media_send_buffer_ioctl(vfh, VIDIOC_QBUF, b);
+ if (ret) {
+ /* Rollback the previous flags as the buffer is not queued. */
+ buffer->buffer.flags = old_flags;
+ return ret;
+ }
+
+ queue->queued_bufs += 1;
+
+ return 0;
+}
+
+static int virtio_media_dqbuf(struct file *file, void *fh,
+ struct v4l2_buffer *b)
+{
+ struct video_device *video_dev = video_devdata(file);
+ struct virtio_media *vv = to_virtio_media(video_dev);
+ struct virtio_media_session *session =
+ fh_to_session(file_to_v4l2_fh(file));
+ struct virtio_media_buffer *dqbuf;
+ struct virtio_media_queue_state *queue;
+ struct list_head *buffer_queue;
+ struct v4l2_plane *planes_backup = NULL;
+ const bool is_multiplanar = V4L2_TYPE_IS_MULTIPLANAR(b->type);
+ int ret;
+ bool error;
+
+ if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
+ return -EINVAL;
+
+ queue = &session->queues[b->type];
+
+ /*
+ * If a buffer with the LAST flag has been returned, subsequent
+ * calls to DQBUF must return -EPIPE until the queue is cleared.
+ */
+ if (queue->is_capture_last)
+ return -EPIPE;
+
+ buffer_queue = &queue->pending_dqbufs;
+
+ if (session->nonblocking_dequeue) {
+ if (list_empty(buffer_queue))
+ return -EAGAIN;
+ } else if (queue->allocated_bufs == 0) {
+ return -EINVAL;
+ } else if (!queue->streaming) {
+ return -EINVAL;
+ }
+
+ /*
+ * vv->lock has been acquired by virtio_media_device_ioctl. Release it
+ * while we wait so that other ioctls for this session can be processed
+ * and potentially trigger dqbuf_wait.
+ */
+ mutex_unlock(&vv->vlock);
+
+ /*
+ * It's not possible to acquire ``session->error_lock`` during the
+ * check, however this shouldn't cause issues because:
+ * - The lock is acquired when actually checking the value
+ * - The ``session->error`` flag is never reset once set
+ */
+ ret = wait_event_interruptible(session->dqbuf_wait,
+ !list_empty(buffer_queue) ||
+ session->error);
+ mutex_lock(&vv->vlock);
+ if (ret)
+ return -EINTR;
+
+ mutex_lock(&session->error_lock);
+ error = session->error;
+ mutex_unlock(&session->error_lock);
+
+ if (error)
+ return -EIO;
+
+ mutex_lock(&session->queues_lock);
+ dqbuf = list_first_entry_or_null(buffer_queue,
+ struct virtio_media_buffer, list);
+
+ if (dqbuf)
+ list_del(&dqbuf->list);
+
+ mutex_unlock(&session->queues_lock);
+
+ if (!dqbuf) {
+ v4l2_err(&vv->v4l2_dev, "No buffer found when dqbuf event triggered\n");
+ return -EIO;
+ }
+
+ /* Clear the DONE flag as the buffer is now being dequeued. */
+ dqbuf->buffer.flags &= ~V4L2_BUF_FLAG_DONE;
+
+ if (is_multiplanar) {
+ size_t nb_planes = min_t(u32, b->length, VIDEO_MAX_PLANES);
+
+ memcpy(b->m.planes, dqbuf->planes,
+ nb_planes * sizeof(struct v4l2_plane));
+ planes_backup = b->m.planes;
+ }
+
+ memcpy(b, &dqbuf->buffer, sizeof(*b));
+
+ if (is_multiplanar)
+ b->m.planes = planes_backup;
+
+ if (V4L2_TYPE_IS_CAPTURE(b->type) && b->flags & V4L2_BUF_FLAG_LAST)
+ queue->is_capture_last = true;
+
+ return 0;
+}
+
/*
* s/g_input/output work with an unsigned int - recast this to a u32 so the
* size is unambiguous.
@@ -532,6 +1089,32 @@ static int virtio_media_s_output(struct file *file, void *fh, unsigned int o)
sizeof(output), sizeof(output));
}
+/*
+ * decoder_cmd can affect the state of the CAPTURE queue.
+ */
+
+static int virtio_media_decoder_cmd(struct file *file, void *fh,
+ struct v4l2_decoder_cmd *cmd)
+{
+ struct v4l2_fh *vfh = file_to_v4l2_fh(file);
+ struct virtio_media_session *session = fh_to_session(vfh);
+
+ int ret = virtio_media_send_wr_ioctl(vfh, VIDIOC_DECODER_CMD, cmd,
+ sizeof(*cmd), sizeof(*cmd));
+ if (ret)
+ return ret;
+
+ /* A START command makes the CAPTURE queue able to dequeue again. */
+ if (cmd->cmd == V4L2_DEC_CMD_START) {
+ session->queues[V4L2_BUF_TYPE_VIDEO_CAPTURE].is_capture_last =
+ false;
+ session->queues[V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE]
+ .is_capture_last = false;
+ }
+
+ return 0;
+}
+
/*
* s_std doesn't work with a pointer, so we cannot use SIMPLE_W_IOCTL.
*/
@@ -607,6 +1190,25 @@ const struct v4l2_ioctl_ops virtio_media_ioctl_ops = {
.vidioc_try_fmt_meta_cap = virtio_media_try_fmt,
.vidioc_try_fmt_meta_out = virtio_media_try_fmt,
+ /* Buffer handlers */
+ .vidioc_reqbufs = virtio_media_reqbufs,
+ .vidioc_querybuf = virtio_media_querybuf,
+ .vidioc_qbuf = virtio_media_qbuf,
+ .vidioc_expbuf = NULL,
+ .vidioc_dqbuf = virtio_media_dqbuf,
+ .vidioc_create_bufs = virtio_media_create_bufs,
+ .vidioc_prepare_buf = virtio_media_prepare_buf,
+ /* Overlay interface not supported yet */
+ .vidioc_overlay = NULL,
+ /* Overlay interface not supported yet */
+ .vidioc_g_fbuf = NULL,
+ /* Overlay interface not supported yet */
+ .vidioc_s_fbuf = NULL,
+
+ /* Stream on/off */
+ .vidioc_streamon = virtio_media_streamon,
+ .vidioc_streamoff = virtio_media_streamoff,
+
/* Standard handling */
.vidioc_g_std = virtio_media_g_std,
.vidioc_s_std = virtio_media_s_std,
@@ -658,6 +1260,7 @@ const struct v4l2_ioctl_ops virtio_media_ioctl_ops = {
.vidioc_g_enc_index = virtio_media_g_enc_index,
.vidioc_encoder_cmd = virtio_media_encoder_cmd,
.vidioc_try_encoder_cmd = virtio_media_try_encoder_cmd,
+ .vidioc_decoder_cmd = virtio_media_decoder_cmd,
.vidioc_try_decoder_cmd = virtio_media_try_decoder_cmd,
/* Stream type-dependent parameter ioctls */
@@ -692,6 +1295,9 @@ const struct v4l2_ioctl_ops virtio_media_ioctl_ops = {
.vidioc_g_edid = NULL,
.vidioc_s_edid = NULL,
+ .vidioc_subscribe_event = virtio_media_subscribe_event,
+ .vidioc_unsubscribe_event = virtio_media_unsubscribe_event,
+
/* For other private ioctls */
.vidioc_default = NULL,
};
@@ -705,9 +1311,19 @@ long virtio_media_device_ioctl(struct file *file, unsigned int cmd,
struct v4l2_standard standard;
v4l2_std_id std_id = 0;
int ret;
+ bool error;
- if (test_bit(V4L2_FL_USES_V4L2_FH, &video_dev->flags))
+ if (test_bit(V4L2_FL_USES_V4L2_FH, &video_dev->flags)) {
vfh = file_to_v4l2_fh(file);
+ struct virtio_media_session *session = fh_to_session(vfh);
+
+ mutex_lock(&session->error_lock);
+ error = session->error;
+ mutex_unlock(&session->error_lock);
+
+ if (error)
+ return -EIO;
+ }
mutex_lock(&vv->vlock);
diff --git a/include/uapi/linux/virtio_media.h b/include/uapi/linux/virtio_media.h
index f117b97fc..da5c5baf0 100644
--- a/include/uapi/linux/virtio_media.h
+++ b/include/uapi/linux/virtio_media.h
@@ -171,4 +171,72 @@ struct virtio_media_sg_entry {
__le32 __reserved;
};
+/* The values for these events are set by the virtio-media specification. */
+#define VIRTIO_MEDIA_EVT_ERROR 0
+#define VIRTIO_MEDIA_EVT_DQBUF 1
+#define VIRTIO_MEDIA_EVT_EVENT 2
+
+/**
+ * struct virtio_media_event_header - Header for events on the eventq.
+ * @event: one of VIRTIO_MEDIA_EVT_*
+ * @session_id: ID of the session the event applies to.
+ */
+struct virtio_media_event_header {
+ __le32 event;
+ __le32 session_id;
+};
+
+/**
+ * struct virtio_media_event_error - Unrecoverable device-side error.
+ * @hdr: header for the event.
+ * @error: error code describing the kind of error that occurred.
+ * @__reserved: must be set to zero by the device.
+ *
+ * Upon receiving this event, the session mentioned in the header is considered
+ * corrupted and closed.
+ */
+struct virtio_media_event_error {
+ struct virtio_media_event_header hdr;
+ __le32 error;
+ __le32 __reserved;
+};
+
+/* This is set to VIDEO_MAX_PLANES defined in include/uapi/linux/videodev2.h.
+ * It is renamed here to match the constant that is defined in the virtio-media
+ * specification.
+ */
+#define VIRTIO_MEDIA_MAX_PLANES VIDEO_MAX_PLANES
+
+/**
+ * struct virtio_media_event_dqbuf - Dequeued buffer event.
+ * @hdr: header for the event.
+ * @buffer: &struct v4l2_buffer describing the buffer that has been dequeued.
+ * @planes: plane information for the dequeued buffer.
+ *
+ * This event is used to signal that a buffer is not being used anymore by the
+ * device and is returned to the driver.
+ */
+struct virtio_media_event_dqbuf {
+ struct virtio_media_event_header hdr;
+ struct v4l2_buffer buffer;
+ struct v4l2_plane planes[VIRTIO_MEDIA_MAX_PLANES];
+};
+
+/**
+ * struct virtio_media_event_event - V4L2 event.
+ * @hdr: header for the event.
+ * @event: description of the event that occurred.
+ *
+ * This event signals that a V4L2 event has been emitted for a session.
+ */
+struct virtio_media_event_event {
+ struct virtio_media_event_header hdr;
+ struct v4l2_event event;
+};
+
+/* Maximum size of an event. We will queue descriptors of this size on the
+ * eventq.
+ */
+#define VIRTIO_MEDIA_EVENT_MAX_SIZE sizeof(struct virtio_media_event_dqbuf)
+
#endif // _UAPI__LINUX_VIRTIO_MEDIA_H
--
2.56.0.317.g454f8302e1-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v10 5/5] media: virtio: Add MMAP buffer mapping support
2026-10-29 15:30 [PATCH v10 0/5] media: add virtio-media driver Brian Daniels
` (3 preceding siblings ...)
2026-10-29 15:30 ` [PATCH v10 4/5] media: virtio: Add buffer streaming, event handling, and poll Brian Daniels
@ 2026-10-29 15:30 ` Brian Daniels
2026-10-29 17:19 ` sashiko-bot
4 siblings, 1 reply; 11+ messages in thread
From: Brian Daniels @ 2026-10-29 15:30 UTC (permalink / raw)
To: Mauro Carvalho Chehab
Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
gurchetansingh, hverkuil, linux-kernel, linux-media, mst,
nicolas.dufresne, virtualization, xuanzhuo, dbassey,
laurent.pinchart, Brian Daniels
From: Alexandre Courbot <gnurou@gmail.com>
Support mapping V4L2_MEMORY_MMAP buffers into guest userspace through
virtio shared memory region 0.
Query the shared memory region during device probe and implement the
mmap file operation and VMA close callback using VIRTIO_MEDIA_CMD_MMAP
and VIRTIO_MEDIA_CMD_MUNMAP, remapping the host-provided shared memory
offset into userspace via io_remap_pfn_range().
Signed-off-by: Alexandre Courbot <gnurou@gmail.com>
Assisted-by: LLM
Co-developed-by: Brian Daniels <briandaniels@google.com>
Signed-off-by: Brian Daniels <briandaniels@google.com>
---
drivers/media/virtio/session.h | 14 +-
drivers/media/virtio/virtio_media.h | 13 +-
drivers/media/virtio/virtio_media_driver.c | 152 +++++++++++++++++++++
include/uapi/linux/virtio_media.h | 67 +++++++++
4 files changed, 236 insertions(+), 10 deletions(-)
diff --git a/drivers/media/virtio/session.h b/drivers/media/virtio/session.h
index 16bb86bf4..c074ffc2c 100644
--- a/drivers/media/virtio/session.h
+++ b/drivers/media/virtio/session.h
@@ -63,12 +63,12 @@ struct virtio_media_queue_state {
* @nonblocking_dequeue: whether dequeue should block or not (nonblocking if
* file opened with O_NONBLOCK).
* @uses_mplane: whether the queues for this session use the MPLANE API or not.
- * @cmd: union of session commands ``close`` and ``ioctl``. A session can have
- * one command currently running. The rest of the commands are handled by
- * &struct virtio_media.
- * @resp: union of responses to session commands. A session can wait on one
- * command only. The rest of the responses are handled by
- * &struct virtio_media.
+ * @cmd: union of session commands ``close``, ``ioctl``, and ``mmap``. A
+ * session can have one command currently running. The rest of the
+ * commands are handled by &struct virtio_media.
+ * @resp: union of responses to session commands ``close``, ``ioctl``, and
+ * ``mmap``. A session can wait on one command only. The rest of the
+ * responses are handled by &struct virtio_media.
* @shadow_buf: shadow buffer where data to be added to the descriptor chain can
* be staged before being sent to the device.
* @command_sgs: SG table gathering descriptors for a given command and its
@@ -91,10 +91,12 @@ struct virtio_media_session {
union {
struct virtio_media_cmd_close close;
struct virtio_media_cmd_ioctl ioctl;
+ struct virtio_media_cmd_mmap mmap;
} cmd;
union {
struct virtio_media_resp_ioctl ioctl;
+ struct virtio_media_resp_mmap mmap;
} resp;
__dma_from_device_group_end();
diff --git a/drivers/media/virtio/virtio_media.h b/drivers/media/virtio/virtio_media.h
index 8b21c1941..7acb2b842 100644
--- a/drivers/media/virtio/virtio_media.h
+++ b/drivers/media/virtio/virtio_media.h
@@ -26,14 +26,15 @@
* @commandq: virtio command queue.
* @eventq: virtio event queue.
* @eventq_work: work to run when events are received on @eventq.
+ * @mmap_region: region into which MMAP buffers are mapped by the host.
* @event_buffer: buffer for event descriptors.
* @sessions: list of active sessions on the device.
* @sessions_lock: protects @sessions and &struct virtio_media_session.list.
* @events_lock: prevents concurrent processing of events.
- * @cmd: union of device-level commands. Session commands are handled by
- * &struct virtio_media_session.
- * @resp: union of responses to device-level commands. Session responses are
- * handled by &struct virtio_media_session.
+ * @cmd: union of the device commands ``open`` and ``munmap``. The other
+ * commands are handled by &struct virtio_media_session
+ * @resp: union of responses to device commands ``open`` and ``munmap``. The
+ * other responses are handled by &struct virtio_media_session
* @vlock: serializes access to the command queue.
* @wq: waitqueue for host responses on the command queue.
*/
@@ -46,6 +47,8 @@ struct virtio_media {
struct virtqueue *eventq;
struct work_struct eventq_work;
+ struct virtio_shm_region mmap_region;
+
void *event_buffer;
struct list_head sessions;
@@ -56,10 +59,12 @@ struct virtio_media {
__dma_from_device_group_begin();
union {
struct virtio_media_cmd_open open;
+ struct virtio_media_cmd_munmap munmap;
} cmd;
union {
struct virtio_media_resp_open open;
+ struct virtio_media_resp_munmap munmap;
} resp;
__dma_from_device_group_end();
diff --git a/drivers/media/virtio/virtio_media_driver.c b/drivers/media/virtio/virtio_media_driver.c
index 877ac154b..865b6dfad 100644
--- a/drivers/media/virtio/virtio_media_driver.c
+++ b/drivers/media/virtio/virtio_media_driver.c
@@ -6,6 +6,7 @@
* Copyright (c) 2024-2026 Google LLC.
*/
+#include <linux/bits.h>
#include <linux/device.h>
#include <linux/dev_printk.h>
#include <linux/mutex.h>
@@ -20,6 +21,7 @@
#include <linux/workqueue.h>
#include <linux/dma-mapping.h>
#include <linux/poll.h>
+#include <linux/mm.h>
#include <media/v4l2-dev.h>
#include <media/v4l2-device.h>
@@ -32,6 +34,12 @@
#define VIRTIO_MEDIA_NUM_EVENT_BUFS 16
+/* ID of the SHM region into which MMAP buffer will be mapped. */
+#define VIRTIO_MEDIA_SHM_MMAP 0
+
+/* Bit mask for the VIRTIO_MEDIA_MMAP_FLAG_RW flag */
+#define VIRTIO_MEDIA_MMAP_FLAG_RW_MASK BIT(VIRTIO_MEDIA_MMAP_FLAG_RW)
+
/**
* virtio_media_session_alloc() - Allocate a new session.
* @vv: virtio-media device the session belongs to.
@@ -687,12 +695,148 @@ static __poll_t virtio_media_device_poll(struct file *file, poll_table *wait)
return rc;
}
+static void virtio_media_vma_close_locked(struct vm_area_struct *vma)
+{
+ struct virtio_media *vv = vma->vm_private_data;
+ struct virtio_media_cmd_munmap *cmd_munmap = &vv->cmd.munmap;
+ struct virtio_media_resp_munmap *resp_munmap = &vv->resp.munmap;
+ struct scatterlist cmd_sg = {}, resp_sg = {};
+ struct scatterlist *sgs[2] = { &cmd_sg, &resp_sg };
+ int ret;
+
+ sg_set_buf(&cmd_sg, cmd_munmap, sizeof(*cmd_munmap));
+ sg_mark_end(&cmd_sg);
+
+ sg_set_buf(&resp_sg, resp_munmap, sizeof(*resp_munmap));
+ sg_mark_end(&resp_sg);
+
+ cmd_munmap->hdr.cmd = VIRTIO_MEDIA_CMD_MUNMAP;
+ cmd_munmap->hdr.__reserved = 0;
+ cmd_munmap->driver_addr =
+ (vma->vm_pgoff << PAGE_SHIFT) - vv->mmap_region.addr;
+ ret = virtio_media_send_command(vv, sgs, 1, 1, sizeof(*resp_munmap),
+ NULL);
+ if (ret < 0) {
+ v4l2_err(&vv->v4l2_dev, "host failed to unmap buffer: %d\n",
+ ret);
+ }
+}
+
+/**
+ * virtio_media_vma_close() - Close a MMAP buffer mapping.
+ * @vma: VMA of the mapping to close.
+ *
+ * Inform the host that a previously created MMAP mapping is no longer needed
+ * and can be removed.
+ */
+static void virtio_media_vma_close(struct vm_area_struct *vma)
+{
+ struct virtio_media *vv = vma->vm_private_data;
+
+ mutex_lock(&vv->vlock);
+ virtio_media_vma_close_locked(vma);
+ mutex_unlock(&vv->vlock);
+}
+
+static const struct vm_operations_struct virtio_media_vm_ops = {
+ .close = virtio_media_vma_close,
+};
+
+/**
+ * virtio_media_device_mmap() - Perform a mmap request from userspace.
+ * @file: opened file of the session to map for.
+ * @vma: VM area struct describing the desired mapping.
+ *
+ * This requests the host to map a MMAP buffer for us, so we can then make that
+ * mapping visible into user-space address space.
+ */
+static int virtio_media_device_mmap(struct file *file,
+ struct vm_area_struct *vma)
+{
+ struct video_device *video_dev = video_devdata(file);
+ struct virtio_media *vv = to_virtio_media(video_dev);
+ struct virtio_media_session *session =
+ fh_to_session(file->private_data);
+ struct virtio_media_cmd_mmap *cmd_mmap = &session->cmd.mmap;
+ struct virtio_media_resp_mmap *resp_mmap = &session->resp.mmap;
+ struct scatterlist cmd_sg = {}, resp_sg = {};
+ struct scatterlist *sgs[2] = { &cmd_sg, &resp_sg };
+ int ret;
+
+ if (!(vma->vm_flags & VM_SHARED))
+ return -EINVAL;
+ if (!(vma->vm_flags & (VM_READ | VM_WRITE)))
+ return -EINVAL;
+
+ mutex_lock(&vv->vlock);
+
+ cmd_mmap->hdr.cmd = VIRTIO_MEDIA_CMD_MMAP;
+ cmd_mmap->hdr.__reserved = 0;
+ cmd_mmap->session_id = session->id;
+ cmd_mmap->flags =
+ (vma->vm_flags & VM_WRITE) ? VIRTIO_MEDIA_MMAP_FLAG_RW_MASK : 0;
+ cmd_mmap->offset = vma->vm_pgoff << PAGE_SHIFT;
+ cmd_mmap->__reserved = 0;
+
+ sg_set_buf(&cmd_sg, cmd_mmap, sizeof(*cmd_mmap));
+ sg_mark_end(&cmd_sg);
+
+ sg_set_buf(&resp_sg, resp_mmap, sizeof(*resp_mmap));
+ sg_mark_end(&resp_sg);
+
+ /*
+ * The host performs reference counting and is smart enough to return
+ * the same guest physical address if this is called several times on
+ * the same
+ * buffer.
+ */
+ ret = virtio_media_send_command(vv, sgs, 1, 1, sizeof(*resp_mmap),
+ NULL);
+ if (ret < 0)
+ goto end;
+
+ vma->vm_private_data = vv;
+ /*
+ * Keep the guest address at which the buffer is mapped since we will
+ * use that to unmap.
+ */
+ vma->vm_pgoff = (resp_mmap->driver_addr + vv->mmap_region.addr) >>
+ PAGE_SHIFT;
+
+ /*
+ * We cannot let the mapping be larger than the buffer.
+ */
+ if (vma->vm_end - vma->vm_start > PAGE_ALIGN(resp_mmap->len)) {
+ dev_dbg(&video_dev->dev,
+ "invalid MMAP, as it would overflow buffer length\n");
+ ret = -EINVAL;
+ goto cleanup;
+ }
+
+ ret = io_remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff,
+ vma->vm_end - vma->vm_start,
+ vma->vm_page_prot);
+ if (ret) {
+ dev_dbg(&video_dev->dev, "io_remap_pfn_range() failed\n");
+ goto cleanup;
+ }
+
+ vma->vm_ops = &virtio_media_vm_ops;
+ goto end;
+cleanup:
+ virtio_media_vma_close_locked(vma);
+end:
+ mutex_unlock(&vv->vlock);
+ return ret;
+}
+
static const struct v4l2_file_operations virtio_media_fops = {
.owner = THIS_MODULE,
.open = virtio_media_device_open,
.release = virtio_media_device_close,
.poll = virtio_media_device_poll,
.unlocked_ioctl = virtio_media_device_ioctl,
+ .mmap = virtio_media_device_mmap,
};
static void virtio_media_release(struct v4l2_device *v4l2_dev)
@@ -763,6 +907,14 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
vv->eventq = vqs[1];
INIT_WORK(&vv->eventq_work, virtio_media_event_work);
+ /* Get MMAP buffer mapping SHM region */
+ if (!virtio_get_shm_region(virtio_dev, &vv->mmap_region,
+ VIRTIO_MEDIA_SHM_MMAP)) {
+ dev_err(&virtio_dev->dev, "Failed to get shared memory region");
+ ret = -ENOMEM;
+ goto err_del_vqs;
+ }
+
vd = &vv->video_dev;
vd->v4l2_dev = &vv->v4l2_dev;
vd->ioctl_ops = &virtio_media_ioctl_ops;
diff --git a/include/uapi/linux/virtio_media.h b/include/uapi/linux/virtio_media.h
index da5c5baf0..2ea28770e 100644
--- a/include/uapi/linux/virtio_media.h
+++ b/include/uapi/linux/virtio_media.h
@@ -171,6 +171,73 @@ struct virtio_media_sg_entry {
__le32 __reserved;
};
+/**
+ * VIRTIO_MEDIA_MMAP_FLAG_RW - Bit position of the VIRTIO_MEDIA_MMAP_FLAG_RW
+ * flag.
+ */
+#define VIRTIO_MEDIA_MMAP_FLAG_RW 0
+
+/**
+ * VIRTIO_MEDIA_CMD_MMAP - Command for mapping a MMAP buffer into the driver's
+ * address space.
+ */
+#define VIRTIO_MEDIA_CMD_MMAP 4
+
+/**
+ * struct virtio_media_cmd_mmap - Driver command for VIRTIO_MEDIA_CMD_MMAP.
+ * @hdr: header with cmd member set to VIRTIO_MEDIA_CMD_MMAP.
+ * @session_id: ID of the session we are mapping for.
+ * @flags: combination of VIRTIO_MEDIA_MMAP_FLAG_*.
+ * @offset: mem_offset field of the plane to map, as returned by
+ * VIDIOC_QUERYBUF.
+ * @__reserved: must be set to zero by the driver.
+ */
+struct virtio_media_cmd_mmap {
+ struct virtio_media_cmd_header hdr;
+ __le32 session_id;
+ __le32 flags;
+ __le32 offset;
+ __le32 __reserved;
+};
+
+/**
+ * struct virtio_media_resp_mmap - Device response for VIRTIO_MEDIA_CMD_MMAP.
+ * @hdr: header containing the status of the command.
+ * @driver_addr: offset into SHM region 0 of the start of the mapping.
+ * @len: length of the mapping.
+ */
+struct virtio_media_resp_mmap {
+ struct virtio_media_resp_header hdr;
+ __le64 driver_addr;
+ __le64 len;
+};
+
+/**
+ * VIRTIO_MEDIA_CMD_MUNMAP - Unmap a MMAP buffer previously mapped using
+ * VIRTIO_MEDIA_CMD_MMAP.
+ */
+#define VIRTIO_MEDIA_CMD_MUNMAP 5
+
+/**
+ * struct virtio_media_cmd_munmap - Driver command for VIRTIO_MEDIA_CMD_MUNMAP.
+ * @hdr: header with cmd member set to VIRTIO_MEDIA_CMD_MUNMAP.
+ * @driver_addr: offset into SHM region 0 at which the buffer has been
+ * previously mapped.
+ */
+struct virtio_media_cmd_munmap {
+ struct virtio_media_cmd_header hdr;
+ __le64 driver_addr;
+};
+
+/**
+ * struct virtio_media_resp_munmap - Device response for
+ * VIRTIO_MEDIA_CMD_MUNMAP.
+ * @hdr: header containing the status of the command.
+ */
+struct virtio_media_resp_munmap {
+ struct virtio_media_resp_header hdr;
+};
+
/* The values for these events are set by the virtio-media specification. */
#define VIRTIO_MEDIA_EVT_ERROR 0
#define VIRTIO_MEDIA_EVT_DQBUF 1
--
2.56.0.317.g454f8302e1-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v10 1/5] media: virtio: Add core driver and session management
2026-10-29 15:30 ` [PATCH v10 1/5] media: virtio: Add core driver and session management Brian Daniels
@ 2026-10-29 15:49 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-29 15:49 UTC (permalink / raw)
To: Brian Daniels; +Cc: virtualization, media-ci, Michael S. Tsirkin, Eugenio Perez
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [Critical] media: virtio: UAF, NULL deref, and deadlock in virtio_media_remove()
- [Critical] media: virtio: stack UAF and vq race in commandq_callback()
- [Critical] media: virtio: dangling stack cb_param on virtqueue_kick() failure
- [Critical] media: virtio: unvalidated status negation in virtio_media_kick_command()
- [High] media: virtio: missing reset and caps check in virtio_media_probe()
- [Low] media: virtio: missing le32 conversions on virtio_media protocol fields
--
commit a1ad1972645a51f0e49e27a507fa954d0bc8dd41
Author: Alexandre Courbot <gnurou@gmail.com>
media: virtio: Add core driver and session management
This commit adds the initial virtio-media driver core, build system
integration, and V4L2 session lifecycle management via VIRTIO_MEDIA_CMD_OPEN
and VIRTIO_MEDIA_CMD_CLOSE over the command virtqueue.
> diff --git a/drivers/media/virtio/virtio_media_driver.c b/drivers/media/virtio/virtio_media_driver.c
> new file mode 100644
> index 0000000000000..10073d863b574
> --- /dev/null
> +++ b/drivers/media/virtio/virtio_media_driver.c
> @@ -0,0 +1,503 @@
[ ... ]
> +static int virtio_media_session_close(struct virtio_media *vv,
> + struct virtio_media_session *session)
> +{
> + struct virtio_media_cmd_close *cmd_close = &session->cmd.close;
> + struct scatterlist cmd_sg = {};
> + struct scatterlist *sgs[1] = { &cmd_sg };
> + int ret;
> +
> + mutex_lock(&vv->vlock);
> +
> + cmd_close->hdr.cmd = VIRTIO_MEDIA_CMD_CLOSE;
> + cmd_close->hdr.__reserved = 0;
> + cmd_close->session_id = session->id;
> + cmd_close->__reserved = 0;
[Severity: Low]
In virtio_media_session_close(), virtio_media_device_open(), and
virtio_media_kick_command(), the protocol fields hdr.cmd, hdr.status, and
session_id are declared as __le32 in include/uapi/linux/virtio_media.h, but
they are read and written directly as host integers.
Should these accesses use cpu_to_le32() and le32_to_cpu() to avoid sparse
endianness warnings when building with C=1?
[ ... ]
> +struct virtio_media_cmd_callback_param {
> + struct virtio_media *vv;
> + bool done;
> + size_t resp_len;
> +};
> +
> +/**
> + * commandq_callback() - Callback for the command queue.
> + * @queue: command virtqueue.
> + *
> + * This just wakes up the thread that was waiting on the command to complete.
> + */
> +static void commandq_callback(struct virtqueue *queue)
> +{
> + unsigned int len;
> + struct virtio_media_cmd_callback_param *param;
> +
> +process_bufs:
> + while ((param = virtqueue_get_buf(queue, &len))) {
> + param->resp_len = len;
> + param->done = true;
> + wake_up(¶m->vv->wq);
> + }
> +
> + if (!virtqueue_enable_cb(queue)) {
> + virtqueue_disable_cb(queue);
> + goto process_bufs;
> + }
> +}
> +
> +/**
> + * virtio_media_kick_command() - send a command to the commandq.
> + * @vv: virtio-media device in use.
> + * @sgs: descriptor chain to send.
> + * @out_sgs: number of device-readable descriptors in @sgs.
> + * @in_sgs: number of device-writable descriptors in @sgs.
> + * @resp_len: output parameter. Upon success, contains the size of the response
> + * in bytes.
> + */
> +static int virtio_media_kick_command(struct virtio_media *vv,
> + struct scatterlist **sgs,
> + const size_t out_sgs, const size_t in_sgs,
> + size_t *resp_len)
> +{
> + struct virtio_media_cmd_callback_param cb_param = {
> + .vv = vv,
> + .done = false,
> + .resp_len = 0,
> + };
> + struct virtio_media_resp_header *resp_header;
> + int ret = virtqueue_add_sgs(vv->commandq, sgs, out_sgs, in_sgs,
> + &cb_param, GFP_ATOMIC);
> + if (ret) {
> + v4l2_err(&vv->v4l2_dev,
> + "failed to add sgs to command virtqueue\n");
> + return ret;
> + }
> +
> + if (!virtqueue_kick(vv->commandq)) {
> + v4l2_err(&vv->v4l2_dev, "failed to kick command virtqueue\n");
> + return -EINVAL;
> + }
[Severity: Critical]
If virtqueue_kick() returns false here in virtio_media_kick_command(), the
stack-allocated cb_param token and the DMA-mapped caller scatterlists have
already been added to vv->commandq by virtqueue_add_sgs().
When virtio_media_kick_command() returns -EINVAL without detaching the unused
buffer or resetting the device, cb_param goes out of scope while still queued
in vv->commandq, and in the close path:
virtio_media_device_close()
virtio_media_session_close() -> fails with -EINVAL
virtio_media_session_remove()
virtio_media_session_free() -> kfree(session)
session->cmd.close is freed while still queued in the virtqueue. Could a
subsequent completion in commandq_callback() dereference the dangling stack
pointer param and access freed session memory?
> +
> + /* Wait for the response. */
> + wait_event(vv->wq, cb_param.done);
[Severity: Critical]
Can commandq_callback() race with virtio_media_kick_command() and trigger a
stack use-after-return or vring state corruption?
In virtio_media_kick_command(), cb_param lives on the stack, and wait_event()
checks cb_param.done locklessly in its fast path before taking the waitqueue
lock. In commandq_callback():
param->resp_len = len;
param->done = true;
wake_up(¶m->vv->wq);
As soon as param->done = true becomes visible on another CPU,
virtio_media_kick_command() can finish wait_event() and return from the
function before commandq_callback() evaluates ¶m->vv->wq on the next
line, dereferencing the popped stack frame (for example, queue->vdev->priv
could be used to reach vv instead of param->vv, or a struct completion could
be used). In addition, param->resp_len and param->done are written and read
without memory barriers (such as smp_store_release() and smp_load_acquire())
when wait_event() takes its lockless fast path.
Furthermore, callers serialize access to vv->commandq only with the mutex
vv->vlock, whereas commandq_callback() runs in interrupt context without a
spinlock. Once a woken caller returns, releases vv->vlock, and issues a new
virtqueue_add_sgs() and virtqueue_kick() (or virtio_media_remove() detaches
buffers), can that run concurrently with commandq_callback() still executing
virtqueue_get_buf() and virtqueue_enable_cb() on vv->commandq?
> +
> + if (resp_len)
> + *resp_len = cb_param.resp_len;
> +
> + if (in_sgs > 0) {
> + /*
> + * If we expect a response, make sure we have at least a
> + * response header - anything shorter is invalid.
> + */
> + if (cb_param.resp_len < sizeof(*resp_header)) {
> + v4l2_err(&vv->v4l2_dev,
> + "received response header is too short\n");
> + return -EINVAL;
> + }
> +
> + resp_header = sg_virt(sgs[out_sgs]);
> + if (resp_header->status)
> + /* Host returns a positive error code. */
> + return -resp_header->status;
[Severity: Critical]
What happens if the host writes an out-of-range value into
resp_header->status (which is a 32-bit unsigned __le32)?
If resp_header->status has bit 31 set (0x80000001..0xffffffff, such as a
negative errno cast to u32), negating it yields a positive int when returned
from virtio_media_kick_command(). Because virtio_media_send_command() only
checks:
if (ret < 0)
return ret;
it treats positive return values as success and returns 0, causing
virtio_media_device_open() to succeed using an uninitialized or stale
resp_open->session_id.
Conversely, if resp_header->status is in (MAX_ERRNO, 0x80000000],
virtio_media_kick_command() returns a negative value smaller than -MAX_ERRNO.
When propagated from virtio_media_device_open() through v4l2_open() and
do_dentry_open() to path_openat(), ERR_PTR(ret) will not satisfy IS_ERR(),
which can cause VFS open to install an invalid pointer into the process file
descriptor table. Should status values outside 1..MAX_ERRNO be mapped to a
fallback error such as -EINVAL or -EIO?
[ ... ]
> +static int virtio_media_probe(struct virtio_device *virtio_dev)
> +{
[ ... ]
> + virtio_cread_le(virtio_dev, struct virtio_media_config, device_caps,
> + &vd->device_caps);
> + if (vd->device_caps & (V4L2_CAP_VIDEO_M2M | V4L2_CAP_VIDEO_M2M_MPLANE))
> + vd->vfl_dir = VFL_DIR_M2M;
> + else if (vd->device_caps &
> + (V4L2_CAP_VIDEO_OUTPUT | V4L2_CAP_VIDEO_OUTPUT_MPLANE))
> + vd->vfl_dir = VFL_DIR_TX;
> + else
> + vd->vfl_dir = VFL_DIR_RX;
> +
> + virtio_device_ready(virtio_dev);
> +
> + __u32 device_type;
> +
> + virtio_cread_le(virtio_dev, struct virtio_media_config, device_type,
> + &device_type);
> + if (device_type >= VFL_TYPE_MAX) {
> + dev_err(&virtio_dev->dev, "Invalid device_type: %u",
> + device_type);
> + ret = -EINVAL;
> + goto err_del_vqs;
> + }
> +
> + ret = video_register_device(vd, device_type, -1);
> + if (ret)
> + goto err_del_vqs;
> +
> + return 0;
> +
> +err_del_vqs:
> + virtio_dev->config->del_vqs(virtio_dev);
> +err_v4l2_device_unregister:
> + v4l2_device_unregister(&vv->v4l2_dev);
> +err_free_virtio_media:
> + kfree(vv);
> + return ret;
> +}
[Severity: High]
In virtio_media_probe(), virtio_device_ready(virtio_dev) is called before
validating device_type and before video_register_device(). If either check
fails and jumps to err_del_vqs, virtio_dev->config->del_vqs(virtio_dev) and
kfree(vv) are called without calling virtio_reset_device(virtio_dev) first,
leaving VIRTIO_CONFIG_S_DRIVER_OK set while the virtqueues and driver state
are freed.
In addition, vd->device_caps is read from config space without checking that
it is non-zero; if device_type != VFL_TYPE_SUBDEV and vd->device_caps == 0,
__video_register_device() triggers a WARN_ON(!vdev->device_caps) and fails
into err_del_vqs.
Should vd->device_caps and device_type be validated before
virtio_device_ready(), and should err_del_vqs reset the virtio device before
deleting the virtqueues?
> +
> +static void virtio_media_remove(struct virtio_device *virtio_dev)
> +{
> + struct virtio_media *vv = virtio_dev->priv;
> + struct list_head *p, *n;
> +
> + /* Remove the video device to stop accessing the commandq virtqueue */
> + video_unregister_device(&vv->video_dev);
> +
> + /*
> + * Acquire vlock to ensure no concurrent accesses to the commandq
> + * virtqueue
> + */
> + mutex_lock(&vv->vlock);
> +
> + /* Stop all virtqueue callbacks */
> + virtio_reset_device(virtio_dev);
> +
> + /*
> + * Only detach the unused buffers to allow internal virtqueue clean up.
> + * The actual buffers are freed in virtio_media_release()
> + */
> + while (virtqueue_detach_unused_buf(vv->commandq))
> + ;
> +
> + /* Delete the virtqueues */
> + virtio_dev->config->del_vqs(virtio_dev);
> +
> + mutex_unlock(&vv->vlock);
> +
> + list_for_each_safe(p, n, &vv->sessions) {
> + struct virtio_media_session *s =
> + list_entry(p, struct virtio_media_session, list);
> +
> + virtio_media_session_remove(vv, s);
> + virtio_media_session_free(s);
> + }
> +
> + v4l2_device_put(&vv->v4l2_dev);
> +}
[Severity: Critical]
What happens in virtio_media_remove() if the virtio device is unbound or
hot-unplugged while userspace still has an open file descriptor (or an
in-flight open/close)?
First, video_unregister_device() only clears V4L2_FL_REGISTERED and unlinks
the device node; it does not close open struct file instances or wait for
in-flight file operations. In virtio_media_remove(), the loop over
vv->sessions (which also iterates without holding vv->sessions_lock) frees
every active session via virtio_media_session_free(), which calls:
virtio_media_session_fh_del(session)
v4l2_fh_del(&session->fh, session->file)
filp->private_data = NULL;
kfree(session);
and virtio_dev->config->del_vqs(virtio_dev) frees vv->commandq. When
userspace later closes its open file descriptor, v4l2_release() still invokes
virtio_media_device_close():
virtio_media_device_close(file)
session = fh_to_session(file->private_data); -> NULL (or freed session)
virtio_media_session_close(vv, session)
cmd_close->session_id = session->id; -> NULL dereference / UAF
virtio_media_send_command(vv, ...) -> uses deleted vv->commandq (UAF)
virtio_media_session_remove(vv, session);
virtio_media_session_free(session); -> double free
Similarly, a concurrent virtio_media_device_open() or
virtio_media_device_close() waiting on mutex_lock(&vv->vlock) during
virtio_media_remove() will acquire vv->vlock after del_vqs() has already
destroyed vv->commandq and attempt to submit a command on the freed
virtqueue.
Second, if a command is in flight in virtio_media_kick_command() waiting in
uninterruptible wait_event(vv->wq, cb_param.done) while holding vv->vlock and
the device stops responding during hot-unplug, won't virtio_media_remove()
deadlock forever on mutex_lock(&vv->vlock) before it can reach
virtio_reset_device()?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261029153038.3800747-1-briandaniels@google.com?part=1
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v10 2/5] media: virtio: Add V4L2 ioctl forwarding infrastructure
2026-10-29 15:30 ` [PATCH v10 2/5] media: virtio: Add V4L2 ioctl forwarding infrastructure Brian Daniels
@ 2026-10-29 16:15 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-29 16:15 UTC (permalink / raw)
To: Brian Daniels; +Cc: virtualization, media-ci, Michael S. Tsirkin, Eugenio Perez
Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [Critical] media: virtio: session and commandq UAF on device removal with open fds
- [High] media: virtio: uninitialized stack leak and wrong dir in g_input/g_output
- [High] media: virtio: scatterlist_builder_add_data() maps stack memory for DMA
- [High] media: virtio: virtio_media_device_ioctl() bypasses V4L2 std validation
- [Medium] media: virtio: missing NUL termination of cap->card in virtio_media_querycap()
- [Medium] media: virtio: KASAN tag mismatch in scatterlist_builder_retrieve_data()
- [Medium] media: virtio: unaligned DMA allocations in scatterlist_builder_add_data()
- [Low] media: virtio: undocumented module_param always_use_shadow_buffer
- [Low] media: virtio: missing cpu_to_le32() in scatterlist_builder_add_ioctl_cmd()
--
commit b04e2fc2518c3e0e5bfe29cd7359b457cc6bf7a3
Author: Alexandre Courbot <gnurou@gmail.com>
media: virtio: Add V4L2 ioctl forwarding infrastructure
This commit introduces a scatterlist builder and synchronous V4L2 ioctl
forwarding via VIRTIO_MEDIA_CMD_IOCTL. It uses this infrastructure to
implement VIDIOC_QUERYCAP and fixed-size read, write, and read-write V4L2
ioctls.
> diff --git a/drivers/media/virtio/scatterlist_builder.c b/drivers/media/virtio/scatterlist_builder.c
> new file mode 100644
> index 0000000000000..ae8ffee82fa6e
> --- /dev/null
> +++ b/drivers/media/virtio/scatterlist_builder.c
> @@ -0,0 +1,222 @@
[ ... ]
> +static bool always_use_shadow_buffer;
> +module_param(always_use_shadow_buffer, bool, 0660);
[Severity: Low]
Should the new always_use_shadow_buffer module parameter be documented in
Documentation/admin-guide/kernel-parameters.txt?
[ ... ]
> +int scatterlist_builder_add_data(struct scatterlist_builder *builder,
> + void *data, size_t len)
> +{
> + const size_t cur_desc = builder->cur_desc;
> +
> + if (len == 0)
> + return 0;
> +
> + if (builder->cur_desc >= builder->num_descs)
> + return -ENOSPC;
> +
> + if (!always_use_shadow_buffer && virt_addr_valid(data) &&
> + virt_addr_valid(data + len)) {
> + /*
> + * If "data" is in the 1:1 physical memory mapping then we can
> + * use a single SG entry and avoid copying.
> + */
> + struct page *page = virt_to_page(data);
> + size_t offset = (((size_t)data) & ~PAGE_MASK);
> + struct scatterlist *next_desc =
> + &builder->descs[builder->cur_desc];
> +
> + memset(next_desc, 0, sizeof(*next_desc));
> + sg_set_page(next_desc, page, len, offset);
> + builder->cur_desc++;
> + } else if (!always_use_shadow_buffer && is_vmalloc_addr(data)) {
[Severity: High]
Can scatterlist_builder_add_data() end up mapping kernel stack memory
directly into the virtqueue scatterlist for DMA when always_use_shadow_buffer
is false?
Most V4L2 ioctl payloads passed here reside on the kernel stack:
video_usercopy() in drivers/media/v4l2-core/v4l2-ioctl.c places any ioctl
argument with _IOC_SIZE(cmd) <= 128 into char sbuf[128] on the stack, and
virtio_media_g_input(), virtio_media_s_input(), virtio_media_g_output(),
virtio_media_s_output(), virtio_media_s_std(), and
virtio_media_device_ioctl() pass addresses of local stack variables (&input,
&output, &s, &standard, and &std_id).
When CONFIG_VMAP_STACK is disabled, stack addresses satisfy virt_addr_valid();
when CONFIG_VMAP_STACK is enabled, stack addresses satisfy is_vmalloc_addr().
Without a !object_is_on_stack(data) check, stack buffers bypass
builder->shadow_buffer and are passed to virtqueue_add_sgs(), triggering
check_for_stack() warnings in kernel/dma/debug.c and risking stack corruption
on non-cache-coherent platforms while sleeping in wait_event().
Also, shouldn't the end-of-buffer check be virt_addr_valid(data + len - 1)
rather than virt_addr_valid(data + len), and doesn't reusing descriptor index
1 in virtio_media_send_wr_ioctl() cause virtqueue_add_sgs() to DMA-map the
same scatterlist entry twice concurrently as both DMA_TO_DEVICE and
DMA_FROM_DEVICE?
[ ... ]
> + } else {
> + /*
> + * As a last resort, copy into the shadow buffer and reference
> + * it with a single SG entry. Calling
> + * scatterlist_builder_retrieve_data() will be necessary to copy
> + * the data written by the device back into @data.
> + */
> + void *shadow_buffer =
> + builder->shadow_buffer + builder->shadow_buffer_pos;
> + struct page *page = virt_to_page(shadow_buffer);
> + unsigned long offset =
> + (((unsigned long)shadow_buffer) & ~PAGE_MASK);
> + struct scatterlist *next_desc =
> + &builder->descs[builder->cur_desc];
> +
> + if (len >
> + builder->shadow_buffer_size - builder->shadow_buffer_pos)
> + return -ENOSPC;
> +
> + memcpy(shadow_buffer, data, len);
> + memset(next_desc, 0, sizeof(*next_desc));
> + sg_set_page(next_desc, page, len, offset);
> + builder->cur_desc++;
> + builder->shadow_buffer_pos += len;
> + }
[Severity: Medium]
When multiple descriptors are sub-allocated from builder->shadow_buffer (for
example when always_use_shadow_buffer is true during
virtio_media_send_r_ioctl() or virtio_media_send_wr_ioctl()), does advancing
builder->shadow_buffer_pos by the unaligned len cause adjacent
DMA_FROM_DEVICE and DMA_TO_DEVICE descriptors to share a CPU cache line?
virtio_media_send_r_ioctl() places the 16-byte virtio_media_cmd_ioctl at
offset 0, the 8-byte virtio_media_resp_ioctl at offset 16, and ioctl_data at
offset 24. When virtqueue_add_sgs() maps each scatterlist entry separately for
DMA, unaligned offsets within session->shadow_buf can trigger
CONFIG_DMA_API_DEBUG overlapping cacheline warnings and corrupt adjacent
response data during cache invalidation on non-coherent DMA platforms unless
shadow_buffer_pos is aligned to dma_get_cache_alignment() /
ARCH_DMA_MINALIGN.
[ ... ]
> +int scatterlist_builder_retrieve_data(struct scatterlist_builder *builder,
> + size_t sg_index, void *data)
> +{
> + void *shadow_buf = builder->shadow_buffer;
> + struct scatterlist *sg;
> + void *kaddr;
> +
> + /* We can only retrieve from the range of sgs currently set. */
> + if (sg_index >= builder->cur_sg)
> + return -ERANGE;
> +
> + sg = builder->sgs[sg_index];
> + kaddr = pfn_to_kaddr(page_to_pfn(sg_page(sg))) + sg->offset;
> +
> + if (kaddr >= shadow_buf &&
> + kaddr < shadow_buf + builder->shadow_buffer_size) {
> + if (kaddr + sg->length >
> + shadow_buf + builder->shadow_buffer_size)
> + return -EINVAL;
> +
> + memcpy(data, kaddr, sg->length);
> + }
> +
> + return 0;
> +}
[Severity: Medium]
On arm64 with CONFIG_KASAN_SW_TAGS or CONFIG_KASAN_HW_TAGS enabled, will the
range check in scatterlist_builder_retrieve_data() fail to match shadow_buf?
In virtio_media_session_alloc(), session->shadow_buf is allocated with
kzalloc(VIRTIO_SHADOW_BUF_SIZE, GFP_KERNEL), which returns a pointer with a
KASAN tag in bits 63:56. Here, kaddr is computed via pfn_to_kaddr(), which
produces an untagged linear address with 0xff in bits 63:56.
Comparing kaddr < shadow_buf + builder->shadow_buffer_size compares an
0xff-tagged address against a tagged pointer, which evaluates to false and
silently skips copying the response payload out of the shadow buffer. Should
kasan_reset_tag() (or page_address(sg_page(sg)) / sg_virt(sg)) be used when
comparing and copying from shadow_buf?
> +
> +/**
> + * scatterlist_builder_add_ioctl_cmd() - Add an ioctl command to the descriptor
> + * chain.
> + * @builder: builder to use.
> + * @session: session on behalf of which the ioctl command is added.
> + * @ioctl_code: code of the ioctl to add (i.e. ``VIDIOC_*``).
> + */
> +int scatterlist_builder_add_ioctl_cmd(struct scatterlist_builder *builder,
> + struct virtio_media_session *session,
> + u32 ioctl_code)
> +{
> + struct virtio_media_cmd_ioctl *cmd_ioctl = &session->cmd.ioctl;
> +
> + cmd_ioctl->hdr.cmd = VIRTIO_MEDIA_CMD_IOCTL;
> + cmd_ioctl->hdr.__reserved = 0;
> + cmd_ioctl->session_id = session->id;
> + cmd_ioctl->code = VIRTIO_MEDIA_IOCTL_CODE(ioctl_code);
[Severity: Low]
Since hdr.cmd, session_id, and code in struct virtio_media_cmd_ioctl are
declared as __le32 in include/uapi/linux/virtio_media.h, should these
assignments use cpu_to_le32() to avoid Sparse -Wbitwise warnings?
[ ... ]
> diff --git a/drivers/media/virtio/virtio_media_ioctls.c b/drivers/media/virtio/virtio_media_ioctls.c
> new file mode 100644
> index 0000000000000..656ecea8a44cc
> --- /dev/null
> +++ b/drivers/media/virtio/virtio_media_ioctls.c
> @@ -0,0 +1,606 @@
[ ... ]
> +static int virtio_media_querycap(struct file *file, void *fh,
> + struct v4l2_capability *cap)
> +{
> + struct video_device *video_dev = video_devdata(file);
> + struct virtio_media *vv = to_virtio_media(video_dev);
> +
> + strscpy(cap->bus_info, "platform:virtio-media");
> + strscpy(cap->driver, VIRTIO_MEDIA_DEFAULT_DRIVER_NAME);
> +
> + virtio_cread_bytes(vv->virtio_dev, 8, cap->card, sizeof(cap->card));
[Severity: Medium]
Does virtio_media_querycap() need to ensure cap->card is NUL-terminated after
virtio_cread_bytes()?
In include/uapi/linux/virtio_media.h, the comment for
struct virtio_media_config states that if all 32 characters of card are used
by the device, it does not need to be NUL-terminated:
* @card: name of the device, a NUL-terminated UTF-8 string. It corresponds
* with the card field of the struct v4l2_capability. If all the
* characters of the field are used, it does not need to be
* NUL-terminated.
However, V4L2 requires struct v4l2_capability.card to always be NUL-terminated
when returned to userspace by VIDIOC_QUERYCAP.
> +
> + cap->capabilities = video_dev->device_caps | V4L2_CAP_DEVICE_CAPS;
> + cap->device_caps = video_dev->device_caps;
> +
> + return 0;
> +}
> +
> +/*
> + * s/g_input/output work with an unsigned int - recast this to a u32 so the
> + * size is unambiguous.
> + */
> +
> +static int virtio_media_g_input(struct file *file, void *fh, unsigned int *i)
> +{
> + u32 input;
> +
> + int ret = virtio_media_send_wr_ioctl(file_to_v4l2_fh(file),
> + VIDIOC_G_INPUT, &input,
> + sizeof(input), sizeof(input));
> + if (ret)
> + return ret;
> +
> + *i = input;
> +
> + return 0;
> +}
> +
> +static int virtio_media_s_input(struct file *file, void *fh, unsigned int i)
> +{
> + u32 input = i;
> +
> + return virtio_media_send_wr_ioctl(file_to_v4l2_fh(file),
> + VIDIOC_S_INPUT, &input,
> + sizeof(input), sizeof(input));
> +}
> +
> +static int virtio_media_g_output(struct file *file, void *fh, unsigned int *o)
> +{
> + u32 output;
> +
> + int ret = virtio_media_send_wr_ioctl(file_to_v4l2_fh(file),
> + VIDIOC_G_OUTPUT, &output,
> + sizeof(output), sizeof(output));
[Severity: High]
Should virtio_media_g_input() and virtio_media_g_output() call
virtio_media_send_r_ioctl() instead of virtio_media_send_wr_ioctl()?
Both VIDIOC_G_INPUT and VIDIOC_G_OUTPUT are defined with _IOR in
include/uapi/linux/videodev2.h. Per the VIRTIO_MEDIA_CMD_IOCTL documentation
in include/uapi/linux/virtio_media.h, _IOR ioctls should only be followed by a
device-writable response descriptor.
Calling virtio_media_send_wr_ioctl() attaches a device-readable command
payload descriptor for an _IOR ioctl and sends 4 bytes of uninitialized stack
memory from input and output to the host (or copies uninitialized bytes into
session->shadow_buf via memcpy()).
[ ... ]
> +long virtio_media_device_ioctl(struct file *file, unsigned int cmd,
> + unsigned long arg)
> +{
> + struct video_device *video_dev = video_devdata(file);
> + struct virtio_media *vv = to_virtio_media(video_dev);
> + struct v4l2_fh *vfh = NULL;
> + struct v4l2_standard standard;
> + v4l2_std_id std_id = 0;
> + int ret;
> +
> + if (test_bit(V4L2_FL_USES_V4L2_FH, &video_dev->flags))
> + vfh = file_to_v4l2_fh(file);
> +
> + mutex_lock(&vv->vlock);
[Severity: Critical]
What prevents a use-after-free of the session and vv->commandq if
virtio_media_remove() runs while a file descriptor is open or an ioctl is
waiting on vv->vlock?
In drivers/media/virtio/virtio_media_driver.c, virtio_media_remove() does:
video_unregister_device(&vv->video_dev);
mutex_lock(&vv->vlock);
virtio_reset_device(virtio_dev);
while (virtqueue_detach_unused_buf(vv->commandq))
;
virtio_dev->config->del_vqs(virtio_dev);
mutex_unlock(&vv->vlock);
list_for_each_safe(p, n, &vv->sessions) {
struct virtio_media_session *s =
list_entry(p, struct virtio_media_session, list);
virtio_media_session_remove(vv, s);
virtio_media_session_free(s);
}
If a thread passes v4l2_ioctl()'s unlocked video_is_registered() check and
waits on mutex_lock(&vv->vlock) in virtio_media_device_ioctl() while
virtio_media_remove() runs, virtio_media_remove() deletes vv->commandq and
frees all sessions (including s->command_sgs and s->shadow_buf). Once
virtio_media_device_ioctl() acquires vv->vlock, it proceeds without checking
video_is_registered(video_dev), dereferencing the freed vfh / session and
submitting descriptors to the deleted vv->commandq.
Furthermore, when userspace later closes the open file descriptor,
v4l2_release() calls virtio_media_device_close(), which looks up the already
freed session from file->private_data, accesses vv->commandq in
virtio_media_session_close(), and calls virtio_media_session_remove() and
virtio_media_session_free() a second time.
> +
> + /*
> + * We need to handle a few ioctls manually because their results
> + * rely on vfd->tvnorms, which is normally updated by the driver
> + * as S_INPUT is called. Since we want to just pass these ioctls
> + * through, we have to hijack them from here.
> + */
> + switch (cmd) {
> + case VIDIOC_S_STD:
> + ret = copy_from_user(&std_id, (void __user *)arg,
> + sizeof(std_id));
> + if (ret) {
> + ret = -EFAULT;
> + break;
> + }
> + ret = virtio_media_s_std(file, vfh, std_id);
> + break;
> + case VIDIOC_ENUMSTD:
> + ret = copy_from_user(&standard, (void __user *)arg,
> + sizeof(standard));
> + if (ret) {
> + ret = -EFAULT;
> + break;
> + }
> + ret = virtio_media_enumstd(file, vfh, &standard);
> + if (ret)
> + break;
> + ret = copy_to_user((void __user *)arg, &standard,
> + sizeof(standard));
> + if (ret)
> + ret = -EFAULT;
> + break;
> + case VIDIOC_QUERYSTD:
> + ret = virtio_media_querystd(file, vfh, &std_id);
> + if (ret)
> + break;
> + ret = copy_to_user((void __user *)arg, &std_id, sizeof(std_id));
> + if (ret)
> + ret = -EFAULT;
> + break;
> + default:
> + ret = video_ioctl2(file, cmd, arg);
> + break;
> + }
[Severity: High]
By intercepting VIDIOC_S_STD, VIDIOC_ENUMSTD, and VIDIOC_QUERYSTD before
video_ioctl2(), doesn't virtio_media_device_ioctl() bypass several V4L2 core
checks and 32-bit compat handling?
Specifically:
1. __video_do_ioctl() checks test_bit(_IOC_NR(cmd), video_dev->valid_ioctls)
and calls v4l2_prio_check(video_dev->prio, vfh->prio) for VIDIOC_S_STD
(which is marked INFO_FL_PRIO). Intercepting them here skips both device
validity and priority checks.
2. For VIDIOC_ENUMSTD, v4l2_ioctls[] uses INFO_FL_CLEAR(v4l2_standard, index)
so video_get_user() zeroes all output fields after index, whereas
copy_from_user() here forwards unzeroed trailing fields from userspace to
the host.
3. Under CONFIG_COMPAT on x86_64, 32-bit userspace issues VIDIOC_ENUMSTD32
because struct v4l2_standard32 differs in size and alignment.
v4l2_compat_ioctl32() passes VIDIOC_ENUMSTD32 to fops->unlocked_ioctl;
because the switch statement only matches native VIDIOC_ENUMSTD,
VIDIOC_ENUMSTD32 falls through to video_ioctl2() -> v4l_enumstd(), where
video_dev->tvnorms is 0, failing with -EINVAL.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261029153038.3800747-1-briandaniels@google.com?part=2
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v10 3/5] media: virtio: Add extended controls support
2026-10-29 15:30 ` [PATCH v10 3/5] media: virtio: Add extended controls support Brian Daniels
@ 2026-10-29 16:37 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-29 16:37 UTC (permalink / raw)
To: Brian Daniels; +Cc: virtualization, media-ci, Michael S. Tsirkin, Eugenio Perez
Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [Critical] media: virtio: premature page unpinning in __scatterlist_builder_add_userptr()
- [High] media: virtio: framevec and page pin leak on ext_ctrls ioctl error paths
- [High] media: virtio: unvalidated ctrls->count after shadow buffer retrieval
- [High] media: virtio: sg vs desc index mismatch in ext_controls response chain
- [High] media: virtio: uninitialized stack ctrl.size used on VIDIOC_G/S_CTRL path
- [Medium] media: virtio: invalid gfp_mask 0 in __scatterlist_builder_add_userptr()
- [Medium] media: virtio: __scatterlist_builder_add_userptr() turns -EFAULT into -EINVAL
- [Medium] media: virtio: missing set_page_dirty_lock() before vb2_destroy_framevec()
--
commit d6379be31216df27a0facbc6a952d125cbfef732
Author: Alexandre Courbot <gnurou@gmail.com>
media: virtio: Add extended controls support
This commit adds support for VIDIOC_G_EXT_CTRLS, VIDIOC_S_EXT_CTRLS, and
VIDIOC_TRY_EXT_CTRLS to the virtio-media driver. It pins userspace control
payloads using vb2_create_framevec() and passes their physical memory segments
to the host as an array of struct virtio_media_sg_entry descriptors in the
session shadow buffer.
> diff --git a/drivers/media/virtio/scatterlist_builder.c b/drivers/media/virtio/scatterlist_builder.c
> index ae8ffee82fa6e..3615f45e15cfb 100644
> --- a/drivers/media/virtio/scatterlist_builder.c
> +++ b/drivers/media/virtio/scatterlist_builder.c
> @@ -220,3 +220,247 @@ int scatterlist_builder_add_ioctl_resp(struct scatterlist_builder *builder,
> return scatterlist_builder_add_data(builder, resp_ioctl,
> sizeof(*resp_ioctl));
> }
> +
> +/**
> + * __scatterlist_builder_add_userptr() - Add user pages to @builder.
> + * @builder: builder to use.
> + * @userptr: pointer to userspace memory that we want to add.
> + * @length: length of the data to add.
> + * @sg_list: output parameter. Upon success, points to the area of the shadow
> + * buffer containing the array of SG entries to be added to the
> + * descriptor chain.
> + * @nents: output parameter. Upon success, contains the number of entries
> + * pointed to by @sg_list.
> + *
> + * Data referenced by userspace pointers can be potentially large and very
> + * scattered, which could overwhelm the descriptor chain if added as-is. For
> + * these, we instead build an array of &struct virtio_media_sg_entry in the
> + * shadow buffer and reference it using a single descriptor.
> + *
> + * This function is a helper to perform that. Callers should then add the
> + * descriptor to the chain properly.
> + *
> + * Returns %-EFAULT if @userptr is not a valid user address, which is a case the
> + * driver should consider as "normal" operation. All other failures signal a
> + * problem with the driver.
> + */
> +static int
> +__scatterlist_builder_add_userptr(struct scatterlist_builder *builder,
> + unsigned long userptr, unsigned long length,
> + struct virtio_media_sg_entry **sg_list,
> + int *nents)
> +{
> + struct sg_table sg_table = {};
> + struct frame_vector *framevec;
> + struct scatterlist *sg_iter;
> + struct page **pages;
> + const unsigned int offset = userptr & ~PAGE_MASK;
> + unsigned int pages_count;
> + size_t entries_size;
> + int i;
> + int ret;
> +
> + if (builder->framevec) {
> + pr_warn("previous framevec found!");
> + vb2_destroy_framevec(builder->framevec);
> + builder->framevec = NULL;
> + }
[Severity: Critical]
When an extended controls ioctl contains two or more controls with
ctrl->size > 0, doesn't this unpin the earlier control's user pages before the
virtio command is even sent to the host?
In struct scatterlist_builder, there is only a single framevec pointer, while
scatterlist_builder_add_ext_ctrls_userptrs() loops over all ctrls->count
entries and calls scatterlist_builder_add_userptr() ->
__scatterlist_builder_add_userptr() for each control with ctrl->size > 0:
virtio_media_send_ext_controls_ioctl()
scatterlist_builder_add_ext_ctrls_userptrs()
for (i = 0; i < ctrls->count; i++)
scatterlist_builder_add_userptr()
__scatterlist_builder_add_userptr()
When the second control with ctrl->size > 0 is processed, builder->framevec is
already set from the first control. Calling vb2_destroy_framevec() here unpins
the first control's pages while their physical addresses in builder->sgs are
still queued and subsequently passed to virtio_media_send_command(), allowing
the host device to DMA to or from unpinned physical pages that may be freed and
reallocated.
> +
> + framevec = vb2_create_framevec(userptr, length, true);
> + if (IS_ERR(framevec)) {
> + if (PTR_ERR(framevec) != -EFAULT) {
> + pr_warn("error %ld creating frame vector for userptr 0x%lx, length 0x%lx\n",
> + PTR_ERR(framevec), userptr, length);
> + } else {
> + /* -EINVAL is expected in case of invalid userptr. */
> + framevec = ERR_PTR(-EINVAL);
> + }
> + return PTR_ERR(framevec);
> + }
[Severity: Medium]
Should __scatterlist_builder_add_userptr() preserve -EFAULT here instead of
converting it to -EINVAL?
The kernel-doc comment above states that __scatterlist_builder_add_userptr()
returns -EFAULT if userptr is not a valid user address, and V4L2 extended
control ioctls (VIDIOC_G_EXT_CTRLS, VIDIOC_S_EXT_CTRLS, VIDIOC_TRY_EXT_CTRLS)
are expected to return -EFAULT when a control's user payload pointer is
invalid.
> +
> + pages = frame_vector_pages(framevec);
> + if (IS_ERR(pages)) {
> + pr_warn("error getting vector pages\n");
> + ret = PTR_ERR(pages);
> + goto free_framevec;
> + }
> + pages_count = frame_vector_count(framevec);
> + ret = sg_alloc_table_from_pages(&sg_table, pages, pages_count, offset,
> + length, 0);
[Severity: Medium]
Should the gfp_mask argument to sg_alloc_table_from_pages() be GFP_KERNEL
rather than 0?
__scatterlist_builder_add_userptr() runs in sleepable ioctl process context
right after vb2_create_framevec() (which allocates with GFP_KERNEL). Passing 0
clears __GFP_DIRECT_RECLAIM, __GFP_KSWAPD_RECLAIM, __GFP_IO, and __GFP_FS,
preventing memory reclaim under pressure and skipping SLUB kmemleak
registration.
[ ... ]
> +static int scatterlist_builder_add_userptr(struct scatterlist_builder *builder,
> + unsigned long userptr,
> + unsigned long length)
> +{
> + int ret;
> + int nents;
> + struct virtio_media_sg_entry *sg_list;
> +
> + ret = __scatterlist_builder_add_userptr(builder, userptr, length,
> + &sg_list, &nents);
> + if (ret)
> + return ret;
> +
> + ret = scatterlist_builder_add_data(builder, sg_list,
> + sizeof(*sg_list) * nents);
> + if (ret)
> + return ret;
> +
> + return 0;
> +}
[ ... ]
> +int scatterlist_builder_retrieve_ext_ctrls(struct scatterlist_builder *builder,
> + size_t sg_index,
> + struct v4l2_ext_controls *ctrls)
> +{
> + struct v4l2_ext_control *controls_backup = ctrls->controls;
> + int ret;
> +
> + ret = scatterlist_builder_retrieve_data(builder, sg_index++, ctrls);
> +
> + if (ret)
> + goto cleanup;
> +
> + ctrls->controls = controls_backup;
> +
> + if (ctrls->count > 0 && ctrls->controls) {
> + ret = scatterlist_builder_retrieve_data(builder, sg_index++,
> + ctrls->controls);
> + if (ret)
> + goto cleanup;
> + }
> +
> +cleanup:
> + // Clean up the framevec created in __scatterlist_builder_add_userptr()
> + if (builder->framevec) {
> + vb2_destroy_framevec(builder->framevec);
> + builder->framevec = NULL;
> + }
> +
> + return ret;
> +}
[Severity: Medium]
Should the pages in builder->framevec be marked dirty with
set_page_dirty_lock() before calling vb2_destroy_framevec()?
__scatterlist_builder_add_userptr() pins the user pages with write = true via
vb2_create_framevec(userptr, length, true) and passes their physical addresses
to the host device. Because host writes to those physical addresses bypass the
guest CPU MMU, the guest PTE dirty bits are not updated, so unpinning the pages
without calling set_page_dirty_lock() (as done in vb2_dma_sg_put_userptr() and
vb2_vmalloc_put_userptr()) can allow modified pages to be treated as clean and
discarded under memory pressure.
> diff --git a/drivers/media/virtio/virtio_media_ioctls.c b/drivers/media/virtio/virtio_media_ioctls.c
> index 656ecea8a44cc..51f46b334800c 100644
> --- a/drivers/media/virtio/virtio_media_ioctls.c
> +++ b/drivers/media/virtio/virtio_media_ioctls.c
[ ... ]
> @@ -215,6 +218,124 @@ static int virtio_media_send_wr_ioctl(struct v4l2_fh *fh, u32 ioctl,
> return 0;
> }
>
> +/**
> + * virtio_media_send_ext_controls_ioctl() - Send an ioctl taking extended
> + * controls as parameters to the device.
> + * @fh: file handler of the session doing the ioctl.
> + * @ioctl: ``VIDIOC_*`` ioctl code.
> + * @ctrls: &struct v4l2_ext_controls to be sent as the ioctl payload.
> + *
> + * Queues an ioctl that sends a &struct v4l2_ext_controls to the host and
> + * receives an updated version.
> + *
> + * &struct v4l2_ext_controls has a pointer to an array of
> + * &struct v4l2_ext_control, and also potentially pointers to user-space memory
> + * that we need to map properly, hence the dedicated function.
> + */
> +static int virtio_media_send_ext_controls_ioctl(struct v4l2_fh *fh, u32 ioctl,
> + struct v4l2_ext_controls *ctrls)
> +{
> + struct video_device *video_dev = fh->vdev;
> + struct virtio_media *vv = to_virtio_media(video_dev);
> + struct virtio_media_session *session = fh_to_session(fh);
> + size_t num_cmd_sgs;
> + size_t end_ctrls_sg;
> + struct v4l2_ext_control *controls_backup = ctrls->controls;
> + const u32 num_ctrls = ctrls->count;
> + struct scatterlist *sgs[64];
> + struct scatterlist_builder builder = {
> + .descs = session->command_sgs.sgl,
> + .num_descs = DESC_CHAIN_MAX_LEN,
> + .cur_desc = 0,
> + .shadow_buffer = session->shadow_buf,
> + .shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
> + .shadow_buffer_pos = 0,
> + .sgs = sgs,
> + .num_sgs = ARRAY_SIZE(sgs),
> + .cur_sg = 0,
> + .framevec = NULL,
> + };
> + size_t resp_len = 0;
> + int i;
> +
> + /* Command descriptor */
> + int ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
> +
> + if (ret)
> + return ret;
> +
> + /* v4l2_controls */
> + ret = scatterlist_builder_add_ext_ctrls(&builder, ctrls);
> + if (ret)
> + return ret;
> +
> + end_ctrls_sg = builder.cur_sg;
> +
> + ret = scatterlist_builder_add_ext_ctrls_userptrs(&builder, ctrls);
> + if (ret)
> + return ret;
> +
> + num_cmd_sgs = builder.cur_sg;
> +
> + /* Response descriptor */
> + ret = scatterlist_builder_add_ioctl_resp(&builder, session);
> + if (ret)
> + return ret;
> +
> + /* Response payload (same as input but without userptrs) */
> + for (i = 1; i < end_ctrls_sg; i++) {
> + ret = scatterlist_builder_add_descriptor(&builder, i);
> + if (ret < 0)
> + return ret;
> + }
[Severity: High]
Isn't passing i (which ranges up to end_ctrls_sg = builder.cur_sg) to
scatterlist_builder_add_descriptor() indexing builder.descs[] using a
builder.sgs[] index?
In scatterlist_builder_add_descriptor():
builder->sgs[builder->cur_sg++] = &builder->descs[desc_index];
When ctrls or ctrls->controls is a vmalloc address (for instance, video_usercopy()
places ctrls in stack buffer sbuf when CONFIG_VMAP_STACK=y) and crosses a
non-contiguous page boundary, scatterlist_builder_add_data() advances
builder->cur_desc for each non-contiguous page while only adding one entry to
builder->sgs[]. If ctrls spans descs[1] and descs[2] (with sgs[1] = &descs[1])
and ctrls->controls is at descs[3] (with sgs[2] = &descs[3] and
end_ctrls_sg = 3), this loop adds &descs[1] and &descs[2] to the response sgs,
mapping the second page of ctrls twice and omitting ctrls->controls from the
writable response chain.
> +
> + ret = virtio_media_send_command(vv, builder.sgs, num_cmd_sgs,
> + builder.cur_sg - num_cmd_sgs,
> + sizeof(struct virtio_media_resp_ioctl) +
> + sizeof(*ctrls),
> + &resp_len);
> +
> + /* Just in case the host touched these. */
> + ctrls->controls = controls_backup;
> + if (ctrls->count != num_ctrls) {
> + v4l2_err(&vv->v4l2_dev,
> + "device returned a number of controls different than the one submitted\n");
> + }
> + if (ctrls->count > num_ctrls)
> + return -ENOSPC;
> + if (ret < 0) {
> + /*
> + * Even if we have received an error, we may need to read our
> + * payloadback.
> + */
> + if (resp_len >= sizeof(struct virtio_media_resp_ioctl) +
> + sizeof(*ctrls)) {
> + /*
> + * Deliberately ignore the error here as we want to
> + * return the previous one.
> + */
> + scatterlist_builder_retrieve_ext_ctrls(&builder,
> + num_cmd_sgs + 1,
> + ctrls);
> + }
> +
> + return ret;
> + }
> +
> + resp_len -= sizeof(struct virtio_media_resp_ioctl);
> +
> + /* Make sure that the reply's length covers our v4l2_ext_controls */
> + if (resp_len < sizeof(*ctrls))
> + return -EINVAL;
> +
> + ret = scatterlist_builder_retrieve_ext_ctrls(&builder, num_cmd_sgs + 1,
> + ctrls);
> + if (ret)
> + return ret;
> +
> + return 0;
> +}
[Severity: High]
Does virtio_media_send_ext_controls_ioctl() leak builder.framevec and its
FOLL_LONGTERM | FOLL_WRITE page pins when an error occurs before
scatterlist_builder_retrieve_ext_ctrls() is reached?
__scatterlist_builder_add_userptr() saves the pinned frame_vector in
builder->framevec, and it is only freed in the cleanup label of
scatterlist_builder_retrieve_ext_ctrls(). If scatterlist_builder_add_data()
fails in scatterlist_builder_add_userptr(), if
scatterlist_builder_add_ioctl_resp() or scatterlist_builder_add_descriptor()
fails, if ctrls->count > num_ctrls, if virtio_media_send_command() fails with
resp_len < sizeof(struct virtio_media_resp_ioctl) + sizeof(*ctrls), or if
resp_len < sizeof(*ctrls), the function returns without calling
vb2_destroy_framevec(builder.framevec), leaving the user pages permanently
pinned.
[Severity: High]
Also, shouldn't the ctrls->count validation happen after
scatterlist_builder_retrieve_ext_ctrls(), and restore ctrls->count = num_ctrls
when ctrls->count > num_ctrls?
First, when ctrls is staged in builder.shadow_buffer (such as when
always_use_shadow_buffer is set), ctrls->count still holds the original
num_ctrls value before scatterlist_builder_retrieve_ext_ctrls() copies the
shadow buffer back into *ctrls. Inside scatterlist_builder_retrieve_ext_ctrls(),
scatterlist_builder_retrieve_data() overwrites ctrls->count from the shadow
buffer and immediately tests:
if (ctrls->count > 0 && ctrls->controls) {
ret = scatterlist_builder_retrieve_data(builder, sg_index++,
ctrls->controls);
If num_ctrls was 0 on entry, check_array_args() in v4l2-ioctl.c left
ctrls->controls (and controls_backup) as the raw untrusted userspace pointer
and no descriptor was added for ctrls->controls, so a non-zero ctrls->count in
the shadow buffer causes scatterlist_builder_retrieve_data() to read the wrong
sg_index or copy directly to an unvalidated userspace pointer.
Second, because VIDIOC_G_EXT_CTRLS, VIDIOC_S_EXT_CTRLS, and
VIDIOC_TRY_EXT_CTRLS have INFO_FL_ALWAYS_COPY set in v4l2_ioctls[], leaving
ctrls->count > num_ctrls on return causes v4l2_compat_put_array_args() in
v4l2-compat-ioctl32.c to loop ecs64->count times over a kernel buffer
allocated for only num_ctrls elements, reading past the end of the buffer and
copying out-of-bounds heap memory to userspace.
[ ... ]
> @@ -472,6 +624,9 @@ const struct v4l2_ioctl_ops virtio_media_ioctl_ops = {
>
> /* Control handling */
> .vidioc_query_ext_ctrl = virtio_media_query_ext_ctrl,
> + .vidioc_g_ext_ctrls = virtio_media_g_ext_ctrls,
> + .vidioc_s_ext_ctrls = virtio_media_s_ext_ctrls,
> + .vidioc_try_ext_ctrls = virtio_media_try_ext_ctrls,
> .vidioc_querymenu = virtio_media_querymenu,
>
> /* Audio ioctls */
[Severity: High]
When userspace invokes VIDIOC_G_CTRL or VIDIOC_S_CTRL, doesn't
scatterlist_builder_add_ext_ctrls_userptrs() read an uninitialized ctrl->size
from the stack?
Registering .vidioc_g_ext_ctrls and .vidioc_s_ext_ctrls in
virtio_media_ioctl_ops without a v4l2_ctrl_handler or .vidioc_g_ctrl /
.vidioc_s_ctrl callbacks enables the fallback paths in v4l_g_ctrl() and
v4l_s_ctrl() in drivers/media/v4l2-core/v4l2-ioctl.c:
v4l_g_ctrl() / v4l_s_ctrl()
struct v4l2_ext_controls ctrls;
struct v4l2_ext_control ctrl;
...
ctrls.which = V4L2_CTRL_ID2WHICH(p->id);
ctrls.count = 1;
ctrls.controls = &ctrl;
ctrl.id = p->id;
ctrl.value = p->value;
ops->vidioc_g_ext_ctrls(file, NULL, &ctrls);
Neither v4l_g_ctrl() nor v4l_s_ctrl() initializes ctrl.size (or ctrls.error_idx,
ctrls.request_fd, and the upper 32 bits of the union containing ctrl.ptr).
When virtio_media_send_ext_controls_ioctl() calls
scatterlist_builder_add_ext_ctrls_userptrs(), it checks if (ctrl->size > 0) on
uninitialized stack memory, causing it to treat ctrl->ptr (containing p->value
and stack garbage) as a user pointer and pass uninitialized stack fields to
vb2_create_framevec() and the host device.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261029153038.3800747-1-briandaniels@google.com?part=3
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v10 4/5] media: virtio: Add buffer streaming, event handling, and poll
2026-10-29 15:30 ` [PATCH v10 4/5] media: virtio: Add buffer streaming, event handling, and poll Brian Daniels
@ 2026-10-29 17:00 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-29 17:00 UTC (permalink / raw)
To: Brian Daniels; +Cc: virtualization, media-ci, Michael S. Tsirkin, Eugenio Perez
Thank you for your contribution! Sashiko AI review found 15 potential issue(s) to consider:
- [Critical] media: virtio: session use-after-free in virtio_media_process_events()
- [Critical] media: virtio: session use-after-free in virtio_media_remove()
- [Critical] media: virtio: pending_dqbufs use-after-free on buffer reallocation
- [Critical] media: virtio: out-of-bounds buffer array access when host increases count
- [Critical] media: virtio: missing queues_lock synchronization on queue state and buffers
- [Critical] media: virtio: compat heap out-of-bounds read on multiplanar buffer ioctls
- [High] media: virtio: deadlock between vlock and events_lock in event processing
- [High] media: virtio: duplicate session close after VIRTIO_MEDIA_EVT_ERROR
- [High] media: virtio: pending_dqbufs list corruption on duplicate QBUF or DQBUF
- [High] media: virtio: broken EPOLLOUT, EPOLLERR, and mplane handling in poll()
- [High] media: virtio: missing streamoff wakeup and broken O_NONBLOCK in dqbuf()
- [High] media: virtio: unlocked eventq setup and missing cleanup in probe()
- [High] media: virtio: eventq_work cancelled before device reset in remove()
- [Medium] media: virtio: memory leak in virtio_media_create_bufs() when count is 0
- [Medium] media: virtio: SG index mismatch when length is 0 and uninitialized resp_len
--
commit ce9e8e91b26679cbd71baf6f6009e5e696c3100b
Author: Alexandre Courbot <gnurou@gmail.com>
media: virtio: Add buffer streaming, event handling, and poll
This commit adds V4L2 buffer queue state tracking, streaming ioctls,
asynchronous event virtqueue processing, V4L2 event subscription, and
poll support to the virtio-media driver.
> diff --git a/drivers/media/virtio/virtio_media_driver.c b/drivers/media/virtio/virtio_media_driver.c
> index 2f2e95077b556..877ac154b3221 100644
> --- a/drivers/media/virtio/virtio_media_driver.c
> +++ b/drivers/media/virtio/virtio_media_driver.c
[ ... ]
> @@ -152,6 +174,31 @@ static int virtio_media_session_close(struct virtio_media *vv,
> return 0;
> }
>
> +/**
> + * virtio_media_find_session() - Look up a session with a given ID.
> + * @vv: virtio-media device to lookup the session from.
> + * @id: ID of the session to lookup.
> + */
> +static struct virtio_media_session *
> +virtio_media_find_session(struct virtio_media *vv, u32 id)
> +{
> + struct list_head *p;
> + struct virtio_media_session *session = NULL;
> +
> + mutex_lock(&vv->sessions_lock);
> + list_for_each(p, &vv->sessions) {
> + struct virtio_media_session *s =
> + list_entry(p, struct virtio_media_session, list);
> + if (s->id == id) {
> + session = s;
> + break;
> + }
> + }
> + mutex_unlock(&vv->sessions_lock);
> +
> + return session;
> +}
[Severity: Critical]
Can this lead to a use-after-free on struct virtio_media_session in
virtio_media_process_events()?
virtio_media_find_session() drops vv->sessions_lock and returns an
unreferenced session pointer. virtio_media_process_events() then uses that
session pointer under vv->events_lock to process VIRTIO_MEDIA_EVT_ERROR,
VIRTIO_MEDIA_EVT_DQBUF, and VIRTIO_MEDIA_EVT_EVENT.
Concurrently, when the file descriptor is closed, virtio_media_device_close()
does not acquire vv->events_lock:
virtio_media_device_close()
virtio_media_session_close(vv, session)
virtio_media_session_remove(vv, session)
virtio_media_session_free(session)
If virtio_media_device_close() removes and frees the session after
virtio_media_find_session() returns it, won't virtio_media_process_events()
read and write freed session and queue->buffers memory?
[ ... ]
> +static void
> +virtio_media_process_dqbuf_event(struct virtio_media *vv,
> + struct virtio_media_session *session,
> + struct virtio_media_event_dqbuf *dqbuf_evt)
> +{
> + struct virtio_media_buffer *dqbuf;
> + const enum v4l2_buf_type queue_type = dqbuf_evt->buffer.type;
> + struct virtio_media_queue_state *queue;
> + typeof(dqbuf->buffer.m) buffer_m;
> + typeof(dqbuf->buffer.m.planes[0].m) plane_m;
> + int i;
> +
> + if (queue_type >= ARRAY_SIZE(session->queues)) {
> + v4l2_err(&vv->v4l2_dev,
> + "unmanaged queue %d passed to dqbuf event",
> + dqbuf_evt->buffer.type);
> + return;
> + }
> + queue = &session->queues[queue_type];
> +
> + if (dqbuf_evt->buffer.index >= queue->allocated_bufs) {
> + v4l2_err(&vv->v4l2_dev,
> + "invalid buffer ID %d for queue %d in dqbuf event",
> + dqbuf_evt->buffer.index, dqbuf_evt->buffer.type);
> + return;
> + }
> +
> + dqbuf = &queue->buffers[dqbuf_evt->buffer.index];
[Severity: Critical]
Is session->queues_lock needed before reading queue->allocated_bufs and
accessing queue->buffers[] here, as well as in the ioctl handlers?
virtio_media_process_dqbuf_event() runs on vv->eventq_work without holding
vv->vlock. It reads queue->allocated_bufs, indexes queue->buffers[], and
writes to dqbuf->buffer and dqbuf->planes before acquiring
session->queues_lock.
Meanwhile, virtio_media_reqbufs() and virtio_media_create_bufs() free and
replace queue->buffers and update queue->allocated_bufs without holding
session->queues_lock, and virtio_media_qbuf(), virtio_media_prepare_buf(),
virtio_media_querybuf(), virtio_media_streamon(), and
virtio_media_decoder_cmd() also modify queue state unlocked.
Could virtio_media_process_dqbuf_event() dereference queue->buffers while
virtio_media_reqbufs() or virtio_media_create_bufs() frees it via vfree(), or
race on queue->queued_bufs with virtio_media_qbuf()?
> +
> + /*
> + * Preserve the 'm' union that was passed to us during QBUF so userspace
> + * gets back the information it submitted.
> + */
> + buffer_m = dqbuf->buffer.m;
> + memcpy(&dqbuf->buffer, &dqbuf_evt->buffer, sizeof(dqbuf->buffer));
> + dqbuf->buffer.m = buffer_m;
> + if (V4L2_TYPE_IS_MULTIPLANAR(dqbuf->buffer.type)) {
> + if (dqbuf->buffer.length > VIDEO_MAX_PLANES) {
> + v4l2_err(&vv->v4l2_dev,
> + "invalid number of planes received from host for a multiplanar buffer\n");
> + return;
> + }
> + for (i = 0; i < dqbuf->buffer.length; i++) {
> + plane_m = dqbuf->planes[i].m;
> + memcpy(&dqbuf->planes[i], &dqbuf_evt->planes[i],
> + sizeof(struct v4l2_plane));
> + dqbuf->planes[i].m = plane_m;
> + }
> + }
> +
> + /* Set the DONE flag as the buffer is waiting to be dequeued. */
> + dqbuf->buffer.flags |= V4L2_BUF_FLAG_DONE;
> +
> + mutex_lock(&session->queues_lock);
> + list_add_tail(&dqbuf->list, &queue->pending_dqbufs);
> + queue->queued_bufs -= 1;
> + mutex_unlock(&session->queues_lock);
[Severity: High]
What happens if userspace calls VIDIOC_QBUF on a buffer that is already in
queue->pending_dqbufs (with V4L2_BUF_FLAG_DONE set) before dequeuing it with
VIDIOC_DQBUF, or if a duplicate VIRTIO_MEDIA_EVT_DQBUF arrives?
In virtio_media_qbuf(), buffer->buffer.flags is overwritten with
V4L2_BUF_FLAG_QUEUED without checking if the buffer is already queued or
waiting in queue->pending_dqbufs. Since the host already considers the buffer
dequeued after sending the first VIRTIO_MEDIA_EVT_DQBUF, it can accept the
VIDIOC_QBUF command and emit another VIRTIO_MEDIA_EVT_DQBUF, causing
virtio_media_process_dqbuf_event() to call list_add_tail(&dqbuf->list,
&queue->pending_dqbufs) on an already-linked list_head and corrupt
queue->pending_dqbufs.
> +
> + wake_up(&session->dqbuf_wait);
> +}
> +
> +/**
> + * virtio_media_process_events() - Process all pending events on a device.
[ ... ]
> + switch (evt->event) {
> + case VIRTIO_MEDIA_EVT_ERROR:
> + if (len < sizeof(*error_evt)) {
> + v4l2_err(&vv->v4l2_dev,
> + "error event is too short: got %u, expected %zu\n",
> + len, sizeof(*error_evt));
> + break;
> + }
> + error_evt = (struct virtio_media_event_error *)evt;
> + v4l2_err(&vv->v4l2_dev,
> + "received error %d for session %d",
> + error_evt->error, error_evt->hdr.session_id);
> +
> + mutex_lock(&session->error_lock);
> + session->error = true;
> + mutex_unlock(&session->error_lock);
> +
> + virtio_media_session_close(vv, session);
[Severity: High]
Can calling virtio_media_session_close() here deadlock on vv->vlock and
vv->events_lock?
When userspace calls VIDIOC_SUBSCRIBE_EVENT with V4L2_EVENT_SUB_FL_SEND_INITIAL,
the call chain is:
virtio_media_device_ioctl()
mutex_lock(&vv->vlock)
video_ioctl2()
...
virtio_media_subscribe_event()
virtio_media_process_events()
mutex_lock(&vv->events_lock)
virtio_media_session_close()
mutex_lock(&vv->vlock)
If a VIRTIO_MEDIA_EVT_ERROR event is processed during
virtio_media_subscribe_event(), this attempts to acquire vv->vlock while
already holding vv->vlock on the same thread, and also inverts the lock order
(vv->events_lock -> vv->vlock) relative to virtio_media_subscribe_event()
(vv->vlock -> vv->events_lock) when virtio_media_event_work() runs on the
workqueue.
[Severity: High]
Also, should VIRTIO_MEDIA_EVT_ERROR call virtio_media_session_close() if
virtio_media_device_close() will also call it when the file is later closed?
The comment on struct virtio_media_event_error in
include/uapi/linux/virtio_media.h states:
* Upon receiving this event, the session mentioned in the header is considered
* corrupted and closed.
Calling virtio_media_session_close() here and then again unconditionally in
virtio_media_device_close() sends VIRTIO_MEDIA_CMD_CLOSE twice for the same
session->id, which could close a newly opened session if the host reuses the
session ID.
[ ... ]
> @@ -358,10 +633,65 @@ static int virtio_media_device_close(struct file *file)
> return ret;
> }
>
> +/**
> + * virtio_media_device_poll() - Poll logic for a virtio-media device.
> + * @file: file of the session to poll.
> + * @wait: poll table to wait on.
> + */
> +static __poll_t virtio_media_device_poll(struct file *file, poll_table *wait)
> +{
> + struct virtio_media_session *session =
> + fh_to_session(file->private_data);
> + enum v4l2_buf_type capture_type =
> + session->uses_mplane ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE :
> + V4L2_BUF_TYPE_VIDEO_CAPTURE;
> + enum v4l2_buf_type output_type =
> + session->uses_mplane ? V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE :
> + V4L2_BUF_TYPE_VIDEO_OUTPUT;
> + struct virtio_media_queue_state *capture_queue =
> + &session->queues[capture_type];
> + struct virtio_media_queue_state *output_queue =
> + &session->queues[output_type];
> + __poll_t req_events = poll_requested_events(wait);
> + __poll_t rc = 0;
> +
> + poll_wait(file, &session->dqbuf_wait, wait);
> + poll_wait(file, &session->fh.wait, wait);
> +
> + mutex_lock(&session->error_lock);
> + if (session->error)
> + rc |= EPOLLERR;
> + mutex_unlock(&session->error_lock);
> +
> + mutex_lock(&session->queues_lock);
> + if (req_events & (EPOLLIN | EPOLLRDNORM)) {
> + if (!capture_queue->streaming ||
> + (capture_queue->queued_bufs == 0 &&
> + list_empty(&capture_queue->pending_dqbufs)))
> + rc |= EPOLLERR;
> + else if (!list_empty(&capture_queue->pending_dqbufs))
> + rc |= EPOLLIN | EPOLLRDNORM;
> + }
> + if (req_events & (EPOLLOUT | EPOLLWRNORM)) {
> + if (!output_queue->streaming)
> + rc |= EPOLLERR;
> + else if (output_queue->queued_bufs <
> + output_queue->allocated_bufs)
> + rc |= EPOLLOUT | EPOLLWRNORM;
> + }
> + mutex_unlock(&session->queues_lock);
[Severity: High]
Are the poll conditions for EPOLLOUT, EPOLLIN, and EPOLLERR here matching V4L2
semantics?
1. For EPOLLOUT | EPOLLWRNORM, checking output_queue->queued_bufs <
output_queue->allocated_bufs causes poll() to return immediately whenever any
allocated output buffer is not currently queued on the host, even when
output_queue->pending_dqbufs is empty and VIDIOC_DQBUF would return -EAGAIN
or block. Shouldn't this check !list_empty(&output_queue->pending_dqbufs)
instead?
2. When the last capture buffer (with V4L2_BUF_FLAG_LAST) has been dequeued,
capture_queue->is_capture_last is true, capture_queue->queued_bufs is 0, and
capture_queue->pending_dqbufs is empty. Doesn't this branch return EPOLLERR
instead of EPOLLIN | EPOLLRDNORM so userspace can call VIDIOC_DQBUF and
receive -EPIPE?
3. If an application polls for both directions (for example select() or
poll(POLLIN | POLLOUT) on an M2M device when only one queue is streaming),
won't checking !capture_queue->streaming and !output_queue->streaming
independently set EPOLLERR even though the other queue is validly streaming?
4. capture_type and output_type rely on session->uses_mplane, which is set in
virtio_media_reqbufs() but is never set in virtio_media_create_bufs(). If
multiplanar buffers are allocated via VIDIOC_CREATE_BUFS, won't
virtio_media_device_poll() inspect the non-MPLANE queues and always return
EPOLLERR?
[ ... ]
> @@ -449,12 +799,25 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
> if (ret)
> goto err_del_vqs;
>
> + for (i = 0; i < VIRTIO_MEDIA_NUM_EVENT_BUFS; i++) {
> + void *ebuf = vv->event_buffer +
> + virtio_media_event_aligned_size * i;
> +
> + ret = virtio_media_send_event_buffer(vv, ebuf);
> + if (ret)
> + goto err_video_unregister_device;
> + }
> +
> return 0;
>
> +err_video_unregister_device:
> + video_unregister_device(&vv->video_dev);
> err_del_vqs:
> virtio_dev->config->del_vqs(virtio_dev);
> err_v4l2_device_unregister:
> v4l2_device_unregister(&vv->v4l2_dev);
> +err_free_event_buffer:
> + kfree(vv->event_buffer);
> err_free_virtio_media:
> kfree(vv);
> return ret;
[Severity: High]
In virtio_media_probe(), virtio_media_send_event_buffer() is called in a loop
after virtio_device_ready() and video_register_device() without holding
vv->events_lock.
If an event callback fires (scheduling vv->eventq_work) or userspace calls
VIDIOC_SUBSCRIBE_EVENT while this loop is still running, can
virtio_media_process_events() access vv->eventq concurrently with
virtio_media_send_event_buffer()?
Also, if virtio_media_send_event_buffer() fails partway through the loop and
jumps to err_video_unregister_device, the error path deletes the virtqueues
and frees vv->event_buffer and vv without calling virtio_reset_device() or
cancel_work_sync(&vv->eventq_work). Can the host or a pending eventq_work
access freed memory on this failure path?
> }
>
> static void virtio_media_remove(struct virtio_device *virtio_dev)
> @@ -468,6 +831,9 @@ static void virtio_media_remove(struct virtio_device *virtio_dev)
> /* Remove the video device to stop accessing the commandq virtqueue */
> video_unregister_device(&vv->video_dev);
>
> + /* Stop event handling work queue from accessing eventq virtqueue */
> + cancel_work_sync(&vv->eventq_work);
> +
> /*
> * Acquire vlock to ensure no concurrent accesses to the commandq
> * virtqueue
> @@ -483,6 +849,8 @@ static void virtio_media_remove(struct virtio_device *virtio_dev)
> */
> while (virtqueue_detach_unused_buf(vv->commandq))
> ;
> + while (virtqueue_detach_unused_buf(vv->eventq))
> + ;
>
> /* Delete the virtqueues */
> virtio_dev->config->del_vqs(virtio_dev);
[Severity: High]
In virtio_media_remove(), cancel_work_sync(&vv->eventq_work) is called before
virtio_reset_device(virtio_dev):
virtio_media_remove()
video_unregister_device(&vv->video_dev)
cancel_work_sync(&vv->eventq_work)
mutex_lock(&vv->vlock)
virtio_reset_device(virtio_dev)
If an eventq interrupt arrives between cancel_work_sync() and
virtio_reset_device(), won't eventq_callback() call
schedule_work(&vv->eventq_work) again, allowing virtio_media_event_work() to
run while the virtqueues and struct virtio_media are being torn down?
[Severity: Critical]
Additionally, right after unlocking vv->vlock in virtio_media_remove(), the
driver frees all sessions in vv->sessions:
list_for_each_safe(p, n, &vv->sessions) {
struct virtio_media_session *s =
list_entry(p, struct virtio_media_session, list);
virtio_media_session_remove(vv, s);
virtio_media_session_free(s);
}
Since virtio_media_dqbuf() drops vv->vlock while sleeping in
wait_event_interruptible(session->dqbuf_wait, ...), and
video_unregister_device() does not wait for open file descriptors or in-flight
ioctls to complete, won't freeing the sessions here cause a use-after-free in
any thread sleeping in virtio_media_dqbuf() as well as when open file
descriptors are subsequently closed in virtio_media_device_close()?
> diff --git a/drivers/media/virtio/virtio_media_ioctls.c b/drivers/media/virtio/virtio_media_ioctls.c
> index 51f46b334800c..9bf2834691fd0 100644
> --- a/drivers/media/virtio/virtio_media_ioctls.c
> +++ b/drivers/media/virtio/virtio_media_ioctls.c
[ ... ]
> +static int virtio_media_send_buffer_ioctl(struct v4l2_fh *fh, u32 ioctl,
> + struct v4l2_buffer *b)
> +{
> + struct video_device *video_dev = fh->vdev;
> + struct virtio_media *vv = to_virtio_media(video_dev);
> + struct virtio_media_session *session = fh_to_session(fh);
> + struct v4l2_plane *orig_planes = NULL;
> + struct scatterlist *sgs[64];
> + /*
> + * End of the device-readable buffer SGs, to reuse in device-writable
> + * section.
> + */
> + size_t num_cmd_sgs;
> + size_t end_buf_sg;
> + struct scatterlist_builder builder = {
> + .descs = session->command_sgs.sgl,
> + .num_descs = DESC_CHAIN_MAX_LEN,
> + .cur_desc = 0,
> + .shadow_buffer = session->shadow_buf,
> + .shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
> + .shadow_buffer_pos = 0,
> + .sgs = sgs,
> + .num_sgs = ARRAY_SIZE(sgs),
> + .cur_sg = 0,
> + .framevec = NULL,
> + };
> + size_t resp_len;
> + int ret;
> + int i;
> +
> + if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
> + return -EINVAL;
> +
> + if (V4L2_TYPE_IS_MULTIPLANAR(b->type))
> + orig_planes = b->m.planes;
> +
> + /* Command descriptor */
> + ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
> + if (ret)
> + return ret;
> +
> + /* Command payload (struct v4l2_buffer) */
> + ret = scatterlist_builder_add_buffer(&builder, b);
> + if (ret < 0)
> + return ret;
> +
> + end_buf_sg = builder.cur_sg;
> + num_cmd_sgs = builder.cur_sg;
> +
> + /* Response descriptor */
> + ret = scatterlist_builder_add_ioctl_resp(&builder, session);
> + if (ret)
> + return ret;
> +
> + /* Response payload (same as input, but no userptr mapping) */
> + for (i = 1; i < end_buf_sg; i++) {
> + ret = scatterlist_builder_add_descriptor(&builder, i);
> + if (ret < 0)
> + return ret;
> + }
> +
> + ret = virtio_media_send_command(vv, builder.sgs, num_cmd_sgs,
> + builder.cur_sg - num_cmd_sgs,
> + sizeof(struct virtio_media_resp_ioctl) +
> + sizeof(*b), &resp_len);
> + if (ret < 0)
> + return ret;
> +
> + resp_len -= sizeof(struct virtio_media_resp_ioctl);
> +
> + /* Make sure that the reply length covers our v4l2_buffer */
> + if (resp_len < sizeof(*b))
> + return -EINVAL;
> +
> + ret = scatterlist_builder_retrieve_buffer(&builder, num_cmd_sgs + 1, b,
> + orig_planes);
[Severity: Medium]
What happens here when V4L2_TYPE_IS_MULTIPLANAR(b->type) is true, b->length is
0, and b->m.planes is non-NULL?
In v4l2-ioctl.c:check_array_args(), b->m.planes is only replaced with a kernel
buffer when b->length > 0. Likewise, scatterlist_builder_add_buffer() only
adds an SG descriptor for b->m.planes when b->length > 0.
However, virtio_media_send_buffer_ioctl() sets orig_planes = b->m.planes
without checking b->length > 0, and scatterlist_builder_retrieve_buffer()
calls scatterlist_builder_retrieve_data(builder, sg_index++, b->m.planes)
whenever orig_planes is non-NULL, which will attempt to retrieve a
non-existent SG entry into an unvalidated user pointer and fail with -ERANGE.
Also, resp_len is uninitialized in virtio_media_send_buffer_ioctl() before
being passed to virtio_media_send_command(), which reads *resp_len on entry:
size_t local_resp_len = resp_len ? *resp_len : 0;
[ ... ]
> +static int virtio_media_reqbufs(struct file *file, void *fh,
> + struct v4l2_requestbuffers *b)
> +{
> + struct v4l2_fh *vfh = file_to_v4l2_fh(file);
> + struct virtio_media_session *session = fh_to_session(vfh);
> + struct virtio_media_buffer *buffers = NULL;
> + struct virtio_media_queue_state *queue;
> + int ret;
> +
> + if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
> + return -EINVAL;
> +
> + if (b->memory == V4L2_MEMORY_USERPTR)
> + return -EINVAL;
> +
> + if (b->count > 0) {
> + buffers =
> + vcalloc(b->count, sizeof(struct virtio_media_buffer));
> + if (!buffers)
> + return -ENOMEM;
> + }
> +
> + ret = virtio_media_send_wr_ioctl(vfh, VIDIOC_REQBUFS, b, sizeof(*b),
> + sizeof(*b));
> + if (ret) {
> + vfree(buffers);
> + return ret;
> + }
> +
> + queue = &session->queues[b->type];
> +
> + /* REQBUFS(0) is an implicit STREAMOFF. */
> + if (b->count == 0)
> + virtio_media_clear_queue(session, queue);
> +
> + vfree(queue->buffers);
> + queue->buffers = buffers;
> +
> + queue->allocated_bufs = b->count;
[Severity: Critical]
Can this overflow queue->buffers[] if the host increases b->count?
In V4L2, a driver or host handling VIDIOC_REQBUFS or VIDIOC_CREATE_BUFS may
round b->count upward to satisfy minimum buffer requirements. Here in
virtio_media_reqbufs(), buffers is allocated using the pre-ioctl b->count, and
then queue->allocated_bufs is set to the host-updated b->count. Similarly, in
virtio_media_create_bufs(), buffers is allocated with queue->allocated_bufs +
b->count elements before the ioctl, and queue->allocated_bufs is set to
b->index + b->count after the ioctl without checking that it fits within the
allocated array.
If the host returns a larger count than requested, won't subsequent indexing of
queue->buffers[] up to queue->allocated_bufs read and write out of bounds?
[ ... ]
> +static int virtio_media_create_bufs(struct file *file, void *fh,
> + struct v4l2_create_buffers *b)
> +{
> + struct v4l2_fh *vfh = file_to_v4l2_fh(file);
> + struct virtio_media_session *session = fh_to_session(vfh);
> + struct virtio_media_queue_state *queue;
> + struct virtio_media_buffer *buffers = NULL;
> + u32 type = b->format.type;
> + int ret;
> +
> + if (type > VIRTIO_MEDIA_LAST_QUEUE)
> + return -EINVAL;
> +
> + queue = &session->queues[type];
> +
> + if (b->count > 0) {
> + /*
> + * This allocates a new buffer large enough for the existing
> + * buffers (``queue->allocated_bufs``) as well as the newly
> + * requested buffers (``b->count``). The host may allocate
> + * fewer buffers than requested, as indicated by the final
> + * value of ``b->count``. In this case, we do not re-allocate
> + * to avoid the case where this new allocation fails and the
> + * host buffers need to be freed. Instead we simply use the
> + * larger buffer.
> + */
> + buffers = vcalloc(queue->allocated_bufs + b->count,
> + sizeof(*queue->buffers));
> + if (!buffers)
> + return -ENOMEM;
> + }
> +
> + ret = virtio_media_send_wr_ioctl(vfh, VIDIOC_CREATE_BUFS, b, sizeof(*b),
> + sizeof(*b));
> + if (ret) {
> + if (buffers)
> + vfree(buffers);
> +
> + return ret;
> + }
> +
> + /* If count is zero, we were just checking for format. */
> + if (b->count == 0)
> + return 0;
[Severity: Medium]
Does this leak buffers if b->count was non-zero on input and the host
overwrites b->count with 0 in the VIDIOC_CREATE_BUFS response?
> +
> + memcpy(buffers, queue->buffers,
> + sizeof(*queue->buffers) * queue->allocated_bufs);
> + vfree(queue->buffers);
> + queue->buffers = buffers;
[Severity: Critical]
Can reallocating queue->buffers here (or in virtio_media_reqbufs() when
b->count > 0) corrupt queue->pending_dqbufs and cause a use-after-free?
Each struct virtio_media_buffer in queue->buffers contains an embedded
struct list_head list node that is linked into queue->pending_dqbufs when a
buffer is ready to be dequeued.
In virtio_media_create_bufs(), memcpy() shallow-copies queue->buffers into the
newly allocated buffers array and then calls vfree(queue->buffers), leaving
queue->pending_dqbufs and any linked list_head pointers pointing into the
freed queue->buffers array.
Similarly, in virtio_media_reqbufs(), virtio_media_clear_queue() is only
called when b->count == 0; if VIDIOC_REQBUFS is called with b->count > 0 while
queue->pending_dqbufs is non-empty, vfree(queue->buffers) frees the active
list nodes while they remain linked in queue->pending_dqbufs.
[ ... ]
> +static int virtio_media_dqbuf(struct file *file, void *fh,
> + struct v4l2_buffer *b)
> +{
> + struct video_device *video_dev = video_devdata(file);
> + struct virtio_media *vv = to_virtio_media(video_dev);
> + struct virtio_media_session *session =
> + fh_to_session(file_to_v4l2_fh(file));
> + struct virtio_media_buffer *dqbuf;
> + struct virtio_media_queue_state *queue;
> + struct list_head *buffer_queue;
> + struct v4l2_plane *planes_backup = NULL;
> + const bool is_multiplanar = V4L2_TYPE_IS_MULTIPLANAR(b->type);
> + int ret;
> + bool error;
> +
> + if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
> + return -EINVAL;
> +
> + queue = &session->queues[b->type];
> +
> + /*
> + * If a buffer with the LAST flag has been returned, subsequent
> + * calls to DQBUF must return -EPIPE until the queue is cleared.
> + */
> + if (queue->is_capture_last)
> + return -EPIPE;
> +
> + buffer_queue = &queue->pending_dqbufs;
> +
> + if (session->nonblocking_dequeue) {
> + if (list_empty(buffer_queue))
> + return -EAGAIN;
> + } else if (queue->allocated_bufs == 0) {
> + return -EINVAL;
> + } else if (!queue->streaming) {
> + return -EINVAL;
> + }
> +
> + /*
> + * vv->lock has been acquired by virtio_media_device_ioctl. Release it
> + * while we wait so that other ioctls for this session can be processed
> + * and potentially trigger dqbuf_wait.
> + */
> + mutex_unlock(&vv->vlock);
> +
> + /*
> + * It's not possible to acquire ``session->error_lock`` during the
> + * check, however this shouldn't cause issues because:
> + * - The lock is acquired when actually checking the value
> + * - The ``session->error`` flag is never reset once set
> + */
> + ret = wait_event_interruptible(session->dqbuf_wait,
> + !list_empty(buffer_queue) ||
> + session->error);
[Severity: High]
What happens if VIDIOC_STREAMOFF or VIDIOC_REQBUFS(0) is called while a thread
is blocked in wait_event_interruptible() here?
virtio_media_clear_queue() sets queue->streaming = false and empties
queue->pending_dqbufs without calling wake_up(&session->dqbuf_wait), and the
wait condition only checks !list_empty(buffer_queue) || session->error. Won't
the blocked VIDIOC_DQBUF thread sleep indefinitely when the queue is streamed
off?
Also, because the checks above use else if:
if (session->nonblocking_dequeue) {
if (list_empty(buffer_queue))
return -EAGAIN;
} else if (queue->allocated_bufs == 0) {
return -EINVAL;
} else if (!queue->streaming) {
return -EINVAL;
}
when nonblocking_dequeue is true, calling VIDIOC_DQBUF on a queue with
allocated_bufs == 0 or !queue->streaming skips the -EINVAL checks and returns
-EAGAIN. And since session->nonblocking_dequeue is only initialized once in
virtio_media_session_alloc() from file->f_flags & O_NONBLOCK, toggling
O_NONBLOCK later via fcntl(F_SETFL) will not be reflected here.
> + mutex_lock(&vv->vlock);
> + if (ret)
> + return -EINTR;
> +
> + mutex_lock(&session->error_lock);
> + error = session->error;
> + mutex_unlock(&session->error_lock);
> +
> + if (error)
> + return -EIO;
> +
> + mutex_lock(&session->queues_lock);
> + dqbuf = list_first_entry_or_null(buffer_queue,
> + struct virtio_media_buffer, list);
> +
> + if (dqbuf)
> + list_del(&dqbuf->list);
> +
> + mutex_unlock(&session->queues_lock);
> +
> + if (!dqbuf) {
> + v4l2_err(&vv->v4l2_dev, "No buffer found when dqbuf event triggered\n");
> + return -EIO;
> + }
> +
> + /* Clear the DONE flag as the buffer is now being dequeued. */
> + dqbuf->buffer.flags &= ~V4L2_BUF_FLAG_DONE;
> +
> + if (is_multiplanar) {
> + size_t nb_planes = min_t(u32, b->length, VIDEO_MAX_PLANES);
> +
> + memcpy(b->m.planes, dqbuf->planes,
> + nb_planes * sizeof(struct v4l2_plane));
> + planes_backup = b->m.planes;
> + }
> +
> + memcpy(b, &dqbuf->buffer, sizeof(*b));
> +
> + if (is_multiplanar)
> + b->m.planes = planes_backup;
[Severity: Critical]
Can overwriting b->length here (and in scatterlist_builder_retrieve_buffer()
for VIDIOC_QUERYBUF, VIDIOC_PREPARE_BUF, and VIDIOC_QBUF) cause a kernel heap
out-of-bounds read in 32-bit compat mode?
In v4l2-ioctl.c:check_array_args() and video_usercopy(), the kernel allocates
array_buf (passed as b->m.planes) sized to the user-supplied input b->length.
Here in virtio_media_dqbuf(), if a 32-bit compat caller passes a smaller
b->length (for example b->length = 1 when dqbuf->buffer.length is 2),
memcpy(b, &dqbuf->buffer, sizeof(*b)) overwrites b->length with
dqbuf->buffer.length (and if b->length was 0, b->m.planes is not even a kernel
pointer). Similarly, scatterlist_builder_retrieve_buffer() overwrites b->length
from the host response without clamping it to the input b->length.
When video_usercopy() finishes a 32-bit compat ioctl,
v4l2_compat_put_array_args() iterates b64->length times copying elements from
array_buf to userspace, reading past the end of the allocated array_buf heap
buffer. Shouldn't multiplanar buffer ioctls validate that b->m.planes is
non-NULL and that the caller's b->length is sufficient before dequeuing or
updating b->length?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261029153038.3800747-1-briandaniels@google.com?part=4
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v10 5/5] media: virtio: Add MMAP buffer mapping support
2026-10-29 15:30 ` [PATCH v10 5/5] media: virtio: Add MMAP buffer mapping support Brian Daniels
@ 2026-10-29 17:19 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-29 17:19 UTC (permalink / raw)
To: Brian Daniels; +Cc: virtualization, media-ci, Michael S. Tsirkin, Eugenio Perez
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] media: virtio: missing VMA open callback and broken munmap offset on VMA split
- [High] media: virtio: missing SHM bounds and offset checks in virtio_media_device_mmap()
- [High] media: virtio: read-only mmap leaves VM_MAYWRITE set in virtio_media_device_mmap()
- [High] media: virtio: access to freed commandq in virtio_media_vma_close()
- [High] media: virtio: ABBA deadlock between vv->vlock and mm->mmap_lock
- [Medium] media: virtio: missing session->error check in virtio_media_device_mmap()
--
commit f2fa31d4e435156536271ee6a79a737826897ef4
Author: Alexandre Courbot <gnurou@gmail.com>
media: virtio: Add MMAP buffer mapping support
This commit adds support for mapping V4L2_MEMORY_MMAP buffers into guest
userspace via virtio shared memory region 0 using VIRTIO_MEDIA_CMD_MMAP and
VIRTIO_MEDIA_CMD_MUNMAP.
> diff --git a/drivers/media/virtio/virtio_media_driver.c b/drivers/media/virtio/virtio_media_driver.c
> index 877ac154b3221..865b6dfad7fb6 100644
> --- a/drivers/media/virtio/virtio_media_driver.c
> +++ b/drivers/media/virtio/virtio_media_driver.c
[ ... ]
> @@ -687,12 +695,148 @@ static __poll_t virtio_media_device_poll(struct file *file, poll_table *wait)
> return rc;
> }
>
> +static void virtio_media_vma_close_locked(struct vm_area_struct *vma)
> +{
> + struct virtio_media *vv = vma->vm_private_data;
> + struct virtio_media_cmd_munmap *cmd_munmap = &vv->cmd.munmap;
> + struct virtio_media_resp_munmap *resp_munmap = &vv->resp.munmap;
> + struct scatterlist cmd_sg = {}, resp_sg = {};
> + struct scatterlist *sgs[2] = { &cmd_sg, &resp_sg };
> + int ret;
> +
> + sg_set_buf(&cmd_sg, cmd_munmap, sizeof(*cmd_munmap));
> + sg_mark_end(&cmd_sg);
> +
> + sg_set_buf(&resp_sg, resp_munmap, sizeof(*resp_munmap));
> + sg_mark_end(&resp_sg);
> +
> + cmd_munmap->hdr.cmd = VIRTIO_MEDIA_CMD_MUNMAP;
> + cmd_munmap->hdr.__reserved = 0;
> + cmd_munmap->driver_addr =
> + (vma->vm_pgoff << PAGE_SHIFT) - vv->mmap_region.addr;
> + ret = virtio_media_send_command(vv, sgs, 1, 1, sizeof(*resp_munmap),
> + NULL);
> + if (ret < 0) {
> + v4l2_err(&vv->v4l2_dev, "host failed to unmap buffer: %d\n",
> + ret);
> + }
> +}
> +
> +/**
> + * virtio_media_vma_close() - Close a MMAP buffer mapping.
> + * @vma: VMA of the mapping to close.
> + *
> + * Inform the host that a previously created MMAP mapping is no longer needed
> + * and can be removed.
> + */
> +static void virtio_media_vma_close(struct vm_area_struct *vma)
> +{
> + struct virtio_media *vv = vma->vm_private_data;
> +
> + mutex_lock(&vv->vlock);
> + virtio_media_vma_close_locked(vma);
> + mutex_unlock(&vv->vlock);
> +}
[Severity: High]
Can virtio_media_vma_close() dereference a freed vv->commandq if the virtio
device is removed while a buffer remains mapped in userspace?
When a process maps a buffer via virtio_media_device_mmap(), vma->vm_file
keeps the struct file and vv->video_dev alive across virtio_media_remove().
However, virtio_media_remove() calls virtio_dev->config->del_vqs(virtio_dev),
which frees vv->commandq, and frees all sessions in vv->sessions:
drivers/media/virtio/virtio_media_driver.c:virtio_media_remove() {
...
video_unregister_device(&vv->video_dev);
virtio_media_device_reset(virtio_dev);
virtio_dev->config->del_vqs(virtio_dev);
...
}
When the process later calls munmap() or exits, remove_vma() -> vma_close()
invokes virtio_media_vma_close() -> virtio_media_vma_close_locked() ->
virtio_media_send_command() -> virtio_media_kick_command(), which accesses
vv->commandq without checking if the device was unregistered.
Similarly, in v4l2_mmap() -> virtio_media_device_mmap(), video_is_registered()
is checked before acquiring vv->vlock. If virtio_media_remove() runs before
virtio_media_device_mmap() acquires vv->vlock, won't virtio_media_device_mmap()
write to the freed session->cmd.mmap and dereference the freed vv->commandq?
> +
> +static const struct vm_operations_struct virtio_media_vm_ops = {
> + .close = virtio_media_vma_close,
> +};
[Severity: High]
What happens when a mapped VMA is duplicated or split via fork(), mremap(), or
partial munmap()/mprotect()?
Because virtio_media_vm_ops only defines .close and omits .open (and
vma->vm_private_data stores vv directly rather than a reference-counted
mapping structure like vb2_common_vm_ops), dup_mmap(), copy_vma(), and
__split_vma() create additional vm_area_struct instances without incrementing
any reference count. When the first VMA closes, virtio_media_vma_close() will
send VIRTIO_MEDIA_CMD_MUNMAP to the host while the remaining VMA(s) are still
mapped in guest userspace, and closing the remaining VMA(s) later will send
unbalanced VIRTIO_MEDIA_CMD_MUNMAP commands.
Furthermore, when __split_vma() splits a VMA at addr, it advances
vma->vm_pgoff on the upper VMA by linear_page_delta(vma, addr). When that
upper VMA is closed, virtio_media_vma_close_locked() computes:
cmd_munmap->driver_addr =
(vma->vm_pgoff << PAGE_SHIFT) - vv->mmap_region.addr;
which sends a shifted interior offset instead of the base driver_addr returned
by VIRTIO_MEDIA_CMD_MMAP, leaking the host mapping.
> +
> +/**
> + * virtio_media_device_mmap() - Perform a mmap request from userspace.
> + * @file: opened file of the session to map for.
> + * @vma: VM area struct describing the desired mapping.
> + *
> + * This requests the host to map a MMAP buffer for us, so we can then make that
> + * mapping visible into user-space address space.
> + */
> +static int virtio_media_device_mmap(struct file *file,
> + struct vm_area_struct *vma)
> +{
> + struct video_device *video_dev = video_devdata(file);
> + struct virtio_media *vv = to_virtio_media(video_dev);
> + struct virtio_media_session *session =
> + fh_to_session(file->private_data);
> + struct virtio_media_cmd_mmap *cmd_mmap = &session->cmd.mmap;
> + struct virtio_media_resp_mmap *resp_mmap = &session->resp.mmap;
> + struct scatterlist cmd_sg = {}, resp_sg = {};
> + struct scatterlist *sgs[2] = { &cmd_sg, &resp_sg };
> + int ret;
> +
> + if (!(vma->vm_flags & VM_SHARED))
> + return -EINVAL;
> + if (!(vma->vm_flags & (VM_READ | VM_WRITE)))
> + return -EINVAL;
> +
> + mutex_lock(&vv->vlock);
[Severity: High]
Could acquiring vv->vlock in virtio_media_device_mmap() and
virtio_media_vma_close() cause an ABBA deadlock with mm->mmap_lock?
Both virtio_media_device_mmap() and virtio_media_vma_close() are called by the
MM core while holding current->mm->mmap_lock, establishing the lock order
mm->mmap_lock -> vv->vlock.
However, in drivers/media/virtio/virtio_media_ioctls.c,
virtio_media_device_ioctl() acquires vv->vlock and holds it across
copy_from_user(), copy_to_user(), and video_ioctl2() (which calls
video_usercopy(), and for VIDIOC_G/S/TRY_EXT_CTRLS calls
virtio_media_send_ext_controls_ioctl() ->
scatterlist_builder_add_ext_ctrls_userptrs() ->
__scatterlist_builder_add_userptr() -> vb2_create_framevec() ->
get_vaddr_frames() -> pin_user_pages_fast()). Those usercopy faults and GUP
paths acquire current->mm->mmap_lock while holding vv->vlock, establishing the
reverse order vv->vlock -> mm->mmap_lock.
> +
> + cmd_mmap->hdr.cmd = VIRTIO_MEDIA_CMD_MMAP;
> + cmd_mmap->hdr.__reserved = 0;
> + cmd_mmap->session_id = session->id;
[Severity: Medium]
Should virtio_media_device_mmap() check session->error before sending
VIRTIO_MEDIA_CMD_MMAP?
When VIRTIO_MEDIA_EVT_ERROR is processed in virtio_media_process_events(), the
driver sets session->error = true and calls virtio_media_session_close(vv,
session) to close session->id on the host while the guest file descriptor is
still open. Unlike virtio_media_device_ioctl(), which checks session->error
and returns -EIO, virtio_media_device_mmap() does not check session->error and
will send VIRTIO_MEDIA_CMD_MMAP with a session_id that has already been closed
on the host.
> + cmd_mmap->flags =
> + (vma->vm_flags & VM_WRITE) ? VIRTIO_MEDIA_MMAP_FLAG_RW_MASK : 0;
[Severity: High]
When a user opens the video device with O_RDWR and calls mmap() with PROT_READ
and MAP_SHARED, the MM core sets VM_MAYWRITE on vma->vm_flags while leaving
VM_WRITE clear. Here, virtio_media_device_mmap() requests a read-only mapping
from the host (cmd_mmap->flags = 0), but leaves VM_MAYWRITE set on the VMA.
Can userspace subsequently call mprotect(addr, len, PROT_READ | PROT_WRITE) to
make the guest PTEs writable for a buffer that was mapped read-only on the
host? Should vm_flags_clear(vma, VM_MAYWRITE) be called when VM_WRITE is not
set?
> + cmd_mmap->offset = vma->vm_pgoff << PAGE_SHIFT;
> + cmd_mmap->__reserved = 0;
> +
> + sg_set_buf(&cmd_sg, cmd_mmap, sizeof(*cmd_mmap));
> + sg_mark_end(&cmd_sg);
> +
> + sg_set_buf(&resp_sg, resp_mmap, sizeof(*resp_mmap));
> + sg_mark_end(&resp_sg);
> +
> + /*
> + * The host performs reference counting and is smart enough to return
> + * the same guest physical address if this is called several times on
> + * the same
> + * buffer.
> + */
> + ret = virtio_media_send_command(vv, sgs, 1, 1, sizeof(*resp_mmap),
> + NULL);
> + if (ret < 0)
> + goto end;
> +
> + vma->vm_private_data = vv;
> + /*
> + * Keep the guest address at which the buffer is mapped since we will
> + * use that to unmap.
> + */
> + vma->vm_pgoff = (resp_mmap->driver_addr + vv->mmap_region.addr) >>
> + PAGE_SHIFT;
> +
> + /*
> + * We cannot let the mapping be larger than the buffer.
> + */
> + if (vma->vm_end - vma->vm_start > PAGE_ALIGN(resp_mmap->len)) {
> + dev_dbg(&video_dev->dev,
> + "invalid MMAP, as it would overflow buffer length\n");
> + ret = -EINVAL;
> + goto cleanup;
> + }
> +
> + ret = io_remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff,
> + vma->vm_end - vma->vm_start,
> + vma->vm_page_prot);
[Severity: High]
Does virtio_media_device_mmap() need to validate cmd_mmap->offset and the
returned resp_mmap->driver_addr and resp_mmap->len against vv->mmap_region?
First, cmd_mmap->offset is a 32-bit __le32, so if userspace passes a page
offset where vma->vm_pgoff << PAGE_SHIFT >= 4 GiB, the upper bits are silently
truncated instead of returning -EINVAL.
Second, resp_mmap->driver_addr and resp_mmap->len are used to compute
vma->vm_pgoff and remap the range via io_remap_pfn_range() without checking
that resp_mmap->driver_addr is page-aligned, that the additions do not
overflow, or that the mapped range [driver_addr, driver_addr + size) actually
lies within vv->mmap_region.len. If the host returns an out-of-bounds
driver_addr or len, could io_remap_pfn_range() map arbitrary guest physical
memory outside the virtio shared memory region into userspace?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261029153038.3800747-1-briandaniels@google.com?part=5
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-10-29 17:19 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-29 15:30 [PATCH v10 0/5] media: add virtio-media driver Brian Daniels
2026-10-29 15:30 ` [PATCH v10 1/5] media: virtio: Add core driver and session management Brian Daniels
2026-10-29 15:49 ` sashiko-bot
2026-10-29 15:30 ` [PATCH v10 2/5] media: virtio: Add V4L2 ioctl forwarding infrastructure Brian Daniels
2026-10-29 16:15 ` sashiko-bot
2026-10-29 15:30 ` [PATCH v10 3/5] media: virtio: Add extended controls support Brian Daniels
2026-10-29 16:37 ` sashiko-bot
2026-10-29 15:30 ` [PATCH v10 4/5] media: virtio: Add buffer streaming, event handling, and poll Brian Daniels
2026-10-29 17:00 ` sashiko-bot
2026-10-29 15:30 ` [PATCH v10 5/5] media: virtio: Add MMAP buffer mapping support Brian Daniels
2026-10-29 17:19 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.