Linux virtualization list
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Yaohan Li" <shunsheng1218@gmail.com>
Cc: virtualization@lists.linux.dev,
	"Michael S. Tsirkin" <mst@redhat.com>,
	"Eugenio Perez" <eperezma@redhat.com>
Subject: Re: [PATCH v2] virtio_console: fix buffer ownership and unplug races in the read path
Date: Thu, 05 Nov 2026 12:06:01 +0000	[thread overview]
Message-ID: <sashiko-outbox-198992@kernel.org> (raw)
In-Reply-To: <20261105114535.972013-1-shunsheng1218@gmail.com>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] virtio_console: NULL portdev dereference and vq access in in_intr() on unplug
- [Medium] virtio_console: poll and O_NONBLOCK read mismatch when inbuf_claimed is set
- [Medium] virtio_console: fill_readbuf() returns 0 instead of -ENODEV on port unplug
--

commit c34e695a42ae05ed26e613884cc4fae84545b23a
Author: Yaohan Li <shunsheng1218@gmail.com>

virtio_console: fix buffer ownership and unplug races in the read path

This commit introduces an ownership claim protocol (port->inbuf_claimed) and
an unplug flag (port->unplugging) under port->inbuf_lock to serialize buffer
access in fill_readbuf() and prevent input virtqueue operations during port
teardown.

> diff --git a/drivers/char/virtio_console.c b/drivers/char/virtio_console.c
> index 019bcae81af5c..191dceb77df1b 100644
> --- a/drivers/char/virtio_console.c
> +++ b/drivers/char/virtio_console.c

[ ... ]

