[PATCH] virtio_console: synchronize HVC polling with port removal
From: Yaohan Li
Date: Wed Nov 04 2026 - 04:46:22 EST
in_intr() may call hvc_poll() after dropping inbuf_lock. At the same
time, a hot-unplug event can remove and free the HVC adapter without
waiting for an in-flight poll. The interrupt callback can then
access freed adapter memory.
Read the HVC pointer inside an RCU read-side critical section and
publish NULL before removing the adapter. Wait for existing RCU
readers before calling hvc_remove(), without holding inbuf_lock
across hvc_poll(), since the HVC operations may take that lock.
The race was reproduced under KASAN with QEMU virtio-serial and
multiple interrupt vectors while host-initiated hot-unplug raced
with a data-queue interrupt. The same injection-assisted sequence
completed without a report after the change.
Fixes: 203baab8ba319 ("virtio: console: Introduce function to hand off data from host to readers")
Assisted-by: ZCode:GLM-4
Signed-off-by: Yaohan Li <shunsheng1218@xxxxxxxxx>
---
drivers/char/virtio_console.c | 28 ++++++++++++++++++++++++++--
1 file changed, 26 insertions(+), 2 deletions(-)
diff --git a/drivers/char/virtio_console.c b/drivers/char/virtio_console.c
index 019bcae81af5c..a3ee4af106d17 100644
--- a/drivers/char/virtio_console.c
+++ b/drivers/char/virtio_console.c
@@ -1551,10 +1551,22 @@ static void unplug_port(struct port *port)
spin_unlock_irq(&port->inbuf_lock);
if (is_console_port(port)) {
+ struct hvc_struct *cons_hvc = port->cons.hvc;
+
spin_lock_irq(&pdrvdata_lock);
list_del(&port->cons.list);
spin_unlock_irq(&pdrvdata_lock);
- hvc_remove(port->cons.hvc);
+
+ /*
+ * See in_intr(): a data vq callback may be running
+ * hvc_poll() on the adapter. Publish a NULL adapter so no
+ * new poll can start, wait for in-flight ones, and only
+ * then remove the adapter.
+ */
+ rcu_assign_pointer(port->cons.hvc, NULL);
+ synchronize_rcu();
+
+ hvc_remove(cons_hvc);
ida_free(&vtermno_ida, port->cons.vtermno);
}
@@ -1782,6 +1794,7 @@ static void out_intr(struct virtqueue *vq)
static void in_intr(struct virtqueue *vq)
{
struct port *port;
+ struct hvc_struct *cons_hvc;
unsigned long flags;
port = find_port_by_vq(vq->vdev->priv, vq);
@@ -1820,8 +1833,19 @@ static void in_intr(struct virtqueue *vq)
wake_up_interruptible(&port->waitqueue);
- if (is_console_port(port) && hvc_poll(port->cons.hvc))
+ /*
+ * The vq callback calls hvc_poll() on the hvc adapter, which
+ * hvc_remove() frees on unplug without any lock shared with this
+ * hard-IRQ path (calling hvc_poll() with port->inbuf_lock held
+ * would deadlock: the hvc ops take that lock themselves). Take an
+ * RCU read lock across the poll and have the unplug path NULL out
+ * the adapter and wait for in-flight callbacks before removing it.
+ */
+ rcu_read_lock();
+ cons_hvc = rcu_dereference(port->cons.hvc);
+ if (cons_hvc && hvc_poll(cons_hvc))
hvc_kick();
+ rcu_read_unlock();
kref_put(&port->kref, remove_port);
}
--
2.34.1