hw/arm/tegra241-cmdqv.c | 54 +++++++++++++++++++++-------------------- hw/arm/tegra241-cmdqv.h | 2 +- 2 files changed, 29 insertions(+), 27 deletions(-)
With CMDQV enabled, resetting a guest after it enables VINTF invokes
the VINTF page0 unmap path. The resulting crash is intermittent and
has been observed on the RCU reclaim thread as:
reboot: Restarting system
double free or corruption (!prev)
...
#5 address_space_dispatch_free
#6 flatview_destroy
#7 call_rcu_thread
FlatViews retain raw MemoryRegion pointers and release their references
asynchronously through RCU. The VINTF page0 unmap path removes the
subregion and immediately unparents and frees it. An old FlatView can
then access the freed region during teardown, resulting in a
use-after-free and heap corruption.
Embed the VINTF page0 MemoryRegion in Tegra241CMDQV and tie its lifetime
to the VINTF page0 mmap. Initialize and add the region disabled after
successful vIOMMU allocation, and remove and unparent it before releasing
the mmap during allocation unwind. Guest VINTF enable and disable then
only toggle the region with memory_region_set_enabled(). New FlatViews
omit the disabled region, while old views continue to reference valid
storage.
Keeping the region initialized across VINTF disable and reset is safe
because the vIOMMU association and its VINTF page0 mmap remain stable
once the guest CMDQ has been initialized. QEMU blocks hot-unplug of the
device that established the association, so later hot-adds reuse it
instead of associating the initialized guest CMDQ with a different host
SMMUv3. The CMDQV free_viommu path is therefore only invoked while
unwinding initial allocation, when the region is still disabled and has
never entered a FlatView.
Fixes: 5965b81ce283 ("hw/arm/tegra241-cmdqv: Use mmap'd host VINTF page0 for virtual VINTF page0")
Suggested-by: Shameer Kolothum <skolothumtho@nvidia.com>
Signed-off-by: Matthew R. Ochs <mochs@nvidia.com>
---
v3:
- Initialize and add the region disabled during vIOMMU allocation, and
remove and unparent it during allocation unwind so its lifetime follows
the VINTF page0 mmap, as suggested by Shameer.
- Avoid calling memory_region_is_mapped() before the region is initialized,
addressing Peter's review.
- Link to v2: https://lore.kernel.org/all/20260911190431.4085464-1-mochs@nvidia.com/
v2:
- Use memory_region_is_mapped() instead of an explicit initialization
flag, as suggested by Shameer.
- Link to v1: https://lore.kernel.org/all/20260903184710.2052780-1-mochs@nvidia.com/
Reproducer:
Start an Arm virt guest with one passed-through device behind an
accelerated SMMUv3 configured with cmdqv=on. Add a QMP socket:
-qmp unix:/tmp/qmon.sock,server,nowait
The failure can be made reliable without an ASan build by starting QEMU
with glibc freed-memory poisoning enabled:
GLIBC_TUNABLES=glibc.malloc.tcache_count=0 \
MALLOC_PERTURB_=165 \
MALLOC_CHECK_=3 \
qemu-system-aarch64 <options>
Wait until the guest has booted and initialized CMDQV/VINTF, then reset
the guest through a QMP client:
./build/run qmp-shell /tmp/qmon.sock
(QEMU) system_reset
With the unpatched binary, QEMU crashed on the first reset with SIGSEGV.
The core showed object_unref() called from address_space_dispatch_free()
with the object pointer set to 0xa5a5a5a5a5a5a5a5.
Testing:
Unpatched, one CMDQV instance: SIGSEGV on first reset
Patched, one CMDQV instance: 100/100 resets completed successfully
Patched, four CMDQV instances: 100/100 resets completed successfully
Patched, forced allocation unwind: 20/20 clean exits
hw/arm/tegra241-cmdqv.c | 54 +++++++++++++++++++++--------------------
hw/arm/tegra241-cmdqv.h | 2 +-
2 files changed, 29 insertions(+), 27 deletions(-)
diff --git a/hw/arm/tegra241-cmdqv.c b/hw/arm/tegra241-cmdqv.c
index 273633e62937..57041408a7f0 100644
--- a/hw/arm/tegra241-cmdqv.c
+++ b/hw/arm/tegra241-cmdqv.c
@@ -131,36 +131,12 @@ static void tegra241_cmdqv_reset_vcmdq_cache(Tegra241CMDQV *cmdqv, int index)
static void tegra241_cmdqv_guest_unmap_vintf_page0(Tegra241CMDQV *cmdqv)
{
- if (!cmdqv->mr_vintf_page0) {
- return;
- }
-
- memory_region_del_subregion(&cmdqv->mmio_cmdqv, cmdqv->mr_vintf_page0);
- object_unparent(OBJECT(cmdqv->mr_vintf_page0));
- g_free(cmdqv->mr_vintf_page0);
- cmdqv->mr_vintf_page0 = NULL;
+ memory_region_set_enabled(&cmdqv->mr_vintf_page0, false);
}
static void tegra241_cmdqv_guest_map_vintf_page0(Tegra241CMDQV *cmdqv)
{
- char *name;
-
- if (cmdqv->mr_vintf_page0) {
- return;
- }
-
- name = g_strdup_printf("%s vintf-page0",
- memory_region_name(&cmdqv->mmio_cmdqv));
- cmdqv->mr_vintf_page0 = g_malloc0(sizeof(*cmdqv->mr_vintf_page0));
- memory_region_init_ram_device_ptr(cmdqv->mr_vintf_page0,
- memory_region_owner(&cmdqv->mmio_cmdqv),
- name, VINTF_PAGE_SIZE,
- cmdqv->vintf_page0);
- memory_region_set_skip_iommu_map(cmdqv->mr_vintf_page0, true);
- memory_region_add_subregion_overlap(&cmdqv->mmio_cmdqv,
- CMDQV_VINTF_PAGE0_BASE,
- cmdqv->mr_vintf_page0, 1);
- g_free(name);
+ memory_region_set_enabled(&cmdqv->mr_vintf_page0, true);
}
static void tegra241_cmdqv_free_vcmdq(Tegra241CMDQV *cmdqv, int index)
@@ -899,6 +875,9 @@ static void tegra241_cmdqv_free_viommu(SMMUv3State *s)
cmdqv->veventq = NULL;
}
if (cmdqv->vintf_page0) {
+ memory_region_del_subregion(&cmdqv->mmio_cmdqv,
+ &cmdqv->mr_vintf_page0);
+ object_unparent(OBJECT(&cmdqv->mr_vintf_page0));
munmap(cmdqv->vintf_page0, VINTF_PAGE_SIZE);
cmdqv->vintf_page0 = NULL;
}
@@ -910,6 +889,7 @@ tegra241_cmdqv_alloc_viommu(SMMUv3State *s, HostIOMMUDeviceIOMMUFD *idev,
uint32_t *out_viommu_id, Error **errp)
{
Tegra241CMDQV *cmdqv = s->s_accel->cmdqv;
+ char *name;
uint32_t viommu_id, veventq_id, veventq_fd;
IOMMUFDVeventq *veventq;
int flags;
@@ -955,6 +935,28 @@ tegra241_cmdqv_alloc_viommu(SMMUv3State *s, HostIOMMUDeviceIOMMUFD *idev,
/* Set up event handler for veventq fd */
qemu_set_fd_handler(veventq_fd, tegra241_cmdqv_event_read, NULL, cmdqv);
+
+ /*
+ * Tie the MemoryRegion lifetime to the VINTF page0 mmap. Initialize it
+ * disabled. If initial vIOMMU setup later unwinds, the region has never
+ * entered a FlatView and can be removed and unparented before releasing
+ * the mmap. After successful setup, keep it parented across guest disable
+ * and reset, and only toggle its enabled state so old FlatViews continue
+ * to reference valid storage.
+ */
+ name = g_strdup_printf("%s vintf-page0",
+ memory_region_name(&cmdqv->mmio_cmdqv));
+ memory_region_init_ram_device_ptr(&cmdqv->mr_vintf_page0,
+ memory_region_owner(&cmdqv->mmio_cmdqv),
+ name, VINTF_PAGE_SIZE,
+ cmdqv->vintf_page0);
+ memory_region_set_skip_iommu_map(&cmdqv->mr_vintf_page0, true);
+ memory_region_set_enabled(&cmdqv->mr_vintf_page0, false);
+ memory_region_add_subregion_overlap(&cmdqv->mmio_cmdqv,
+ CMDQV_VINTF_PAGE0_BASE,
+ &cmdqv->mr_vintf_page0, 1);
+ g_free(name);
+
*out_viommu_id = viommu_id;
return true;
diff --git a/hw/arm/tegra241-cmdqv.h b/hw/arm/tegra241-cmdqv.h
index de4c1e53358f..bba985099e3b 100644
--- a/hw/arm/tegra241-cmdqv.h
+++ b/hw/arm/tegra241-cmdqv.h
@@ -49,7 +49,7 @@ typedef struct Tegra241CMDQV {
IOMMUFDVeventq *veventq;
IOMMUFDHWqueue *vcmdq[TEGRA241_CMDQV_MAX_CMDQ];
void *vintf_page0;
- MemoryRegion *mr_vintf_page0;
+ MemoryRegion mr_vintf_page0;
/* CMDQ-V Config page register cache */
uint32_t config;
--
2.50.1
© 2016 - 2026 Red Hat, Inc.