From 4061ee07f037541c6fd38c7f0d9b8633e8f78a08 Mon Sep 17 00:00:00 2001 From: Makr Date: Thu, 22 Oct 2026 15:01:23 +0100 Subject: [PATCH] physmem, dart: flush another CPU's TLB on that CPU (port from MakrSas/Inferno) A DART mapping is revoked on whichever thread wrote to the IOMMU. On the t8030 that is seldom the thread of the CPU whose TLB holds the translation: an application core tears down a mapping that the SEP core, or another application core, has been translating through. tcg_iommu_unmap_notify called tlb_flush(notifier->cpu) from that foreign thread. Pyre's tlb_flush_by_mmuidx documents that it must run on the owning CPU (assert_cpu_is_self, compiled out in release builds). It frees and reallocates the tables it resizes while the owner keeps reading them without a lock, so the owner can read freed memory as TLB entries and write guest data wherever those point. Upstream MakrSas saw this as a heap that malloc spun in forever, dirty bitmaps holding ASCII where pointers belonged, and "Bad ram pointer". A flush for another CPU is now queued to that CPU with async_run_on_cpu, the way tcg_commit hands over a changed memory map. The owner is kicked and flushes at its next exit. Until then it can still use the stale translation, which is the same as a few cycles of DART latency. apple_dart_notify_remap also replayed the whole page table for every notifier after each remap. Only a notifier that asked for MAP events gets anything from a replay. TCG registers one notifier per CPU for UNMAP alone, and the UNMAP event just before it already did that notifier's work, so the replay is now skipped for UNMAP-only notifiers. Ported from MakrSas/Inferno commit 80b488f884bd ("tcg: flush another CPU's TLB on that CPU", branch ios), under the same licences as the files it touches. Original author: Makr . Co-Authored-By: Claude Opus 5.5 --- hw/arm/dart.c | 8 +++++++- system/physmem.c | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/hw/arm/dart.c b/hw/arm/dart.c index 3c6dd11e43..3a5e67a3be 100644 --- a/hw/arm/dart.c +++ b/hw/arm/dart.c @@ -360,7 +360,13 @@ static void apple_dart_notify_remap(AppleDARTMapperInstance* mapper, uint32_t in event.entry.addr_mask = HWADDR_MAX; memory_region_notify_iommu(iommu, 0, event); - IOMMU_NOTIFIER_FOREACH (notifier, iommu) { memory_region_iommu_replay(iommu, notifier); } + // A replay walks the whole page table, and only a notifier that asked for mappings gets anything + // out of it. TCG registers one notifier per CPU that has translated through this stream, for unmaps + // alone, and drops every mapping it is handed — so for those the walk was pure cost, paid again on + // each TLB operation the guest issues. + IOMMU_NOTIFIER_FOREACH (notifier, iommu) { + if ((notifier->notifier_flags & IOMMU_NOTIFIER_MAP) != 0) { memory_region_iommu_replay(iommu, notifier); } + } } static void apple_dart_mapper_reg_write(void* opaque, hwaddr addr, uint64_t data, unsigned size) diff --git a/system/physmem.c b/system/physmem.c index 7710f71421..32e2f777af 100644 --- a/system/physmem.c +++ b/system/physmem.c @@ -570,12 +570,29 @@ typedef struct TCGIOMMUNotifier bool active; } TCGIOMMUNotifier; +static void tcg_iommu_flush_cpu(CPUState* cpu, run_on_cpu_data data) { tlb_flush(cpu); } + static void tcg_iommu_unmap_notify(IOMMUNotifier* n, IOMMUTLBEntry* iotlb) { TCGIOMMUNotifier* notifier = container_of(n, TCGIOMMUNotifier, n); if (!notifier->active) { return; } - tlb_flush(notifier->cpu); + /* + * The IOMMU changes on whichever thread wrote to it, which is seldom the thread of the CPU + * whose TLB this is: on the t8030 a DART mapping revoked by an application core is one the + * SEP core, or another application core, has translated through. tlb_flush works on the + * calling thread's TLB — it frees and reallocates the tables it resizes — while their owner + * goes on reading them without a lock. Done from here, the owner read freed memory as TLB + * entries and wrote guest data wherever those pointed: a heap that malloc then spun in + * forever, dirty bitmaps with ASCII for pointers, "Bad ram pointer". A flush for another CPU + * is queued to that CPU instead, the way tcg_commit hands over a changed memory map. + */ + if (notifier->cpu->halt_cond && !qemu_cpu_is_self(notifier->cpu)) { + async_run_on_cpu(notifier->cpu, tcg_iommu_flush_cpu, RUN_ON_CPU_NULL); + } + else { + tlb_flush(notifier->cpu); + } notifier->active = false; /* We leave the notifier struct on the list to avoid reallocating it later. * Generally the number of IOMMUs a CPU deals with will be small.