mbox series

[0/3] virtio: restore elem->in/out_sg after iov_discard_front/back()

Message ID 20200812104918.107116-1-stefanha@redhat.com
Headers show
Series virtio: restore elem->in/out_sg after iov_discard_front/back() | expand

Message

Stefan Hajnoczi Aug. 12, 2020, 10:49 a.m. UTC
Both virtio-blk and virtio-crypto use destructive iov_discard_front/back()
operations on elem->in/out_sg. virtqueue_push() calls dma_memory_unmap() on t=
he
modified iovec arrays. The memory addresses may not match those originally
mapped with dma_memory_map().

This raises several issues:
1. MemoryRegion references can be leaked.
2. Dirty memory may not be tracked.
3. The non-RAM bounce buffer can be leaked.

This patch series solves the issue in two ways:
1. virtio-blk uses a new iov_discard_undo() API to restore iovec arrays.
2. virtio-crypto uses g_memdup() to avoid modifying the original iovec arrays.

The g_memdup() approach is slower than iov_discard_undo() but less
complex/fragile. I am less familiar with the virtio-crypto code and it uses
more complex sequences of iov_discard_front/back() calls than virtio-blk. If
anyone feels like optimizing virtio-crypto, please go ahead.

The virtio-blk bug was found by Alexander Bulekov's fuzzing effort. I found t=
he
virtio-crypto bug through code inspection.

Stefan Hajnoczi (3):
  util/iov: add iov_discard_undo()
  virtio-blk: undo destructive iov_discard_*() operations
  virtio-crypto: don't modify elem->in/out_sg

 include/hw/virtio/virtio-blk.h |   2 +
 include/qemu/iov.h             |  23 +++++
 hw/block/virtio-blk.c          |   9 +-
 hw/virtio/virtio-crypto.c      |  17 +++-
 tests/test-iov.c               | 165 +++++++++++++++++++++++++++++++++
 util/iov.c                     |  50 +++++++++-
 6 files changed, 257 insertions(+), 9 deletions(-)

--=20
2.26.2

Comments

Stefan Hajnoczi Sept. 16, 2020, 10:09 a.m. UTC | #1
On Sun, Aug 16, 2020 at 04:26:45PM +0800, Li Qiang wrote:
> Stefan Hajnoczi <stefanha@redhat.com> 于2020年8月12日周三 下午6:52写道:

Thanks for your review!

> > +    /* Discard more bytes than vector size */
> > +    iov_random(&iov, &iov_cnt);
> > +    iov_orig = g_memdup(iov, sizeof(iov[0]) * iov_cnt);
> > +    iov_tmp = iov;
> > +    iov_cnt_tmp = iov_cnt;
> > +    size = iov_size(iov, iov_cnt);
> > +    iov_discard_front_undoable(&iov_tmp, &iov_cnt_tmp, size + 1, &undo);
> > +    iov_discard_undo(&undo);
> > +    assert(iov_equals(iov, iov_orig, iov_cnt));
> >
> 
> The 'iov_discard_front_undoable' will change the 'iov_tmp' it will not
> touch 'iov_orig'.
> So the test will be always passed. The actually function will not be tested.

The test verifies that the iovec elements are restored to their previous
state by iov_discard_undo().

I think you are saying you'd like iov_discard_undo() to reset the
iov_tmp pointer? Currently that is not how the API works. The caller is
assumed to have the original pointer (e.g. virtio-blk has
req->elem.in/out_sg) and therefore it is not necessary to reset iov_tmp.

> Also, maybe we could abstract a function to do these discard test?

The structure of the test cases is similar but they vary in different
places. I'm not sure if this can be abstracted nicely.

