On Tue, Jun 09, 2026 at 09:43:58AM -0400, Gavin Li wrote:
> commit a663b3c47ab1 ("i2c: virtio: Avoid hang by using interruptible
> completion wait") switched virtio_i2c_complete_reqs() to
> wait_for_completion_interruptible() so a stuck device cannot hang a
> task forever. That left a use-after-free: if the wait returns early on
> a signal, virtio_i2c_xfer() frees reqs and DMA bounce buffers while the
> device may still hold virtqueue tokens pointing at &reqs[i] and DMA
> into read buffers. When those requests complete later,
> virtio_i2c_msg_done() calls complete() on freed memory.
> 
> Waiting uninterruptibly for every completion before freeing avoids the
> UAF but can hang the caller indefinitely if the virtio side never
> completes the request. The virtio spec provides no way to cancel an
> in-flight transfer, so that is not an acceptable tradeoff.

except for a queue reset, or a device reset if that's not
available.

> This commit manages the freeing of the xfer allocations via kref, and
> ensures that each in-flight request holds a reference. This fixes the
> use-after-free by ensuring that the virtio device has a valid location
> to write to until the request completes. This will cause a memory leak
> in cases where the device hangs, but that is much preferable to memory
> corruption.
> 
> Additionally, force usage of a bounce buffer even if the i2c_msg buf is
> DMA-safe, since the buffer passed to the virtqueue must remain valid
> even if the transfer is interrupted. Remove usage of
> i2c_get_dma_safe_msg_buf() since it may pass through msg->buf directly.
> This results in an extra allocation+copy when I2C_M_DMA_SAFE is used.
> 
> Signed-off-by: Gavin Li <[email protected]>

It's possible that copying unconditionally is the best you could do.
Some vague ideas below but it's possible they are not practical.


> ---

pls start a new thread with each version, and include a changelog there.


>  drivers/i2c/busses/i2c-virtio.c | 110 ++++++++++++++++++++++++--------
>  1 file changed, 83 insertions(+), 27 deletions(-)
> 
> diff --git a/drivers/i2c/busses/i2c-virtio.c b/drivers/i2c/busses/i2c-virtio.c
> index 5da6fef92bec3..7ee96b08f6685 100644
> --- a/drivers/i2c/busses/i2c-virtio.c
> +++ b/drivers/i2c/busses/i2c-virtio.c
> @@ -13,6 +13,7 @@
>  #include <linux/err.h>
>  #include <linux/i2c.h>
>  #include <linux/kernel.h>
> +#include <linux/kref.h>
>  #include <linux/module.h>
>  #include <linux/virtio.h>
>  #include <linux/virtio_ids.h>
> @@ -31,39 +32,72 @@ struct virtio_i2c {
>       struct virtqueue *vq;
>  };
>  
> +struct virtio_i2c_xfer;
> +
>  /**
>   * struct virtio_i2c_req - the virtio I2C request structure
> + * @xfer: owning transfer
>   * @completion: completion of virtio I2C message
>   * @out_hdr: the OUT header of the virtio I2C message
> - * @buf: the buffer into which data is read, or from which it's written
> + * @buf: bounce buffer for i2c_msg.buf, set to NULL upon free
>   * @in_hdr: the IN header of the virtio I2C message
>   */
>  struct virtio_i2c_req {
> +     struct virtio_i2c_xfer *xfer;
>       struct completion completion;
>       struct virtio_i2c_out_hdr out_hdr       ____cacheline_aligned;
>       uint8_t *buf                            ____cacheline_aligned;
>       struct virtio_i2c_in_hdr in_hdr         ____cacheline_aligned;
>  };
>  
> +/**
> + * struct virtio_i2c_xfer - a queued I2C transfer
> + * @ref: one ref for the caller, plus one per in-flight virtqueue request
> + * @num: number of messages
> + * @reqs: the virtio I2C requests
> + */
> +struct virtio_i2c_xfer {
> +     struct kref ref;
> +     int num;
> +     struct virtio_i2c_req reqs[];
> +};
> +
> +static void virtio_i2c_xfer_release(struct kref *ref)
> +{
> +     struct virtio_i2c_xfer *xfer = container_of(ref, struct 
> virtio_i2c_xfer, ref);
> +     int i;
> +
> +     for (i = 0; i < xfer->num; i++) {
> +             struct virtio_i2c_req *req = &xfer->reqs[i];
> +             kfree(req->buf);
> +     }
> +
> +     kfree(xfer);
> +}
> +
>  static void virtio_i2c_msg_done(struct virtqueue *vq)
>  {
>       struct virtio_i2c_req *req;
>       unsigned int len;
>  
> -     while ((req = virtqueue_get_buf(vq, &len)))
> +     while ((req = virtqueue_get_buf(vq, &len))) {
>               complete(&req->completion);
> +             kref_put(&req->xfer->ref, virtio_i2c_xfer_release);
> +     }
>  }
>  
> -static int virtio_i2c_prepare_reqs(struct virtqueue *vq,
> -                                struct virtio_i2c_req *reqs,
> +static int virtio_i2c_prepare_xfer(struct virtqueue *vq,
> +                                struct virtio_i2c_xfer *xfer,
>                                  struct i2c_msg *msgs, int num)
>  {
>       struct scatterlist *sgs[3], out_hdr, msg_buf, in_hdr;
> +     struct virtio_i2c_req *reqs = xfer->reqs;
>       int i;
>  
>       for (i = 0; i < num; i++) {
>               int outcnt = 0, incnt = 0;
>  
> +             reqs[i].xfer = xfer;
>               init_completion(&reqs[i].completion);
>  
>               /*
> @@ -82,9 +116,16 @@ static int virtio_i2c_prepare_reqs(struct virtqueue *vq,
>               sgs[outcnt++] = &out_hdr;
>  
>               if (msgs[i].len) {
> -                     reqs[i].buf = i2c_get_dma_safe_msg_buf(&msgs[i], 1);
> +                     /*
> +                      * Even if msg->flags has I2C_M_DMA_SAFE set, a bounce
> +                      * buffer is required because the transfer may be
> +                      * interrupted, after which msg->buf is no longer valid.
> +                      */
> +                     reqs[i].buf = kzalloc(msgs[i].len, GFP_KERNEL);

is len always reasonably small?

>                       if (!reqs[i].buf)
>                               break;
> +                     if (!(msgs[i].flags & I2C_M_RD))
> +                             memcpy(reqs[i].buf, msgs[i].buf, msgs[i].len);
>  
>                       sg_init_one(&msg_buf, reqs[i].buf, msgs[i].len);
>

maybe there's some way to hang on to the original memory?


  
> @@ -97,38 +138,51 @@ static int virtio_i2c_prepare_reqs(struct virtqueue *vq,
>               sg_init_one(&in_hdr, &reqs[i].in_hdr, sizeof(reqs[i].in_hdr));
>               sgs[outcnt + incnt++] = &in_hdr;
>  
> +             /* This reference is released in virtio_i2c_msg_done(). */
> +             kref_get(&xfer->ref);
> +
>               if (virtqueue_add_sgs(vq, sgs, outcnt, incnt, &reqs[i], 
> GFP_KERNEL)) {
> -                     i2c_put_dma_safe_msg_buf(reqs[i].buf, &msgs[i], false);
> +                     kref_put(&xfer->ref, virtio_i2c_xfer_release);
> +
> +                     kfree(reqs[i].buf);
> +                     reqs[i].buf = NULL; /* prevent free by 
> virtio_i2c_xfer_release */
> +
>                       break;
>               }
>       }
>  
> +     xfer->num = i;
>       return i;
>  }
>  
> -static int virtio_i2c_complete_reqs(struct virtqueue *vq,
> -                                 struct virtio_i2c_req *reqs,
> -                                 struct i2c_msg *msgs, int num)
> +static int virtio_i2c_complete_xfer(struct virtio_i2c_xfer *xfer,
> +                                 struct i2c_msg *msgs)
>  {
> -     bool failed = false;
> -     int i, j = 0;
> +     struct virtio_i2c_req *reqs = xfer->reqs;
> +     int i, fail_index = -1;
>  
> -     for (i = 0; i < num; i++) {
> +     for (i = 0; i < xfer->num; i++) {
>               struct virtio_i2c_req *req = &reqs[i];
> +             struct i2c_msg *msg = &msgs[i];
>  
> -             if (!failed) {
> -                     if (wait_for_completion_interruptible(&req->completion))
> -                             failed = true;
> -                     else if (req->in_hdr.status != VIRTIO_I2C_MSG_OK)
> -                             failed = true;
> -                     else
> -                             j++;
> +             if (wait_for_completion_interruptible(&req->completion))
> +                     return -EINTR;
> +
> +             if (req->in_hdr.status != VIRTIO_I2C_MSG_OK) {
> +                     /* Don't break yet. Try to wait until all requests 
> complete. */
> +                     if (fail_index < 0)
> +                             fail_index = i;
>               }
>  
> -             i2c_put_dma_safe_msg_buf(reqs[i].buf, &msgs[i], !failed);
> +             if (fail_index < 0 && (msg->flags & I2C_M_RD))
> +                     memcpy(msg->buf, req->buf, msg->len);
> +
> +             kfree(req->buf);
> +             req->buf = NULL; /* prevent free by virtio_i2c_xfer_release */
>       }
>  
> -     return j;
> +     /* Return number of successful transactions */
> +     return fail_index >= 0 ? fail_index : xfer->num;
>  }
>  
>  static int virtio_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs,
> @@ -136,14 +190,16 @@ static int virtio_i2c_xfer(struct i2c_adapter *adap, 
> struct i2c_msg *msgs,
>  {
>       struct virtio_i2c *vi = i2c_get_adapdata(adap);
>       struct virtqueue *vq = vi->vq;
> -     struct virtio_i2c_req *reqs;
> +     struct virtio_i2c_xfer *xfer;
>       int count;
>  
> -     reqs = kzalloc_objs(*reqs, num);
> -     if (!reqs)
> +     xfer = kzalloc(struct_size(xfer, reqs, num), GFP_KERNEL);
> +     if (!xfer)
>               return -ENOMEM;
>  
> -     count = virtio_i2c_prepare_reqs(vq, reqs, msgs, num);
> +     kref_init(&xfer->ref);
> +
> +     count = virtio_i2c_prepare_xfer(vq, xfer, msgs, num);
>       if (!count)
>               goto err_free;
>  
> @@ -157,10 +213,10 @@ static int virtio_i2c_xfer(struct i2c_adapter *adap, 
> struct i2c_msg *msgs,
>        */
>       virtqueue_kick(vq);
>  
> -     count = virtio_i2c_complete_reqs(vq, reqs, msgs, count);
> +     count = virtio_i2c_complete_xfer(xfer, msgs);
>  
>  err_free:
> -     kfree(reqs);
> +     kref_put(&xfer->ref, virtio_i2c_xfer_release);
>       return count;
>  }
>  
> -- 
> 2.54.0
> 


Reply via email to