mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-08-31 07:03:28 -04:00
virtio_console: fix hot-unplug races in TX paths
When a port is hot-unplugged, unplug_port() nullifies port->portdev. However, concurrent TX paths (__send_to_port, put_chars) could read a stale pointer or encounter a NULL pointer dereference. Add READ_ONCE(port->portdev) and NULL checks in the TX paths. In __send_to_port(), move the out_vq assignment inside the outvq_lock and check portdev under the lock. Correspondingly, update unplug_port() to NULL out port->portdev while holding the outvq_lock to serialize with __send_to_port(). In put_chars(), return count instead of 0 on unplug to prevent the hvc layer from spinning in an infinite retry loop. Signed-off-by: Sungho Bae <baver.bae@lge.com> Link: https://patch.msgid.link/20260603183757.21587-3-baver.bae@gmail.com Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
committed by
Greg Kroah-Hartman
parent
fc220d6be3
commit
a616ad344d
@@ -608,11 +608,19 @@ static ssize_t __send_to_port(struct port *port, struct scatterlist *sg,
|
||||
int err;
|
||||
unsigned long flags;
|
||||
unsigned int len;
|
||||
|
||||
out_vq = port->out_vq;
|
||||
struct ports_device *portdev;
|
||||
|
||||
spin_lock_irqsave(&port->outvq_lock, flags);
|
||||
|
||||
portdev = READ_ONCE(port->portdev);
|
||||
|
||||
if (!portdev) {
|
||||
in_count = 0;
|
||||
goto free_and_done;
|
||||
}
|
||||
|
||||
out_vq = port->out_vq;
|
||||
|
||||
reclaim_consumed_buffers(port);
|
||||
|
||||
err = virtqueue_add_outbuf(out_vq, sg, nents, buf, GFP_ATOMIC);
|
||||
@@ -1118,12 +1126,24 @@ static ssize_t put_chars(u32 vtermno, const u8 *buf, size_t count)
|
||||
struct port *port;
|
||||
struct scatterlist sg[1];
|
||||
struct port_buffer *pbuf;
|
||||
struct ports_device *portdev;
|
||||
|
||||
port = find_port_by_vtermno(vtermno);
|
||||
if (!port)
|
||||
return -EPIPE;
|
||||
|
||||
pbuf = alloc_buf(port->portdev->vdev, count, 0, GFP_ATOMIC);
|
||||
/*
|
||||
* Silently drop output if device hot-unplug is in progress.
|
||||
* portdev was NULLed by unplug_port() after hvc_remove() was
|
||||
* already called, so the hvc layer will stop invoking put_chars()
|
||||
* very soon. Returning count avoids a pointless retry loop in the
|
||||
* interim.
|
||||
*/
|
||||
portdev = READ_ONCE(port->portdev);
|
||||
if (!portdev)
|
||||
return count;
|
||||
|
||||
pbuf = alloc_buf(portdev->vdev, count, 0, GFP_ATOMIC);
|
||||
if (!pbuf)
|
||||
return -ENOMEM;
|
||||
|
||||
@@ -1511,11 +1531,16 @@ static void unplug_port(struct port *port)
|
||||
remove_port_data(port);
|
||||
|
||||
/*
|
||||
* We should just assume the device itself has gone off --
|
||||
* else a close on an open port later will try to send out a
|
||||
* control message.
|
||||
* Null out portdev under outvq_lock so that __send_to_port()
|
||||
* cannot race: it checks port->portdev inside the same lock
|
||||
* and bails out if NULL, preventing any buffer from being
|
||||
* enqueued to an already torn-down virtqueue. Also prevents
|
||||
* a close on an open port later from sending a stale control
|
||||
* message.
|
||||
*/
|
||||
spin_lock_irq(&port->outvq_lock);
|
||||
port->portdev = NULL;
|
||||
spin_unlock_irq(&port->outvq_lock);
|
||||
|
||||
sysfs_remove_group(&port->dev->kobj, &port_attribute_group);
|
||||
device_destroy(&port_class, port->dev->devt);
|
||||
|
||||
Reference in New Issue
Block a user