Commit message: vhost-vsock: detach the virqueue element in case of error

In vhost_vsock_common_send_transport_reset(), if an element popped from
the virtqueue is invalid, we should call virtqueue_detach_element() to
detach it from the virtqueue before freeing its memory.

Fixes: fc0b9b0e1c ("vhost-vsock: add virtio sockets device")
Fixes: CVE-2022-26354
Cc: qemu-stable@nongnu.org
Reported-by: VictorV <vv474172261@gmail.com>
Signed-off-by: Stefano Garzarella <sgarzare@redhat.com>
Message-Id: <20220228095058.27899-1-sgarzare@redhat.com>
Reviewed-by: Stefan Hajnoczi <stefanha@redhat.com>
Reviewed-by: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
CVE: CVE-2022-26354
CWE: CWE-772
Vulnerability description: A flaw was found in the vhost-vsock device of QEMU. In case of error, an invalid element was not detached from the virtqueue before freeing its memory, leading to memory leakage and other unexpected results. Affected QEMU versions <= 6.2.0.

Project: qemu
File: vhost-vsock-common.c

static void vhost_vsock_common_handle_output(VirtIODevice *vdev, VirtQueue *vq)
{
    /* Do nothing */
}

Project: qemu
File: vhost-vsock-common.c

static void vhost_vsock_common_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);

    device_class_set_props(dc, vhost_vsock_common_properties);
    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
    vdc->guest_notifier_mask = vhost_vsock_common_guest_notifier_mask;
    vdc->guest_notifier_pending = vhost_vsock_common_guest_notifier_pending;
}

Project: qemu
File: vhost-vsock-common.c

static void vhost_vsock_common_register_types(void)
{
    type_register_static(&vhost_vsock_common_info);
}

Project: qemu
File: vhost-vsock-common.c

static void vhost_vsock_common_post_load_timer_cleanup(VHostVSockCommon *vvc)
{
    if (!vvc->post_load_timer) {
        return;
    }

    timer_free(vvc->post_load_timer);
    vvc->post_load_timer = NULL;
}

Project: qemu
File: vhost-vsock-common.c

int vhost_vsock_common_post_load(void *opaque, int version_id)
{
    VHostVSockCommon *vvc = opaque;
    VirtIODevice *vdev = VIRTIO_DEVICE(vvc);

    if (virtio_queue_get_addr(vdev, 2)) {
        /*
         * Defer transport reset event to a vm clock timer so that virtqueue
         * changes happen after migration has completed.
         */
        assert(!vvc->post_load_timer);
        vvc->post_load_timer =
            timer_new_ns(QEMU_CLOCK_VIRTUAL,
                         vhost_vsock_common_post_load_timer_cb,
                         vvc);
        timer_mod(vvc->post_load_timer, 1);
    }
    return 0;
}

Project: qemu
File: vhost-vsock-common.c

static void vhost_vsock_common_post_load_timer_cb(void *opaque)
{
    VHostVSockCommon *vvc = opaque;

    vhost_vsock_common_post_load_timer_cleanup(vvc);
    vhost_vsock_common_send_transport_reset(vvc);
}

Project: qemu
File: vhost-vsock-common.c

void vhost_vsock_common_unrealize(VirtIODevice *vdev)
{
    VHostVSockCommon *vvc = VHOST_VSOCK_COMMON(vdev);

    vhost_vsock_common_post_load_timer_cleanup(vvc);

    virtio_delete_queue(vvc->recv_vq);
    virtio_delete_queue(vvc->trans_vq);
    virtio_delete_queue(vvc->event_vq);
    virtio_cleanup(vdev);
}

Project: qemu
File: vhost-vsock-common.c

uint64_t vhost_vsock_common_get_features(VirtIODevice *vdev, uint64_t features,
                                         Error **errp)
{
    VHostVSockCommon *vvc = VHOST_VSOCK_COMMON(vdev);

    if (vvc->seqpacket != ON_OFF_AUTO_OFF) {
        virtio_add_feature(&features, VIRTIO_VSOCK_F_SEQPACKET);
    }

    features = vhost_get_features(&vvc->vhost_dev, feature_bits, features);

    if (vvc->seqpacket == ON_OFF_AUTO_ON &&
        !virtio_has_feature(features, VIRTIO_VSOCK_F_SEQPACKET)) {
        error_setg(errp, "vhost-vsock backend doesn't support seqpacket");
    }

    return features;
}

Project: qemu
File: vhost-vsock-common.c

void vhost_vsock_common_stop(VirtIODevice *vdev)
{
    VHostVSockCommon *vvc = VHOST_VSOCK_COMMON(vdev);
    BusState *qbus = BUS(qdev_get_parent_bus(DEVICE(vdev)));
    VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
    int ret;

    if (!k->set_guest_notifiers) {
        return;
    }

    vhost_dev_stop(&vvc->vhost_dev, vdev);

    ret = k->set_guest_notifiers(qbus->parent, vvc->vhost_dev.nvqs, false);
    if (ret < 0) {
        error_report("vhost guest notifier cleanup failed: %d", ret);
        return;
    }

    vhost_dev_disable_notifiers(&vvc->vhost_dev, vdev);
}

