From: Sungho Bae <baver.bae@gmail.com>
To: amit@kernel.org, arnd@arndb.de, gregkh@linuxfoundation.org
Cc: virtualization@lists.linux.dev, linux-kernel@vger.kernel.org,
Sungho Bae <baver.bae@lge.com>
Subject: [PATCH v3 1/4] virtio_console: refactor __send_to_port() buffer ownership
Date: Thu, 4 Jun 2026 03:37:54 +0900 [thread overview]
Message-ID: <20260603183757.21587-2-baver.bae@gmail.com> (raw)
In-Reply-To: <20260603183757.21587-1-baver.bae@gmail.com>
From: Sungho Bae <baver.bae@lge.com>
Modify __send_to_port() to take ownership of a struct port_buffer *
instead of a void * raw buffer.
Previously, put_chars() would pass a raw kmemdup'd buffer and free it
immediately after __send_to_port() returned. This caused a potential
Use-After-Free and data corruption if the virtqueue was shared with
nonblocking writers, as virtqueue_get_buf() might return an older
completed buffer, causing the newly added buffer to be kfree'd while the
host is still DMAing from it.
By transferring ownership of the allocated port_buffer to __send_to_port(),
we ensure that the exact buffer returned by the host is the one that gets
freed, resolving the memory lifecycle mismatch.
Signed-off-by: Sungho Bae <baver.bae@lge.com>
---
drivers/char/virtio_console.c | 69 +++++++++++++++++++----------------
1 file changed, 37 insertions(+), 32 deletions(-)
diff --git a/drivers/char/virtio_console.c b/drivers/char/virtio_console.c
index 9a33217c68d9..bbf5b3825f12 100644
--- a/drivers/char/virtio_console.c
+++ b/drivers/char/virtio_console.c
@@ -402,7 +402,7 @@ static void reclaim_dma_bufs(void)
}
static struct port_buffer *alloc_buf(struct virtio_device *vdev, size_t buf_size,
- int pages)
+ int pages, gfp_t gfp)
{
struct port_buffer *buf;
@@ -436,11 +436,10 @@ static struct port_buffer *alloc_buf(struct virtio_device *vdev, size_t buf_size
/* Increase device refcnt to avoid freeing it */
get_device(buf->dev);
- buf->buf = dma_alloc_coherent(buf->dev, buf_size, &buf->dma,
- GFP_KERNEL);
+ buf->buf = dma_alloc_coherent(buf->dev, buf_size, &buf->dma, gfp);
} else {
buf->dev = NULL;
- buf->buf = kmalloc(buf_size, GFP_KERNEL);
+ buf->buf = kmalloc(buf_size, gfp);
}
if (!buf->buf)
@@ -595,7 +594,7 @@ static void reclaim_consumed_buffers(struct port *port)
static ssize_t __send_to_port(struct port *port, struct scatterlist *sg,
int nents, size_t in_count,
- void *data, bool nonblock)
+ struct port_buffer *buf, bool nonblock)
{
struct virtqueue *out_vq;
int err;
@@ -608,14 +607,14 @@ static ssize_t __send_to_port(struct port *port, struct scatterlist *sg,
reclaim_consumed_buffers(port);
- err = virtqueue_add_outbuf(out_vq, sg, nents, data, GFP_ATOMIC);
+ err = virtqueue_add_outbuf(out_vq, sg, nents, buf, GFP_ATOMIC);
/* Tell Host to go! */
virtqueue_kick(out_vq);
if (err) {
in_count = 0;
- goto done;
+ goto free_and_done;
}
if (out_vq->num_free == 0)
@@ -632,10 +631,19 @@ static ssize_t __send_to_port(struct port *port, struct scatterlist *sg,
* buffer and relax the spinning requirement. The downside is
* we need to kmalloc a GFP_ATOMIC buffer each time the
* console driver writes something out.
+ *
+ * Spin until host returns the buffer.
+ * Capture the returned buf so we can free it.
+ * If broken, buf == NULL and buf stays in the vq;
+ * remove_vqs() will call virtqueue_detach_unused_buf() -> free_buf().
*/
- while (!virtqueue_get_buf(out_vq, &len)
+ while (!(buf = virtqueue_get_buf(out_vq, &len))
&& !virtqueue_is_broken(out_vq))
cpu_relax();
+
+free_and_done:
+ if (buf)
+ free_buf(buf, false);
done:
spin_unlock_irqrestore(&port->outvq_lock, flags);
@@ -816,14 +824,14 @@ static ssize_t port_fops_write(struct file *filp, const char __user *ubuf,
count = min((size_t)(32 * 1024), count);
- buf = alloc_buf(port->portdev->vdev, count, 0);
+ buf = alloc_buf(port->portdev->vdev, count, 0, GFP_KERNEL);
if (!buf)
return -ENOMEM;
ret = copy_from_user(buf->buf, ubuf, count);
if (ret) {
- ret = -EFAULT;
- goto free_buf;
+ free_buf(buf, true);
+ return -EFAULT;
}
/*
@@ -835,15 +843,7 @@ static ssize_t port_fops_write(struct file *filp, const char __user *ubuf,
*/
nonblock = true;
sg_init_one(sg, buf->buf, count);
- ret = __send_to_port(port, sg, 1, count, buf, nonblock);
-
- if (nonblock && ret > 0)
- goto out;
-
-free_buf:
- free_buf(buf, true);
-out:
- return ret;
+ return __send_to_port(port, sg, 1, count, buf, nonblock);
}
struct sg_list {
@@ -932,7 +932,7 @@ static ssize_t port_fops_splice_write(struct pipe_inode_info *pipe,
goto error_out;
occupancy = pipe_buf_usage(pipe);
- buf = alloc_buf(port->portdev->vdev, 0, occupancy);
+ buf = alloc_buf(port->portdev->vdev, 0, occupancy, GFP_KERNEL);
if (!buf) {
ret = -ENOMEM;
@@ -946,11 +946,12 @@ static ssize_t port_fops_splice_write(struct pipe_inode_info *pipe,
sg_init_table(sgl.sg, sgl.size);
ret = __splice_from_pipe(pipe, &sd, pipe_to_sg);
pipe_unlock(pipe);
+
if (likely(ret > 0))
ret = __send_to_port(port, buf->sg, sgl.n, sgl.len, buf, true);
-
- if (unlikely(ret <= 0))
+ else
free_buf(buf, true);
+
return ret;
error_out:
@@ -1108,21 +1109,25 @@ static ssize_t put_chars(u32 vtermno, const u8 *buf, size_t count)
{
struct port *port;
struct scatterlist sg[1];
- void *data;
- int ret;
+ struct port_buffer *pbuf;
port = find_port_by_vtermno(vtermno);
if (!port)
return -EPIPE;
- data = kmemdup(buf, count, GFP_ATOMIC);
- if (!data)
+ pbuf = alloc_buf(port->portdev->vdev, count, 0, GFP_ATOMIC);
+ if (!pbuf)
return -ENOMEM;
- sg_init_one(sg, data, count);
- ret = __send_to_port(port, sg, 1, count, data, false);
- kfree(data);
- return ret;
+ memcpy(pbuf->buf, buf, count);
+ pbuf->len = count;
+ sg_init_one(sg, pbuf->buf, count);
+
+ /*
+ * Ownership of pbuf is transferred to __send_to_port().
+ * Do not touch or free pbuf after this call.
+ */
+ return __send_to_port(port, sg, 1, count, pbuf, false);
}
/*
@@ -1295,7 +1300,7 @@ static int fill_queue(struct virtqueue *vq, spinlock_t *lock)
nr_added_bufs = 0;
do {
- buf = alloc_buf(vq->vdev, PAGE_SIZE, 0);
+ buf = alloc_buf(vq->vdev, PAGE_SIZE, 0, GFP_KERNEL);
if (!buf)
return -ENOMEM;
--
2.34.1
next prev parent reply other threads:[~2026-06-03 18:39 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-03 18:37 [PATCH v3 0/4] virtio_console: fix suspend/resume and hot-unplug races Sungho Bae
2026-06-03 18:37 ` Sungho Bae [this message]
2026-06-03 18:37 ` [PATCH v3 2/4] virtio_console: fix hot-unplug races in TX paths Sungho Bae
2026-06-03 18:37 ` [PATCH v3 3/4] virtio_console: fix control queue race during restore Sungho Bae
2026-06-03 18:37 ` [PATCH v3 4/4] virtio_console: fix race between hvc put_chars and virtqueue teardown on freeze Sungho Bae
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=20260603183757.21587-2-baver.bae@gmail.com \
--to=baver.bae@gmail.com \
--cc=amit@kernel.org \
--cc=arnd@arndb.de \
--cc=baver.bae@lge.com \
--cc=gregkh@linuxfoundation.org \
--cc=linux-kernel@vger.kernel.org \
--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