> > diff --git a/util/iov.c b/util/iov.c
> > index 45ef3043ee..efcf04b445 100644
> > --- a/util/iov.c
> > +++ b/util/iov.c
> > @@ -636,14 +636,33 @@ void qemu_iovec_clone(QEMUIOVector *dest, const
> > QEMUIOVector *src, void *buf)
> >      }
> >  }
> >
> > -size_t iov_discard_front(struct iovec **iov, unsigned int *iov_cnt,
> > -                         size_t bytes)
> > +void iov_discard_undo(IOVDiscardUndo *undo)
> > +{
> > +    /* Restore original iovec if it was modified */
> > +    if (undo->modified_iov) {
> > +        *undo->modified_iov = undo->orig;
> > +    }
> > +}
> > +
> > +size_t iov_discard_front_undoable(struct iovec **iov,
> > +                                  unsigned int *iov_cnt,
> > +                                  size_t bytes,
> > +                                  IOVDiscardUndo *undo)
> >  {
> >      size_t total = 0;
> >      struct iovec *cur;
> >
> > +    if (undo) {
> > +        undo->modified_iov = NULL;
> > +    }
> > +
> >      for (cur = *iov; *iov_cnt > 0; cur++) {
> >          if (cur->iov_len > bytes) {
> > +            if (undo) {
> > +                undo->modified_iov = cur;
> > +                undo->orig = *cur;
> > +            }
> > +
> >
> 
> Why here we remember the 'cur'? 'cur' is the some of the 'iov'.
> Maybe we remember the 'iov'. I think we need the undo structure like this:
> 
> struct IOVUndo {
>     iov **modified_iov;
>     iov *orig;
> };
> 
> Then we can remeber the origin 'iov'.

Yes, this could be done but it's not needed (yet?). VirtQueueElement has
the original struct iovec pointers so adding this would be redundant.
Li Qiang Sept. 16, 2020, 3:36 p.m. UTC | #2
Stefan Hajnoczi <stefanha@redhat.com> 于2020年9月16日周三 下午6:09写道:
>

> On Sun, Aug 16, 2020 at 04:26:45PM +0800, Li Qiang wrote:

> > Stefan Hajnoczi <stefanha@redhat.com> 于2020年8月12日周三 下午6:52写道:

>

> Thanks for your review!

>

> > > +    /* Discard more bytes than vector size */

> > > +    iov_random(&iov, &iov_cnt);

> > > +    iov_orig = g_memdup(iov, sizeof(iov[0]) * iov_cnt);

> > > +    iov_tmp = iov;

> > > +    iov_cnt_tmp = iov_cnt;

> > > +    size = iov_size(iov, iov_cnt);

> > > +    iov_discard_front_undoable(&iov_tmp, &iov_cnt_tmp, size + 1, &undo);

> > > +    iov_discard_undo(&undo);

> > > +    assert(iov_equals(iov, iov_orig, iov_cnt));

> > >

> >

> > The 'iov_discard_front_undoable' will change the 'iov_tmp' it will not

> > touch 'iov_orig'.

> > So the test will be always passed. The actually function will not be tested.

>

> The test verifies that the iovec elements are restored to their previous

> state by iov_discard_undo().

>

> I think you are saying you'd like iov_discard_undo() to reset the

> iov_tmp pointer? Currently that is not how the API works. The caller is

> assumed to have the original pointer (e.g. virtio-blk has

> req->elem.in/out_sg) and therefore it is not necessary to reset iov_tmp.

>


Hi Stefan,

You're right, I just have the idea in my mind the the 'discard'
function change the *iov, but the caller have the origin pointer.
So

Reviewed-by: Li Qiang <liq3ea@gmail.com>


> > Also, maybe we could abstract a function to do these discard test?

>

> The structure of the test cases is similar but they vary in different

> places. I'm not sure if this can be abstracted nicely.

>

> > > diff --git a/util/iov.c b/util/iov.c

> > > index 45ef3043ee..efcf04b445 100644

> > > --- a/util/iov.c

> > > +++ b/util/iov.c

> > > @@ -636,14 +636,33 @@ void qemu_iovec_clone(QEMUIOVector *dest, const

> > > QEMUIOVector *src, void *buf)

> > >      }

> > >  }

> > >

> > > -size_t iov_discard_front(struct iovec **iov, unsigned int *iov_cnt,

> > > -                         size_t bytes)

> > > +void iov_discard_undo(IOVDiscardUndo *undo)

> > > +{

> > > +    /* Restore original iovec if it was modified */

> > > +    if (undo->modified_iov) {

> > > +        *undo->modified_iov = undo->orig;

> > > +    }

> > > +}

> > > +

> > > +size_t iov_discard_front_undoable(struct iovec **iov,

> > > +                                  unsigned int *iov_cnt,

> > > +                                  size_t bytes,

> > > +                                  IOVDiscardUndo *undo)

> > >  {

> > >      size_t total = 0;

> > >      struct iovec *cur;

> > >

> > > +    if (undo) {

> > > +        undo->modified_iov = NULL;

> > > +    }

> > > +

> > >      for (cur = *iov; *iov_cnt > 0; cur++) {

> > >          if (cur->iov_len > bytes) {

> > > +            if (undo) {

> > > +                undo->modified_iov = cur;

> > > +                undo->orig = *cur;

> > > +            }

> > > +

> > >

> >

> > Why here we remember the 'cur'? 'cur' is the some of the 'iov'.

> > Maybe we remember the 'iov'. I think we need the undo structure like this:

> >

> > struct IOVUndo {

> >     iov **modified_iov;

> >     iov *orig;

> > };

> >

> > Then we can remeber the origin 'iov'.

>

> Yes, this could be done but it's not needed (yet?). VirtQueueElement has

> the original struct iovec pointers so adding this would be redundant.
Li Qiang Sept. 16, 2020, 3:38 p.m. UTC | #3
Stefan Hajnoczi <stefanha@redhat.com> 于2020年8月12日周三 下午6:51写道:
>
> Fuzzing discovered that virtqueue_unmap_sg() is being called on modified
> req->in/out_sg iovecs. This means dma_memory_map() and
> dma_memory_unmap() calls do not have matching memory addresses.
>
> Fuzzing discovered that non-RAM addresses trigger a bug:
>
>   void address_space_unmap(AddressSpace *as, void *buffer, hwaddr len,
>                            bool is_write, hwaddr access_len)
>   {
>       if (buffer != bounce.buffer) {
>           ^^^^^^^^^^^^^^^^^^^^^^^
>
> A modified iov->iov_base is no longer recognized as a bounce buffer and
> the wrong branch is taken.
>
> There are more potential bugs: dirty memory is not tracked correctly and
> MemoryRegion refcounts can be leaked.
>
> Use the new iov_discard_undo() API to restore elem->in/out_sg before
> virtqueue_push() is called.
>
> Reported-by: Alexander Bulekov <alxndr@bu.edu>
> Buglink: https://bugs.launchpad.net/qemu/+bug/1890360
> Fixes: 827805a2492c1bbf1c0712ed18ee069b4ebf3dd6 ("virtio-blk: Convert VirtIOBlockReq.out to structrue")
> Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
> ---
>  include/hw/virtio/virtio-blk.h | 2 ++
>  hw/block/virtio-blk.c          | 9 +++++++--
>  2 files changed, 9 insertions(+), 2 deletions(-)
>
> diff --git a/include/hw/virtio/virtio-blk.h b/include/hw/virtio/virtio-blk.h
> index b1334c3904..0af654cace 100644
> --- a/include/hw/virtio/virtio-blk.h
> +++ b/include/hw/virtio/virtio-blk.h
> @@ -68,6 +68,8 @@ typedef struct VirtIOBlockReq {
>      int64_t sector_num;
>      VirtIOBlock *dev;
>      VirtQueue *vq;
> +    IOVDiscardUndo inhdr_undo;
> +    IOVDiscardUndo outhdr_undo;
>      struct virtio_blk_inhdr *in;
>      struct virtio_blk_outhdr out;
>      QEMUIOVector qiov;
> diff --git a/hw/block/virtio-blk.c b/hw/block/virtio-blk.c
> index 413783693c..2b7cc3e1c8 100644
> --- a/hw/block/virtio-blk.c
> +++ b/hw/block/virtio-blk.c
> @@ -80,6 +80,8 @@ static void virtio_blk_req_complete(VirtIOBlockReq *req, unsigned char status)
>      trace_virtio_blk_req_complete(vdev, req, status);
>
>      stb_p(&req->in->status, status);
> +    iov_discard_undo(&req->inhdr_undo);
> +    iov_discard_undo(&req->outhdr_undo);
>      virtqueue_push(req->vq, &req->elem, req->in_len);
>      if (s->dataplane_started && !s->dataplane_disabled) {
>          virtio_blk_data_plane_notify(s->dataplane, req->vq);
> @@ -632,10 +634,12 @@ static int virtio_blk_handle_request(VirtIOBlockReq *req, MultiReqBuffer *mrb)
>          return -1;
>      }
>
> -    iov_discard_front(&out_iov, &out_num, sizeof(req->out));
> +    iov_discard_front_undoable(&out_iov, &out_num, sizeof(req->out),
> +                               &req->outhdr_undo);
>
>      if (in_iov[in_num - 1].iov_len < sizeof(struct virtio_blk_inhdr)) {
>          virtio_error(vdev, "virtio-blk request inhdr too short");
> +        iov_discard_undo(&req->outhdr_undo);
>          return -1;
>      }
>
> @@ -644,7 +648,8 @@ static int virtio_blk_handle_request(VirtIOBlockReq *req, MultiReqBuffer *mrb)
>      req->in = (void *)in_iov[in_num - 1].iov_base
>                + in_iov[in_num - 1].iov_len
>                - sizeof(struct virtio_blk_inhdr);
> -    iov_discard_back(in_iov, &in_num, sizeof(struct virtio_blk_inhdr));
> +    iov_discard_back_undoable(in_iov, &in_num, sizeof(struct virtio_blk_inhdr),
> +                              &req->inhdr_undo);
>
>      type = virtio_ldl_p(vdev, &req->out.type);
>

It seems there is another error path need to do the undo operations.
case VIRTIO_BLK_T_WRITE_ZEROS & ~VIRTIO_BLK_T_OUT
handler has an error path.

Thanks,
Li Qiang

> --
> 2.26.2
>
Stefan Hajnoczi Sept. 17, 2020, 9:34 a.m. UTC | #4
On Wed, Sep 16, 2020 at 11:38:36PM +0800, Li Qiang wrote:
> Stefan Hajnoczi <stefanha@redhat.com> 于2020年8月12日周三 下午6:51写道:
> > @@ -644,7 +648,8 @@ static int virtio_blk_handle_request(VirtIOBlockReq *req, MultiReqBuffer *mrb)
> >      req->in = (void *)in_iov[in_num - 1].iov_base
> >                + in_iov[in_num - 1].iov_len
> >                - sizeof(struct virtio_blk_inhdr);
> > -    iov_discard_back(in_iov, &in_num, sizeof(struct virtio_blk_inhdr));
> > +    iov_discard_back_undoable(in_iov, &in_num, sizeof(struct virtio_blk_inhdr),
> > +                              &req->inhdr_undo);
> >
> >      type = virtio_ldl_p(vdev, &req->out.type);
> >
> 
> It seems there is another error path need to do the undo operations.
> case VIRTIO_BLK_T_WRITE_ZEROS & ~VIRTIO_BLK_T_OUT
> handler has an error path.

Yes, thank you! I'll fix it in the next revision.

Stefan