Project: qemu
File: vhost-vsock-common.c

static bool vhost_vsock_common_guest_notifier_pending(VirtIODevice *vdev,
                                               int idx)
{
    VHostVSockCommon *vvc = VHOST_VSOCK_COMMON(vdev);

    return vhost_virtqueue_pending(&vvc->vhost_dev, idx);
}

Project: qemu
File: vhost-vsock-common.c

static void vhost_vsock_common_send_transport_reset(VHostVSockCommon *vvc)
{
    VirtQueueElement *elem;
    VirtQueue *vq = vvc->event_vq;
    struct virtio_vsock_event event = {
        .id = cpu_to_le32(VIRTIO_VSOCK_EVENT_TRANSPORT_RESET),
    };

    elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
    if (!elem) {
        error_report("vhost-vsock missed transport reset event");
        return;
    }

    if (elem->out_num) {
        error_report("invalid vhost-vsock event virtqueue element with "
                     "out buffers");
        goto err;
    }

    if (iov_from_buf(elem->in_sg, elem->in_num, 0,
                     &event, sizeof(event)) != sizeof(event)) {
        error_report("vhost-vsock event virtqueue element is too short");
        goto err;
    }

    virtqueue_push(vq, elem, sizeof(event));
    virtio_notify(VIRTIO_DEVICE(vvc), vq);

    g_free(elem);
    return;

err:
    virtqueue_detach_element(vq, elem, 0);
    g_free(elem);
}

Project: qemu
File: vhost-vsock-common.c

static void vhost_vsock_common_guest_notifier_mask(VirtIODevice *vdev, int idx,
                                            bool mask)
{
    VHostVSockCommon *vvc = VHOST_VSOCK_COMMON(vdev);

    vhost_virtqueue_mask(&vvc->vhost_dev, vdev, idx, mask);
}

Project: qemu
File: vhost-vsock-common.c

void vhost_vsock_common_realize(VirtIODevice *vdev, const char *name)
{
    VHostVSockCommon *vvc = VHOST_VSOCK_COMMON(vdev);

    virtio_init(vdev, name, VIRTIO_ID_VSOCK,
                sizeof(struct virtio_vsock_config));

    /* Receive and transmit queues belong to vhost */
    vvc->recv_vq = virtio_add_queue(vdev, VHOST_VSOCK_QUEUE_SIZE,
                                      vhost_vsock_common_handle_output);
    vvc->trans_vq = virtio_add_queue(vdev, VHOST_VSOCK_QUEUE_SIZE,
                                       vhost_vsock_common_handle_output);

    /* The event queue belongs to QEMU */
    vvc->event_vq = virtio_add_queue(vdev, VHOST_VSOCK_QUEUE_SIZE,
                                       vhost_vsock_common_handle_output);

    vvc->vhost_dev.nvqs = ARRAY_SIZE(vvc->vhost_vqs);
    vvc->vhost_dev.vqs = vvc->vhost_vqs;

    vvc->post_load_timer = NULL;
}

Project: qemu
File: vhost-vsock-common.c

int vhost_vsock_common_start(VirtIODevice *vdev)
{
    VHostVSockCommon *vvc = VHOST_VSOCK_COMMON(vdev);
    BusState *qbus = BUS(qdev_get_parent_bus(DEVICE(vdev)));
    VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
    int ret;
    int i;

    if (!k->set_guest_notifiers) {
        error_report("binding does not support guest notifiers");
        return -ENOSYS;
    }

    ret = vhost_dev_enable_notifiers(&vvc->vhost_dev, vdev);
    if (ret < 0) {
        error_report("Error enabling host notifiers: %d", -ret);
        return ret;
    }

    ret = k->set_guest_notifiers(qbus->parent, vvc->vhost_dev.nvqs, true);
    if (ret < 0) {
        error_report("Error binding guest notifier: %d", -ret);
        goto err_host_notifiers;
    }

    vvc->vhost_dev.acked_features = vdev->guest_features;
    ret = vhost_dev_start(&vvc->vhost_dev, vdev);
    if (ret < 0) {
        error_report("Error starting vhost: %d", -ret);
        goto err_guest_notifiers;
    }

    /*
     * guest_notifier_mask/pending not used yet, so just unmask
     * everything here.  virtio-pci will do the right thing by
     * enabling/disabling irqfd.
     */
    for (i = 0; i < vvc->vhost_dev.nvqs; i++) {
        vhost_virtqueue_mask(&vvc->vhost_dev, vdev, i, false);
    }

    return 0;

err_guest_notifiers:
    k->set_guest_notifiers(qbus->parent, vvc->vhost_dev.nvqs, false);
err_host_notifiers:
    vhost_dev_disable_notifiers(&vvc->vhost_dev, vdev);
    return ret;
}

Project: qemu
File: vhost-vsock-common.c

int vhost_vsock_common_pre_save(void *opaque)
{
    VHostVSockCommon *vvc = opaque;

    /*
     * At this point, backend must be stopped, otherwise
     * it might keep writing to memory.
     */
    assert(!vvc->vhost_dev.started);

    return 0;
}