* [PATCH v1] i2c: virtio: wait uninterruptibly for completions to avoid UAF
@ 2026-06-07 14:36 Gavin Li
2026-06-08 3:44 ` Viresh Kumar
` (2 more replies)
0 siblings, 3 replies; 9+ messages in thread
From: Gavin Li @ 2026-06-07 14:36 UTC (permalink / raw)
To: linux-i2c, viresh.kumar
Cc: Chen, Jian Jun, andi.shyti, virtualization, Gavin Li
virtio_i2c_complete_reqs() uses wait_for_completion_interruptible() and stops
waiting when a signal arrives. virtio_i2c_xfer() then frees reqs and the
per-request DMA bounce buffers while the device may still hold virtqueue tokens
pointing at &reqs[i] and DMA into read bounce buffers. Additionally, when the
device later completes those requests, virtio_i2c_msg_done() calls complete()
on freed memory and can corrupt the slab freelist.
Wait uninterruptibly for every completion before freeing reqs. This
matches how other virtio drivers retain request storage until the device
completes it. The virtio spec unfortunately does not provide a way to
cancel an in-flight request, so waiting uninterruptibly is required.
Signed-off-by: Gavin Li <gavin.li@samsara.com>
---
drivers/i2c/busses/i2c-virtio.c | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/drivers/i2c/busses/i2c-virtio.c b/drivers/i2c/busses/i2c-virtio.c
index 5da6fef92bec3..12acc049f5999 100644
--- a/drivers/i2c/busses/i2c-virtio.c
+++ b/drivers/i2c/busses/i2c-virtio.c
@@ -116,14 +116,13 @@ static int virtio_i2c_complete_reqs(struct virtqueue *vq,
for (i = 0; i < num; i++) {
struct virtio_i2c_req *req = &reqs[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++;
- }
+ /* Wait uninterruptibly: device still owns token/bounce buf until completion. */
+ wait_for_completion(&req->completion);
+
+ if (!failed)
+ failed = req->in_hdr.status != VIRTIO_I2C_MSG_OK;
+ if (!failed)
+ j++;
i2c_put_dma_safe_msg_buf(reqs[i].buf, &msgs[i], !failed);
}
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* Re: [PATCH v1] i2c: virtio: wait uninterruptibly for completions to avoid UAF 2026-06-07 14:36 [PATCH v1] i2c: virtio: wait uninterruptibly for completions to avoid UAF Gavin Li @ 2026-06-08 3:44 ` Viresh Kumar 2026-06-08 17:46 ` Gavin Li 2026-06-08 17:44 ` [PATCH v2] i2c: virtio: retain xfer with kref to fix UAF on interrupted wait Gavin Li 2026-06-09 13:43 ` [PATCH v3] " Gavin Li 2 siblings, 1 reply; 9+ messages in thread From: Viresh Kumar @ 2026-06-08 3:44 UTC (permalink / raw) To: Gavin Li; +Cc: linux-i2c, Chen, Jian Jun, andi.shyti, virtualization On 07-06-26, 10:36, Gavin Li wrote: > virtio_i2c_complete_reqs() uses wait_for_completion_interruptible() and stops > waiting when a signal arrives. virtio_i2c_xfer() then frees reqs and the > per-request DMA bounce buffers while the device may still hold virtqueue tokens > pointing at &reqs[i] and DMA into read bounce buffers. Additionally, when the > device later completes those requests, virtio_i2c_msg_done() calls complete() > on freed memory and can corrupt the slab freelist. > > Wait uninterruptibly for every completion before freeing reqs. This > matches how other virtio drivers retain request storage until the device > completes it. The virtio spec unfortunately does not provide a way to > cancel an in-flight request, so waiting uninterruptibly is required. > > Signed-off-by: Gavin Li <gavin.li@samsara.com> > --- > drivers/i2c/busses/i2c-virtio.c | 15 +++++++-------- > 1 file changed, 7 insertions(+), 8 deletions(-) This is a revert of (and maybe better if that is mentioned in the logs): commit a663b3c47ab1 ("i2c: virtio: Avoid hang by using interruptible completion wait") I don't think this is the right approach here. We shouldn't hang the kernel indefinitely if the other side is dead. -- viresh ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v1] i2c: virtio: wait uninterruptibly for completions to avoid UAF 2026-06-08 3:44 ` Viresh Kumar @ 2026-06-08 17:46 ` Gavin Li 0 siblings, 0 replies; 9+ messages in thread From: Gavin Li @ 2026-06-08 17:46 UTC (permalink / raw) To: Viresh Kumar; +Cc: linux-i2c, Chen, Jian Jun, andi.shyti, virtualization Noted. I have sent a v2 patch that allows a hung process to be killed. On Sun, Jun 7, 2026 at 11:44 PM Viresh Kumar <viresh.kumar@linaro.org> wrote: > > On 07-06-26, 10:36, Gavin Li wrote: > > virtio_i2c_complete_reqs() uses wait_for_completion_interruptible() and stops > > waiting when a signal arrives. virtio_i2c_xfer() then frees reqs and the > > per-request DMA bounce buffers while the device may still hold virtqueue tokens > > pointing at &reqs[i] and DMA into read bounce buffers. Additionally, when the > > device later completes those requests, virtio_i2c_msg_done() calls complete() > > on freed memory and can corrupt the slab freelist. > > > > Wait uninterruptibly for every completion before freeing reqs. This > > matches how other virtio drivers retain request storage until the device > > completes it. The virtio spec unfortunately does not provide a way to > > cancel an in-flight request, so waiting uninterruptibly is required. > > > > Signed-off-by: Gavin Li <gavin.li@samsara.com> > > --- > > drivers/i2c/busses/i2c-virtio.c | 15 +++++++-------- > > 1 file changed, 7 insertions(+), 8 deletions(-) > > This is a revert of (and maybe better if that is mentioned in the logs): > > commit a663b3c47ab1 ("i2c: virtio: Avoid hang by using interruptible completion wait") > > I don't think this is the right approach here. We shouldn't hang the kernel > indefinitely if the other side is dead. > > -- > viresh ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2] i2c: virtio: retain xfer with kref to fix UAF on interrupted wait 2026-06-07 14:36 [PATCH v1] i2c: virtio: wait uninterruptibly for completions to avoid UAF Gavin Li 2026-06-08 3:44 ` Viresh Kumar @ 2026-06-08 17:44 ` Gavin Li 2026-06-09 7:35 ` Viresh Kumar 2026-06-09 13:43 ` [PATCH v3] " Gavin Li 2 siblings, 1 reply; 9+ messages in thread From: Gavin Li @ 2026-06-08 17:44 UTC (permalink / raw) To: linux-i2c, viresh.kumar Cc: Chen, Jian Jun, andi.shyti, virtualization, Gavin Li 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. This commit makes two changes: - Manage the freeing of the xfer allocations via kref, and ensure 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. - Use wait_for_completion_killable() instead of _interruptible(). Even partial I2C transactions can have side effects, so the only time it makes sense to interrupt a transaction is when a process needs to be killed. Most existing I2C drivers don't support interruption at all, so this should not break userspace applications. This also addresses issues with Go programs accessing devices via the I2C userspace API, since the Go runtime stochastically signals SIGURG to running threads; leaving this as _interruptible() may cause partial side effects from which it is impossible to cleanly restart. Signed-off-by: Gavin Li <gavin.li@samsara.com> --- drivers/i2c/busses/i2c-virtio.c | 89 ++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/drivers/i2c/busses/i2c-virtio.c b/drivers/i2c/busses/i2c-virtio.c index 726c162cabd86..f7320a67a3409 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,77 @@ struct virtio_i2c { struct virtqueue *vq; }; +struct virtio_i2c_xfer; + /** * struct virtio_i2c_req - the virtio I2C request structure + * @xfer: owning transfer + * @msg: copy of the I2C message for virtio_i2c_xfer_release * @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 * @in_hdr: the IN header of the virtio I2C message */ struct virtio_i2c_req { + struct virtio_i2c_xfer *xfer; + struct i2c_msg msg; 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]; + i2c_put_dma_safe_msg_buf(req->buf, &req->msg, false); + } + + 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, + 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; + kref_init(&xfer->ref); + for (i = 0; i < num; i++) { int outcnt = 0, incnt = 0; + reqs[i].xfer = xfer; + reqs[i].msg = msgs[i]; init_completion(&reqs[i].completion); /* @@ -99,36 +138,36 @@ static int virtio_i2c_prepare_reqs(struct virtqueue *vq, if (virtqueue_add_sgs(vq, sgs, outcnt, incnt, &reqs[i], GFP_KERNEL)) { i2c_put_dma_safe_msg_buf(reqs[i].buf, &msgs[i], false); + reqs[i].buf = NULL; /* prevent free by virtio_i2c_xfer_release */ break; } + + kref_get(&xfer->ref); /* released in virtio_i2c_msg_done() */ } + 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_reqs(struct virtio_i2c_xfer *xfer) { - 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]; - - 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_killable(&req->completion)) { + return -EINTR; + } else 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); + i2c_put_dma_safe_msg_buf(req->buf, &req->msg, fail_index < 0); + req->buf = NULL; /* prevent free by virtio_i2c_xfer_release */ } - return j; + return fail_index >= 0 ? fail_index : xfer->num; /* number of successful transactions */ } static int virtio_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, @@ -136,14 +175,14 @@ 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 = kcalloc(num, sizeof(*reqs), GFP_KERNEL); - if (!reqs) + xfer = kzalloc(struct_size(xfer, reqs, num), GFP_KERNEL); + if (!xfer) return -ENOMEM; - count = virtio_i2c_prepare_reqs(vq, reqs, msgs, num); + count = virtio_i2c_prepare_reqs(vq, xfer, msgs, num); if (!count) goto err_free; @@ -157,10 +196,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_reqs(xfer); err_free: - kfree(reqs); + kref_put(&xfer->ref, virtio_i2c_xfer_release); return count; } -- 2.54.0 ^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH v2] i2c: virtio: retain xfer with kref to fix UAF on interrupted wait 2026-06-08 17:44 ` [PATCH v2] i2c: virtio: retain xfer with kref to fix UAF on interrupted wait Gavin Li @ 2026-06-09 7:35 ` Viresh Kumar 2026-06-09 13:52 ` Gavin Li 0 siblings, 1 reply; 9+ messages in thread From: Viresh Kumar @ 2026-06-09 7:35 UTC (permalink / raw) To: Gavin Li; +Cc: linux-i2c, Chen, Jian Jun, andi.shyti, virtualization On 08-06-26, 13:44, 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. > > This commit makes two changes: > > - Manage the freeing of the xfer allocations via kref, and ensure 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. > > - Use wait_for_completion_killable() instead of _interruptible(). Even > partial I2C transactions can have side effects, so the only time it > makes sense to interrupt a transaction is when a process needs to be > killed. Most existing I2C drivers don't support interruption at all, > so this should not break userspace applications. This also addresses > issues with Go programs accessing devices via the I2C userspace API, > since the Go runtime stochastically signals SIGURG to running threads; > leaving this as _interruptible() may cause partial side effects from > which it is impossible to cleanly restart. > > Signed-off-by: Gavin Li <gavin.li@samsara.com> > --- > drivers/i2c/busses/i2c-virtio.c | 89 ++++++++++++++++++++++++--------- > 1 file changed, 64 insertions(+), 25 deletions(-) > > diff --git a/drivers/i2c/busses/i2c-virtio.c b/drivers/i2c/busses/i2c-virtio.c > index 726c162cabd86..f7320a67a3409 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,77 @@ struct virtio_i2c { > struct virtqueue *vq; > }; > > +struct virtio_i2c_xfer; > + > /** > * struct virtio_i2c_req - the virtio I2C request structure > + * @xfer: owning transfer > + * @msg: copy of the I2C message for virtio_i2c_xfer_release > * @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 > * @in_hdr: the IN header of the virtio I2C message > */ > struct virtio_i2c_req { > + struct virtio_i2c_xfer *xfer; > + struct i2c_msg msg; > 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]; > + i2c_put_dma_safe_msg_buf(req->buf, &req->msg, false); > + } > + > + 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, > + 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; > > + kref_init(&xfer->ref); > + > for (i = 0; i < num; i++) { > int outcnt = 0, incnt = 0; > > + reqs[i].xfer = xfer; > + reqs[i].msg = msgs[i]; > init_completion(&reqs[i].completion); > > /* > @@ -99,36 +138,36 @@ static int virtio_i2c_prepare_reqs(struct virtqueue *vq, > > if (virtqueue_add_sgs(vq, sgs, outcnt, incnt, &reqs[i], GFP_KERNEL)) { > i2c_put_dma_safe_msg_buf(reqs[i].buf, &msgs[i], false); > + reqs[i].buf = NULL; /* prevent free by virtio_i2c_xfer_release */ > break; > } > + > + kref_get(&xfer->ref); /* released in virtio_i2c_msg_done() */ Maybe move the comment above the code ? Can be dropped too. Also, maybe there is a small race here, not sure. What if the other side (polls and) processes the message as soon as it is added to the queue with virtqueue_add_sgs() ? In that case virtio_i2c_msg_done() will call complete (which won't harm) and kref_put(). If this happens for the first req of the xfer, it may end up freeing the xfer while being used here ? > } > > + 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_reqs(struct virtio_i2c_xfer *xfer) Maybe rename to complete_xfer now ? > { > - 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]; > - > - 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_killable(&req->completion)) { Maybe do this in a separate patch ? > + return -EINTR; > + } else 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); > + i2c_put_dma_safe_msg_buf(req->buf, &req->msg, fail_index < 0); > + req->buf = NULL; /* prevent free by virtio_i2c_xfer_release */ > } > > - return j; > + return fail_index >= 0 ? fail_index : xfer->num; /* number of successful transactions */ If this comment is required, maybe add it above the line instead. > } > > static int virtio_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, > @@ -136,14 +175,14 @@ 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 = kcalloc(num, sizeof(*reqs), GFP_KERNEL); > - if (!reqs) > + xfer = kzalloc(struct_size(xfer, reqs, num), GFP_KERNEL); > + if (!xfer) > return -ENOMEM; > > - count = virtio_i2c_prepare_reqs(vq, reqs, msgs, num); > + count = virtio_i2c_prepare_reqs(vq, xfer, msgs, num); > if (!count) > goto err_free; > > @@ -157,10 +196,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_reqs(xfer); > > err_free: > - kfree(reqs); > + kref_put(&xfer->ref, virtio_i2c_xfer_release); > return count; > } Nice work Gavin. -- viresh ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2] i2c: virtio: retain xfer with kref to fix UAF on interrupted wait 2026-06-09 7:35 ` Viresh Kumar @ 2026-06-09 13:52 ` Gavin Li 0 siblings, 0 replies; 9+ messages in thread From: Gavin Li @ 2026-06-09 13:52 UTC (permalink / raw) To: Viresh Kumar; +Cc: linux-i2c, Chen, Jian Jun, andi.shyti, virtualization Thanks for the review, Viresh! On Tue, Jun 9, 2026 at 3:35 AM Viresh Kumar <viresh.kumar@linaro.org> wrote: > Maybe move the comment above the code ? Can be dropped too. > > Also, maybe there is a small race here, not sure. What if the other > side (polls and) processes the message as soon as it is added to the > queue with virtqueue_add_sgs() ? In that case virtio_i2c_msg_done() > will call complete (which won't harm) and kref_put(). If this happens > for the first req of the xfer, it may end up freeing the xfer while > being used here ? Good eye, thanks for the catch. Moved kref_get() to before virtqueue_add_sgs() > > -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_reqs(struct virtio_i2c_xfer *xfer) > > Maybe rename to complete_xfer now ? Done > > - 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_killable(&req->completion)) { > > Maybe do this in a separate patch ? Good idea, reverted to wait_for_completion_interruptible() > > - return j; > > + return fail_index >= 0 ? fail_index : xfer->num; /* number of successful transactions */ > > If this comment is required, maybe add it above the line instead. Done ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v3] i2c: virtio: retain xfer with kref to fix UAF on interrupted wait 2026-06-07 14:36 [PATCH v1] i2c: virtio: wait uninterruptibly for completions to avoid UAF Gavin Li 2026-06-08 3:44 ` Viresh Kumar 2026-06-08 17:44 ` [PATCH v2] i2c: virtio: retain xfer with kref to fix UAF on interrupted wait Gavin Li @ 2026-06-09 13:43 ` Gavin Li 2026-06-10 6:03 ` Viresh Kumar 2026-06-10 6:50 ` Michael S. Tsirkin 2 siblings, 2 replies; 9+ messages in thread From: Gavin Li @ 2026-06-09 13:43 UTC (permalink / raw) To: linux-i2c, viresh.kumar Cc: Chen, Jian Jun, andi.shyti, virtualization, Gavin Li 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. 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 <gavin.li@samsara.com> --- 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); 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); @@ -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 ^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH v3] i2c: virtio: retain xfer with kref to fix UAF on interrupted wait 2026-06-09 13:43 ` [PATCH v3] " Gavin Li @ 2026-06-10 6:03 ` Viresh Kumar 2026-06-10 6:50 ` Michael S. Tsirkin 1 sibling, 0 replies; 9+ messages in thread From: Viresh Kumar @ 2026-06-10 6:03 UTC (permalink / raw) To: Gavin Li Cc: linux-i2c, Chen, Jian Jun, andi.shyti, virtualization, Wolfram Sang, Michael S. Tsirkin, Jason Wang Ccing I2C/Virtio maintainers. On 09-06-26, 09:43, Gavin Li wrote: > 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. Please always create separate patches for such changes. It would be better to not fix multiple issues in the same patch. That would also allow for a clean revert of the change, just in case. > @@ -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); > 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); I don't think this is acceptable, bringing the performance down because the remote device may misbehave. Maybe its time for I2C and Virtio maintainers to provide some feedback here. -- viresh ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v3] i2c: virtio: retain xfer with kref to fix UAF on interrupted wait 2026-06-09 13:43 ` [PATCH v3] " Gavin Li 2026-06-10 6:03 ` Viresh Kumar @ 2026-06-10 6:50 ` Michael S. Tsirkin 1 sibling, 0 replies; 9+ messages in thread From: Michael S. Tsirkin @ 2026-06-10 6:50 UTC (permalink / raw) To: Gavin Li Cc: linux-i2c, viresh.kumar, Chen, Jian Jun, andi.shyti, virtualization 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 <gavin.li@samsara.com> 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 > ^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-06-10 6:50 UTC | newest] Thread overview: 9+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-06-07 14:36 [PATCH v1] i2c: virtio: wait uninterruptibly for completions to avoid UAF Gavin Li 2026-06-08 3:44 ` Viresh Kumar 2026-06-08 17:46 ` Gavin Li 2026-06-08 17:44 ` [PATCH v2] i2c: virtio: retain xfer with kref to fix UAF on interrupted wait Gavin Li 2026-06-09 7:35 ` Viresh Kumar 2026-06-09 13:52 ` Gavin Li 2026-06-09 13:43 ` [PATCH v3] " Gavin Li 2026-06-10 6:03 ` Viresh Kumar 2026-06-10 6:50 ` Michael S. Tsirkin
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.