> @@ -541,10 +558,28 @@ static bool port_has_data(struct port *port)
>  
>  	ret = false;
>  	spin_lock_irqsave(&port->inbuf_lock, flags);
> +
> +	/* The port is going away; its vqs are being torn down. */
> +	if (port->unplugging || !port->portdev)
> +		goto out_unlock;
> +
> +	/*
> +	 * A reader owns the claimed buffer, possibly sleeping in
> +	 * copy_to_user().  Report data as pending for the
> +	 * EOF/host-disconnect logic, but do not prefetch the next
> +	 * buffer: the claimer's unconsumed remainder must go back to
> +	 * inbuf first, or the prefetched buffer would be dropped.
> +	 */
> +	if (port->inbuf_claimed) {
> +		ret = true;
> +		goto out_unlock;
> +	}
> +
>  	port->inbuf = get_inbuf(port);
>  	if (port->inbuf)
>  		ret = true;
>  
> +out_unlock:
>  	spin_unlock_irqrestore(&port->inbuf_lock, flags);
>  	return ret;
>  }
> @@ -692,24 +727,111 @@ static ssize_t __send_to_port(struct port *port, struct scatterlist *sg,
>   * Give out the data that's requested from the buffer that we have
>   * queued up.
>   */
> +static bool will_read_block(struct port *port);
> +
>  static ssize_t fill_readbuf(struct port *port, u8 __user *out_buf,
> -			    size_t out_count, bool to_user)
> +			    size_t out_count, bool to_user, bool nonblock)
>  {
>  	struct port_buffer *buf;
>  	unsigned long flags;
> +	bool free_requeue_buf = false;
> +	int err;
> +
> +	if (!out_count)
> +		return 0;
> +
> +retry:
> +	spin_lock_irqsave(&port->inbuf_lock, flags);
>  
> -	if (!out_count || !port_has_data(port))
> +	/* The port is going away; its vqs are being torn down. */
> +	if (port->unplugging || !port->portdev) {
> +		spin_unlock_irqrestore(&port->inbuf_lock, flags);
>  		return 0;
> +	}

[Severity: Medium]
Should fill_readbuf() return -ENODEV instead of 0 when to_user is true and
port->unplugging || !port->portdev is set?

In port_fops_read() and in the second wait path of fill_readbuf(), hot-unplug
(!port->guest_connected) returns -ENODEV to userspace to distinguish port
removal from a normal host disconnect (0):

drivers/char/virtio_console.c:port_fops_read() {
	if (!port->guest_connected)
		return -ENODEV;
}

However, if a blocking userspace reader waits on port->inbuf_claimed in
fill_readbuf() and is woken by unplug_port() setting port->unplugging = true,
jumping to retry hits this check and returns 0 instead of -ENODEV.

Similarly, if unplug_port() runs after port_fops_read() checks
!port->guest_connected, or between the two spin_lock_irq(&port->inbuf_lock)
critical sections in unplug_port() (where port->unplugging = true is set
before port->guest_connected = false), port_has_data() returns false and
port_fops_read() or fill_readbuf() can return 0 or -EAGAIN instead of
-ENODEV.

>  
> +	/*
> +	 * Another reader (a userspace read() or the HVC console poll)
> +	 * owns the claimed buffer, possibly sleeping in copy_to_user().
> +	 * Prefetching the next buffer here would drop or reorder the
> +	 * claimed data once the owner returns its unconsumed remainder,
> +	 * so report no data instead.  The HVC poll will simply retry;
> +	 * a userspace reader waits for the claim to be released.
> +	 */
> +	if (port->inbuf_claimed) {
> +		spin_unlock_irqrestore(&port->inbuf_lock, flags);
> +		if (!to_user)
> +			return 0;
> +		if (nonblock)
> +			return -EAGAIN;

[Severity: Medium]
Can returning true from port_has_data() while returning -EAGAIN here cause a
busy loop between poll() and non-blocking read() when port->inbuf_claimed is
set?

When one reader has claimed port->inbuf and dropped port->inbuf_lock across
copy_to_user(), port_has_data() returns true, so will_read_block() evaluates
to false:

drivers/char/virtio_console.c:will_read_block() {
	if (!port->guest_connected) {
		/* Port got hot-unplugged. Let's exit. */
		return false;
	}
	return !port_has_data(port) && port->host_connected;
}

drivers/char/virtio_console.c:port_fops_poll() {
	...
	if (!will_read_block(port))
		ret |= EPOLLIN | EPOLLRDNORM;
}

A peer thread polling the same descriptor with O_NONBLOCK will see
EPOLLIN | EPOLLRDNORM from port_fops_poll(), call port_fops_read() ->
fill_readbuf(), and immediately get -EAGAIN while port->inbuf_claimed remains
true. In addition, a blocking reader waiting in fill_readbuf() at
wait_event_freezable(port->waitqueue, !will_read_block(port)) will wake up as
soon as another reader claims a buffer, even though no buffer can be acquired
yet.

[ ... ]

> @@ -1791,7 +1971,15 @@ static void in_intr(struct virtqueue *vq)
>  	}
>  
>  	spin_lock_irqsave(&port->inbuf_lock, flags);
> -	port->inbuf = get_inbuf(port);
> +
> +	/*
> +	 * A reader may own an exclusively claimed buffer, possibly
> +	 * sleeping in copy_to_user().  Do not prefetch the next buffer
> +	 * now: the claimer's unconsumed remainder must go back to
> +	 * port->inbuf first, or the prefetched buffer would be lost.
> +	 */
> +	if (!port->unplugging && !port->inbuf_claimed)
> +		port->inbuf = get_inbuf(port);
>  
>  	/*
>  	 * Normally the port should not accept data when the port is

[Severity: High]
If in_intr() races with unplug_port(), can falling through after skipping
get_inbuf() dereference a NULL port->portdev or touch the virtqueue during
teardown?

When in_intr() runs concurrently with unplug_port(), find_port_by_vq() can
obtain a port reference just before unplug_port() removes the port from
portdev->ports. In unplug_port():

drivers/char/virtio_console.c:unplug_port() {
	spin_lock_irq(&port->inbuf_lock);
	port->unplugging = true;
	spin_unlock_irq(&port->inbuf_lock);
	...
	spin_lock_irq(&port->inbuf_lock);
	if (port->guest_connected) {
		...
		port->guest_connected = false;
		port->host_connected = false;
		...
	}
	spin_unlock_irq(&port->inbuf_lock);
	...
	spin_lock_irq(&port->outvq_lock);
	port->portdev = NULL;
	spin_unlock_irq(&port->outvq_lock);
}

When in_intr() then acquires port->inbuf_lock, the new check skips
port->inbuf = get_inbuf(port), but execution continues directly into:

drivers/char/virtio_console.c:in_intr() {
	if (!port->guest_connected && !is_rproc_serial(port->portdev->vdev))
		discard_port_data(port);

	/* Send a SIGIO indicating new data in case the process asked for it */
	send_sigio_to_port(port);

	spin_unlock_irqrestore(&port->inbuf_lock, flags);

	wake_up_interruptible(&port->waitqueue);

	if (is_console_port(port) && hvc_poll(port->cons.hvc))
		hvc_kick();
}

Because unplug_port() cleared port->guest_connected, in_intr() evaluates
is_rproc_serial(port->portdev->vdev). If unplug_port() has already set
port->portdev = NULL under outvq_lock, this dereferences a NULL port->portdev
before reaching the !port->portdev check inside discard_port_data(). Even if
port->portdev is not yet NULL, discard_port_data() will call get_inbuf() and
add_inbuf() on the unplugging virtqueue (or while port->inbuf_claimed is
held), and in_intr() can then call hvc_poll(port->cons.hvc) after
hvc_remove().

Should in_intr() bail out when port->unplugging || !port->portdev is set?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261105114535.972013-1-shunsheng1218@gmail.com?part=1

      reply	other threads:[~2026-11-05 12:06 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-05 11:45 [PATCH v2] virtio_console: fix buffer ownership and unplug races in the read path Yaohan Li
2026-11-05 12:06 ` sashiko-bot [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-198992@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=eperezma@redhat.com \
    --cc=mst@redhat.com \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=shunsheng1218@gmail.com \
    --cc=virtualization@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox