[Devel] [PATCH VZ10 v3 5/5] drivers/vhost/blk: rework queue/backend setup

Konstantin Khorenko khorenko at virtuozzo.com
Tue Aug 25 16:42:57 MSK 2026


A side finding, unrelated to this patch, but still, please fix it in a separate patch:

  In drivers/vhost/vhost.h the backend access rules are documented explicitly (drivers/vhost/vhost.h:288-310):

  /**
   * vhost_vq_set_backend - Set backend.
   * ...
   * Context: Need to call with vq->mutex acquired.
   */

  /**
   * vhost_vq_get_backend - Get backend.
   * ...
   * Context: Need to call with vq->mutex acquired.
   */

  So vq->private_data is a plain field with no barriers whatsoever - its consistency is guaranteed solely by both sides
  holding vq->mutex. Every other vhost driver honors that: handle_tx/handle_rx in vhost-net take
  mutex_lock_nested(&vq->mutex, ...) (drivers/vhost/net.c:948, 1117), vsock and scsi do the same in their kick paths. Only
  vhost_blk_handle_guest_kick() calls vhost_vq_get_backend() and then works with the ring without taking the mutex at all.

  Where exactly the race is

  The writer is vhost_blk_setup_vqs() (ioctl thread, under dev.mutex); the reader is the vhost worker (a separate kernel
  thread, no mutex). Side by side:

  CPU0: ioctl VHOST_BLK_SET_BACKEND             CPU1: vhost worker
        vhost_blk_setup_vqs()                         vhost_blk_handle_guest_kick()
  ------------------------------------------    ------------------------------------------
  blk->vqs[i].req = kvmalloc_array(...);
                      /* store 1: req array */
  mutex_lock(&vq->mutex);
  vhost_vq_set_backend(vq, blk->backend);
                      /* store 2: publish
                         the backend */
  mutex_unlock(&vq->mutex);
                                                f = vhost_vq_get_backend(vq);
                                                                  /* load 1: backend */
                                                if (!f)
                                                        return;
                                                ...
                                                head = vhost_get_vq_desc(...);
                                                ...
                                                req = &blk_vq->req[head];
                                                                  /* load 2: req array
                                                                     pointer, then
                                                                     dereference */

  Correctness rests on the invariant: if the reader observes backend != NULL, it must also observe the already-written
  blk_vq->req pointer. For that to hold, two things are required: (a) the writer's stores must become visible in program
  order (req first, backend second), and (b) the reader's loads must be performed in program order (backend first, req
  second).

  The writer-side mutex does not help here: a lock orders memory only between critical sections of that same lock. The
  reader never takes it, so there is no happens-before edge between the writer's mutex_unlock() and the worker's loads.
  Worse, mutex_lock() is an acquire operation, and acquire is one-directional: it forbids later accesses from moving up
  above it, but it does not forbid an earlier store (the req write) from sinking down below it. So even within the writer,
  store 1 and store 2 are formally unordered with respect to each other as far as an external observer is concerned.

  The failure interleaving on a weakly ordered CPU (e.g. arm64) then looks like this:

  CPU0                                          CPU1
  ------------------------------------------    ------------------------------------------
  store 2 becomes globally visible
     (backend != NULL)
                                                load 1: sees the new backend -> proceeds
                                                load 2: sees the *old* blk_vq->req
                                                        (NULL on first SET_BACKEND, or a
                                                         freed pointer after a -1/fd cycle)
                                                req = &NULL[head]   -> NULL-offset oops
                                                (or use-after-free)
  store 1 becomes globally visible
     (req array pointer) - too late

  On x86-64 this cannot happen, because TSO forbids both store-store reordering on CPU0 and load-load reordering on CPU1 -
  which is the only reason the current code works in practice.


Suggested fix:

  --- a/drivers/vhost/blk.c
  +++ b/drivers/vhost/blk.c
  @@ static void vhost_blk_handle_guest_kick(struct vhost_work *work)
        vq = container_of(work, struct vhost_virtqueue, poll.work);
        blk = container_of(vq->dev, struct vhost_blk, dev);
        blk_vq = container_of(vq, struct vhost_blk_vq, vq);

  +     mutex_lock(&vq->mutex);
  +
        f = vhost_vq_get_backend(vq);
        if (!f)
  -             return;
  +             goto out;

        vhost_disable_notify(&blk->dev, vq);
        for (;;) {
                head = vhost_get_vq_desc(vq, vq->iov,
                                         ARRAY_SIZE(vq->iov),
                                         &out, &in, NULL, NULL);
  @@
                if (!llist_empty(&blk_vq->llhead)) {
                        vhost_poll_queue(&vq->poll);
                        break;
                }
        }
  +out:
  +     mutex_unlock(&vq->mutex);
   }
  @@ static void vhost_blk_handle_host_kick(struct vhost_work *work)
        blk_vq = container_of(work, struct vhost_blk_vq, work);
        vq = &blk_vq->vq;
  +
  +     mutex_lock(&vq->mutex);
        llnode = llist_del_all(&blk_vq->llhead);
        added = false;
        while (llnode) {
  @@
                forget_request(req);
        }

        if (likely(added))
                vhost_signal(&blk->dev, vq);
  +     mutex_unlock(&vq->mutex);
   }



--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team




More information about the Devel mailing list