From 0611d393031f9941fcfaf3f17c3ac2b7a0ea00d1 Mon Sep 17 00:00:00 2001 From: David Hoppenbrouwers Date: Tue, 30 Jun 2026 22:08:02 +0000 Subject: [PATCH 01/44] amd_iommu: Fix opcode reported in invalid command handling According to the AMD I/O Virtualization Technology (IOMMU) Specification (Rev 3.10), Section 2.4 Commands, the Generic Command Buffer Entry Format encodes the opcode in bits [63:60] of the command buffer. When handling illegal opcodes, the traces for unhandled commands and event log info extract the opcode from an incorrect offset in the command buffer. Fix this issue to avoid potential confusion with mismatched opcodes in traces and unlikely errors in guest event processing. Fixes: d29a09ca68428 ("hw/i386: Introduce AMD IOMMU") Signed-off-by: David Hoppenbrouwers Reviewed-by: Sairaj Kodilkar Acked-by: Igor Mammedov Signed-off-by: Alejandro Jimenez Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260630220806.1758748-2-alejandro.j.jimenez@oracle.com> --- hw/i386/amd_iommu.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hw/i386/amd_iommu.c b/hw/i386/amd_iommu.c index 79216fb305..05b7c638f4 100644 --- a/hw/i386/amd_iommu.c +++ b/hw/i386/amd_iommu.c @@ -1509,9 +1509,9 @@ static void amdvi_cmdbuf_exec(AMDVIState *s) amdvi_inval_all(s, cmd); break; default: - trace_amdvi_unhandled_command(extract64(cmd[1], 60, 4)); + trace_amdvi_unhandled_command(extract64(cmd[0], 60, 4)); /* log illegal command */ - amdvi_log_illegalcom_error(s, extract64(cmd[1], 60, 4), + amdvi_log_illegalcom_error(s, extract64(cmd[0], 60, 4), s->cmdbuf + s->cmdbuf_head); } } From 784768f7faccf02fcf904db023cd29c790b57443 Mon Sep 17 00:00:00 2001 From: Alejandro Jimenez Date: Tue, 30 Jun 2026 22:08:03 +0000 Subject: [PATCH 02/44] amd_iommu: Return int from page walk status helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetch_pte() returns a status code 0 on success, and (small) negative values on failure. The PTE value itself is returned via an output parameter. amdvi_get_top_pt_level_and_perms() follows the same return convention. Both functions currently return uint64_t, which means any negative error values are returned as unsigned and then converted back to int by the callers. This does not cause any issues in the current implementation, but Coverity flags the type mismatch and potential overflow. Make both helpers return int, so the type matches what the return variable is (0 on success, small negative value on failure), and also the type used by all callers to store their return values. No functional changes are intended. Fixes: a1c97c395729 ("amd_iommu: Sync shadow page tables on page invalidation") Fixes: 786550e2d38a ("amd_iommu: Follow root pointer before page walk and use 1-based levels") Reported-by: Peter Maydell Suggested-by: Peter Maydell Signed-off-by: Alejandro Jimenez Reviewed-by: Peter Maydell Reviewed-by: Philippe Mathieu-Daudé Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260630220806.1758748-3-alejandro.j.jimenez@oracle.com> --- hw/i386/amd_iommu.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hw/i386/amd_iommu.c b/hw/i386/amd_iommu.c index 05b7c638f4..a0835a20d7 100644 --- a/hw/i386/amd_iommu.c +++ b/hw/i386/amd_iommu.c @@ -659,7 +659,7 @@ static uint64_t large_pte_page_size(uint64_t pte) * - IOVA exceeds the address width supported by DTE[Mode] * In all such cases a page walk must be aborted. */ -static uint64_t amdvi_get_top_pt_level_and_perms(hwaddr address, uint64_t dte, +static int amdvi_get_top_pt_level_and_perms(hwaddr address, uint64_t dte, uint8_t *top_level, IOMMUAccessFlags *dte_perms) { @@ -702,7 +702,7 @@ static uint64_t amdvi_get_top_pt_level_and_perms(hwaddr address, uint64_t dte, * page table walk. This means that the DTE has valid data, but one of the * lower level entries in the Page Table could not be read. */ -static uint64_t fetch_pte(AMDVIAddressSpace *as, hwaddr address, uint64_t dte, +static int fetch_pte(AMDVIAddressSpace *as, hwaddr address, uint64_t dte, uint64_t *pte, hwaddr *page_size) { uint64_t pte_addr; From 83ec1c7fcb243c232186df790f5847808cc26982 Mon Sep 17 00:00:00 2001 From: Alejandro Jimenez Date: Tue, 30 Jun 2026 22:08:04 +0000 Subject: [PATCH 03/44] amd_iommu: Decode XT interrupt control register without bitfields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The XT IOMMU General Interrupt Control Register is a guest-visible MMIO register. Decoding it with bitfields depends on host bitfield layout and is not portable to big-endian hosts. Fix this by removing union mmio_xt_intr and explicitly extracting fields with FIELD_EX64() from the full register value returned by amdvi_readq(), which has already been converted to host endianness. Using a designated initializer for X86IOMMUIrq also ensures fields not provided by the XT register (e.g. msi_addr_last_bits) are initialized before x86_iommu_irq_to_msi_message() uses them. CID: 1660056 Fixes: cf0210df65aa ("amd_iommu: Generate XT interrupts when xt support is enabled") Reported-by: Peter Maydell Suggested-by: Peter Maydell Signed-off-by: Alejandro Jimenez Reviewed-by: Philippe Mathieu-Daudé Reviewed-by: Peter Maydell Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260630220806.1758748-4-alejandro.j.jimenez@oracle.com> --- hw/i386/amd_iommu.c | 28 ++++++++++++++++++---------- hw/i386/amd_iommu.h | 14 -------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/hw/i386/amd_iommu.c b/hw/i386/amd_iommu.c index a0835a20d7..291cb368a9 100644 --- a/hw/i386/amd_iommu.c +++ b/hw/i386/amd_iommu.c @@ -34,6 +34,7 @@ #include "hw/core/qdev-properties.h" #include "kvm/kvm_i386.h" #include "qemu/iova-tree.h" +#include "hw/core/registerfields.h" struct AMDVIAddressSpace { PCIBus *bus; /* PCIBus (for bus number) */ @@ -88,6 +89,13 @@ typedef struct AMDVIIOTLBKey { uint16_t devid; } AMDVIIOTLBKey; +/* XT IOMMU General Interrupt Control Register layout */ +FIELD(AMDVI_XT_GEN_INTR, DEST_MODE, 2, 1) +FIELD(AMDVI_XT_GEN_INTR, DEST_LO, 8, 24) +FIELD(AMDVI_XT_GEN_INTR, VECTOR, 32, 8) +FIELD(AMDVI_XT_GEN_INTR, DELIVERY_MODE, 40, 1) +FIELD(AMDVI_XT_GEN_INTR, DEST_HI, 56, 8) + uint64_t amdvi_extended_feature_register(AMDVIState *s) { uint64_t feature = AMDVI_DEFAULT_EXT_FEATURES; @@ -194,17 +202,17 @@ static void amdvi_assign_andq(AMDVIState *s, hwaddr addr, uint64_t val) static void amdvi_build_xt_msi_msg(AMDVIState *s, MSIMessage *msg) { - union mmio_xt_intr xt_reg; - struct X86IOMMUIrq irq; + uint64_t xt_reg = amdvi_readq(s, AMDVI_MMIO_XT_GEN_INTR); - xt_reg.val = amdvi_readq(s, AMDVI_MMIO_XT_GEN_INTR); - - irq.vector = xt_reg.vector; - irq.delivery_mode = xt_reg.delivery_mode; - irq.dest_mode = xt_reg.destination_mode; - irq.dest = (xt_reg.destination_hi << 24) | xt_reg.destination_lo; - irq.trigger_mode = 0; - irq.redir_hint = 0; + X86IOMMUIrq irq = { + .vector = FIELD_EX64(xt_reg, AMDVI_XT_GEN_INTR, VECTOR), + .delivery_mode = FIELD_EX64(xt_reg, AMDVI_XT_GEN_INTR, DELIVERY_MODE), + .dest_mode = FIELD_EX64(xt_reg, AMDVI_XT_GEN_INTR, DEST_MODE), + .dest = (FIELD_EX64(xt_reg, AMDVI_XT_GEN_INTR, DEST_HI) << 24) | + FIELD_EX64(xt_reg, AMDVI_XT_GEN_INTR, DEST_LO), + .trigger_mode = 0, + .redir_hint = 0, + }; x86_iommu_irq_to_msi_message(&irq, msg); } diff --git a/hw/i386/amd_iommu.h b/hw/i386/amd_iommu.h index 3cab04a6d4..ca4440a4c1 100644 --- a/hw/i386/amd_iommu.h +++ b/hw/i386/amd_iommu.h @@ -340,20 +340,6 @@ struct irte_ga { union irte_ga_hi hi; }; -union mmio_xt_intr { - uint64_t val; - struct { - uint64_t rsvd_1:2, - destination_mode:1, - rsvd_2:5, - destination_lo:24, - vector:8, - delivery_mode:1, - rsvd_3:15, - destination_hi:8; - }; -}; - #define TYPE_AMD_IOMMU_DEVICE "amd-iommu" OBJECT_DECLARE_SIMPLE_TYPE(AMDVIState, AMD_IOMMU_DEVICE) From 5bebd769ec33aaeafbdefa7aebc0ea016ba07f09 Mon Sep 17 00:00:00 2001 From: Alejandro Jimenez Date: Tue, 30 Jun 2026 22:08:05 +0000 Subject: [PATCH 04/44] amd_iommu: Decode IRTEs without bitfields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interrupt remapping table entries are data stored in guest memory in little-endian format. Decoding them with bitfields depends on host bitfield layout and the value returned from dma_memory_read() is not portable to big-endian hosts. Replace the legacy and GA IRTE bitfield definitions with explicit FIELD() definitions. Convert the guest memory values returned from dma_memory_read() with le32_to_cpu() or le64_to_cpu(), then extract relevant fields using FIELD_EX32() or FIELD_EX64() as appropriate to match the IRTE format. Fixes: b44159fe0078 ("x86_iommu/amd: Add interrupt remap support when VAPIC is not enabled") Fixes: 135f866e609c ("x86_iommu/amd: Add interrupt remap support when VAPIC is enabled") Reported-by: Peter Maydell Suggested-by: Peter Maydell Signed-off-by: Alejandro Jimenez Reviewed-by: Peter Maydell Reviewed-by: Philippe Mathieu-Daudé Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260630220806.1758748-5-alejandro.j.jimenez@oracle.com> --- hw/i386/amd_iommu.c | 93 +++++++++++++++++++++++++++++++++------------ hw/i386/amd_iommu.h | 49 ------------------------ 2 files changed, 69 insertions(+), 73 deletions(-) diff --git a/hw/i386/amd_iommu.c b/hw/i386/amd_iommu.c index 291cb368a9..c7bf21b762 100644 --- a/hw/i386/amd_iommu.c +++ b/hw/i386/amd_iommu.c @@ -89,6 +89,11 @@ typedef struct AMDVIIOTLBKey { uint16_t devid; } AMDVIIOTLBKey; +typedef struct AMDVIIrteGA { + uint64_t ga_lo; + uint64_t ga_hi; +} AMDVIIrteGA; + /* XT IOMMU General Interrupt Control Register layout */ FIELD(AMDVI_XT_GEN_INTR, DEST_MODE, 2, 1) FIELD(AMDVI_XT_GEN_INTR, DEST_LO, 8, 24) @@ -96,6 +101,37 @@ FIELD(AMDVI_XT_GEN_INTR, VECTOR, 32, 8) FIELD(AMDVI_XT_GEN_INTR, DELIVERY_MODE, 40, 1) FIELD(AMDVI_XT_GEN_INTR, DEST_HI, 56, 8) +/* Interrupt Remapping Table Fields Formats */ + +/* Basic 32-bit IRTE layout (GAEn=0) */ +FIELD(AMDVI_IRTE, VALID, 0, 1) +FIELD(AMDVI_IRTE, SUP_IOPF, 1, 1) +FIELD(AMDVI_IRTE, INT_TYPE, 2, 3) +FIELD(AMDVI_IRTE, RQ_EOI, 5, 1) +FIELD(AMDVI_IRTE, DM, 6, 1) +FIELD(AMDVI_IRTE, GUEST_MODE, 7, 1) +FIELD(AMDVI_IRTE, DESTINATION, 8, 8) +FIELD(AMDVI_IRTE, VECTOR, 16, 8) + +/* 128-bit IRTE layout (GAEn=1) */ +FIELD(AMDVI_IRTE_GA_LO, VALID, 0, 1) +FIELD(AMDVI_IRTE_GA_LO, SUP_IOPF, 1, 1) +FIELD(AMDVI_IRTE_GA_LO, INT_TYPE, 2, 3) +FIELD(AMDVI_IRTE_GA_LO, RQ_EOI, 5, 1) +FIELD(AMDVI_IRTE_GA_LO, DM, 6, 1) +FIELD(AMDVI_IRTE_GA_LO, GUEST_MODE, 7, 1) +/* + * In the 128-bit IRTE format, XT mode uses IRTE_GA_LOW.Destination[23:0] + * together with IRTE_GA_HI.DestinationHi[7:0] to construct a 32-bit x2APIC + * destination. + * Without XTEn (i.e. when x2APIC support is not enabled), only + * IRTE_GA_LOW.Destination[7:0] is used. + */ +FIELD(AMDVI_IRTE_GA_LO, DESTINATION, 8, 24) + +FIELD(AMDVI_IRTE_GA_HI, VECTOR, 0, 8) +FIELD(AMDVI_IRTE_GA_HI, DESTINATION_HI, 56, 8) + uint64_t amdvi_extended_feature_register(AMDVIState *s) { uint64_t feature = AMDVI_DEFAULT_EXT_FEATURES; @@ -1983,7 +2019,7 @@ static IOMMUTLBEntry amdvi_translate(IOMMUMemoryRegion *iommu, hwaddr addr, } static int amdvi_get_irte(AMDVIState *s, MSIMessage *origin, uint64_t *dte, - union irte *irte, uint16_t devid) + uint32_t *irte, uint16_t devid) { uint64_t irte_root, offset; @@ -1998,7 +2034,8 @@ static int amdvi_get_irte(AMDVIState *s, MSIMessage *origin, uint64_t *dte, return -AMDVI_IR_GET_IRTE; } - trace_amdvi_ir_irte_val(irte->val); + *irte = le32_to_cpu(*irte); + trace_amdvi_ir_irte_val(*irte); return 0; } @@ -2010,8 +2047,9 @@ static int amdvi_int_remap_legacy(AMDVIState *iommu, X86IOMMUIrq *irq, uint16_t sid) { + uint8_t int_type; + uint32_t irte; int ret; - union irte irte; /* get interrupt remapping table */ ret = amdvi_get_irte(iommu, origin, dte, &irte, sid); @@ -2019,32 +2057,33 @@ static int amdvi_int_remap_legacy(AMDVIState *iommu, return ret; } - if (!irte.fields.valid) { + if (!FIELD_EX32(irte, AMDVI_IRTE, VALID)) { trace_amdvi_ir_target_abort("RemapEn is disabled"); return -AMDVI_IR_TARGET_ABORT; } - if (irte.fields.guest_mode) { + if (FIELD_EX32(irte, AMDVI_IRTE, GUEST_MODE)) { error_report_once("guest mode is not zero"); return -AMDVI_IR_ERR; } - if (irte.fields.int_type > AMDVI_IOAPIC_INT_TYPE_ARBITRATED) { + int_type = FIELD_EX32(irte, AMDVI_IRTE, INT_TYPE); + if (int_type > AMDVI_IOAPIC_INT_TYPE_ARBITRATED) { error_report_once("reserved int_type"); return -AMDVI_IR_ERR; } - irq->delivery_mode = irte.fields.int_type; - irq->vector = irte.fields.vector; - irq->dest_mode = irte.fields.dm; - irq->redir_hint = irte.fields.rq_eoi; - irq->dest = irte.fields.destination; + irq->delivery_mode = int_type; + irq->vector = FIELD_EX32(irte, AMDVI_IRTE, VECTOR); + irq->dest_mode = FIELD_EX32(irte, AMDVI_IRTE, DM); + irq->redir_hint = FIELD_EX32(irte, AMDVI_IRTE, RQ_EOI); + irq->dest = FIELD_EX32(irte, AMDVI_IRTE, DESTINATION); return 0; } static int amdvi_get_irte_ga(AMDVIState *s, MSIMessage *origin, uint64_t *dte, - struct irte_ga *irte, uint16_t devid) + AMDVIIrteGA *irte, uint16_t devid) { uint64_t irte_root, offset; @@ -2058,7 +2097,9 @@ static int amdvi_get_irte_ga(AMDVIState *s, MSIMessage *origin, uint64_t *dte, return -AMDVI_IR_GET_IRTE; } - trace_amdvi_ir_irte_ga_val(irte->hi.val, irte->lo.val); + irte->ga_lo = le64_to_cpu(irte->ga_lo); + irte->ga_hi = le64_to_cpu(irte->ga_hi); + trace_amdvi_ir_irte_ga_val(irte->ga_hi, irte->ga_lo); return 0; } @@ -2069,8 +2110,9 @@ static int amdvi_int_remap_ga(AMDVIState *iommu, X86IOMMUIrq *irq, uint16_t sid) { + AMDVIIrteGA irte; + uint8_t int_type; int ret; - struct irte_ga irte; /* get interrupt remapping table */ ret = amdvi_get_irte_ga(iommu, origin, dte, &irte, sid); @@ -2078,30 +2120,33 @@ static int amdvi_int_remap_ga(AMDVIState *iommu, return ret; } - if (!irte.lo.fields_remap.valid) { + if (!FIELD_EX64(irte.ga_lo, AMDVI_IRTE_GA_LO, VALID)) { trace_amdvi_ir_target_abort("RemapEn is disabled"); return -AMDVI_IR_TARGET_ABORT; } - if (irte.lo.fields_remap.guest_mode) { + if (FIELD_EX64(irte.ga_lo, AMDVI_IRTE_GA_LO, GUEST_MODE)) { error_report_once("guest mode is not zero"); return -AMDVI_IR_ERR; } - if (irte.lo.fields_remap.int_type > AMDVI_IOAPIC_INT_TYPE_ARBITRATED) { + int_type = FIELD_EX64(irte.ga_lo, AMDVI_IRTE_GA_LO, INT_TYPE); + if (int_type > AMDVI_IOAPIC_INT_TYPE_ARBITRATED) { error_report_once("reserved int_type is set"); return -AMDVI_IR_ERR; } - irq->delivery_mode = irte.lo.fields_remap.int_type; - irq->vector = irte.hi.fields.vector; - irq->dest_mode = irte.lo.fields_remap.dm; - irq->redir_hint = irte.lo.fields_remap.rq_eoi; + irq->delivery_mode = int_type; + irq->vector = FIELD_EX64(irte.ga_hi, AMDVI_IRTE_GA_HI, VECTOR); + irq->dest_mode = FIELD_EX64(irte.ga_lo, AMDVI_IRTE_GA_LO, DM); + irq->redir_hint = FIELD_EX64(irte.ga_lo, AMDVI_IRTE_GA_LO, RQ_EOI); if (iommu->xten) { - irq->dest = irte.lo.fields_remap.destination | - (irte.hi.fields.destination_hi << 24); + irq->dest = FIELD_EX64(irte.ga_lo, AMDVI_IRTE_GA_LO, DESTINATION) | + (FIELD_EX64(irte.ga_hi, AMDVI_IRTE_GA_HI, DESTINATION_HI) + << 24); } else { - irq->dest = irte.lo.fields_remap.destination & 0xff; + irq->dest = FIELD_EX64(irte.ga_lo, AMDVI_IRTE_GA_LO, DESTINATION) & + 0xff; } return 0; diff --git a/hw/i386/amd_iommu.h b/hw/i386/amd_iommu.h index ca4440a4c1..687691ec1c 100644 --- a/hw/i386/amd_iommu.h +++ b/hw/i386/amd_iommu.h @@ -291,55 +291,6 @@ #define AMDVI_DEV_LINT0_PASS_MASK (1ULL << 62) #define AMDVI_DEV_LINT1_PASS_MASK (1ULL << 63) -/* Interrupt remapping table fields (Guest VAPIC not enabled) */ -union irte { - uint32_t val; - struct { - uint32_t valid:1, - no_fault:1, - int_type:3, - rq_eoi:1, - dm:1, - guest_mode:1, - destination:8, - vector:8, - rsvd:8; - } fields; -}; - -/* Interrupt remapping table fields (Guest VAPIC is enabled) */ -union irte_ga_lo { - uint64_t val; - - /* For int remapping */ - struct { - uint64_t valid:1, - no_fault:1, - /* ------ */ - int_type:3, - rq_eoi:1, - dm:1, - /* ------ */ - guest_mode:1, - destination:24, - rsvd_1:32; - } fields_remap; -}; - -union irte_ga_hi { - uint64_t val; - struct { - uint64_t vector:8, - rsvd_2:48, - destination_hi:8; - } fields; -}; - -struct irte_ga { - union irte_ga_lo lo; - union irte_ga_hi hi; -}; - #define TYPE_AMD_IOMMU_DEVICE "amd-iommu" OBJECT_DECLARE_SIMPLE_TYPE(AMDVIState, AMD_IOMMU_DEVICE) From fdb2f65c513257c783aca6e031350e6d29f1e186 Mon Sep 17 00:00:00 2001 From: Alejandro Jimenez Date: Tue, 30 Jun 2026 22:08:06 +0000 Subject: [PATCH 05/44] amd_iommu: Fix endianness handling for command buffer entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AMD IOMMU command buffer entries are stored in guest memory in little-endian format. Convert command buffer with le64_to_cpu() after dma_memory_read(), so that command handlers can all operate using host native endianness. Remove the cpu_to_le*() conversions from command handlers, since the values are used internally by device emulation and do not need translation. Conversion is only necessary when reading or writing to guest memory e.g. writing completion-wait data and event log entries. The flow for command buffer handling is: - Retrieve command buffer (cmd[]) from guest memory (via dma_memory_read()) - Convert command buffer to host endianness (via le64_to_cpu()) - All handlers decode fields from cmd[] in host-endian format - All emulation code uses decoded values in host-endian format - Use cpu_to_le*() when writing back data to guest memory Fixes: d29a09ca6842 ("hw/i386: Introduce AMD IOMMU") Signed-off-by: Alejandro Jimenez Reviewed-by: Peter Maydell Reviewed-by: Philippe Mathieu-Daudé Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260630220806.1758748-6-alejandro.j.jimenez@oracle.com> --- hw/i386/amd_iommu.c | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/hw/i386/amd_iommu.c b/hw/i386/amd_iommu.c index c7bf21b762..90252c52af 100644 --- a/hw/i386/amd_iommu.c +++ b/hw/i386/amd_iommu.c @@ -278,6 +278,7 @@ static uint32_t get_next_eventlog_entry(AMDVIState *s) static void amdvi_log_event(AMDVIState *s, uint64_t *evt) { + uint64_t le_evt[2]; uint32_t evtlog_tail_next; /* event logging not enabled */ @@ -298,8 +299,14 @@ static void amdvi_log_event(AMDVIState *s, uint64_t *evt) return; } + /* + * Convert event buffer to little-endian before writing it to guest memory. + */ + le_evt[0] = cpu_to_le64(evt[0]); + le_evt[1] = cpu_to_le64(evt[1]); + if (dma_memory_write(&address_space_memory, s->evtlog + s->evtlog_tail, - evt, AMDVI_EVENT_LEN, MEMTXATTRS_UNSPECIFIED)) { + le_evt, AMDVI_EVENT_LEN, MEMTXATTRS_UNSPECIFIED)) { trace_amdvi_evntlog_fail(s->evtlog, s->evtlog_tail); } @@ -545,15 +552,18 @@ static void amdvi_update_iotlb(AMDVIState *s, uint16_t devid, static void amdvi_completion_wait(AMDVIState *s, uint64_t *cmd) { /* pad the last 3 bits */ - hwaddr addr = cpu_to_le64(extract64(cmd[0], 3, 49)) << 3; - uint64_t data = cpu_to_le64(cmd[1]); + hwaddr addr = extract64(cmd[0], 3, 49) << 3; + uint64_t data = cmd[1]; + + /* Format the data to be written to guest memory as little-endian */ + uint64_t le_data = cpu_to_le64(data); if (extract64(cmd[0], 52, 8)) { amdvi_log_illegalcom_error(s, extract64(cmd[0], 60, 4), s->cmdbuf + s->cmdbuf_head); } if (extract64(cmd[0], 0, 1)) { - if (dma_memory_write(&address_space_memory, addr, &data, + if (dma_memory_write(&address_space_memory, addr, &le_data, AMDVI_COMPLETION_DATA_SIZE, MEMTXATTRS_UNSPECIFIED)) { trace_amdvi_completion_wait_fail(addr); @@ -1281,7 +1291,7 @@ static void amdvi_update_addr_translation_mode(AMDVIState *s, uint16_t devid) /* log error without aborting since linux seems to be using reserved bits */ static void amdvi_inval_devtab_entry(AMDVIState *s, uint64_t *cmd) { - uint16_t devid = cpu_to_le16((uint16_t)extract64(cmd[0], 0, 16)); + uint16_t devid = extract64(cmd[0], 0, 16); trace_amdvi_devtab_inval(PCI_BUS_NUM(devid), PCI_SLOT(devid), PCI_FUNC(devid)); @@ -1448,9 +1458,9 @@ static void amdvi_sync_domain(AMDVIState *s, uint16_t domid, uint64_t addr, /* we don't have devid - we can't remove pages by address */ static void amdvi_inval_pages(AMDVIState *s, uint64_t *cmd) { - uint16_t domid = cpu_to_le16((uint16_t)extract64(cmd[0], 32, 16)); - uint64_t addr = cpu_to_le64(extract64(cmd[1], 12, 52)) << 12; - uint16_t flags = cpu_to_le16((uint16_t)extract64(cmd[1], 0, 3)); + uint16_t domid = extract64(cmd[0], 32, 16); + uint64_t addr = extract64(cmd[1], 12, 52) << 12; + uint16_t flags = extract64(cmd[1], 0, 3); if (extract64(cmd[0], 20, 12) || extract64(cmd[0], 48, 12) || extract64(cmd[1], 3, 9)) { @@ -1497,7 +1507,7 @@ static void amdvi_inval_inttable(AMDVIState *s, uint64_t *cmd) static void iommu_inval_iotlb(AMDVIState *s, uint64_t *cmd) { - uint16_t devid = cpu_to_le16(extract64(cmd[0], 0, 16)); + uint16_t devid = extract64(cmd[0], 0, 16); if (extract64(cmd[1], 1, 1) || extract64(cmd[1], 3, 1) || extract64(cmd[1], 6, 6)) { amdvi_log_illegalcom_error(s, extract64(cmd[0], 60, 4), @@ -1509,7 +1519,7 @@ static void iommu_inval_iotlb(AMDVIState *s, uint64_t *cmd) g_hash_table_foreach_remove(s->iotlb, amdvi_iotlb_remove_by_devid, &devid); } else { - amdvi_iotlb_remove_page(s, cpu_to_le64(extract64(cmd[1], 12, 52)) << 12, + amdvi_iotlb_remove_page(s, extract64(cmd[1], 12, 52) << 12, devid); } trace_amdvi_iotlb_inval(); @@ -1527,6 +1537,15 @@ static void amdvi_cmdbuf_exec(AMDVIState *s) return; } + /* + * Commands in guest memory are little-endian. Convert once after reading + * so that command handlers can decode values in host native endianness. + * Convert back to little-endian only when writing data to guest memory via + * dma_memory_write(). + */ + cmd[0] = le64_to_cpu(cmd[0]); + cmd[1] = le64_to_cpu(cmd[1]); + switch (extract64(cmd[0], 60, 4)) { case AMDVI_CMD_COMPLETION_WAIT: amdvi_completion_wait(s, cmd); From e83b01083631544bec27cbb3a381420b1ac7dc38 Mon Sep 17 00:00:00 2001 From: fanhuang Date: Tue, 23 Jun 2026 15:50:42 +0800 Subject: [PATCH 06/44] hw/mem: add sp-mem device for Specific Purpose Memory Introduce a TYPE_MEMORY_DEVICE subclass `sp-mem` for boot-time SOFT_RESERVED memory exposed to the guest with a per-device NUMA proximity domain. The device targets accelerator memory (HBM and similar) that the firmware hands to the guest OS as SOFT_RESERVED memory, so a driver in the guest -- rather than the kernel's general allocator -- owns the range. Usage: -object memory-backend-ram,id=spm0,size=$SIZE -numa node,nodeid=$N -device sp-mem,id=dev0,memdev=spm0,node=$N[,addr=$GPA] The device is boot-time only (no hotplug). Signed-off-by: FangSheng Huang Reviewed-by: Igor Mammedov Reviewed-by: David Hildenbrand (Arm) Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260623075051.3797975-2-FangSheng.Huang@amd.com> --- hw/mem/Kconfig | 4 ++ hw/mem/meson.build | 1 + hw/mem/sp-mem.c | 109 ++++++++++++++++++++++++++++++++++++++++ include/hw/mem/sp-mem.h | 33 ++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 hw/mem/sp-mem.c create mode 100644 include/hw/mem/sp-mem.h diff --git a/hw/mem/Kconfig b/hw/mem/Kconfig index 73c5ae8ad9..39ddb36710 100644 --- a/hw/mem/Kconfig +++ b/hw/mem/Kconfig @@ -16,3 +16,7 @@ config CXL_MEM_DEVICE bool default y if CXL select MEM_DEVICE + +config SP_MEM + bool + select MEM_DEVICE diff --git a/hw/mem/meson.build b/hw/mem/meson.build index 8c2beeb7d4..f410d75475 100644 --- a/hw/mem/meson.build +++ b/hw/mem/meson.build @@ -4,6 +4,7 @@ mem_ss.add(when: 'CONFIG_DIMM', if_true: files('pc-dimm.c')) mem_ss.add(when: 'CONFIG_NPCM7XX', if_true: files('npcm7xx_mc.c')) mem_ss.add(when: 'CONFIG_NVDIMM', if_true: files('nvdimm.c')) mem_ss.add(when: 'CONFIG_CXL_MEM_DEVICE', if_true: files('cxl_type3.c')) +mem_ss.add(when: 'CONFIG_SP_MEM', if_true: files('sp-mem.c')) stub_ss.add(files('cxl_type3_stubs.c')) stub_ss.add(files('memory-device-stubs.c')) diff --git a/hw/mem/sp-mem.c b/hw/mem/sp-mem.c new file mode 100644 index 0000000000..d088222f54 --- /dev/null +++ b/hw/mem/sp-mem.c @@ -0,0 +1,109 @@ +/* + * Specific Purpose Memory (SPM) device + * + * Copyright (c) 2026 Advanced Micro Devices, Inc. + * + * Authors: + * FangSheng Huang + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qemu/module.h" +#include "qapi/error.h" +#include "hw/core/qdev-properties.h" +#include "hw/core/qdev.h" +#include "hw/mem/sp-mem.h" +#include "hw/mem/memory-device.h" +#include "system/hostmem.h" + +#define SP_MEM_MEMDEV_PROP "memdev" +#define SP_MEM_NODE_PROP "node" +#define SP_MEM_ADDR_PROP "addr" + +static const Property sp_mem_properties[] = { + DEFINE_PROP_LINK(SP_MEM_MEMDEV_PROP, SpMemDevice, hostmem, + TYPE_MEMORY_BACKEND, HostMemoryBackend *), + DEFINE_PROP_UINT32(SP_MEM_NODE_PROP, SpMemDevice, node, 0), + DEFINE_PROP_UINT64(SP_MEM_ADDR_PROP, SpMemDevice, addr, 0), +}; + +static uint64_t sp_mem_get_addr(const MemoryDeviceState *md) +{ + return object_property_get_uint(OBJECT(md), SP_MEM_ADDR_PROP, + &error_abort); +} + +static void sp_mem_set_addr(MemoryDeviceState *md, uint64_t addr, + Error **errp) +{ + object_property_set_uint(OBJECT(md), SP_MEM_ADDR_PROP, addr, errp); +} + +static MemoryRegion *sp_mem_get_memory_region(MemoryDeviceState *md, + Error **errp) +{ + SpMemDevice *spm = SP_MEM(md); + + if (!spm->hostmem) { + error_setg(errp, "'%s' property must be set", SP_MEM_MEMDEV_PROP); + return NULL; + } + return host_memory_backend_get_memory(spm->hostmem); +} + +static void sp_mem_realize(DeviceState *dev, Error **errp) +{ + SpMemDevice *spm = SP_MEM(dev); + + if (!spm->hostmem) { + error_setg(errp, "'%s' property is required", SP_MEM_MEMDEV_PROP); + return; + } + if (host_memory_backend_is_mapped(spm->hostmem)) { + error_setg(errp, "memory backend '%s' is already in use", + object_get_canonical_path_component(OBJECT(spm->hostmem))); + return; + } + host_memory_backend_set_mapped(spm->hostmem, true); +} + +static void sp_mem_unrealize(DeviceState *dev) +{ + SpMemDevice *spm = SP_MEM(dev); + + host_memory_backend_set_mapped(spm->hostmem, false); +} + +static void sp_mem_class_init(ObjectClass *oc, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(oc); + MemoryDeviceClass *mdc = MEMORY_DEVICE_CLASS(oc); + + dc->desc = "SPM (Specific Purpose Memory) device"; + dc->hotpluggable = false; + dc->realize = sp_mem_realize; + dc->unrealize = sp_mem_unrealize; + device_class_set_props(dc, sp_mem_properties); + + mdc->get_addr = sp_mem_get_addr; + mdc->set_addr = sp_mem_set_addr; + mdc->get_memory_region = sp_mem_get_memory_region; + mdc->get_plugged_size = memory_device_get_region_size; +} + +static const TypeInfo sp_mem_types[] = { + { + .name = TYPE_SP_MEM, + .parent = TYPE_DEVICE, + .class_init = sp_mem_class_init, + .instance_size = sizeof(SpMemDevice), + .interfaces = (InterfaceInfo[]) { + { TYPE_MEMORY_DEVICE }, + { } + }, + }, +}; + +DEFINE_TYPES(sp_mem_types) diff --git a/include/hw/mem/sp-mem.h b/include/hw/mem/sp-mem.h new file mode 100644 index 0000000000..a8951b49e6 --- /dev/null +++ b/include/hw/mem/sp-mem.h @@ -0,0 +1,33 @@ +/* + * Specific Purpose Memory (SPM) device + * + * TYPE_MEMORY_DEVICE subclass for boot-time-only memory exposed to the + * guest as an E820 SOFT_RESERVED range with a SRAT memory-affinity entry. + * + * Copyright (c) 2026 Advanced Micro Devices, Inc. + * + * Authors: + * FangSheng Huang + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef QEMU_SP_MEM_H +#define QEMU_SP_MEM_H + +#include "hw/core/qdev.h" +#include "qom/object.h" + +#define TYPE_SP_MEM "sp-mem" + +OBJECT_DECLARE_SIMPLE_TYPE(SpMemDevice, SP_MEM) + +struct SpMemDevice { + DeviceState parent_obj; + + HostMemoryBackend *hostmem; + uint32_t node; + uint64_t addr; +}; + +#endif /* QEMU_SP_MEM_H */ From 839c0a2bd8132cdef2faab952d7c3f49b9fa984c Mon Sep 17 00:00:00 2001 From: fanhuang Date: Tue, 23 Jun 2026 15:50:43 +0800 Subject: [PATCH 07/44] qapi, hmp: introspection for the sp-mem device Add a SpMemDeviceInfo variant to MemoryDeviceInfo so `query-memory-devices` reports each sp-mem instance (id, addr, size, node, memdev), and print it from HMP `info memory-devices`. Signed-off-by: FangSheng Huang Reviewed-by: Igor Mammedov Reviewed-by: David Hildenbrand (Arm) Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260623075051.3797975-3-FangSheng.Huang@amd.com> --- hw/core/machine-hmp-cmds.c | 11 ++++++++++ hw/mem/sp-mem.c | 19 +++++++++++++++++ qapi/machine.json | 43 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/hw/core/machine-hmp-cmds.c b/hw/core/machine-hmp-cmds.c index 46846f741a..686304bafa 100644 --- a/hw/core/machine-hmp-cmds.c +++ b/hw/core/machine-hmp-cmds.c @@ -279,6 +279,7 @@ void hmp_info_memory_devices(Monitor *mon, const QDict *qdict) PCDIMMDeviceInfo *di; SgxEPCDeviceInfo *se; HvBalloonDeviceInfo *hi; + SpMemDeviceInfo *spmi; for (info = info_list; info; info = info->next) { value = info->value; @@ -350,6 +351,16 @@ void hmp_info_memory_devices(Monitor *mon, const QDict *qdict) monitor_printf(mon, " memdev: %s\n", hi->memdev); } break; + case MEMORY_DEVICE_INFO_KIND_SP_MEM: + spmi = value->u.sp_mem.data; + monitor_printf(mon, "Memory device [%s]: \"%s\"\n", + MemoryDeviceInfoKind_str(value->type), + spmi->id ? spmi->id : ""); + monitor_printf(mon, " addr: 0x%" PRIx64 "\n", spmi->addr); + monitor_printf(mon, " node: %" PRId64 "\n", spmi->node); + monitor_printf(mon, " size: %" PRIu64 "\n", spmi->size); + monitor_printf(mon, " memdev: %s\n", spmi->memdev); + break; default: g_assert_not_reached(); } diff --git a/hw/mem/sp-mem.c b/hw/mem/sp-mem.c index d088222f54..962d0f937e 100644 --- a/hw/mem/sp-mem.c +++ b/hw/mem/sp-mem.c @@ -53,6 +53,24 @@ static MemoryRegion *sp_mem_get_memory_region(MemoryDeviceState *md, return host_memory_backend_get_memory(spm->hostmem); } +static void sp_mem_fill_device_info(const MemoryDeviceState *md, + MemoryDeviceInfo *info) +{ + SpMemDeviceInfo *di = g_new0(SpMemDeviceInfo, 1); + SpMemDevice *spm = SP_MEM(md); + DeviceState *dev = DEVICE(md); + + di->id = dev->id ? g_strdup(dev->id) : NULL; + di->addr = spm->addr; + di->size = memory_region_size( + host_memory_backend_get_memory(spm->hostmem)); + di->node = spm->node; + di->memdev = object_get_canonical_path(OBJECT(spm->hostmem)); + + info->u.sp_mem.data = di; + info->type = MEMORY_DEVICE_INFO_KIND_SP_MEM; +} + static void sp_mem_realize(DeviceState *dev, Error **errp) { SpMemDevice *spm = SP_MEM(dev); @@ -91,6 +109,7 @@ static void sp_mem_class_init(ObjectClass *oc, const void *data) mdc->set_addr = sp_mem_set_addr; mdc->get_memory_region = sp_mem_get_memory_region; mdc->get_plugged_size = memory_device_get_region_size; + mdc->fill_device_info = sp_mem_fill_device_info; } static const TypeInfo sp_mem_types[] = { diff --git a/qapi/machine.json b/qapi/machine.json index 685e4e29b8..9b2248038f 100644 --- a/qapi/machine.json +++ b/qapi/machine.json @@ -1413,6 +1413,32 @@ } } +## +# @SpMemDeviceInfo: +# +# sp-mem device state information +# +# @id: device's ID +# +# @addr: physical address, where device is mapped +# +# @size: size of memory that the device provides, in bytes +# +# @node: NUMA proximity domain to which the device is assigned +# +# @memdev: memory backend linked with device +# +# Since: 11.1 +## +{ 'struct': 'SpMemDeviceInfo', + 'data': { '*id': 'str', + 'addr': 'size', + 'size': 'size', + 'node': 'int', + 'memdev': 'str' + } +} + ## # @MemoryDeviceInfoKind: # @@ -1426,11 +1452,13 @@ # # @hv-balloon: since 8.2. # +# @sp-mem: since 11.1. +# # Since: 2.1 ## { 'enum': 'MemoryDeviceInfoKind', 'data': [ 'dimm', 'nvdimm', 'virtio-pmem', 'virtio-mem', 'sgx-epc', - 'hv-balloon' ] } + 'hv-balloon', 'sp-mem' ] } ## # @PCDIMMDeviceInfoWrapper: @@ -1482,6 +1510,16 @@ { 'struct': 'HvBalloonDeviceInfoWrapper', 'data': { 'data': 'HvBalloonDeviceInfo' } } +## +# @SpMemDeviceInfoWrapper: +# +# @data: sp-mem device state information +# +# Since: 11.1 +## +{ 'struct': 'SpMemDeviceInfoWrapper', + 'data': { 'data': 'SpMemDeviceInfo' } } + ## # @MemoryDeviceInfo: # @@ -1499,7 +1537,8 @@ 'virtio-pmem': 'VirtioPMEMDeviceInfoWrapper', 'virtio-mem': 'VirtioMEMDeviceInfoWrapper', 'sgx-epc': 'SgxEPCDeviceInfoWrapper', - 'hv-balloon': 'HvBalloonDeviceInfoWrapper' + 'hv-balloon': 'HvBalloonDeviceInfoWrapper', + 'sp-mem': 'SpMemDeviceInfoWrapper' } } From 78f08a0f47e787921d8beb78a5bade9a8a64400b Mon Sep 17 00:00:00 2001 From: fanhuang Date: Tue, 23 Jun 2026 15:50:44 +0800 Subject: [PATCH 08/44] i386/acpi-build: partition device_memory SRAT umbrella for sp-mem Restructure the device_memory SRAT umbrella entry into a per-kind partition: each TYPE_SP_MEM device gets an ENABLED entry at its own proximity_domain; the remaining sub-ranges get HOTPLUGGABLE | ENABLED placeholders at the highest PXM, preserving the existing umbrella convention. Signed-off-by: FangSheng Huang Reviewed-by: Igor Mammedov Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260623075051.3797975-4-FangSheng.Huang@amd.com> --- hw/i386/acpi-build.c | 96 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/hw/i386/acpi-build.c b/hw/i386/acpi-build.c index 2ee061558c..8837b69687 100644 --- a/hw/i386/acpi-build.c +++ b/hw/i386/acpi-build.c @@ -52,6 +52,7 @@ #include "migration/vmstate.h" #include "hw/mem/memory-device.h" #include "hw/mem/nvdimm.h" +#include "hw/mem/sp-mem.h" #include "system/numa.h" #include "system/reset.h" #include "hw/hyperv/vmbus-bridge.h" @@ -1351,6 +1352,96 @@ build_tpm_tcpa(GArray *table_data, BIOSLinker *linker, GArray *tcpalog, } #endif +typedef struct { + uint64_t addr; + uint64_t size; + uint32_t node; +} SpMemRange; + +static int sp_mem_collect_ranges_cb(Object *obj, void *opaque) +{ + GArray *ranges = opaque; + SpMemDevice *spm; + MemoryDeviceClass *mdc; + SpMemRange r; + + if (!object_dynamic_cast(obj, TYPE_SP_MEM)) { + return 0; + } + spm = SP_MEM(obj); + mdc = MEMORY_DEVICE_GET_CLASS(MEMORY_DEVICE(spm)); + r.addr = mdc->get_addr(MEMORY_DEVICE(spm)); + r.size = memory_region_size( + host_memory_backend_get_memory(spm->hostmem)); + r.node = spm->node; + g_array_append_val(ranges, r); + return 0; +} + +static gint sp_mem_range_compare(gconstpointer a, gconstpointer b) +{ + const SpMemRange *range_a = a; + const SpMemRange *range_b = b; + + if (range_a->addr < range_b->addr) { + return -1; + } + if (range_a->addr > range_b->addr) { + return 1; + } + return 0; +} + +/* + * Emit SRAT memory-affinity entries covering the device_memory region. + * + * For each plugged TYPE_SP_MEM device, emit an ENABLED entry at the + * device's own proximity_domain. All remaining sub-ranges (gaps + * between sp-mem devices, leading and trailing padding, and ranges + * occupied by other memory devices) are covered by HOTPLUGGABLE | + * ENABLED placeholder entries at PXM = nb_numa_nodes - 1. + */ +static void build_srat_device_memory(GArray *table_data, MachineState *ms) +{ + g_autoptr(GArray) ranges = g_array_new(FALSE, TRUE, sizeof(SpMemRange)); + uint32_t hotplug_pxm = ms->numa_state->num_nodes - 1; + uint64_t region_start, region_end; + guint i; + + region_start = ms->device_memory->base; + region_end = region_start + memory_region_size(&ms->device_memory->mr); + + object_child_foreach_recursive(qdev_get_machine(), + sp_mem_collect_ranges_cb, ranges); + g_array_sort(ranges, sp_mem_range_compare); + + for (i = 0; i < ranges->len; i++) { + SpMemRange *r = &g_array_index(ranges, SpMemRange, i); + + if (region_start < r->addr) { + build_srat_memory(table_data, region_start, r->addr - region_start, + hotplug_pxm, + MEM_AFFINITY_HOTPLUGGABLE | + MEM_AFFINITY_ENABLED); + } + build_srat_memory(table_data, r->addr, r->size, r->node, + MEM_AFFINITY_ENABLED); + region_start = r->addr + r->size; + } + + /* + * Cover the rest of the device_memory window that no sp-mem device + * occupies. Keeping it HOTPLUGGABLE preserves the umbrella entry's + * role for future pc-dimm / virtio-mem hot-add into this window. + */ + if (region_start < region_end) { + build_srat_memory(table_data, region_start, region_end - region_start, + hotplug_pxm, + MEM_AFFINITY_HOTPLUGGABLE | + MEM_AFFINITY_ENABLED); + } +} + #define HOLE_640K_START (640 * KiB) #define HOLE_640K_END (1 * MiB) @@ -1487,10 +1578,7 @@ build_srat(GArray *table_data, BIOSLinker *linker, MachineState *machine) * providing _PXM method if necessary. */ if (machine->device_memory) { - build_srat_memory(table_data, machine->device_memory->base, - memory_region_size(&machine->device_memory->mr), - nb_numa_nodes - 1, - MEM_AFFINITY_HOTPLUGGABLE | MEM_AFFINITY_ENABLED); + build_srat_device_memory(table_data, machine); } acpi_table_end(linker, &table); From a597e6791f6ebf2b012f54462d0c44c33a781334 Mon Sep 17 00:00:00 2001 From: fanhuang Date: Tue, 23 Jun 2026 15:50:45 +0800 Subject: [PATCH 09/44] hw/i386: hook sp-mem into the pc machine plug path Add the pc machine hookup for TYPE_SP_MEM so each sp-mem instance is placed by the memory-device framework and reported to the guest as E820_SOFT_RESERVED. Signed-off-by: FangSheng Huang Reviewed-by: Igor Mammedov Reviewed-by: David Hildenbrand (Arm) Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260623075051.3797975-5-FangSheng.Huang@amd.com> --- hw/i386/Kconfig | 2 ++ hw/i386/e820_memory_layout.h | 11 ++++++----- hw/i386/pc.c | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig index 12473acaa7..e27d8816e5 100644 --- a/hw/i386/Kconfig +++ b/hw/i386/Kconfig @@ -84,6 +84,7 @@ config I440FX select PCI_I440FX select PIIX select DIMM + select SP_MEM select SMBIOS select SMBIOS_LEGACY select FW_CFG_DMA @@ -113,6 +114,7 @@ config Q35 select LPC_ICH9 select AHCI_ICH9 select DIMM + select SP_MEM select SMBIOS select FW_CFG_DMA diff --git a/hw/i386/e820_memory_layout.h b/hw/i386/e820_memory_layout.h index b50acfa201..6ef169db9c 100644 --- a/hw/i386/e820_memory_layout.h +++ b/hw/i386/e820_memory_layout.h @@ -10,11 +10,12 @@ #define HW_I386_E820_MEMORY_LAYOUT_H /* e820 types */ -#define E820_RAM 1 -#define E820_RESERVED 2 -#define E820_ACPI 3 -#define E820_NVS 4 -#define E820_UNUSABLE 5 +#define E820_RAM 1 +#define E820_RESERVED 2 +#define E820_ACPI 3 +#define E820_NVS 4 +#define E820_UNUSABLE 5 +#define E820_SOFT_RESERVED 0xefffffff struct e820_entry { uint64_t address; diff --git a/hw/i386/pc.c b/hw/i386/pc.c index 73a625327c..2bacece249 100644 --- a/hw/i386/pc.c +++ b/hw/i386/pc.c @@ -63,6 +63,7 @@ #include "hw/i386/kvm/xen_gnttab.h" #include "hw/i386/kvm/xen_xenstore.h" #include "hw/mem/memory-device.h" +#include "hw/mem/sp-mem.h" #include "e820_memory_layout.h" #include "trace.h" #include "sev.h" @@ -1285,11 +1286,43 @@ static void pc_hv_balloon_plug(HotplugHandler *hotplug_dev, memory_device_plug(MEMORY_DEVICE(dev), MACHINE(hotplug_dev)); } +static void pc_sp_mem_pre_plug(HotplugHandler *hotplug_dev, + DeviceState *dev, Error **errp) +{ + MachineState *ms = MACHINE(hotplug_dev); + SpMemDevice *spm = SP_MEM(dev); + + if (ms->numa_state && spm->node >= ms->numa_state->num_nodes) { + error_setg(errp, + "'node' property value %" PRIu32 + " exceeds the number of NUMA nodes (%d)", + spm->node, ms->numa_state->num_nodes); + return; + } + memory_device_pre_plug(MEMORY_DEVICE(dev), ms, errp); +} + +static void pc_sp_mem_plug(HotplugHandler *hotplug_dev, + DeviceState *dev, Error **errp) +{ + SpMemDevice *spm = SP_MEM(dev); + MemoryDeviceClass *mdc = MEMORY_DEVICE_GET_CLASS(MEMORY_DEVICE(dev)); + uint64_t addr, size; + + memory_device_plug(MEMORY_DEVICE(dev), MACHINE(hotplug_dev)); + + addr = mdc->get_addr(MEMORY_DEVICE(dev)); + size = memory_region_size(host_memory_backend_get_memory(spm->hostmem)); + e820_add_entry(addr, size, E820_SOFT_RESERVED); +} + static void pc_machine_device_pre_plug_cb(HotplugHandler *hotplug_dev, DeviceState *dev, Error **errp) { if (object_dynamic_cast(OBJECT(dev), TYPE_PC_DIMM)) { pc_memory_pre_plug(hotplug_dev, dev, errp); + } else if (object_dynamic_cast(OBJECT(dev), TYPE_SP_MEM)) { + pc_sp_mem_pre_plug(hotplug_dev, dev, errp); } else if (object_dynamic_cast(OBJECT(dev), TYPE_CPU)) { x86_cpu_pre_plug(hotplug_dev, dev, errp); } else if (object_dynamic_cast(OBJECT(dev), TYPE_VIRTIO_MD_PCI)) { @@ -1326,6 +1359,8 @@ static void pc_machine_device_plug_cb(HotplugHandler *hotplug_dev, { if (object_dynamic_cast(OBJECT(dev), TYPE_PC_DIMM)) { pc_memory_plug(hotplug_dev, dev, errp); + } else if (object_dynamic_cast(OBJECT(dev), TYPE_SP_MEM)) { + pc_sp_mem_plug(hotplug_dev, dev, errp); } else if (object_dynamic_cast(OBJECT(dev), TYPE_CPU)) { x86_cpu_plug(hotplug_dev, dev, errp); } else if (object_dynamic_cast(OBJECT(dev), TYPE_VIRTIO_MD_PCI)) { @@ -1370,6 +1405,7 @@ static HotplugHandler *pc_get_hotplug_handler(MachineState *machine, DeviceState *dev) { if (object_dynamic_cast(OBJECT(dev), TYPE_PC_DIMM) || + object_dynamic_cast(OBJECT(dev), TYPE_SP_MEM) || object_dynamic_cast(OBJECT(dev), TYPE_CPU) || object_dynamic_cast(OBJECT(dev), TYPE_VIRTIO_MD_PCI) || object_dynamic_cast(OBJECT(dev), TYPE_VIRTIO_IOMMU_PCI) || From ca44fcee5a0d49cc4cf2feae6adacb57d7936f02 Mon Sep 17 00:00:00 2001 From: fanhuang Date: Tue, 23 Jun 2026 15:50:46 +0800 Subject: [PATCH 10/44] MAINTAINERS: cover sp-mem under Memory devices, add R: tag Signed-off-by: FangSheng Huang Acked-by: Igor Mammedov Acked-by: David Hildenbrand Reviewed-by: Michael S. Tsirkin Acked-by: David Hildenbrand (Arm) Signed-off-by: Michael S. Tsirkin Message-ID: <20260623075051.3797975-6-FangSheng.Huang@amd.com> --- MAINTAINERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 97dcc78ded..5ee7c1f601 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -3417,13 +3417,16 @@ F: tests/unit/test-ram-discard-manager-stubs.c Memory devices M: David Hildenbrand M: Igor Mammedov +R: FangSheng Huang S: Supported F: hw/mem/memory-device*.c F: hw/mem/nvdimm.c F: hw/mem/pc-dimm.c +F: hw/mem/sp-mem.c F: include/hw/mem/memory-device.h F: include/hw/mem/nvdimm.h F: include/hw/mem/pc-dimm.h +F: include/hw/mem/sp-mem.h F: docs/nvdimm.txt SPICE From 54f0db2f592af764d329892bd85bd35fa03bde52 Mon Sep 17 00:00:00 2001 From: fanhuang Date: Tue, 23 Jun 2026 15:50:47 +0800 Subject: [PATCH 11/44] tests/acpi: add empty expected blobs for sp-mem SRAT test Add empty SRAT.spmem and DSDT.spmem stubs and list them in bios-tables-test-allowed-diff.h. Signed-off-by: FangSheng Huang Acked-by: Igor Mammedov Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260623075051.3797975-7-FangSheng.Huang@amd.com> --- tests/data/acpi/x86/q35/DSDT.spmem | 0 tests/data/acpi/x86/q35/SRAT.spmem | 0 tests/qtest/bios-tables-test-allowed-diff.h | 2 ++ 3 files changed, 2 insertions(+) create mode 100644 tests/data/acpi/x86/q35/DSDT.spmem create mode 100644 tests/data/acpi/x86/q35/SRAT.spmem diff --git a/tests/data/acpi/x86/q35/DSDT.spmem b/tests/data/acpi/x86/q35/DSDT.spmem new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/data/acpi/x86/q35/SRAT.spmem b/tests/data/acpi/x86/q35/SRAT.spmem new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/qtest/bios-tables-test-allowed-diff.h b/tests/qtest/bios-tables-test-allowed-diff.h index dfb8523c8b..188003fa90 100644 --- a/tests/qtest/bios-tables-test-allowed-diff.h +++ b/tests/qtest/bios-tables-test-allowed-diff.h @@ -1 +1,3 @@ /* List of comma-separated changed AML files to ignore */ +"tests/data/acpi/x86/q35/DSDT.spmem", +"tests/data/acpi/x86/q35/SRAT.spmem", From ff1d11b7bfb34941c13cd70668f4f9e253c588ba Mon Sep 17 00:00:00 2001 From: fanhuang Date: Tue, 23 Jun 2026 15:50:48 +0800 Subject: [PATCH 12/44] tests/acpi: add bios-tables-test case for sp-mem Add a q35 bios-tables-test case that boots two sp-mem devices on distinct NUMA nodes within the device_memory window, exercising the per-kind SRAT partition (per-device ENABLED entries plus HOTPLUGGABLE placeholders for the remaining sub-ranges). Signed-off-by: FangSheng Huang Reviewed-by: Igor Mammedov Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260623075051.3797975-8-FangSheng.Huang@amd.com> --- tests/qtest/bios-tables-test.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/qtest/bios-tables-test.c b/tests/qtest/bios-tables-test.c index af6d9b5136..45abd8bd8c 100644 --- a/tests/qtest/bios-tables-test.c +++ b/tests/qtest/bios-tables-test.c @@ -1416,6 +1416,26 @@ static void test_acpi_q35_tcg_numamem(void) free_test_data(&data); } +static void test_acpi_q35_tcg_sp_mem(void) +{ + test_data data = {}; + + data.machine = MACHINE_Q35; + data.arch = "x86", + data.variant = ".spmem"; + test_acpi_one(" -m 128M,slots=4,maxmem=1G" + " -object memory-backend-ram,id=ram0,size=128M" + " -numa node,nodeid=0,memdev=ram0" + " -numa node,nodeid=1" + " -numa node,nodeid=2" + " -object memory-backend-ram,id=spm0,size=128M" + " -object memory-backend-ram,id=spm1,size=128M" + " -device sp-mem,id=sp0,memdev=spm0,node=1" + " -device sp-mem,id=sp1,memdev=spm1,node=2", + &data); + free_test_data(&data); +} + static void test_acpi_q35_kvm_xapic(void) { test_data data = {}; @@ -2807,6 +2827,7 @@ int main(int argc, char *argv[]) if (strcmp(arch, "i386")) { qtest_add_func("acpi/q35/memhp", test_acpi_q35_tcg_memhp); qtest_add_func("acpi/q35/dimmpxm", test_acpi_q35_tcg_dimm_pxm); + qtest_add_func("acpi/q35/sp-mem", test_acpi_q35_tcg_sp_mem); qtest_add_func("acpi/q35/acpihmat", test_acpi_q35_tcg_acpi_hmat); qtest_add_func("acpi/q35/mmio64", test_acpi_q35_tcg_mmio64); From f46bd7b21ed3ae92e6c1820d7987807af67150fa Mon Sep 17 00:00:00 2001 From: fanhuang Date: Tue, 23 Jun 2026 15:50:49 +0800 Subject: [PATCH 13/44] tests/acpi: generate expected blobs for sp-mem SRAT test Populate the expected ACPI blobs for the sp-mem test and clear the allowed-diff list. SRAT memory-affinity entries for the device_memory window (q35, -m 128M,maxmem=1G, sp0 on node 1 and sp1 on node 2, each 128M): Proximity Domain : 1 Base : 0x100000000 Length : 0x08000000 (Enabled) Proximity Domain : 2 Base : 0x108000000 Length : 0x08000000 (Enabled) Proximity Domain : 2 Base : 0x110000000 Length : 0x128000000 (Hot Pluggable) Each sp-mem device gets an ENABLED entry at its own proximity domain; the remaining device_memory window is covered by a HOTPLUGGABLE placeholder at the highest proximity domain. (DSDT.spmem differs from the base only by the memory-hotplug AML enabled by -m,maxmem.) Signed-off-by: FangSheng Huang Acked-by: Igor Mammedov Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260623075051.3797975-9-FangSheng.Huang@amd.com> --- tests/data/acpi/x86/q35/DSDT.spmem | Bin 0 -> 9910 bytes tests/data/acpi/x86/q35/SRAT.spmem | Bin 0 -> 384 bytes tests/qtest/bios-tables-test-allowed-diff.h | 2 -- 3 files changed, 2 deletions(-) diff --git a/tests/data/acpi/x86/q35/DSDT.spmem b/tests/data/acpi/x86/q35/DSDT.spmem index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7e6f850e7a25aaa3cb7abc0ff2aeab4ca7fdb716 100644 GIT binary patch literal 9910 zcmZ<>b_v<0#=yXs<>c?|5v<@85#X$#prF9Wz`y`vgJ=OymKcWU1{Wvyct&m}7RKmC zZZO-$$=!)VM90M`)SYogM#8j&gp&3I#>4`Kq(uT#lM)M>I}(x@K#EF}3b?pH3;}Q9 z_+Y2_=q6A80B27Fj(87G7aqq8TpS$n&JY$eM|`NK3xkLcd%SaIO3gyf(7NdWZK0U77o$b z5bWv|&&Yj(hl?e;ksHKub~14FQiyJF^79R1p3ZIzB0TW{p3aN~86ciOQy3#C#08vR zfdZW)ASfh2$h){fh?|A`|9=J_KW|qC2%FKz&)bbbhzBIj#pL7X?GEEG`}lc#FbMIo z@cjSJ$N|mWR7qEkM!j!T@biy3Z4$%p- zg#)Y;<^m3|PMA^-h)$T}IUqV=ws3-V!d$=!)(KO}3DF62JSRja%oZ-NPM8b0z&c?{ zxga`Wj^~2tgxSIk)(LX~H&`c3DK|tX%<6Q-08q7&wLK8Q}3E&O1eFcE)W6hgeet) z=!7|51fmmWizrwp%mt!goiL@M5S=i`i$Zk5Y!L(NgtQVEDonByfNI$^d*f^|AVOHE0zPA6z# zCkfH%jNpPwO&6$IDHNSjC_1H3bxJ|(gz1z<(J76hQyNvLG^$P+6rD0CI%QCG%Ao3$ zMbRmXqEi-Cr!1;YIj~Mx*vf%*!opS#q7xRjauA)cu$2evgoUjO z>B0c2Kpl}PQ1@U@cm?Vn?1`vA-Ge<56{vf#C(Ket1_p>aXm!d6Q3$UAEfI$6OwVOfV2tP>Uwpb8YC6s!|efkGS)*2xCe3G*i# zSSQS%pb8YC6s!|efkGS)*2xam3G*jASSQS%pb8YC6s!|efkGS)*2w|Z3G*ihSSQS% zpb8YC6s!|efkGS)*2xLh3G*i>SSQS%pb8YC6s!|efkGS)*2x9d3G*ixSSQS%pb8YC z6s!|efkGS)*2xXl3G*j6SSQS%pb8YC6s!|efkGS)*2x3b3G*ipSSQS%pb8YC6s!|e zfkGS)*2xRj3G*i}SSQS%pb8YC6s!|efkGS)*2xFf3G*i(SSQS%pb8YC6s!|efkGS) z*2xdn3G*jESSQS%pb8YC6s!|efkGS))+qqi3G=4_SSQS%pb8YC6s!|efkGS))+q?q z3G=5QSSQS%pb8YC6s!|efkGS))+q$m3G=5ASSQS%pb8YC6s!|efkGS))+r3u3G=5g zSSQS%pb8YC6s!|efkGS))+qwk3G=52SSQS%pb8YC6s!|efkGS))+q|s3G=5YSSQS% zpb8YC6s!|efkGS))+q+o3G=5ISSQS%pb8YC6s!|efkGS))+r9w3G=5oSSQS%pb8YC z6s!|efkGS))+qtj3G=4}SSQS%pb8YC6s!|efkGS))+q_r2`l6!!8&1uJg5S75B7u= z@{$Z(pz;=04N9TtltR%7sz8x+f+|q=U{9D%X%wB(C^|tED3VT41&X9o21Ta~icU}k zilh@%fg3KS`9<-s~(VJi>T z2@6|L1&U-Rr~(DsDIygg5ER0&fM+tOd&;l`)FEBS2BI8Aggpa;Ll_ruR&zCEFf8HX zT*AdIV&WO-9K^{H5ENt}$j8Oq$;QA59$cto=mLpxu?7SMc@|V>aPc#MSW7^H(M>+= zK7QVg;1LQQ#|u3CkP#V<_<*2bK`}1jPCf=sW>y9U1}+8$25trhhDrtz{`lY!M}{RJ zCj(C-Wkn$XB^f$L#=l~^E?+b z3D&zHS?>b2-UZEi7aZ2RK&^L0x1NOr>s^tocLiJTie|kl4(nZ^*1Mrw&q{*zZb;U< zfvtB#v)&Dd^=?q>-O;UQBf)xiBZuICbfSPv^a1i;xHGF*u&Cc*~J@*-^Dd<``YJsWcgq8cZN#W;u< zR^u@8r4XudLRgH0h+#DjBcBPQ8Yhg!IEWZl<1q4#2&!=+Sd4>+VKojTABdtFCyK>5 zh!|GmFw(Ucs&QgijDv__H4Y=4ilZ7Qj>R~L7^-msz8s(t*61cz4vzrW5LoJGgiZrQ zH+Teu_%iS?FkJY@&A<}fC;;LMhq?N>C4h0YRWq{2M%6JkgCBAdVx0f^&eY zAEUFUUofL{kZ%~Hv#XZ@h;UWC^6bc9m4qQexk4w;ND0djQ#7k@&uI);po_mTt=UsjM$NVA9$-6&Tt1FYtYcuF}3 zJNt3q@CJvopKG`f(|-Uq!chF6fh(fFr+T>f#uZycC@7xc*O0<@#U1n6k70o1z6O7$FIY z`hw&Rked@3N)rnh6R;=(B@K`wS1+GbhSJmm#)Q&j(8QZ73uNXEq&tZr85G~ij0MTj zhS5yGB+!KFzcgPd5VCZ!fMcYuNnW)K%Mk){OshLt8^k11bHXaC?39vm@MkbxdZ zpg_lPrL(U~K_*v$phmO^U%Y>C2sg+x3>leO9Pt4Wz6>JLF?ygPB;M7)hyhw!fN~@# zaYi@!vOqu&8GC z1(jUB{;pvPzW%|^3L?VbGJFB&WRRN}Km{~+G@~zgF4B?BH`v*a0U=kwkPxj5lKcOE zk~_T7{CT{PX_x5QmBDdCx|#o_&Ns#bHbbeQ_JBSWM(30%w^D}&A`CJ3`su- z@*iln5;S266@qgAS2A>gG;;Czdiwc5m_ojuejX5}nvW|?RLjQ|CMxFZ7y(r)f2kR}kr2V_uzZ;+1%NCSxB12UutBn)DJ^ny5GX^`#(B2z(PAo(H?pAkeX5CjV{ zmVh{55fIA<#PV6-!af-!%)sFrWNZYVvxa*ah3=AN-)1bltRG~taE$}qJ zn%AI20a~{J>j;C=6qFAt7M6hA4CV`h3JqqE>p znppZ&2nZ-T86|xEkt*!uxBeh{U zKqWL6H^j%_DoBA%fsIk1k|DauizUD#APAgl&<~g8~^r zqg)&TL4i&n!Wl%ka4;wY1O>W+*lr-g9YlC=L^qjuJGsPzLzc(!0y8rtw{@(Bnrgqh6(X?!rE zs1@Mib!V(#u3+ZiP-+01Z0zjs3)jQS2+qZ*dIY)n+!=o`|6pd|P!<3;QJPePLY(2w z;Rf|af`SFXW*0CNFfei`F))H$%^&RM2n~2926=|)CjMYIC#V23gFJ%(52#@!!WQpp HV8{Rfavqh4 literal 0 HcmV?d00001 diff --git a/tests/data/acpi/x86/q35/SRAT.spmem b/tests/data/acpi/x86/q35/SRAT.spmem index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9ecd266eda68d035f72f170cb1346b97da623c28 100644 GIT binary patch literal 384 zcmWFzatvu;WME+AbMklg2v%^42yj+VP*7lGU|;~TK{N<6z<>Y)12Ugc1115ZxnK+& z>I9(jAK0M`9O_`Q*lA3ADOHE?6_mvRGZD%Ma~U<5z%&B`0|!)`kU9aVIt?hzzzj2i Hfq?-4AFc@j literal 0 HcmV?d00001 diff --git a/tests/qtest/bios-tables-test-allowed-diff.h b/tests/qtest/bios-tables-test-allowed-diff.h index 188003fa90..dfb8523c8b 100644 --- a/tests/qtest/bios-tables-test-allowed-diff.h +++ b/tests/qtest/bios-tables-test-allowed-diff.h @@ -1,3 +1 @@ /* List of comma-separated changed AML files to ignore */ -"tests/data/acpi/x86/q35/DSDT.spmem", -"tests/data/acpi/x86/q35/SRAT.spmem", From 0819ca9dbd882fff0a1cf9a241a444b4bd34ebbd Mon Sep 17 00:00:00 2001 From: fanhuang Date: Tue, 23 Jun 2026 15:50:50 +0800 Subject: [PATCH 14/44] tests/qtest: add e820 fw_cfg test Add a qtest that reads the "etc/e820" fw_cfg table and checks its structural invariants: the file is a whole number of e820 entries and every entry has a non-zero length. The baseline q35 case asserts the guest sees RAM and, with no sp-mem device, no SOFT_RESERVED range. Signed-off-by: FangSheng Huang Acked-by: Igor Mammedov Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260623075051.3797975-10-FangSheng.Huang@amd.com> --- tests/qtest/e820-test.c | 95 +++++++++++++++++++++++++++++++++++++++++ tests/qtest/meson.build | 1 + 2 files changed, 96 insertions(+) create mode 100644 tests/qtest/e820-test.c diff --git a/tests/qtest/e820-test.c b/tests/qtest/e820-test.c new file mode 100644 index 0000000000..1db0744c08 --- /dev/null +++ b/tests/qtest/e820-test.c @@ -0,0 +1,95 @@ +/* + * qtest e820 fw_cfg test case + * + * Validate the "etc/e820" fw_cfg table that QEMU hands to the firmware. + * + * Copyright (c) 2026 Advanced Micro Devices, Inc. + * + * Authors: + * FangSheng Huang + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" + +#include "libqtest.h" +#include "libqos/fw_cfg.h" +#include "qemu/bswap.h" + +/* e820 entry layout and types (cf. hw/i386/e820_memory_layout.h) */ +#define E820_RAM 1 +#define E820_SOFT_RESERVED 0xefffffff + +struct e820_entry { + uint64_t address; + uint64_t length; + uint32_t type; +} QEMU_PACKED; + +#define E820_MAX_ENTRIES 128 + +/* + * Read and structurally validate "etc/e820": the file is a packed array + * of struct e820_entry, so its size must be a whole multiple of the entry + * size and every entry must have a non-zero length. Returns the entry + * count and fills @table. + */ +static size_t get_e820_table(QFWCFG *fw_cfg, struct e820_entry *table) +{ + size_t filesize, n, i; + + filesize = qfw_cfg_get_file(fw_cfg, "etc/e820", table, + E820_MAX_ENTRIES * sizeof(*table)); + g_assert_cmpint(filesize, >, 0); + g_assert_cmpint(filesize % sizeof(struct e820_entry), ==, 0); + + n = filesize / sizeof(struct e820_entry); + g_assert_cmpint(n, <=, E820_MAX_ENTRIES); + + for (i = 0; i < n; i++) { + g_assert_cmpint(le64_to_cpu(table[i].length), >, 0); + } + + return n; +} + +static void test_e820_basic(void) +{ + struct e820_entry table[E820_MAX_ENTRIES]; + QFWCFG *fw_cfg; + QTestState *s; + size_t n, i; + bool found_ram = false, found_soft_reserved = false; + + s = qtest_init("-machine q35 -m 256M"); + fw_cfg = pc_fw_cfg_init(s); + + n = get_e820_table(fw_cfg, table); + for (i = 0; i < n; i++) { + switch (le32_to_cpu(table[i].type)) { + case E820_RAM: + found_ram = true; + break; + case E820_SOFT_RESERVED: + found_soft_reserved = true; + break; + } + } + + /* baseline: RAM present, no SOFT_RESERVED range */ + g_assert_true(found_ram); + g_assert_false(found_soft_reserved); + + pc_fw_cfg_uninit(fw_cfg); + qtest_quit(s); +} + +int main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + + qtest_add_func("e820/basic", test_e820_basic); + + return g_test_run(); +} diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build index 5e18b947c7..93175baa87 100644 --- a/tests/qtest/meson.build +++ b/tests/qtest/meson.build @@ -58,6 +58,7 @@ qtests_i386 = \ (config_all_devices.has_key('CONFIG_AHCI_ICH9') ? ['tco-test'] : []) + \ (config_all_devices.has_key('CONFIG_FDC_ISA') ? ['fdc-test'] : []) + \ (config_all_devices.has_key('CONFIG_I440FX') ? ['fw_cfg-test'] : []) + \ + (config_all_devices.has_key('CONFIG_Q35') ? ['e820-test'] : []) + \ (config_all_devices.has_key('CONFIG_FW_CFG_DMA') ? ['vmcoreinfo-test'] : []) + \ (config_all_devices.has_key('CONFIG_Q35') ? ['dump-test'] : []) + \ (config_all_devices.has_key('CONFIG_I440FX') ? ['i440fx-test'] : []) + \ From c31b39063fe9af23ba78dd15155f73ef8f51e8ac Mon Sep 17 00:00:00 2001 From: fanhuang Date: Tue, 23 Jun 2026 15:50:51 +0800 Subject: [PATCH 15/44] tests/qtest: cover sp-mem SOFT_RESERVED e820 entry Boot one sp-mem device and assert the guest's e820 table gains exactly one E820_SOFT_RESERVED range whose length matches the device's backend size. Signed-off-by: FangSheng Huang Acked-by: Igor Mammedov Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260623075051.3797975-11-FangSheng.Huang@amd.com> --- tests/qtest/e820-test.c | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/qtest/e820-test.c b/tests/qtest/e820-test.c index 1db0744c08..aafa3c1aa2 100644 --- a/tests/qtest/e820-test.c +++ b/tests/qtest/e820-test.c @@ -16,6 +16,7 @@ #include "libqtest.h" #include "libqos/fw_cfg.h" #include "qemu/bswap.h" +#include "qemu/units.h" /* e820 entry layout and types (cf. hw/i386/e820_memory_layout.h) */ #define E820_RAM 1 @@ -85,11 +86,44 @@ static void test_e820_basic(void) qtest_quit(s); } +static void test_e820_sp_mem(void) +{ + struct e820_entry table[E820_MAX_ENTRIES]; + QFWCFG *fw_cfg; + QTestState *s; + size_t n, i; + int soft_reserved = 0; + uint64_t soft_reserved_len = 0; + + s = qtest_init("-machine q35 -m 256M,slots=2,maxmem=2G " + "-object memory-backend-ram,id=ram0,size=256M " + "-numa node,nodeid=0,memdev=ram0 " + "-object memory-backend-ram,id=spm0,size=128M " + "-device sp-mem,id=sp0,memdev=spm0,node=0"); + fw_cfg = pc_fw_cfg_init(s); + + n = get_e820_table(fw_cfg, table); + for (i = 0; i < n; i++) { + if (le32_to_cpu(table[i].type) == E820_SOFT_RESERVED) { + soft_reserved++; + soft_reserved_len = le64_to_cpu(table[i].length); + } + } + + /* exactly one SOFT_RESERVED range, sized to the backend */ + g_assert_cmpint(soft_reserved, ==, 1); + g_assert_cmpint(soft_reserved_len, ==, 128 * MiB); + + pc_fw_cfg_uninit(fw_cfg); + qtest_quit(s); +} + int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); qtest_add_func("e820/basic", test_e820_basic); + qtest_add_func("e820/sp-mem", test_e820_sp_mem); return g_test_run(); } From dbc0ed56223011281e14b5cb8c02453915d35d12 Mon Sep 17 00:00:00 2001 From: no92 Date: Wed, 24 Jun 2026 12:39:35 +0200 Subject: [PATCH 16/44] intel_iommu: Correctly set pt bit in extended capability register With the changes in c7b2e22bd957, the `pt` bit was set in the (wrong) capability register, instead of the (correct) extended capability register. Fixes: c7b2e22bd957 ("hw/i386/x86-iommu: Remove X86IOMMUState::pt_supported field") Signed-off-by: no92 Reviewed-by: Clement Mathieu--Drif Reviewed-by: Yi Liu Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260624103933.1793586-3-leo@managarm.org> --- hw/i386/intel_iommu.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hw/i386/intel_iommu.c b/hw/i386/intel_iommu.c index 744cdfd2e6..d1af7a3135 100644 --- a/hw/i386/intel_iommu.c +++ b/hw/i386/intel_iommu.c @@ -4988,7 +4988,7 @@ static void vtd_cap_init(IntelIOMMUState *s) { X86IOMMUState *x86_iommu = X86_IOMMU_DEVICE(s); - s->cap = VTD_CAP_FRO | VTD_CAP_NFR | VTD_CAP_ND | VTD_ECAP_PT | + s->cap = VTD_CAP_FRO | VTD_CAP_NFR | VTD_CAP_ND | VTD_CAP_MAMV | VTD_CAP_PSI | VTD_CAP_SSLPS | VTD_CAP_DRAIN | VTD_CAP_ESRTPS | VTD_CAP_MGAW(s->aw_bits); if (x86_iommu->dma_translation) { @@ -4999,7 +4999,7 @@ static void vtd_cap_init(IntelIOMMUState *s) s->cap |= VTD_CAP_SAGAW_48bit; } } - s->ecap = VTD_ECAP_QI | VTD_ECAP_IRO; + s->ecap = VTD_ECAP_QI | VTD_ECAP_IRO | VTD_ECAP_PT; if (x86_iommu_ir_supported(x86_iommu)) { s->ecap |= VTD_ECAP_IR | VTD_ECAP_MHMV; From 37b14d86d65a29e94a1d50cb4b9593dffc612e76 Mon Sep 17 00:00:00 2001 From: Stefan Hajnoczi Date: Thu, 4 Jun 2026 16:10:27 -0400 Subject: [PATCH 17/44] vhost-user.rst: clarify when rings are started Jorge Moreira pointed out that the ring state machine is underspecified. In the discussion that followed, we discovered that the spec says one thing and implementations do something else. This patch updates the spec to reflect how things are actually implemented across widely-used front-ends and back-ends including QEMU, crosvm, rust-vmm, and DPDK. Do this while taking care not to make any other existing implementations non-compliant by changing the spec. The spec says rings are started when a kick is received but the implementations actually start rings when VHOST_USER_SET_VRING_KICK is received. Reconcile this as follows: - Clarify that a ring can be stopped and then started again. The back-end must resume processing available requests when the ring is restarted. - Update the spec to say rings are started when VHOST_USER_SET_VRING_KICK is received. - Ensure compatibility by saying front-ends SHOULD inject a kick in case the back-end strictly implemented the old spec. - Avoid future back-end dependencies on injected kicks by saying that back-ends SHOULD NOT expect a kick to start rings. This way implementors have clarity on how things work while still allowing compatibility for existing implementations. Reported-by: Jorge Moreira Cc: "Michael S . Tsirkin" Signed-off-by: Stefan Hajnoczi Reviewed-by: Stefano Garzarella Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260604201029.250450-2-stefanha@redhat.com> --- docs/interop/vhost-user.rst | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/interop/vhost-user.rst b/docs/interop/vhost-user.rst index c83ae2accb..a704f3cb53 100644 --- a/docs/interop/vhost-user.rst +++ b/docs/interop/vhost-user.rst @@ -518,12 +518,26 @@ Rings have two independent states: started/stopped, and enabled/disabled. * started and enabled: The back-end must process the ring normally, i.e. process all requests and execute them. -Each ring is initialized in a stopped and disabled state. The back-end -must start a ring upon receiving a kick (that is, detecting that file -descriptor is readable) on the descriptor specified by -``VHOST_USER_SET_VRING_KICK`` or receiving the in-band message -``VHOST_USER_VRING_KICK`` if negotiated, and stop a ring upon receiving -``VHOST_USER_GET_VRING_BASE``. +Each ring is initialized in a stopped and disabled state. Rings are started +with ``VHOST_USER_SET_VRING_KICK`` (or ``VHOST_USER_VRING_KICK`` if +``VHOST_USER_PROTOCOL_F_INBAND_NOTIFICATIONS`` is negotiated) and stopped with +``VHOST_USER_GET_VRING_BASE``. A stopped ring enters the started state again +with ``VHOST_USER_SET_VRING_KICK`` (or ``VHOST_USER_VRING_KICK`` if +``VHOST_USER_PROTOCOL_F_INBAND_NOTIFICATIONS`` is negotiated) and the back-end +resumes processing requests. + +Note that previous versions of this specification stated that rings start when +the back-end receives a kick (that is, detecting that file descriptor is +readable) on the descriptor specified by ``VHOST_USER_SET_VRING_KICK`` or +receiving the in-band message ``VHOST_USER_VRING_KICK`` if negotiated. +Widely-used front-ends and back-ends did not implement this behavior and it +complicates poll mode back-ends that do not rely on the kick file descriptor. + +For compatibility with back-ends that implemented the start on kick behavior, +front-ends SHOULD inject a kick after ``VHOST_USER_SET_VRING_KICK``. This +ensures that the back-end processes any available requests in the ring. +Back-ends SHOULD NOT rely on receiving a kick after +``VHOST_USER_SET_VRING_KICK``. Rings can be enabled or disabled by ``VHOST_USER_SET_VRING_ENABLE``. From 1ace07171a4f772afd85a419bcd55fe56631966f Mon Sep 17 00:00:00 2001 From: Stefan Hajnoczi Date: Thu, 4 Jun 2026 16:10:28 -0400 Subject: [PATCH 18/44] libvhost-user: look for available vq buffers upon SET_VRING_KICK When a vring is started the back-end must look for available vq buffers and process them. This scenario can happen if the back-end is stopped with unprocessed available buffers and then started again. The inflight I/O tracking code already did this, but it should also be done when inflight I/O tracking is not enabled. Move the code and make it robust in case of EINTR or EAGAIN. Signed-off-by: Stefan Hajnoczi Reviewed-by: Stefano Garzarella Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260604201029.250450-3-stefanha@redhat.com> --- subprojects/libvhost-user/libvhost-user.c | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/subprojects/libvhost-user/libvhost-user.c b/subprojects/libvhost-user/libvhost-user.c index d2df50e3d0..2c35bddd6f 100644 --- a/subprojects/libvhost-user/libvhost-user.c +++ b/subprojects/libvhost-user/libvhost-user.c @@ -1390,11 +1390,6 @@ vu_check_queue_inflights(VuDev *dev, VuVirtq *vq) vq->counter = vq->resubmit_list[0].counter + 1; } - /* in case of I/O hang after reconnecting */ - if (eventfd_write(vq->kick_fd, 1)) { - return -1; - } - return 0; } @@ -1436,6 +1431,20 @@ vu_set_vring_kick_exec(VuDev *dev, VhostUserMsg *vmsg) vu_panic(dev, "Failed to check inflights for vq: %d\n", index); } + /* Inject a kick to look for available vq buffers */ + if (dev->vq[index].kick_fd != -1) { + int ret; + + do { + ret = eventfd_write(dev->vq[index].kick_fd, 1); + } while (ret != 0 && errno == EINTR); + + if (ret != 0 && errno != EAGAIN /* already readable */) { + vu_panic(dev, "Failed to inject kick during SET_VRING_KICK " + "on vq: %d with error: %m\n", index); + } + } + return false; } From ef02c6becfb1383bb50f7cf724f77a9bccf17bdd Mon Sep 17 00:00:00 2001 From: Stefan Hajnoczi Date: Thu, 4 Jun 2026 16:10:29 -0400 Subject: [PATCH 19/44] vhost-user: inject kick after SET_VRING_KICK The vhost-user specification was updated to say that front-ends should inject a kick after SET_VRING_KICK in case the back-end implements the old spec wording which said vrings start when a kick is received. Do this in QEMU's front-end. An example scenario where this behavior helps: the back-end fails to check if the vring has available buffers when SET_VRING_KICK is received and the front-end stopped and then restarted the vring. In the case the back-end may not notice the available buffers unless the front-end injects a kick. Signed-off-by: Stefan Hajnoczi Reviewed-by: Stefano Garzarella Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260604201029.250450-4-stefanha@redhat.com> --- hw/virtio/vhost-user.c | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/hw/virtio/vhost-user.c b/hw/virtio/vhost-user.c index d627351f45..517cc4ca71 100644 --- a/hw/virtio/vhost-user.c +++ b/hw/virtio/vhost-user.c @@ -1487,7 +1487,29 @@ static int vhost_set_vring_file(struct vhost_dev *dev, static int vhost_user_set_vring_kick(struct vhost_dev *dev, struct vhost_vring_file *file) { - return vhost_set_vring_file(dev, VHOST_USER_SET_VRING_KICK, file); + int ret = vhost_set_vring_file(dev, VHOST_USER_SET_VRING_KICK, file); + if (ret < 0) { + return ret; + } + + /* + * Inject a kick in case the back-end only starts vring processing upon + * receiving a kick. The spec suggests this to improve compatibility. + */ + if (file->fd != -1) { + uint64_t val = 1; + ssize_t nwritten; + + do { + nwritten = write(file->fd, &val, sizeof(val)); + } while (nwritten < 0 && errno == EINTR); + + if (nwritten < 0 && errno != EAGAIN /* back-end can already read */) { + return -errno; + } + } + + return 0; } static int vhost_user_set_vring_call(struct vhost_dev *dev, From 36b37f8494800ed9c5d4b6ef03924bd1636cb810 Mon Sep 17 00:00:00 2001 From: Laurent Vivier Date: Mon, 22 Jun 2026 18:11:44 +0200 Subject: [PATCH 20/44] hw/char/virtio-serial-bus: fix guest-triggerable OOM in control_out() A malicious guest can craft virtqueue descriptors with arbitrary lengths. control_out() calls iov_size() on the guest-supplied scatter-gather list and passes the result directly to g_malloc(), allowing a guest to force QEMU to attempt multi-gigabyte allocations and crash the host process. Fix this by copying at most sizeof(struct virtio_console_control) into a stack-local variable instead of allocating a buffer sized by the guest. handle_control_message() only accesses the fixed-size id, event, and value fields, so no data beyond the struct was ever needed. Cc: qemu-stable@nongnu.org Resolves: https://gitlab.com/qemu-project/qemu/-/issues/3585 Signed-off-by: Laurent Vivier Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260622161144.2883799-1-lvivier@redhat.com> --- hw/char/virtio-serial-bus.c | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/hw/char/virtio-serial-bus.c b/hw/char/virtio-serial-bus.c index cd234dc6db..c1973f0248 100644 --- a/hw/char/virtio-serial-bus.c +++ b/hw/char/virtio-serial-bus.c @@ -344,22 +344,16 @@ void virtio_serial_throttle_port(VirtIOSerialPort *port, bool throttle) } /* Guest wants to notify us of some event */ -static void handle_control_message(VirtIOSerial *vser, void *buf, size_t len) +static void handle_control_message(VirtIOSerial *vser, + struct virtio_console_control *gcpkt) { VirtIODevice *vdev = VIRTIO_DEVICE(vser); struct VirtIOSerialPort *port; VirtIOSerialPortClass *vsc; - struct virtio_console_control cpkt, *gcpkt; + struct virtio_console_control cpkt; uint8_t *buffer; size_t buffer_len; - gcpkt = buf; - - if (len < sizeof(cpkt)) { - /* The guest sent an invalid control packet */ - return; - } - cpkt.event = virtio_lduw_p(vdev, &gcpkt->event); cpkt.value = virtio_lduw_p(vdev, &gcpkt->value); @@ -457,41 +451,27 @@ static void control_in(VirtIODevice *vdev, VirtQueue *vq) static void control_out(VirtIODevice *vdev, VirtQueue *vq) { + struct virtio_console_control cpkt; VirtQueueElement *elem; VirtIOSerial *vser; - uint8_t *buf; size_t len; vser = VIRTIO_SERIAL(vdev); - len = 0; - buf = NULL; for (;;) { - size_t cur_len; - elem = virtqueue_pop(vq, sizeof(VirtQueueElement)); if (!elem) { break; } - cur_len = iov_size(elem->out_sg, elem->out_num); - /* - * Allocate a new buf only if we didn't have one previously or - * if the size of the buf differs - */ - if (cur_len > len) { - g_free(buf); - - buf = g_malloc(cur_len); - len = cur_len; + len = iov_to_buf(elem->out_sg, elem->out_num, 0, &cpkt, sizeof(cpkt)); + if (len == sizeof(cpkt)) { + handle_control_message(vser, &cpkt); } - iov_to_buf(elem->out_sg, elem->out_num, 0, buf, cur_len); - handle_control_message(vser, buf, cur_len); virtqueue_push(vq, elem, 0); g_free(elem); } - g_free(buf); virtio_notify(vdev, vq); } From 302fec996c31ac312278a6a756d66d053b80d2fa Mon Sep 17 00:00:00 2001 From: Junjie Cao Date: Fri, 3 Jul 2026 15:21:56 +0800 Subject: [PATCH 21/44] tests/qtest/libqos: share Intel IOMMU test setup helpers iommu-intel-test.c keeps the iommu-testdev PCI setup (save_fn(), setup_qtest_pci_device()) and the VT-d command-line / capability helpers (qvtd_iommu_args(), qvtd_check_caps()) as file-local statics. A second Intel IOMMU test would have to copy them, which defeats the purpose of the shared qos-intel-iommu module. Move them into qos-intel-iommu so sibling tests can reuse them: save_fn() becomes qvtd_save_pci_dev() and setup_qtest_pci_device() becomes qvtd_setup_qtest_pci_device(); qvtd_iommu_args() and qvtd_check_caps() keep their names. No functional change: iommu-intel-test now calls the public qvtd_setup_qtest_pci_device() instead of its file-local copy. Signed-off-by: Junjie Cao Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260703072200.463082-2-junjie.cao@intel.com> --- tests/qtest/iommu-intel-test.c | 78 +--------------------------- tests/qtest/libqos/qos-intel-iommu.c | 77 +++++++++++++++++++++++++++ tests/qtest/libqos/qos-intel-iommu.h | 23 ++++++++ 3 files changed, 101 insertions(+), 77 deletions(-) diff --git a/tests/qtest/iommu-intel-test.c b/tests/qtest/iommu-intel-test.c index 703bbfef73..c2744def6f 100644 --- a/tests/qtest/iommu-intel-test.c +++ b/tests/qtest/iommu-intel-test.c @@ -24,82 +24,6 @@ static uint64_t intel_iommu_expected_gpa(uint64_t iova) return (QVTD_PT_VAL & VTD_PAGE_MASK_4K) + (iova & 0xfff); } -static void save_fn(QPCIDevice *dev, int devfn, void *data) -{ - QPCIDevice **pdev = (QPCIDevice **) data; - - *pdev = dev; -} - -static QPCIDevice *setup_qtest_pci_device(QTestState *qts, QPCIBus **pcibus, - QPCIBar *bar) -{ - QPCIDevice *dev = NULL; - - *pcibus = qpci_new_pc(qts, NULL); - g_assert(*pcibus != NULL); - - qpci_device_foreach(*pcibus, IOMMU_TESTDEV_VENDOR_ID, - IOMMU_TESTDEV_DEVICE_ID, save_fn, &dev); - - g_assert(dev); - qpci_device_enable(dev); - *bar = qpci_iomap(dev, 0, NULL); - g_assert_false(bar->is_io); - - return dev; -} - -static const char *qvtd_iommu_args(QVTDTransMode mode) -{ - switch (mode) { - case QVTD_TM_SCALABLE_FLT: - return "-device intel-iommu,scalable-mode=on,fsts=on "; - case QVTD_TM_SCALABLE_PT: - case QVTD_TM_SCALABLE_SLT: - return "-device intel-iommu,scalable-mode=on "; - default: - return "-device intel-iommu "; - } -} - -static bool qvtd_check_caps(QTestState *qts, QVTDTransMode mode) -{ - uint64_t ecap = qtest_readq(qts, - Q35_HOST_BRIDGE_IOMMU_ADDR + DMAR_ECAP_REG); - - /* All scalable modes require SMTS */ - if (qvtd_is_scalable(mode) && !(ecap & VTD_ECAP_SMTS)) { - g_test_skip("ECAP.SMTS not supported"); - return false; - } - - switch (mode) { - case QVTD_TM_SCALABLE_PT: - if (!(ecap & VTD_ECAP_PT)) { - g_test_skip("ECAP.PT not supported"); - return false; - } - break; - case QVTD_TM_SCALABLE_SLT: - if (!(ecap & VTD_ECAP_SSTS)) { - g_test_skip("ECAP.SSTS not supported"); - return false; - } - break; - case QVTD_TM_SCALABLE_FLT: - if (!(ecap & VTD_ECAP_FSTS)) { - g_test_skip("ECAP.FSTS not supported"); - return false; - } - break; - default: - break; - } - - return true; -} - static void run_intel_iommu_translation(const QVTDTestConfig *cfg) { QTestState *qts; @@ -124,7 +48,7 @@ static void run_intel_iommu_translation(const QVTDTestConfig *cfg) } /* Setup and configure IOMMU-testdev PCI device */ - dev = setup_qtest_pci_device(qts, &pcibus, &bar); + dev = qvtd_setup_qtest_pci_device(qts, &pcibus, &bar); g_assert(dev); g_test_message("### Intel IOMMU translation mode=%d ###", cfg->trans_mode); diff --git a/tests/qtest/libqos/qos-intel-iommu.c b/tests/qtest/libqos/qos-intel-iommu.c index f8ca4c871b..4095ac0917 100644 --- a/tests/qtest/libqos/qos-intel-iommu.c +++ b/tests/qtest/libqos/qos-intel-iommu.c @@ -11,6 +11,7 @@ #include "qemu/osdep.h" #include "hw/i386/intel_iommu_internal.h" #include "tests/qtest/libqos/pci.h" +#include "tests/qtest/libqos/pci-pc.h" #include "qos-iommu-testdev.h" #include "qos-intel-iommu.h" @@ -452,3 +453,79 @@ void qvtd_run_translation_case(QTestState *qts, QPCIDevice *dev, } } } + +const char *qvtd_iommu_args(QVTDTransMode mode) +{ + switch (mode) { + case QVTD_TM_SCALABLE_FLT: + return "-device intel-iommu,scalable-mode=on,fsts=on "; + case QVTD_TM_SCALABLE_PT: + case QVTD_TM_SCALABLE_SLT: + return "-device intel-iommu,scalable-mode=on "; + default: + return "-device intel-iommu "; + } +} + +bool qvtd_check_caps(QTestState *qts, QVTDTransMode mode) +{ + uint64_t ecap = qtest_readq(qts, + Q35_HOST_BRIDGE_IOMMU_ADDR + DMAR_ECAP_REG); + + /* All scalable modes require SMTS */ + if (qvtd_is_scalable(mode) && !(ecap & VTD_ECAP_SMTS)) { + g_test_skip("ECAP.SMTS not supported"); + return false; + } + + switch (mode) { + case QVTD_TM_SCALABLE_PT: + if (!(ecap & VTD_ECAP_PT)) { + g_test_skip("ECAP.PT not supported"); + return false; + } + break; + case QVTD_TM_SCALABLE_SLT: + if (!(ecap & VTD_ECAP_SSTS)) { + g_test_skip("ECAP.SSTS not supported"); + return false; + } + break; + case QVTD_TM_SCALABLE_FLT: + if (!(ecap & VTD_ECAP_FSTS)) { + g_test_skip("ECAP.FSTS not supported"); + return false; + } + break; + default: + break; + } + + return true; +} + +static void qvtd_save_pci_dev(QPCIDevice *dev, int devfn, void *data) +{ + QPCIDevice **pdev = (QPCIDevice **)data; + + *pdev = dev; +} + +QPCIDevice *qvtd_setup_qtest_pci_device(QTestState *qts, QPCIBus **pcibus, + QPCIBar *bar) +{ + QPCIDevice *dev = NULL; + + *pcibus = qpci_new_pc(qts, NULL); + g_assert(*pcibus != NULL); + + qpci_device_foreach(*pcibus, IOMMU_TESTDEV_VENDOR_ID, + IOMMU_TESTDEV_DEVICE_ID, qvtd_save_pci_dev, &dev); + + g_assert(dev); + qpci_device_enable(dev); + *bar = qpci_iomap(dev, 0, NULL); + g_assert_false(bar->is_io); + + return dev; +} diff --git a/tests/qtest/libqos/qos-intel-iommu.h b/tests/qtest/libqos/qos-intel-iommu.h index c6cacc5c3f..dd6f920464 100644 --- a/tests/qtest/libqos/qos-intel-iommu.h +++ b/tests/qtest/libqos/qos-intel-iommu.h @@ -182,4 +182,27 @@ void qvtd_run_translation_case(QTestState *qts, QPCIDevice *dev, QPCIBar bar, uint64_t iommu_base, const QVTDTestConfig *cfg); +/* + * qvtd_iommu_args - Build the -device intel-iommu command-line fragment + * for the requested translation mode. + */ +const char *qvtd_iommu_args(QVTDTransMode mode); + +/* + * qvtd_check_caps - Check whether the running QEMU exposes the ECAP bits + * required by @mode. Calls g_test_skip() and returns + * false when a required capability is missing. + */ +bool qvtd_check_caps(QTestState *qts, QVTDTransMode mode); + +/* + * qvtd_setup_qtest_pci_device - Create a QPCIBus, locate the iommu-testdev + * PCI function, enable it and map BAR0. + * + * On success, *pcibus and *bar are populated and the returned QPCIDevice + * must be released by the caller via g_free(). + */ +QPCIDevice *qvtd_setup_qtest_pci_device(QTestState *qts, QPCIBus **pcibus, + QPCIBar *bar); + #endif /* QTEST_LIBQOS_INTEL_IOMMU_H */ From 3c774326fe1ee8182666b00748609a7751b9d0b6 Mon Sep 17 00:00:00 2001 From: Junjie Cao Date: Fri, 3 Jul 2026 15:21:57 +0800 Subject: [PATCH 22/44] tests/qtest/libqos: add Intel IOMMU invalidation helpers Add the building blocks a queued-invalidation test needs on top of the existing translation helpers: - qvtd_leaf_pte_addr() / qvtd_make_leaf_pte() let a test locate and rewrite the leaf PTE built by qvtd_setup_translation_tables() without re-deriving the page-table index or leaf attributes by hand. The attributes reuse qvtd_get_fl_pte_attrs()/qvtd_get_pte_attrs() so the first- and second-level leaf formats stay defined in one place. - qvtd_submit_iotlb_global_inv() / _domain_inv() / _page_inv() write an IOTLB Invalidation Descriptor (global / domain-selective / page-selective) into the Invalidation Queue and advance IQT_REG. - qvtd_submit_inv_wait_and_poll() submits an Invalidation Wait Descriptor with Status Write and polls the status word with a bounded retry loop, asserting on timeout. No caller yet; used by the IOTLB invalidation test that follows. Signed-off-by: Junjie Cao Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260703072200.463082-3-junjie.cao@intel.com> --- tests/qtest/libqos/qos-intel-iommu.c | 108 +++++++++++++++++++++++++++ tests/qtest/libqos/qos-intel-iommu.h | 51 +++++++++++++ 2 files changed, 159 insertions(+) diff --git a/tests/qtest/libqos/qos-intel-iommu.c b/tests/qtest/libqos/qos-intel-iommu.c index 4095ac0917..c793ec899c 100644 --- a/tests/qtest/libqos/qos-intel-iommu.c +++ b/tests/qtest/libqos/qos-intel-iommu.c @@ -15,6 +15,10 @@ #include "qos-iommu-testdev.h" #include "qos-intel-iommu.h" +/* Bounded poll for the invalidation-wait Status Write. */ +#define QVTD_INV_WAIT_POLL_MAX_ITERS 1000 +#define QVTD_INV_WAIT_POLL_INTERVAL_US 1000 + #define QVTD_AW_48BIT_ENCODING 2 uint32_t qvtd_expected_dma_result(QVTDTestContext *ctx) @@ -232,6 +236,30 @@ static uint64_t qvtd_get_fl_pte_attrs(bool is_leaf) return attrs; } +uint64_t qvtd_leaf_pte_addr(uint64_t iova) +{ + return qvtd_get_table_addr(QVTD_PT_L1_BASE, 1, iova); +} + +uint64_t qvtd_make_leaf_pte(uint64_t pa, QVTDTransMode mode) +{ + uint64_t attrs; + + /* + * Reuse the same leaf attributes qvtd_setup_translation_tables() writes, + * so a rewritten PTE stays consistent with the initial mapping. US=1 in + * the first-level format is required because the PASID entry runs with + * SRE=0, which makes every DMA appear as a user-mode access + * (VT-d 3.6.2 / 9.9). + */ + if (mode == QVTD_TM_SCALABLE_FLT) { + attrs = qvtd_get_fl_pte_attrs(true); + } else { + attrs = qvtd_get_pte_attrs(); + } + return (pa & VTD_PAGE_MASK_4K) | attrs; +} + void qvtd_setup_translation_tables(QTestState *qts, uint64_t iova, QVTDTransMode mode) { @@ -529,3 +557,83 @@ QPCIDevice *qvtd_setup_qtest_pci_device(QTestState *qts, QPCIBus **pcibus, return dev; } + +/* + * Write a 128-bit invalidation descriptor at the current tail and advance + * IQT_REG. Internal helper for the IOTLB / wait variants below. + */ +static uint32_t qvtd_submit_inv_desc(QTestState *qts, uint64_t iommu_base, + uint64_t desc_lo, uint64_t desc_hi, + uint32_t tail) +{ + uint64_t desc_addr = QVTD_IQ_BASE + (uint64_t)tail * QVTD_IQ_DESC_SIZE; + + qtest_writeq(qts, desc_addr, desc_lo); + qtest_writeq(qts, desc_addr + 8, desc_hi); + tail++; + + qtest_writeq(qts, iommu_base + DMAR_IQT_REG, + (uint64_t)tail << QVTD_IQT_SHIFT); + return tail; +} + +uint32_t qvtd_submit_inv_wait_and_poll(QTestState *qts, uint64_t iommu_base, + uint32_t tail) +{ + uint64_t lo, hi; + uint32_t status = 0; + int i; + + qtest_writel(qts, QVTD_INV_WAIT_ADDR, 0); + + lo = VTD_INV_DESC_WAIT | VTD_INV_DESC_WAIT_SW | + ((uint64_t)QVTD_INV_WAIT_DATA << VTD_INV_DESC_WAIT_DATA_SHIFT); + hi = QVTD_INV_WAIT_ADDR; + + tail = qvtd_submit_inv_desc(qts, iommu_base, lo, hi, tail); + + for (i = 0; i < QVTD_INV_WAIT_POLL_MAX_ITERS; i++) { + status = qtest_readl(qts, QVTD_INV_WAIT_ADDR); + if (status == QVTD_INV_WAIT_DATA) { + return tail; + } + g_usleep(QVTD_INV_WAIT_POLL_INTERVAL_US); + } + + g_assert_cmphex(status, ==, QVTD_INV_WAIT_DATA); + return tail; +} + +uint32_t qvtd_submit_iotlb_global_inv(QTestState *qts, uint64_t iommu_base, + uint32_t tail) +{ + uint64_t lo = VTD_INV_DESC_IOTLB | VTD_INV_DESC_IOTLB_GLOBAL; + + return qvtd_submit_inv_desc(qts, iommu_base, lo, 0, tail); +} + +uint32_t qvtd_submit_iotlb_domain_inv(QTestState *qts, uint64_t iommu_base, + uint16_t domain_id, uint32_t tail) +{ + uint64_t lo = VTD_INV_DESC_IOTLB | VTD_INV_DESC_IOTLB_DOMAIN | + ((uint64_t)domain_id << 16); + + return qvtd_submit_inv_desc(qts, iommu_base, lo, 0, tail); +} + +uint32_t qvtd_submit_iotlb_page_inv(QTestState *qts, uint64_t iommu_base, + uint16_t domain_id, uint64_t addr, + uint8_t am, uint32_t tail) +{ + uint64_t lo = VTD_INV_DESC_IOTLB | VTD_INV_DESC_IOTLB_PAGE | + ((uint64_t)domain_id << 16); + /* + * AM selects the invalidation range per VT-d 6.5.2.4: + * am=0 → 4 KB, am=9 → 2 MB, am=18 → 1 GB. + * IH (hi[6]) is left clear, requesting full invalidation + * including non-leaf paging-structure caches. + */ + uint64_t hi = (addr & ~0xfffULL) | (am & 0x3fULL); + + return qvtd_submit_inv_desc(qts, iommu_base, lo, hi, tail); +} diff --git a/tests/qtest/libqos/qos-intel-iommu.h b/tests/qtest/libqos/qos-intel-iommu.h index dd6f920464..3a97d14786 100644 --- a/tests/qtest/libqos/qos-intel-iommu.h +++ b/tests/qtest/libqos/qos-intel-iommu.h @@ -40,6 +40,16 @@ */ #define QVTD_IQ_BASE (QVTD_MEM_BASE + 0x00020000) #define QVTD_IQ_QS 0 /* QS=0 → 256 entries */ +#define QVTD_IQ_DESC_SIZE 16 /* 128-bit descriptor (iq_dw=0) */ +#define QVTD_IQT_SHIFT 4 /* IQT_REG[18:4] is the tail index */ + +/* + * Invalidation Wait Descriptor with Status Write (VT-d 6.5.2.8). + * The IOMMU writes QVTD_INV_WAIT_DATA to QVTD_INV_WAIT_ADDR after all + * preceding invalidation descriptors complete. + */ +#define QVTD_INV_WAIT_ADDR (QVTD_MEM_BASE + 0x00040000) +#define QVTD_INV_WAIT_DATA 0x1u /* * Fault Event MSI configuration. @@ -205,4 +215,45 @@ bool qvtd_check_caps(QTestState *qts, QVTDTransMode mode); QPCIDevice *qvtd_setup_qtest_pci_device(QTestState *qts, QPCIBus **pcibus, QPCIBar *bar); +/* + * qvtd_leaf_pte_addr - Address of the leaf (L1) PTE for @iova, assuming + * the hierarchy was built by + * qvtd_setup_translation_tables() with 4 KB leaves. + */ +uint64_t qvtd_leaf_pte_addr(uint64_t iova); + +/* + * qvtd_make_leaf_pte - Build a leaf PTE value mapping @pa, for the + * 4-level / 4 KB-leaf layout used by + * qvtd_setup_translation_tables(). + * Selects FLT or SLT/legacy attributes from @mode. + */ +uint64_t qvtd_make_leaf_pte(uint64_t pa, QVTDTransMode mode); + +/* + * qvtd_submit_inv_wait_and_poll - Submit an Invalidation Wait Descriptor + * with Status Write and poll until the + * IOMMU writes the expected status data. + * + * Asserts on timeout. Returns the new tail index. + */ +uint32_t qvtd_submit_inv_wait_and_poll(QTestState *qts, uint64_t iommu_base, + uint32_t tail); + +/* + * qvtd_submit_iotlb_global_inv - global IOTLB invalidation. + * qvtd_submit_iotlb_domain_inv - domain-selective IOTLB invalidation. + * qvtd_submit_iotlb_page_inv - page-selective IOTLB invalidation; + * @addr must be 4 KB aligned. @am + * selects the address mask per VT-d + * 6.5.2.4 (am=0 → single 4 KB page). + */ +uint32_t qvtd_submit_iotlb_global_inv(QTestState *qts, uint64_t iommu_base, + uint32_t tail); +uint32_t qvtd_submit_iotlb_domain_inv(QTestState *qts, uint64_t iommu_base, + uint16_t domain_id, uint32_t tail); +uint32_t qvtd_submit_iotlb_page_inv(QTestState *qts, uint64_t iommu_base, + uint16_t domain_id, uint64_t addr, + uint8_t am, uint32_t tail); + #endif /* QTEST_LIBQOS_INTEL_IOMMU_H */ From a3fbd2b524b9847e5c3eebf64089bd5af6a660b2 Mon Sep 17 00:00:00 2001 From: Junjie Cao Date: Fri, 3 Jul 2026 15:21:58 +0800 Subject: [PATCH 23/44] tests/qtest: add IOTLB invalidation test for Intel IOMMU Nothing in tree exercises IOTLB invalidation for any emulated vIOMMU: the existing iommu-testdev tests only check one-shot translation, so a regression that failed to flush a stale IOTLB entry would go unnoticed. Add a test that drives the queued-invalidation path end to end (vtd_process_inv_desc -> vtd_process_iotlb_desc -> vtd_iotlb_{global,domain,page}_invalidate). For each {legacy, scalable-slt, scalable-flt} x {global, domain, page} combination it: 1. maps IOVA -> PA_A and DMAs, populating the IOTLB; 2. rewrites the leaf PTE to PA_B *without* invalidating and DMAs again, asserting the stale entry is still served (MISMATCH); 3. submits the IOTLB invalidation plus a wait descriptor, then DMAs and asserts the fresh page walk now reaches PA_B. Step 2 makes the flush observable: it fails loudly if the IOTLB is not actually caching the first translation. For scalable first-level (flt), QEMU keeps first- and second-level mappings in a single IOTLB that the legacy VTD_INV_DESC_IOTLB descriptor flushes for every level, so the test uses that descriptor across all modes. PASID-selective invalidation (VTD_INV_DESC_PIOTLB, vtd_piotlb_*) is a separate path and is left for a follow-up. It also adds three page-selective cases that cache a second page and check its fate after invalidating the first: for second-level (legacy, scalable-slt) the second page survives, while for first-level (scalable-flt) QEMU flushes all first-stage entries of the domain, so it does not. This distinguishes a page-selective flush from a domain-wide or global one. Signed-off-by: Junjie Cao Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260703072200.463082-4-junjie.cao@intel.com> --- MAINTAINERS | 1 + tests/qtest/iommu-intel-inv-test.c | 346 +++++++++++++++++++++++++++++ tests/qtest/meson.build | 2 +- 3 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 tests/qtest/iommu-intel-inv-test.c diff --git a/MAINTAINERS b/MAINTAINERS index 5ee7c1f601..cbdfce4866 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4106,6 +4106,7 @@ F: include/hw/i386/intel_iommu.h F: tests/functional/x86_64/test_intel_iommu.py F: tests/qtest/intel-iommu-test.c F: tests/qtest/iommu-intel-test.c +F: tests/qtest/iommu-intel-inv-test.c AMD-Vi Emulation M: Alejandro Jimenez diff --git a/tests/qtest/iommu-intel-inv-test.c b/tests/qtest/iommu-intel-inv-test.c new file mode 100644 index 0000000000..1467eef647 --- /dev/null +++ b/tests/qtest/iommu-intel-inv-test.c @@ -0,0 +1,346 @@ +/* + * QTest for Intel IOMMU (VT-d) IOTLB invalidation via Invalidation Queue + * + * Validates that IOTLB invalidation descriptors submitted through the + * queued invalidation interface correctly flush cached translations, + * forcing the IOMMU to re-walk page tables on subsequent DMA. + * + * Copyright (c) 2026 Intel Corporation. + * + * Author: Junjie Cao + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "libqtest.h" +#include "libqos/pci.h" +#include "libqos/pci-pc.h" +#include "hw/i386/intel_iommu_internal.h" +#include "hw/misc/iommu-testdev.h" +#include "libqos/qos-intel-iommu.h" +#include "libqos/qos-iommu-testdev.h" + +#define DMA_LEN 4 + +/* + * Second DMA target page, chosen to fall well outside any address used by + * qos-intel-iommu's fixed structure layout. + */ +#define QVTD_PT_VAL_B (QVTD_MEM_BASE + 0x00200000) + +/* + * A second IOVA/target page for the page-selectivity test. QVTD_IOVA_2 is + * QVTD_IOVA + 4K: it shares the L4/L3/L2 walk built by + * qvtd_build_translation() and differs only in the leaf (L1) slot, so mapping + * it costs one extra leaf PTE. QVTD_PT_VAL_2 is its distinct target page. + */ +#define QVTD_IOVA_2 (QVTD_IOVA + 0x1000) +#define QVTD_PT_VAL_2 (QVTD_MEM_BASE + 0x00300000) + +typedef enum { + IOTLB_INV_GLOBAL, + IOTLB_INV_DOMAIN, + IOTLB_INV_PAGE, +} IOTLBInvGranularity; + +/* + * Core invalidation test, parameterized by translation mode and + * invalidation granularity. + * + * The iommu-testdev device performs DMA writes via the IOMMU (using the + * IOVA) and verifies by reading back from the expected GPA directly. If + * the IOTLB is stale, the DMA write lands at the old PA while readback + * uses the GPA we supply, causing a mismatch (ITD_DMA_ERR_MISMATCH). + * + * Test sequence: + * 1. Setup translation: IOVA -> PA_A + * 2. DMA(gpa=PA_A) -> success (IOTLB populates cache) + * 3. Modify PTE: IOVA -> PA_B (no invalidation) + * 4. DMA(gpa=PA_B) -> MISMATCH (stale IOTLB directs write to PA_A) + * 5. Issue IOTLB invalidation + wait + * 6. DMA(gpa=PA_B) -> success (cache flushed, fresh page walk) + * + * Phase 4 depends on QEMU's IOTLB caching the Phase 1 translation; if a + * future change makes IOTLB caching lazy this assertion would no longer + * exercise the stale-cache path. + */ +static void run_iotlb_inv_test(QVTDTransMode mode, IOTLBInvGranularity gran) +{ + QTestState *qts; + QPCIBus *pcibus; + QPCIDevice *dev; + QPCIBar bar; + uint32_t tail = 0; + uint32_t result; + uint64_t pa_a, pa_b; + + if (!qtest_has_machine("q35")) { + g_test_skip("q35 machine not available"); + return; + } + + qts = qtest_initf("-machine q35 -smp 1 -m 512 -net none " + "%s -device iommu-testdev", + qvtd_iommu_args(mode)); + + if (!qvtd_check_caps(qts, mode)) { + qtest_quit(qts); + return; + } + + dev = qvtd_setup_qtest_pci_device(qts, &pcibus, &bar); + + /* + * The IOMMU translates an IOVA to a page base, then the page offset + * from the IOVA is added. So GPA = page_base + (IOVA & 0xfff). + */ + pa_a = (QVTD_PT_VAL & VTD_PAGE_MASK_4K) + (QVTD_IOVA & 0xfff); + pa_b = (QVTD_PT_VAL_B & VTD_PAGE_MASK_4K) + (QVTD_IOVA & 0xfff); + + /* --- Phase 1: Setup and initial DMA (populates IOTLB) --- */ + qvtd_build_translation(qts, mode, dev->devfn); + qvtd_program_regs(qts, Q35_HOST_BRIDGE_IOMMU_ADDR, mode); + + qtest_memset(qts, pa_a, 0, DMA_LEN); + qtest_memset(qts, pa_b, 0, DMA_LEN); + + result = qos_iommu_testdev_trigger_dma(dev, bar, QVTD_IOVA, pa_a, + DMA_LEN, 0); + g_assert_cmpuint(result, ==, 0); + + /* --- Phase 2: Modify PTE without invalidation -> stale IOTLB --- */ + qtest_writeq(qts, qvtd_leaf_pte_addr(QVTD_IOVA), + qvtd_make_leaf_pte(QVTD_PT_VAL_B & VTD_PAGE_MASK_4K, mode)); + qtest_memset(qts, pa_a, 0, DMA_LEN); + qtest_memset(qts, pa_b, 0, DMA_LEN); + + result = qos_iommu_testdev_trigger_dma(dev, bar, QVTD_IOVA, pa_b, + DMA_LEN, 0); + g_assert_cmpuint(result, ==, ITD_DMA_ERR_MISMATCH); + + /* --- Phase 3: Invalidate IOTLB -> fresh page walk succeeds --- */ + switch (gran) { + case IOTLB_INV_GLOBAL: + tail = qvtd_submit_iotlb_global_inv(qts, Q35_HOST_BRIDGE_IOMMU_ADDR, + tail); + break; + case IOTLB_INV_DOMAIN: + tail = qvtd_submit_iotlb_domain_inv(qts, Q35_HOST_BRIDGE_IOMMU_ADDR, + QVTD_DOMAIN_ID, tail); + break; + case IOTLB_INV_PAGE: + tail = qvtd_submit_iotlb_page_inv(qts, Q35_HOST_BRIDGE_IOMMU_ADDR, + QVTD_DOMAIN_ID, QVTD_IOVA, 0, + tail); + break; + } + tail = qvtd_submit_inv_wait_and_poll(qts, Q35_HOST_BRIDGE_IOMMU_ADDR, + tail); + + qtest_memset(qts, pa_a, 0, DMA_LEN); + qtest_memset(qts, pa_b, 0, DMA_LEN); + + result = qos_iommu_testdev_trigger_dma(dev, bar, QVTD_IOVA, pa_b, + DMA_LEN, 0); + g_assert_cmpuint(result, ==, 0); + + g_free(dev); + qpci_free_pc(pcibus); + qtest_quit(qts); +} + +/* + * Page-selectivity test: verify that a page-selective invalidation flushes + * the named page and touches other cached pages only as far as the model + * intends. run_iotlb_inv_test() caches a single entry, so it cannot tell a + * page-selective flush from a domain-wide or global one; this test caches two + * pages in the same domain and checks the second one's fate. + * + * The expected fate of the second page depends on the translation level: + * + * - second-level (legacy / scalable-slt): a page-selective descriptor + * evicts only the matching gfn, so IOVA_2 survives. + * - first-level (scalable-flt): QEMU invalidates all first-stage entries of + * the domain on a page-selective descriptor (vtd_hash_remove_by_page() + * returns true for any pgtt==FST entry of the domain, matching the VT-d + * spec for first-stage IOTLB invalidation), so IOVA_2 is flushed too. + * + * Method: map IOVA -> PA_A and IOVA_2 -> PA_A_2, DMA both to populate two + * IOTLB entries, rewrite both leaf PTEs to PA_B* without invalidating, then + * page-invalidate IOVA only. IOVA always re-walks to PA_B. For IOVA_2 we + * verify the DMA against its *original* page PA_A_2: if the entry survived, + * the stale cache still serves PA_A_2 (success); if it was flushed, the fresh + * walk reaches PA_B_2 and mismatches PA_A_2. So a survived entry gives + * success and a flushed entry gives MISMATCH, and we assert whichever the + * mode requires -- catching both an over-matching second-level flush and a + * regression that stopped flushing first-stage entries domain-wide. + */ +static void run_page_selectivity_test(QVTDTransMode mode) +{ + QTestState *qts; + QPCIBus *pcibus; + QPCIDevice *dev; + QPCIBar bar; + uint32_t tail = 0; + uint32_t result; + uint64_t pa_a, pa_b, pa_a2, pa_b2; + bool fl_domain_wide = (mode == QVTD_TM_SCALABLE_FLT); + + if (!qtest_has_machine("q35")) { + g_test_skip("q35 machine not available"); + return; + } + + qts = qtest_initf("-machine q35 -smp 1 -m 512 -net none " + "%s -device iommu-testdev", + qvtd_iommu_args(mode)); + + if (!qvtd_check_caps(qts, mode)) { + qtest_quit(qts); + return; + } + + dev = qvtd_setup_qtest_pci_device(qts, &pcibus, &bar); + + pa_a = (QVTD_PT_VAL & VTD_PAGE_MASK_4K) + (QVTD_IOVA & 0xfff); + pa_b = (QVTD_PT_VAL_B & VTD_PAGE_MASK_4K) + (QVTD_IOVA & 0xfff); + pa_a2 = (QVTD_PT_VAL_2 & VTD_PAGE_MASK_4K) + (QVTD_IOVA_2 & 0xfff); + pa_b2 = (QVTD_PT_VAL_B & VTD_PAGE_MASK_4K) + (QVTD_IOVA_2 & 0xfff); + + /* --- Setup: IOVA -> PA_A (built by helper) and IOVA_2 -> PA_A_2 --- */ + qvtd_build_translation(qts, mode, dev->devfn); + qtest_writeq(qts, qvtd_leaf_pte_addr(QVTD_IOVA_2), + qvtd_make_leaf_pte(QVTD_PT_VAL_2 & VTD_PAGE_MASK_4K, mode)); + qvtd_program_regs(qts, Q35_HOST_BRIDGE_IOMMU_ADDR, mode); + + /* Populate both IOTLB entries. */ + qtest_memset(qts, pa_a, 0, DMA_LEN); + qtest_memset(qts, pa_a2, 0, DMA_LEN); + result = qos_iommu_testdev_trigger_dma(dev, bar, QVTD_IOVA, pa_a, + DMA_LEN, 0); + g_assert_cmpuint(result, ==, 0); + result = qos_iommu_testdev_trigger_dma(dev, bar, QVTD_IOVA_2, pa_a2, + DMA_LEN, 0); + g_assert_cmpuint(result, ==, 0); + + /* Rewrite both leaf PTEs to PA_B* without invalidating. */ + qtest_writeq(qts, qvtd_leaf_pte_addr(QVTD_IOVA), + qvtd_make_leaf_pte(QVTD_PT_VAL_B & VTD_PAGE_MASK_4K, mode)); + qtest_writeq(qts, qvtd_leaf_pte_addr(QVTD_IOVA_2), + qvtd_make_leaf_pte(QVTD_PT_VAL_B & VTD_PAGE_MASK_4K, mode)); + + /* Page-selective invalidation of IOVA only. */ + tail = qvtd_submit_iotlb_page_inv(qts, Q35_HOST_BRIDGE_IOMMU_ADDR, + QVTD_DOMAIN_ID, QVTD_IOVA, 0, tail); + tail = qvtd_submit_inv_wait_and_poll(qts, Q35_HOST_BRIDGE_IOMMU_ADDR, + tail); + + /* IOVA was flushed: fresh walk reaches PA_B. */ + qtest_memset(qts, pa_a, 0, DMA_LEN); + qtest_memset(qts, pa_b, 0, DMA_LEN); + result = qos_iommu_testdev_trigger_dma(dev, bar, QVTD_IOVA, pa_b, + DMA_LEN, 0); + g_assert_cmpuint(result, ==, 0); + + /* + * IOVA_2's fate, verified against its original page PA_A_2: + * - second-level: entry survives, stale cache serves PA_A_2 -> success; + * - first-level: entry was flushed domain-wide, fresh walk reaches + * PA_B_2 -> MISMATCH against PA_A_2. + */ + qtest_memset(qts, pa_a2, 0, DMA_LEN); + qtest_memset(qts, pa_b2, 0, DMA_LEN); + result = qos_iommu_testdev_trigger_dma(dev, bar, QVTD_IOVA_2, pa_a2, + DMA_LEN, 0); + if (fl_domain_wide) { + g_assert_cmpuint(result, ==, ITD_DMA_ERR_MISMATCH); + } else { + g_assert_cmpuint(result, ==, 0); + } + + g_free(dev); + qpci_free_pc(pcibus); + qtest_quit(qts); +} + +/* + * scalable-flt is covered here even though, per the VT-d spec, first-level + * mappings are invalidated with the PASID-based descriptor + * (VTD_INV_DESC_PIOTLB). QEMU keeps first- and second-level mappings in a + * single IOTLB that the legacy VTD_INV_DESC_IOTLB descriptor flushes for + * every level, so this test drives that descriptor across all modes. + * PASID-selective (PIOTLB) invalidation is a separate path, left for a + * follow-up. + */ +static const struct { + const char *name; + QVTDTransMode mode; +} trans_modes[] = { + { "legacy", QVTD_TM_LEGACY_TRANS }, + { "scalable-slt", QVTD_TM_SCALABLE_SLT }, + { "scalable-flt", QVTD_TM_SCALABLE_FLT }, +}; + +static const struct { + const char *name; + IOTLBInvGranularity gran; +} granularities[] = { + { "global", IOTLB_INV_GLOBAL }, + { "domain", IOTLB_INV_DOMAIN }, + { "page", IOTLB_INV_PAGE }, +}; + +typedef struct { + QVTDTransMode mode; + IOTLBInvGranularity gran; +} TestCase; + +static void test_iotlb_inv(const void *opaque) +{ + const TestCase *tc = opaque; + + run_iotlb_inv_test(tc->mode, tc->gran); +} + +static void test_page_selectivity(const void *opaque) +{ + const QVTDTransMode *mode = opaque; + + run_page_selectivity_test(*mode); +} + +int main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + + for (size_t m = 0; m < ARRAY_SIZE(trans_modes); m++) { + for (size_t g = 0; g < ARRAY_SIZE(granularities); g++) { + TestCase *tc = g_new(TestCase, 1); + char *path; + + tc->mode = trans_modes[m].mode; + tc->gran = granularities[g].gran; + + path = g_strdup_printf("/iommu-testdev/intel/iotlb-inv/%s-%s", + granularities[g].name, + trans_modes[m].name); + qtest_add_data_func_full(path, tc, test_iotlb_inv, g_free); + g_free(path); + } + } + + for (size_t m = 0; m < ARRAY_SIZE(trans_modes); m++) { + QVTDTransMode *mode = g_new(QVTDTransMode, 1); + char *path; + + *mode = trans_modes[m].mode; + path = g_strdup_printf( + "/iommu-testdev/intel/iotlb-inv/page-selective/%s", + trans_modes[m].name); + qtest_add_data_func_full(path, mode, test_page_selectivity, g_free); + g_free(path); + } + + return g_test_run(); +} diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build index 93175baa87..56ff860e21 100644 --- a/tests/qtest/meson.build +++ b/tests/qtest/meson.build @@ -100,7 +100,7 @@ qtests_i386 = \ (config_all_devices.has_key('CONFIG_AMD_IOMMU') ? ['amd-iommu-test'] : []) + \ (config_all_devices.has_key('CONFIG_VTD') ? ['intel-iommu-test'] : []) + \ (config_all_devices.has_key('CONFIG_VTD') and - config_all_devices.has_key('CONFIG_IOMMU_TESTDEV') ? ['iommu-intel-test'] : []) + \ + config_all_devices.has_key('CONFIG_IOMMU_TESTDEV') ? ['iommu-intel-test', 'iommu-intel-inv-test'] : []) + \ (host_os != 'windows' and \ config_all_devices.has_key('CONFIG_ACPI_ERST') ? ['erst-test'] : []) + \ (config_all_devices.has_key('CONFIG_PCIE_PORT') and \ From ed963d3967fd8e5ca551c1cc6fcbabaef010d72e Mon Sep 17 00:00:00 2001 From: GuoHan Zhao Date: Mon, 29 Jun 2026 17:26:18 +0800 Subject: [PATCH 24/44] vhost-user-base: free virtqueue array during cleanup vhost-user-base stores the VirtQueue pointers in a GPtrArray, but its cleanup helper only deletes the VirtQueues and leaves the array itself allocated. Free the GPtrArray after deleting the queues and clear the pointer so cleanup remains safe if the error path reaches it with no queues to release. Fixes: 6275989647ef (virtio: split into vhost-user-base and vhost-user-device) Signed-off-by: GuoHan Zhao Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260629092619.2607275-1-zhaoguohan@kylinos.cn> --- hw/virtio/vhost-user-base.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/hw/virtio/vhost-user-base.c b/hw/virtio/vhost-user-base.c index 39b5e637fc..6ac523f9eb 100644 --- a/hw/virtio/vhost-user-base.c +++ b/hw/virtio/vhost-user-base.c @@ -193,9 +193,13 @@ static void do_vhost_user_cleanup(VirtIODevice *vdev, VHostUserBase *vub) { vhost_user_cleanup(&vub->vhost_user); - for (int i = 0; i < vub->num_vqs; i++) { - VirtQueue *vq = g_ptr_array_index(vub->vqs, i); - virtio_delete_queue(vq); + if (vub->vqs) { + for (int i = 0; i < vub->num_vqs; i++) { + VirtQueue *vq = g_ptr_array_index(vub->vqs, i); + virtio_delete_queue(vq); + } + g_ptr_array_free(vub->vqs, true); + vub->vqs = NULL; } virtio_cleanup(vdev); From bb3e0daaf8ea0bc98697c11a15bc30d7fa0b6173 Mon Sep 17 00:00:00 2001 From: GuoHan Zhao Date: Tue, 30 Jun 2026 09:20:58 +0800 Subject: [PATCH 25/44] vhost-user-base: clean up vhost_dev on realize failure Failures after vhost_dev_init() currently skip vhost_dev_cleanup(), leaking initialized vhost_dev state. Add a separate unwind label for those paths. Keep a copy of vhost_dev.vqs so the array can still be freed after vhost_dev_cleanup() clears struct vhost_dev. Fixes: 6608dca74ecf (vhost-user-device: Add shared memory BAR) Signed-off-by: GuoHan Zhao Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260630012058.2663259-1-zhaoguohan@kylinos.cn> --- hw/virtio/vhost-user-base.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/hw/virtio/vhost-user-base.c b/hw/virtio/vhost-user-base.c index 6ac523f9eb..478ec68f09 100644 --- a/hw/virtio/vhost-user-base.c +++ b/hw/virtio/vhost-user-base.c @@ -287,6 +287,7 @@ static void vub_device_realize(DeviceState *dev, Error **errp) VirtIODevice *vdev = VIRTIO_DEVICE(dev); VHostUserBase *vub = VHOST_USER_BASE(dev); uint64_t memory_sizes[VIRTIO_MAX_SHMEM_REGIONS]; + struct vhost_virtqueue *vhost_vqs = NULL; int i, ret, nregions, regions_processed = 0; if (!vub->chardev.chr) { @@ -338,6 +339,7 @@ static void vub_device_realize(DeviceState *dev, Error **errp) vub->vhost_dev.nvqs = vub->num_vqs; vub->vhost_dev.vqs = g_new0(struct vhost_virtqueue, vub->vhost_dev.nvqs); + vhost_vqs = vub->vhost_dev.vqs; /* connect to backend */ ret = vhost_dev_init(&vub->vhost_dev, &vub->vhost_user, @@ -353,7 +355,7 @@ static void vub_device_realize(DeviceState *dev, Error **errp) errp); if (ret < 0) { - goto err; + goto err_vhost_dev; } for (i = 0; i < VIRTIO_MAX_SHMEM_REGIONS && regions_processed < nregions; i++) { @@ -368,14 +370,14 @@ static void vub_device_realize(DeviceState *dev, Error **errp) errp); if (ret < 0) { - goto err; + goto err_vhost_dev; } } if (memory_sizes[i] % qemu_real_host_page_size() != 0) { error_setg(errp, "Shared memory %d size must be a multiple of " "the host page size", i); - goto err; + goto err_vhost_dev; } virtio_new_shmem_region(vdev, i, memory_sizes[i]); @@ -385,7 +387,10 @@ static void vub_device_realize(DeviceState *dev, Error **errp) qemu_chr_fe_set_handlers(&vub->chardev, NULL, NULL, vub_event, NULL, dev, NULL, true); return; +err_vhost_dev: + vhost_dev_cleanup(&vub->vhost_dev); err: + g_free(vhost_vqs); do_vhost_user_cleanup(vdev, vub); } From e03463812e74d1befc8d7f6f158eca531e2d6406 Mon Sep 17 00:00:00 2001 From: lizhaoxin04 Date: Mon, 22 Jun 2026 18:01:45 +0800 Subject: [PATCH 26/44] vdpa: fix use-after-free of vqs in vhost_vdpa_device_unrealize vhost_vdpa_device_unrealize() frees s->dev.vqs before vhost_dev_cleanup(), but vhost_dev_cleanup() still accesses hdev->vqs while tearing down virtqueues. This leads to a use-after-free and may crash QEMU with SIGSEGV during vDPA hot-unplug. Save the vqs pointer in a local variable, call vhost_dev_cleanup(), and free it afterward. This matches the cleanup pattern used by vhost-scsi. Fixes: b430a2bd23 ("vdpa: add vdpa-dev support") Co-developed-by: Miao Kezhan Signed-off-by: Li Zhaoxin Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260622100145.18924-1-lizhaoxin04@baidu.com> --- hw/virtio/vdpa-dev.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hw/virtio/vdpa-dev.c b/hw/virtio/vdpa-dev.c index 94188d37bb..089a77f4d0 100644 --- a/hw/virtio/vdpa-dev.c +++ b/hw/virtio/vdpa-dev.c @@ -172,6 +172,7 @@ static void vhost_vdpa_device_unrealize(DeviceState *dev) { VirtIODevice *vdev = VIRTIO_DEVICE(dev); VhostVdpaDevice *s = VHOST_VDPA_DEVICE(vdev); + struct vhost_virtqueue *vqs = s->dev.vqs; int i; virtio_set_status(vdev, 0); @@ -183,8 +184,8 @@ static void vhost_vdpa_device_unrealize(DeviceState *dev) virtio_cleanup(vdev); g_free(s->config); - g_free(s->dev.vqs); vhost_dev_cleanup(&s->dev); + g_free(vqs); g_free(s->vdpa.shared); qemu_close(s->vhostfd); s->vhostfd = -1; From 8e1aee6db42122cd1f781bf6048103fc76436c41 Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:44 +0200 Subject: [PATCH 27/44] arm: sbsa_gwdt: fixup default "clock-frequency" Comment about keeping legacy freq, is wrong to begin with (should be 62.5MHz), but that value also doesn't make sense anymore as the watchdog is used only by un-versioned SBSA board and the later has hard-coded it to 1GHz. Other potential user (arm/virt) also has system clock at 1GHz. Drop misleading comment about legacy and set default to 1GHz to match both boards. (not that it does matter, since users are setting frequency to match CPU's one explicitly) Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-2-imammedo@redhat.com> --- hw/watchdog/sbsa_gwdt.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hw/watchdog/sbsa_gwdt.c b/hw/watchdog/sbsa_gwdt.c index 7ade5c6f18..b739a3ce3c 100644 --- a/hw/watchdog/sbsa_gwdt.c +++ b/hw/watchdog/sbsa_gwdt.c @@ -264,11 +264,10 @@ static void wdt_sbsa_gwdt_realize(DeviceState *dev, Error **errp) static const Property wdt_sbsa_gwdt_props[] = { /* * Timer frequency in Hz. This must match the frequency used by - * the CPU's generic timer. Default 62.5Hz matches QEMU's legacy - * CPU timer frequency default. + * the CPU's generic timer. */ DEFINE_PROP_UINT64("clock-frequency", struct SBSA_GWDTState, freq, - 62500000), + 1000000000), }; static void wdt_sbsa_gwdt_class_init(ObjectClass *klass, const void *data) From 88e1d327fdb73e22479c97139fb237dabf6788ff Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:45 +0200 Subject: [PATCH 28/44] arm: add tracing events to sbsa_gwdt Signed-off-by: Igor Mammedov Reviewed-by: Ani Sinha Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-3-imammedo@redhat.com> --- hw/watchdog/sbsa_gwdt.c | 8 ++++++++ hw/watchdog/trace-events | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/hw/watchdog/sbsa_gwdt.c b/hw/watchdog/sbsa_gwdt.c index b739a3ce3c..acb970e8b3 100644 --- a/hw/watchdog/sbsa_gwdt.c +++ b/hw/watchdog/sbsa_gwdt.c @@ -24,6 +24,7 @@ #include "migration/vmstate.h" #include "qemu/log.h" #include "qemu/module.h" +#include "trace.h" static const VMStateDescription vmstate_sbsa_gwdt = { .name = "sbsa-gwdt", @@ -62,6 +63,7 @@ static uint64_t sbsa_gwdt_rread(void *opaque, hwaddr addr, unsigned int size) qemu_log_mask(LOG_GUEST_ERROR, "bad address in refresh frame read :" " 0x%x\n", (int)addr); } + trace_sbsa_gwdt_refresh_read(addr, ret); return ret; } @@ -93,6 +95,7 @@ static uint64_t sbsa_gwdt_read(void *opaque, hwaddr addr, unsigned int size) qemu_log_mask(LOG_GUEST_ERROR, "bad address in control frame read :" " 0x%x\n", (int)addr); } + trace_sbsa_gwdt_control_read(addr, ret); return ret; } @@ -127,6 +130,7 @@ static void sbsa_gwdt_rwrite(void *opaque, hwaddr offset, uint64_t data, unsigned size) { SBSA_GWDTState *s = SBSA_GWDT(opaque); + trace_sbsa_gwdt_refresh_write(offset, data); if (offset == SBSA_GWDT_WRR) { s->wcs &= ~(SBSA_GWDT_WCS_WS0 | SBSA_GWDT_WCS_WS1); @@ -141,6 +145,7 @@ static void sbsa_gwdt_write(void *opaque, hwaddr offset, uint64_t data, unsigned size) { SBSA_GWDTState *s = SBSA_GWDT(opaque); + trace_sbsa_gwdt_control_write(offset, data); switch (offset) { case SBSA_GWDT_WCS: s->wcs = data & SBSA_GWDT_WCS_EN; @@ -180,6 +185,7 @@ static void wdt_sbsa_gwdt_reset(DeviceState *dev) { SBSA_GWDTState *s = SBSA_GWDT(dev); + trace_sbsa_gwdt_reset(); timer_del(s->timer); s->wcs = 0; @@ -196,10 +202,12 @@ static void sbsa_gwdt_timer_sysinterrupt(void *opaque) if (!(s->wcs & SBSA_GWDT_WCS_WS0)) { s->wcs |= SBSA_GWDT_WCS_WS0; + trace_sbsa_gwdt_ws0_asserted(); sbsa_gwdt_update_timer(s, TIMEOUT_REFRESH); qemu_set_irq(s->irq, 1); } else { s->wcs |= SBSA_GWDT_WCS_WS1; + trace_sbsa_gwdt_ws1_asserted(); qemu_log_mask(CPU_LOG_RESET, "Watchdog timer expired.\n"); /* * Reset the watchdog only if the guest gets notified about diff --git a/hw/watchdog/trace-events b/hw/watchdog/trace-events index d85b3ca769..b388f7b250 100644 --- a/hw/watchdog/trace-events +++ b/hw/watchdog/trace-events @@ -42,3 +42,12 @@ k230_wdt_interrupt(void) "K230 WDT interrupt" k230_wdt_reset(void) "K230 WDT system reset" k230_wdt_restart(void) "K230 WDT restart" k230_wdt_reset_device(void) "K230 WDT device reset" + +#sbsa_gwdt.c +sbsa_gwdt_refresh_read(uint64_t addr, uint32_t value) "[0x%" PRIx64 "] -> 0x%" PRIx32 +sbsa_gwdt_refresh_write(uint64_t addr, uint64_t value) "[0x%" PRIx64 "] <- 0x%" PRIx64 +sbsa_gwdt_control_read(uint64_t addr, uint32_t value) "[0x%" PRIx64 "] -> 0x%" PRIx32 +sbsa_gwdt_control_write(uint64_t addr, uint64_t value) "[0x%" PRIx64 "] <- 0x%" PRIx64 +sbsa_gwdt_ws0_asserted(void) "WS0 signal is asserted" +sbsa_gwdt_ws1_asserted(void) "WS1 signal is asserted" +sbsa_gwdt_reset(void) "reset watchdog to default state" From 5cb6dfc12d36ede1f79deb58385e324e7dc1cce9 Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:46 +0200 Subject: [PATCH 29/44] arm: sbsa_gwdt: rename device type to sbsa-gwdt Use hyphenated name to follow QEMU device naming convention. Migration compatibility is preserved since the vmstate name is already "sbsa-gwdt". Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-4-imammedo@redhat.com> --- include/hw/watchdog/sbsa_gwdt.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/hw/watchdog/sbsa_gwdt.h b/include/hw/watchdog/sbsa_gwdt.h index 307a4f291a..8941dcff9e 100644 --- a/include/hw/watchdog/sbsa_gwdt.h +++ b/include/hw/watchdog/sbsa_gwdt.h @@ -16,7 +16,7 @@ #include "hw/core/sysbus.h" #include "hw/core/irq.h" -#define TYPE_WDT_SBSA "sbsa_gwdt" +#define TYPE_WDT_SBSA "sbsa-gwdt" #define SBSA_GWDT(obj) \ OBJECT_CHECK(SBSA_GWDTState, (obj), TYPE_WDT_SBSA) #define SBSA_GWDT_CLASS(klass) \ From 2f27572eb5ca08c71cdd872d018ea5ad8791dce4 Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:47 +0200 Subject: [PATCH 30/44] arm: virt: create sbsa-gwdt watchdog Allow to use SBSA generic watchdog with virt machine type. (includes conditional generation of corresponding FDT and ACPI GTDT descriptors) Use '-device sbsa-gwdt' to command line to enable it. Instead of using dynamic sysbus infra to wire up MMIO/IRQ/FDT, statically assign resources in machine's mem/irq maps and wire them up at device (pre_)plug handlers. It's similar to dynamic sysbus wiring, modulo resources are nailed down statically, and wiring is limited to virt machine only. (Benefit is that tests don't break anymore on rebase due to address being stable) Tested with Fedora 43: FDT: -M virt,acpi=off -device sbsa-gwdt ACPI: -M virt -device sbsa-gwdt Note: Windows sees GTDT, initializes watchdog but instead pinging WRR it sets/advances WOR to way too large value, so it's never going to trigger watchdog reboot (it's Windows driver issue though). Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-5-imammedo@redhat.com> --- docs/system/arm/virt.rst | 9 ++++++++ hw/arm/Kconfig | 1 + hw/arm/virt-acpi-build.c | 29 ++++++++++++++++++++++++-- hw/arm/virt.c | 44 ++++++++++++++++++++++++++++++++++++++++ hw/core/sysbus-fdt.c | 2 ++ hw/watchdog/sbsa_gwdt.c | 2 ++ include/hw/arm/virt.h | 3 +++ 7 files changed, 88 insertions(+), 2 deletions(-) diff --git a/docs/system/arm/virt.rst b/docs/system/arm/virt.rst index ae0be35d57..bf5a9c8f6c 100644 --- a/docs/system/arm/virt.rst +++ b/docs/system/arm/virt.rst @@ -39,6 +39,7 @@ The virt board supports: - A PL061 GPIO controller - An optional machine-wide SMMUv3 IOMMU - User-creatable SMMUv3 devices (see below for example) +- An optional SBSA Generic Watchdog Timer (see below) - hotpluggable DIMMs - hotpluggable NVDIMMs - An MSI controller (GICv2m or ITS). @@ -314,6 +315,14 @@ User-creatable SMMUv3 devices bypassing QEMU and improving throughput for workloads that issue many invalidations. Without it, every invalidation command traps into QEMU. +SBSA Generic Watchdog +""""""""""""""""""""" + +The SBSA Generic Watchdog Timer (GWDT) can be added to the virt machine +using ``-device sbsa-gwdt``. It is only supported on the virt machine, +which wires up statically assigned MMIO regions and IRQs via +machine-specific plug handlers. + Linux guest kernel configuration """""""""""""""""""""""""""""""" diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig index 5f5c4899ad..f9a225c19b 100644 --- a/hw/arm/Kconfig +++ b/hw/arm/Kconfig @@ -36,6 +36,7 @@ config ARM_VIRT select VIRTIO_MEM_SUPPORTED select ACPI_CXL select ACPI_HMAT + select WDT_SBSA config CUBIEBOARD bool diff --git a/hw/arm/virt-acpi-build.c b/hw/arm/virt-acpi-build.c index 99490aa7b1..f5b3b4ce48 100644 --- a/hw/arm/virt-acpi-build.c +++ b/hw/arm/virt-acpi-build.c @@ -64,6 +64,7 @@ #include "hw/virtio/virtio-acpi.h" #include "target/arm/cpu.h" #include "target/arm/multiprocessing.h" +#include "hw/watchdog/sbsa_gwdt.h" #include "smmuv3-accel.h" #include "tegra241-cmdqv.h" @@ -868,6 +869,8 @@ build_gtdt(GArray *table_data, BIOSLinker *linker, VirtMachineState *vms) const uint32_t irqflags = 0; /* Interrupt is Level triggered */ AcpiTable table = { .sig = "GTDT", .rev = 3, .oem_id = vms->oem_id, .oem_table_id = vms->oem_table_id }; + uint32_t gtdt_start = table_data->len; + Object *wdt = object_resolve_type_unambiguous(TYPE_WDT_SBSA, NULL); acpi_table_begin(&table, table_data); @@ -898,10 +901,15 @@ build_gtdt(GArray *table_data, BIOSLinker *linker, VirtMachineState *vms) build_append_int_noprefix(table_data, irqflags, 4); /* CntReadBase Physical address */ build_append_int_noprefix(table_data, 0xFFFFFFFFFFFFFFFF, 8); + /* Platform Timer Count */ - build_append_int_noprefix(table_data, 0, 4); + build_append_int_noprefix(table_data, wdt ? 1 : 0, 4); /* Platform Timer Offset */ - build_append_int_noprefix(table_data, 0, 4); + build_append_int_noprefix(table_data, + wdt ? (table_data->len - gtdt_start) + + 4 + 4 + 4 /* len of this & following 2 fields to skip */ + : 0, 4); + if (vms->ns_el2_virt_timer_irq) { /* Virtual EL2 Timer GSIV */ build_append_int_noprefix(table_data, ARCH_TIMER_NS_EL2_VIRT_IRQ, 4); @@ -911,6 +919,23 @@ build_gtdt(GArray *table_data, BIOSLinker *linker, VirtMachineState *vms) build_append_int_noprefix(table_data, 0, 4); build_append_int_noprefix(table_data, 0, 4); } + + /* ACPI 6.5 spec: 5.2.25.2 ARM Generic Watchdog Structure (Table 5-124) */ + if (wdt) { + hwaddr rbase = vms->memmap[VIRT_GWDT_REFRESH].base; + hwaddr cbase = vms->memmap[VIRT_GWDT_CONTROL].base; + int irq = ARM_SPI_BASE + vms->irqmap[VIRT_GWDT_WS0]; + + build_append_int_noprefix(table_data, 1 /* Type: Watchdog GT */, 1); + build_append_int_noprefix(table_data, 28 /* Length */, 2); + build_append_int_noprefix(table_data, 0, 1); /* Reserved */ + /* RefreshFrame Physical Address */ + build_append_int_noprefix(table_data, rbase, 8); + /* WatchdogControlFrame Physical Address */ + build_append_int_noprefix(table_data, cbase, 8); + build_append_int_noprefix(table_data, irq, 4); /* Watchdog Timer GSIV */ + build_append_int_noprefix(table_data, 0, 4); /* Watchdog Timer Flags */ + } acpi_table_end(linker, &table); } diff --git a/hw/arm/virt.c b/hw/arm/virt.c index d8d27f2ef6..eabc5274d5 100644 --- a/hw/arm/virt.c +++ b/hw/arm/virt.c @@ -95,6 +95,7 @@ #include "hw/cxl/cxl.h" #include "hw/cxl/cxl_host.h" #include "qemu/guest-random.h" +#include "hw/watchdog/sbsa_gwdt.h" static GlobalProperty arm_virt_compat_defaults[] = { { TYPE_VIRTIO_IOMMU_PCI, "aw-bits", "48" }, @@ -214,6 +215,8 @@ static const MemMapEntry base_memmap[] = { /* ...repeating for a total of NUM_VIRTIO_TRANSPORTS, each of that size */ [VIRT_PLATFORM_BUS] = { 0x0c000000, 0x02000000 }, [VIRT_SECURE_MEM] = { 0x0e000000, 0x01000000 }, + [VIRT_GWDT_REFRESH] = { 0x0f000000, 0x00001000 }, + [VIRT_GWDT_CONTROL] = { 0x0f001000, 0x00001000 }, [VIRT_PCIE_MMIO] = { 0x10000000, 0x2eff0000 }, [VIRT_PCIE_PIO] = { 0x3eff0000, 0x00010000 }, [VIRT_PCIE_ECAM] = { 0x3f000000, 0x01000000 }, @@ -264,6 +267,7 @@ static const int a15irqmap[] = { [VIRT_GPIO] = 7, [VIRT_UART1] = 8, [VIRT_ACPI_GED] = 9, + [VIRT_GWDT_WS0] = 10, [VIRT_MMIO] = 16, /* ...to 16 + NUM_VIRTIO_TRANSPORTS - 1 */ [VIRT_GIC_V2M] = 48, /* ...to 48 + NUM_GICV2M_SPIS - 1 */ [VIRT_SMMU] = 74, /* ...to 74 + NUM_SMMU_IRQS - 1 */ @@ -2085,6 +2089,27 @@ static void create_smmu(const VirtMachineState *vms, PCIBus *bus) create_smmuv3_dt_bindings(vms, base, size, irq); } +static void create_gwdt_dt_bindings(VirtMachineState *vms) +{ + MachineState *ms = MACHINE(vms); + hwaddr rbase = vms->memmap[VIRT_GWDT_REFRESH].base; + hwaddr cbase = vms->memmap[VIRT_GWDT_CONTROL].base; + int irq = vms->irqmap[VIRT_GWDT_WS0]; + char *nodename = g_strdup_printf("/watchdog@%" PRIx64, cbase); + + qemu_fdt_add_subnode(ms->fdt, nodename); + qemu_fdt_setprop_string(ms->fdt, nodename, + "compatible", "arm,sbsa-gwdt"); + qemu_fdt_setprop_sized_cells(ms->fdt, nodename, "reg", + 2, cbase, 2, SBSA_GWDT_CMMIO_SIZE, + 2, rbase, 2, SBSA_GWDT_RMMIO_SIZE); + qemu_fdt_setprop_cells(ms->fdt, nodename, "interrupts", + GIC_FDT_IRQ_TYPE_SPI, irq, + GIC_FDT_IRQ_FLAGS_LEVEL_HI); + qemu_fdt_setprop_cell(ms->fdt, nodename, "timeout-sec", 30); + g_free(nodename); +} + static void create_virtio_iommu_dt_bindings(VirtMachineState *vms) { const char compat[] = "virtio,pci-iommu\0pci1af4,1057"; @@ -3820,6 +3845,11 @@ static void virt_machine_device_pre_plug_cb(HotplugHandler *hotplug_dev, qlist_append_str(reserved_regions, resv_prop_str); qdev_prop_set_array(dev, "reserved-regions", reserved_regions); g_free(resv_prop_str); + } else if (object_dynamic_cast(OBJECT(dev), TYPE_WDT_SBSA)) { + uint64_t cntfrq = object_property_get_int(OBJECT(qemu_get_cpu(0)), + "cntfrq", &error_abort); + + qdev_prop_set_uint64(dev, "clock-frequency", cntfrq); } else if (object_dynamic_cast(OBJECT(dev), TYPE_ARM_SMMUV3)) { if (vms->legacy_smmuv3_present || vms->iommu == VIRT_IOMMU_VIRTIO) { error_setg(errp, "virt machine already has %s set. " @@ -3871,6 +3901,19 @@ static void virt_machine_device_plug_cb(HotplugHandler *hotplug_dev, { VirtMachineState *vms = VIRT_MACHINE(hotplug_dev); + if (object_dynamic_cast(OBJECT(dev), TYPE_WDT_SBSA)) { + SysBusDevice *s = SYS_BUS_DEVICE(dev); + hwaddr rbase = vms->memmap[VIRT_GWDT_REFRESH].base; + hwaddr cbase = vms->memmap[VIRT_GWDT_CONTROL].base; + int irq = vms->irqmap[VIRT_GWDT_WS0]; + + sysbus_mmio_map(s, 0, rbase); + sysbus_mmio_map(s, 1, cbase); + sysbus_connect_irq(s, 0, qdev_get_gpio_in(vms->gic, irq)); + + create_gwdt_dt_bindings(vms); + } + if (vms->platform_bus_dev) { MachineClass *mc = MACHINE_GET_CLASS(vms); @@ -4123,6 +4166,7 @@ static void virt_machine_class_init(ObjectClass *oc, const void *data) machine_class_allow_dynamic_sysbus_dev(mc, TYPE_RAMFB_DEVICE); machine_class_allow_dynamic_sysbus_dev(mc, TYPE_UEFI_VARS_SYSBUS); machine_class_allow_dynamic_sysbus_dev(mc, TYPE_ARM_SMMUV3); + machine_class_allow_dynamic_sysbus_dev(mc, TYPE_WDT_SBSA); #ifdef CONFIG_TPM machine_class_allow_dynamic_sysbus_dev(mc, TYPE_TPM_TIS_SYSBUS); #endif diff --git a/hw/core/sysbus-fdt.c b/hw/core/sysbus-fdt.c index 89d0c46445..12238570b5 100644 --- a/hw/core/sysbus-fdt.c +++ b/hw/core/sysbus-fdt.c @@ -36,6 +36,7 @@ #include "hw/display/ramfb.h" #include "hw/uefi/var-service-api.h" #include "hw/arm/fdt.h" +#include "hw/watchdog/sbsa_gwdt.h" /* * internal struct that contains the information to create dynamic @@ -140,6 +141,7 @@ static const BindingEntry bindings[] = { TYPE_BINDING(TYPE_ARM_SMMUV3, no_fdt_node), TYPE_BINDING(TYPE_RAMFB_DEVICE, no_fdt_node), TYPE_BINDING(TYPE_UEFI_VARS_SYSBUS, add_uefi_vars_node), + TYPE_BINDING(TYPE_WDT_SBSA, no_fdt_node), TYPE_BINDING("", NULL), /* last element */ }; diff --git a/hw/watchdog/sbsa_gwdt.c b/hw/watchdog/sbsa_gwdt.c index acb970e8b3..49fdd723c2 100644 --- a/hw/watchdog/sbsa_gwdt.c +++ b/hw/watchdog/sbsa_gwdt.c @@ -285,6 +285,8 @@ static void wdt_sbsa_gwdt_class_init(ObjectClass *klass, const void *data) dc->realize = wdt_sbsa_gwdt_realize; device_class_set_legacy_reset(dc, wdt_sbsa_gwdt_reset); dc->hotpluggable = false; + /* requires machine-specific wiring (MMIO/IRQ/FDT) at plug time */ + dc->user_creatable = true; set_bit(DEVICE_CATEGORY_WATCHDOG, dc->categories); dc->vmsd = &vmstate_sbsa_gwdt; dc->desc = "SBSA-compliant generic watchdog device"; diff --git a/include/hw/arm/virt.h b/include/hw/arm/virt.h index 171d44c644..22e66d1a11 100644 --- a/include/hw/arm/virt.h +++ b/include/hw/arm/virt.h @@ -97,6 +97,9 @@ enum { VIRT_NVDIMM_ACPI, VIRT_PVTIME, VIRT_ACPI_PCIHP, + VIRT_GWDT_WS0, + VIRT_GWDT_REFRESH, + VIRT_GWDT_CONTROL, VIRT_LOWMEMMAP_LAST, }; From 51011f7f36b4406718d9698530c4e9d920184755 Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:48 +0200 Subject: [PATCH 31/44] arm: sbsa-gwdt: add 'wdat' option it will be used by arm/virt board, to pick WDAT compatible watchdog impl. and act as switch over to WDAT ACPI table vesus default GTDT ACPI table. Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-6-imammedo@redhat.com> --- hw/watchdog/sbsa_gwdt.c | 9 +++++++++ include/hw/watchdog/sbsa_gwdt.h | 1 + 2 files changed, 10 insertions(+) diff --git a/hw/watchdog/sbsa_gwdt.c b/hw/watchdog/sbsa_gwdt.c index 49fdd723c2..330a74798a 100644 --- a/hw/watchdog/sbsa_gwdt.c +++ b/hw/watchdog/sbsa_gwdt.c @@ -265,6 +265,14 @@ static void wdt_sbsa_gwdt_realize(DeviceState *dev, Error **errp) sysbus_init_irq(sbd, &s->irq); + /* + * WDAT spec: "The clock interval that the WDT uses must be + * greater than or equal to 1 millisecond." + */ + if (s->wdat) { + s->freq = 1000; + } + s->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, sbsa_gwdt_timer_sysinterrupt, dev); } @@ -276,6 +284,7 @@ static const Property wdt_sbsa_gwdt_props[] = { */ DEFINE_PROP_UINT64("clock-frequency", struct SBSA_GWDTState, freq, 1000000000), + DEFINE_PROP_BOOL("wdat", struct SBSA_GWDTState, wdat, false), }; static void wdt_sbsa_gwdt_class_init(ObjectClass *klass, const void *data) diff --git a/include/hw/watchdog/sbsa_gwdt.h b/include/hw/watchdog/sbsa_gwdt.h index 8941dcff9e..5ad7c7a798 100644 --- a/include/hw/watchdog/sbsa_gwdt.h +++ b/include/hw/watchdog/sbsa_gwdt.h @@ -73,6 +73,7 @@ typedef struct SBSA_GWDTState { uint32_t woru; uint32_t wcvl; uint32_t wcvu; + bool wdat; } SBSA_GWDTState; #endif /* WDT_SBSA_GWDT_H */ From beadd272c10289c0086d66fc5d1a0c5229127916 Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:49 +0200 Subject: [PATCH 32/44] acpi: introduce WDAT table for GWDT Add build_gwdt_wdat() to generate a Watchdog Action Table designed for SBSA Generic Watchdog Timer. Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-7-imammedo@redhat.com> --- hw/acpi/meson.build | 2 + hw/acpi/wdat-gwdt-stub.c | 16 ++++++ hw/acpi/wdat-gwdt.c | 99 +++++++++++++++++++++++++++++++++++++ include/hw/acpi/wdat-gwdt.h | 19 +++++++ 4 files changed, 136 insertions(+) create mode 100644 hw/acpi/wdat-gwdt-stub.c create mode 100644 hw/acpi/wdat-gwdt.c create mode 100644 include/hw/acpi/wdat-gwdt.h diff --git a/hw/acpi/meson.build b/hw/acpi/meson.build index 0c7bfb278a..09ad488a04 100644 --- a/hw/acpi/meson.build +++ b/hw/acpi/meson.build @@ -31,11 +31,13 @@ acpi_ss.add(when: 'CONFIG_ACPI_ERST', if_true: files('erst.c')) acpi_ss.add(when: 'CONFIG_IPMI', if_true: files('ipmi.c')) stub_ss.add(files('ipmi-stub.c')) stub_ss.add(files('acpi-x86-stub.c')) +acpi_ss.add(when: 'CONFIG_WDT_SBSA', if_true: files('wdat-gwdt.c')) if have_tpm acpi_ss.add(files('tpm.c')) endif stub_ss.add(files('acpi-stub.c', 'aml-build-stub.c', 'ghes-stub.c')) stub_ss.add(files('pci-bridge-stub.c')) +stub_ss.add(files('wdat-gwdt-stub.c')) system_ss.add_all(when: 'CONFIG_ACPI', if_true: acpi_ss) system_ss.add(when: 'CONFIG_GHES_CPER', if_true: files('ghes_cper.c')) stub_ss.add(files('ghes_cper_stub.c')) diff --git a/hw/acpi/wdat-gwdt-stub.c b/hw/acpi/wdat-gwdt-stub.c new file mode 100644 index 0000000000..4d43783f70 --- /dev/null +++ b/hw/acpi/wdat-gwdt-stub.c @@ -0,0 +1,16 @@ +/* + * Copyright Red Hat, Inc. 2026 + * Author(s): Igor Mammedov + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "hw/acpi/wdat-gwdt.h" + +void build_gwdt_wdat(GArray *table_data, BIOSLinker *linker, const char *oem_id, + const char *oem_table_id, uint64_t rbase, uint64_t cbase, + uint64_t freq) +{ + g_assert_not_reached(); +} diff --git a/hw/acpi/wdat-gwdt.c b/hw/acpi/wdat-gwdt.c new file mode 100644 index 0000000000..b30566ef6a --- /dev/null +++ b/hw/acpi/wdat-gwdt.c @@ -0,0 +1,99 @@ +/* + * SBSA GWDT Watchdog Action Table (WDAT) + * + * Copyright Red Hat, Inc. 2026 + * Author(s): Igor Mammedov + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "hw/acpi/aml-build.h" +#include "hw/acpi/wdat-gwdt.h" +#include "hw/acpi/wdat.h" +#include "hw/watchdog/sbsa_gwdt.h" + +#define GWDT_REG(base, reg_offset, reg_width) { \ + .space_id = AML_AS_SYSTEM_MEMORY, \ + .address = base + reg_offset, .bit_width = reg_width, \ + .access_width = AML_DWORD_ACC }; + +/* + * "Hardware Watchdog Timers Design Specification" + * https://uefi.org/acpi 'Watchdog Action Table (WDAT)' + */ +void build_gwdt_wdat(GArray *table_data, BIOSLinker *linker, const char *oem_id, + const char *oem_table_id, uint64_t rbase, uint64_t cbase, + uint64_t freq) +{ + AcpiTable table = { .sig = "WDAT", .rev = 1, .oem_id = oem_id, + .oem_table_id = oem_table_id }; + + struct AcpiGenericAddress wrr = GWDT_REG(rbase, 0x0, 32); + struct AcpiGenericAddress wor_l = GWDT_REG(cbase, SBSA_GWDT_WOR, 32); + struct AcpiGenericAddress wcs = GWDT_REG(cbase, SBSA_GWDT_WCS, 32); + + acpi_table_begin(&table, table_data); + build_append_int_noprefix(table_data, 0x20, 4); /* Watchdog Header Length */ + /* + * PCI location fields are set to 0xff to indicate + * that the watchdog is not a PCI device. + */ + build_append_int_noprefix(table_data, 0xff, 2); /* PCI Segment */ + build_append_int_noprefix(table_data, 0xff, 1); /* PCI Bus Number */ + build_append_int_noprefix(table_data, 0xff, 1); /* PCI Device Number */ + build_append_int_noprefix(table_data, 0xff, 1); /* PCI Function Number */ + build_append_int_noprefix(table_data, 0, 3); /* Reserved */ + /* + * WDAT spec: "The clock interval that the WDT uses must be + * greater than or equal to 1 millisecond." + */ + g_assert(freq <= 1000); + /* Timer Period, ms */ + build_append_int_noprefix(table_data, 1000 / freq, 4); + /* + * WDAT spec: "The time-out period before the WDT fires is recommended + * to be at least 5 minutes." + * Set max count to 10min and min count to 5sec. + */ + build_append_int_noprefix(table_data, 600 * freq, 4); /* Maximum Count */ + build_append_int_noprefix(table_data, 5 * freq, 4); /* Minimum Count */ + /* + * WATCHDOG_ENABLED | WATCHDOG_STOPPED_IN_SLEEP_STATE + */ + build_append_int_noprefix(table_data, 0x81, 1); /* Watchdog Flags */ + build_append_int_noprefix(table_data, 0, 3); /* Reserved */ + /* + * watchdog instruction entries + */ + build_append_int_noprefix(table_data, 8, 4); + /* Action table: WCS (control/status) register actions */ + build_append_wdat_ins(table_data, WDAT_ACTION_QUERY_RUNNING_STATE, + WDAT_INS_READ_VALUE, + wcs, 0x1, 0x1); + build_append_wdat_ins(table_data, WDAT_ACTION_SET_RUNNING_STATE, + WDAT_INS_WRITE_VALUE | WDAT_INS_PRESERVE_REGISTER, + wcs, 1, 0x00000001); + build_append_wdat_ins(table_data, WDAT_ACTION_QUERY_STOPPED_STATE, + WDAT_INS_READ_VALUE, + wcs, 0x0, 0x00000001); + build_append_wdat_ins(table_data, WDAT_ACTION_SET_STOPPED_STATE, + WDAT_INS_WRITE_VALUE | WDAT_INS_PRESERVE_REGISTER, + wcs, 0x0, 0x00000001); + build_append_wdat_ins(table_data, WDAT_ACTION_QUERY_WATCHDOG_STATUS, + WDAT_INS_READ_VALUE, + wcs, 0x4, 0x00000004); + /* WOR (offset) and WRR (refresh) register actions */ + build_append_wdat_ins(table_data, WDAT_ACTION_SET_COUNTDOWN_PERIOD, + WDAT_INS_WRITE_COUNTDOWN, + wor_l, 0, 0xffffffff); + /* WRR: any write refreshes the watchdog, value is ignored */ + build_append_wdat_ins(table_data, WDAT_ACTION_RESET, + WDAT_INS_WRITE_VALUE, + wrr, 0x1, 0x1); + build_append_wdat_ins(table_data, WDAT_ACTION_SET_WATCHDOG_STATUS, + WDAT_INS_WRITE_VALUE | WDAT_INS_PRESERVE_REGISTER, + wrr, 0x4, 0x4); + + acpi_table_end(linker, &table); +} diff --git a/include/hw/acpi/wdat-gwdt.h b/include/hw/acpi/wdat-gwdt.h new file mode 100644 index 0000000000..42339e031e --- /dev/null +++ b/include/hw/acpi/wdat-gwdt.h @@ -0,0 +1,19 @@ +/* + * GWDT Watchdog Action Table (WDAT) definition + * + * Copyright Red Hat, Inc. 2026 + * Author(s): Igor Mammedov + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ +#ifndef QEMU_HW_ACPI_WDAT_GWDT_H +#define QEMU_HW_ACPI_WDAT_GWDT_H + +#include "hw/acpi/aml-build.h" +#include "hw/watchdog/sbsa_gwdt.h" + +void build_gwdt_wdat(GArray *table_data, BIOSLinker *linker, const char *oem_id, + const char *oem_table_id, uint64_t rbase, uint64_t cbase, + uint64_t freq); + +#endif /* QEMU_HW_ACPI_WDAT_GWDT_H */ From 285bc90d5b31775b7803e50822930a2914bf42de Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:50 +0200 Subject: [PATCH 33/44] arm: virt: add support for WDAT based watchdog Add WDAT handling for sbsa-gwdt on arm/virt machine. WDAT mode is enabled by 'wdat' option: ex: "-device sbsa-gwdt,wdat=on" When WDAT is enabled: - Build the WDAT ACPI table instead of the GTDT watchdog entry, since they are mutually exclusive due to different timer resolution (WDAT uses 1 kHz vs GTDT's system counter frequency). - Skip FDT watchdog node creation, as the DT-based Linux driver would use the system counter frequency which doesn't match the WDAT-mode 1 kHz clock. Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-8-imammedo@redhat.com> --- docs/system/arm/virt.rst | 15 +++++++++++++++ hw/arm/virt-acpi-build.c | 34 ++++++++++++++++++++++++++-------- hw/arm/virt.c | 12 ++++++++---- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/docs/system/arm/virt.rst b/docs/system/arm/virt.rst index bf5a9c8f6c..5f6dd17978 100644 --- a/docs/system/arm/virt.rst +++ b/docs/system/arm/virt.rst @@ -323,6 +323,21 @@ using ``-device sbsa-gwdt``. It is only supported on the virt machine, which wires up statically assigned MMIO regions and IRQs via machine-specific plug handlers. +Two modes are available: + +Native mode (default) + The watchdog is described via the ACPI GTDT table and FDT, using + the system counter frequency. Example:: + + -device sbsa-gwdt + +WDAT mode + The watchdog is described via the ACPI WDAT table (no FDT node), + using a 1 kHz timer frequency. WDAT and GTDT watchdog entries are + mutually exclusive. Example:: + + -device sbsa-gwdt,wdat=on + Linux guest kernel configuration """""""""""""""""""""""""""""""" diff --git a/hw/arm/virt-acpi-build.c b/hw/arm/virt-acpi-build.c index f5b3b4ce48..f6386088b6 100644 --- a/hw/arm/virt-acpi-build.c +++ b/hw/arm/virt-acpi-build.c @@ -65,6 +65,7 @@ #include "target/arm/cpu.h" #include "target/arm/multiprocessing.h" #include "hw/watchdog/sbsa_gwdt.h" +#include "hw/acpi/wdat-gwdt.h" #include "smmuv3-accel.h" #include "tegra241-cmdqv.h" @@ -859,7 +860,8 @@ build_srat(GArray *table_data, BIOSLinker *linker, VirtMachineState *vms) * 5.2.25 Generic Timer Description Table (GTDT) */ static void -build_gtdt(GArray *table_data, BIOSLinker *linker, VirtMachineState *vms) +build_gtdt(GArray *table_data, BIOSLinker *linker, VirtMachineState *vms, + bool add_watchdog) { /* * Table 5-117 Flag Definitions @@ -870,7 +872,6 @@ build_gtdt(GArray *table_data, BIOSLinker *linker, VirtMachineState *vms) AcpiTable table = { .sig = "GTDT", .rev = 3, .oem_id = vms->oem_id, .oem_table_id = vms->oem_table_id }; uint32_t gtdt_start = table_data->len; - Object *wdt = object_resolve_type_unambiguous(TYPE_WDT_SBSA, NULL); acpi_table_begin(&table, table_data); @@ -903,12 +904,12 @@ build_gtdt(GArray *table_data, BIOSLinker *linker, VirtMachineState *vms) build_append_int_noprefix(table_data, 0xFFFFFFFFFFFFFFFF, 8); /* Platform Timer Count */ - build_append_int_noprefix(table_data, wdt ? 1 : 0, 4); + build_append_int_noprefix(table_data, add_watchdog ? 1 : 0, 4); /* Platform Timer Offset */ build_append_int_noprefix(table_data, - wdt ? (table_data->len - gtdt_start) + - 4 + 4 + 4 /* len of this & following 2 fields to skip */ - : 0, 4); + add_watchdog ? (table_data->len - gtdt_start) + + 4 + 4 + 4 /* len of this & following 2 fields to skip */ + : 0, 4); if (vms->ns_el2_virt_timer_irq) { /* Virtual EL2 Timer GSIV */ @@ -921,7 +922,7 @@ build_gtdt(GArray *table_data, BIOSLinker *linker, VirtMachineState *vms) } /* ACPI 6.5 spec: 5.2.25.2 ARM Generic Watchdog Structure (Table 5-124) */ - if (wdt) { + if (add_watchdog) { hwaddr rbase = vms->memmap[VIRT_GWDT_REFRESH].base; hwaddr cbase = vms->memmap[VIRT_GWDT_CONTROL].base; int irq = ARM_SPI_BASE + vms->irqmap[VIRT_GWDT_WS0]; @@ -1332,13 +1333,19 @@ void virt_acpi_build(VirtMachineState *vms, AcpiBuildTables *tables) VirtMachineClass *vmc = VIRT_MACHINE_GET_CLASS(vms); GArray *table_offsets; unsigned dsdt, xsdt; + bool has_wdat = false; GArray *tables_blob = tables->table_data; MachineState *ms = MACHINE(vms); CPUCoreCaches caches[CPU_MAX_CACHES]; unsigned int num_caches; + Object *wdt = object_resolve_type_unambiguous(TYPE_WDT_SBSA, NULL); num_caches = virt_get_caches(vms, caches); + if (wdt) { + has_wdat = object_property_get_bool(wdt, "wdat", &error_abort); + } + table_offsets = g_array_new(false, true /* clear */, sizeof(uint32_t)); @@ -1357,6 +1364,17 @@ void virt_acpi_build(VirtMachineState *vms, AcpiBuildTables *tables) acpi_add_table(table_offsets, tables_blob); build_madt(tables_blob, tables->linker, vms); + acpi_add_table(table_offsets, tables_blob); + if (wdt && has_wdat) { + uint64_t freq = object_property_get_uint(wdt, "clock-frequency", + &error_abort); + build_gwdt_wdat(tables_blob, tables->linker, + vms->oem_id, vms->oem_table_id, + vms->memmap[VIRT_GWDT_REFRESH].base, + vms->memmap[VIRT_GWDT_CONTROL].base, + freq); + } + if (!vmc->no_cpu_topology) { acpi_add_table(table_offsets, tables_blob); build_pptt(tables_blob, tables->linker, ms, vms->oem_id, @@ -1364,7 +1382,7 @@ void virt_acpi_build(VirtMachineState *vms, AcpiBuildTables *tables) } acpi_add_table(table_offsets, tables_blob); - build_gtdt(tables_blob, tables->linker, vms); + build_gtdt(tables_blob, tables->linker, vms, wdt && !has_wdat); acpi_add_table(table_offsets, tables_blob); { diff --git a/hw/arm/virt.c b/hw/arm/virt.c index eabc5274d5..fb916c341b 100644 --- a/hw/arm/virt.c +++ b/hw/arm/virt.c @@ -3846,10 +3846,12 @@ static void virt_machine_device_pre_plug_cb(HotplugHandler *hotplug_dev, qdev_prop_set_array(dev, "reserved-regions", reserved_regions); g_free(resv_prop_str); } else if (object_dynamic_cast(OBJECT(dev), TYPE_WDT_SBSA)) { - uint64_t cntfrq = object_property_get_int(OBJECT(qemu_get_cpu(0)), - "cntfrq", &error_abort); + if (!object_property_get_bool(OBJECT(dev), "wdat", &error_abort)) { + uint64_t cntfrq = object_property_get_int(OBJECT(qemu_get_cpu(0)), + "cntfrq", &error_abort); - qdev_prop_set_uint64(dev, "clock-frequency", cntfrq); + qdev_prop_set_uint64(dev, "clock-frequency", cntfrq); + } } else if (object_dynamic_cast(OBJECT(dev), TYPE_ARM_SMMUV3)) { if (vms->legacy_smmuv3_present || vms->iommu == VIRT_IOMMU_VIRTIO) { error_setg(errp, "virt machine already has %s set. " @@ -3911,7 +3913,9 @@ static void virt_machine_device_plug_cb(HotplugHandler *hotplug_dev, sysbus_mmio_map(s, 1, cbase); sysbus_connect_irq(s, 0, qdev_get_gpio_in(vms->gic, irq)); - create_gwdt_dt_bindings(vms); + if (!object_property_get_bool(OBJECT(dev), "wdat", &error_abort)) { + create_gwdt_dt_bindings(vms); + } } if (vms->platform_bus_dev) { From e9b2acec1de98cb9a71d85828c469757136a2691 Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:51 +0200 Subject: [PATCH 34/44] tests: acpi: arm/virt: whitelist new WDAT table Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-9-imammedo@redhat.com> --- tests/data/acpi/aarch64/virt/WDAT.wdat | 0 tests/qtest/bios-tables-test-allowed-diff.h | 1 + 2 files changed, 1 insertion(+) create mode 100644 tests/data/acpi/aarch64/virt/WDAT.wdat diff --git a/tests/data/acpi/aarch64/virt/WDAT.wdat b/tests/data/acpi/aarch64/virt/WDAT.wdat new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/qtest/bios-tables-test-allowed-diff.h b/tests/qtest/bios-tables-test-allowed-diff.h index dfb8523c8b..135186b40b 100644 --- a/tests/qtest/bios-tables-test-allowed-diff.h +++ b/tests/qtest/bios-tables-test-allowed-diff.h @@ -1 +1,2 @@ /* List of comma-separated changed AML files to ignore */ +"tests/data/acpi/aarch64/virt/WDAT.wdat", From a9618086355030ed0629d960659960adc27b255d Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:52 +0200 Subject: [PATCH 35/44] tests: acpi: arm/virt: add WDAT table test case Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-10-imammedo@redhat.com> --- tests/qtest/bios-tables-test.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/qtest/bios-tables-test.c b/tests/qtest/bios-tables-test.c index 45abd8bd8c..cee3dff31f 100644 --- a/tests/qtest/bios-tables-test.c +++ b/tests/qtest/bios-tables-test.c @@ -2295,6 +2295,25 @@ static void test_acpi_aarch64_virt_tcg_msi_gicv2m(void) free_test_data(&data); } +static void test_acpi_aarch64_virt_tcg_wdat(void) +{ + test_data data = { + .machine = "virt", + .arch = "aarch64", + .variant = ".wdat", + .tcg_only = true, + .uefi_fl1 = "pc-bios/edk2-aarch64-code.fd", + .uefi_fl2 = "pc-bios/edk2-arm-vars.fd", + .cd = "tests/data/uefi-boot-images/bios-tables-test.aarch64.iso.qcow2", + .ram_start = 0x40000000ULL, + .scan_len = 128ULL * MiB, + }; + + test_acpi_one("-cpu cortex-a57 " + "-device sbsa-gwdt,wdat=on", &data); + free_test_data(&data); +} + static void test_acpi_q35_viot(void) { test_data data = { @@ -2914,6 +2933,8 @@ int main(int argc, char *argv[]) qtest_add_func("acpi/virt/smmuv3-dev", test_acpi_aarch64_virt_smmuv3_dev); } + qtest_add_func("acpi/virt/acpi-watchdog", + test_acpi_aarch64_virt_tcg_wdat); } } else if (strcmp(arch, "riscv64") == 0) { if (has_tcg && qtest_has_device("virtio-blk-pci")) { From e84210642ce5b5f8eb528a5b6d86e29445f701de Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:53 +0200 Subject: [PATCH 36/44] tests: acpi: arm/virt: update expected WDAT blob replace blank table with a new one: [000h 0000 004h] Signature : "WDAT" [Watchdog Action Table] [004h 0004 004h] Table Length : 00000104 [008h 0008 001h] Revision : 01 [009h 0009 001h] Checksum : 3B [00Ah 0010 006h] Oem ID : "BOCHS " [010h 0016 008h] Oem Table ID : "BXPC " [018h 0024 004h] Oem Revision : 00000001 [01Ch 0028 004h] Asl Compiler ID : "BXPC" [020h 0032 004h] Asl Compiler Revision : 00000001 [024h 0036 004h] Header Length : 00000020 [028h 0040 002h] PCI Segment : 00FF [02Ah 0042 001h] PCI Bus : FF [02Bh 0043 001h] PCI Device : FF [02Ch 0044 001h] PCI Function : FF [02Dh 0045 003h] Reserved : 000000 [030h 0048 004h] Timer Period : 00000001 [034h 0052 004h] Max Count : 000927C0 [038h 0056 004h] Min Count : 00001388 [03Ch 0060 001h] Flags (decoded below) : 81 Enabled : 1 Stopped When Asleep : 1 [03Dh 0061 003h] Reserved : 000000 [040h 0064 004h] Watchdog Entry Count : 00000008 [044h 0068 001h] Watchdog Action : 08 [045h 0069 001h] Instruction : 00 [046h 0070 002h] Reserved : 0000 [048h 0072 00Ch] Register Region : [Generic Address Structure] [048h 0072 001h] Space ID : 00 [SystemMemory] [049h 0073 001h] Bit Width : 20 [04Ah 0074 001h] Bit Offset : 00 [04Bh 0075 001h] Encoded Access Width : 03 [DWord Access:32] [04Ch 0076 008h] Address : 000000000C001000 [054h 0084 004h] Value : 00000001 [058h 0088 004h] Register Mask : 00000001 [05Ch 0092 001h] Watchdog Action : 01 [05Dh 0093 001h] Instruction : 02 [05Eh 0094 002h] Reserved : 0000 [060h 0096 00Ch] Register Region : [Generic Address Structure] [060h 0096 001h] Space ID : 00 [SystemMemory] [061h 0097 001h] Bit Width : 20 [062h 0098 001h] Bit Offset : 00 [063h 0099 001h] Encoded Access Width : 03 [DWord Access:32] [064h 0100 008h] Address : 000000000C000000 [06Ch 0108 004h] Value : 00000001 [070h 0112 004h] Register Mask : 00000007 [074h 0116 001h] Watchdog Action : 06 [075h 0117 001h] Instruction : 03 [076h 0118 002h] Reserved : 0000 [078h 0120 00Ch] Register Region : [Generic Address Structure] [078h 0120 001h] Space ID : 00 [SystemMemory] [079h 0121 001h] Bit Width : 20 [07Ah 0122 001h] Bit Offset : 00 [07Bh 0123 001h] Encoded Access Width : 03 [DWord Access:32] [07Ch 0124 008h] Address : 000000000C001008 [084h 0132 004h] Value : 00000000 [088h 0136 004h] Register Mask : FFFFFFFF [08Ch 0140 001h] Watchdog Action : 09 [08Dh 0141 001h] Instruction : 82 [08Eh 0142 002h] Reserved : 0000 [090h 0144 00Ch] Register Region : [Generic Address Structure] [090h 0144 001h] Space ID : 00 [SystemMemory] [091h 0145 001h] Bit Width : 20 [092h 0146 001h] Bit Offset : 00 [093h 0147 001h] Encoded Access Width : 03 [DWord Access:32] [094h 0148 008h] Address : 000000000C001000 [09Ch 0156 004h] Value : 00000001 [0A0h 0160 004h] Register Mask : 00000001 [0A4h 0164 001h] Watchdog Action : 0A [0A5h 0165 001h] Instruction : 00 [0A6h 0166 002h] Reserved : 0000 [0A8h 0168 00Ch] Register Region : [Generic Address Structure] [0A8h 0168 001h] Space ID : 00 [SystemMemory] [0A9h 0169 001h] Bit Width : 20 [0AAh 0170 001h] Bit Offset : 00 [0ABh 0171 001h] Encoded Access Width : 03 [DWord Access:32] [0ACh 0172 008h] Address : 000000000C001000 [0B4h 0180 004h] Value : 00000000 [0B8h 0184 004h] Register Mask : 00000001 [0BCh 0188 001h] Watchdog Action : 0B [0BDh 0189 001h] Instruction : 82 [0BEh 0190 002h] Reserved : 0000 [0C0h 0192 00Ch] Register Region : [Generic Address Structure] [0C0h 0192 001h] Space ID : 00 [SystemMemory] [0C1h 0193 001h] Bit Width : 20 [0C2h 0194 001h] Bit Offset : 00 [0C3h 0195 001h] Encoded Access Width : 03 [DWord Access:32] [0C4h 0196 008h] Address : 000000000C001000 [0CCh 0204 004h] Value : 00000000 [0D0h 0208 004h] Register Mask : 00000001 [0D4h 0212 001h] Watchdog Action : 20 [0D5h 0213 001h] Instruction : 00 [0D6h 0214 002h] Reserved : 0000 [0D8h 0216 00Ch] Register Region : [Generic Address Structure] [0D8h 0216 001h] Space ID : 00 [SystemMemory] [0D9h 0217 001h] Bit Width : 20 [0DAh 0218 001h] Bit Offset : 00 [0DBh 0219 001h] Encoded Access Width : 03 [DWord Access:32] [0DCh 0220 008h] Address : 000000000C001000 [0E4h 0228 004h] Value : 00000004 [0E8h 0232 004h] Register Mask : 00000004 [0ECh 0236 001h] Watchdog Action : 21 [0EDh 0237 001h] Instruction : 82 [0EEh 0238 002h] Reserved : 0000 [0F0h 0240 00Ch] Register Region : [Generic Address Structure] [0F0h 0240 001h] Space ID : 00 [SystemMemory] [0F1h 0241 001h] Bit Width : 20 [0F2h 0242 001h] Bit Offset : 00 [0F3h 0243 001h] Encoded Access Width : 03 [DWord Access:32] [0F4h 0244 008h] Address : 000000000C000000 [0FCh 0252 004h] Value : 00000004 [100h 0256 004h] Register Mask : 00000004 Raw Table Data: Length 260 (0x104) Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-11-imammedo@redhat.com> --- tests/data/acpi/aarch64/virt/WDAT.wdat | Bin 0 -> 260 bytes tests/qtest/bios-tables-test-allowed-diff.h | 1 - 2 files changed, 1 deletion(-) diff --git a/tests/data/acpi/aarch64/virt/WDAT.wdat b/tests/data/acpi/aarch64/virt/WDAT.wdat index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d5adacf9114637d8803d08906ff1e29ac61f5fe0 100644 GIT binary patch literal 260 zcmWG{aSUN$WME*_bnZIp5t4ck7o?sWO+8E;WIwVwEDQ_` kAk4-LF`ol!FG&6W|Ns9pGC{-{7*O4#2s0ll4l)CT0T&h&0RR91 literal 0 HcmV?d00001 diff --git a/tests/qtest/bios-tables-test-allowed-diff.h b/tests/qtest/bios-tables-test-allowed-diff.h index 135186b40b..dfb8523c8b 100644 --- a/tests/qtest/bios-tables-test-allowed-diff.h +++ b/tests/qtest/bios-tables-test-allowed-diff.h @@ -1,2 +1 @@ /* List of comma-separated changed AML files to ignore */ -"tests/data/acpi/aarch64/virt/WDAT.wdat", From 01ba94bb27b003d9421dec125137c5701d261c07 Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:54 +0200 Subject: [PATCH 37/44] tests: acpi: arm/virt: whitelist GTDT table Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-12-imammedo@redhat.com> --- tests/qtest/bios-tables-test-allowed-diff.h | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/qtest/bios-tables-test-allowed-diff.h b/tests/qtest/bios-tables-test-allowed-diff.h index dfb8523c8b..3e9be28cc4 100644 --- a/tests/qtest/bios-tables-test-allowed-diff.h +++ b/tests/qtest/bios-tables-test-allowed-diff.h @@ -1 +1,2 @@ /* List of comma-separated changed AML files to ignore */ +"tests/data/acpi/aarch64/virt/GTDT", From c2a283fe9989a1839e931daf664f192339f5ce7e Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:55 +0200 Subject: [PATCH 38/44] tests: acpi: arm/virt: add GTDT watchdog table test case Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-13-imammedo@redhat.com> --- tests/qtest/bios-tables-test.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/qtest/bios-tables-test.c b/tests/qtest/bios-tables-test.c index cee3dff31f..5cc526510a 100644 --- a/tests/qtest/bios-tables-test.c +++ b/tests/qtest/bios-tables-test.c @@ -2314,6 +2314,24 @@ static void test_acpi_aarch64_virt_tcg_wdat(void) free_test_data(&data); } +static void test_acpi_aarch64_virt_tcg_gtdt_wd(void) +{ + test_data data = { + .machine = "virt", + .arch = "aarch64", + .variant = ".gwdt", + .tcg_only = true, + .uefi_fl1 = "pc-bios/edk2-aarch64-code.fd", + .uefi_fl2 = "pc-bios/edk2-arm-vars.fd", + .cd = "tests/data/uefi-boot-images/bios-tables-test.aarch64.iso.qcow2", + .ram_start = 0x40000000ULL, + .scan_len = 128ULL * MiB, + }; + + test_acpi_one("-cpu cortex-a57 -device sbsa-gwdt", &data); + free_test_data(&data); +} + static void test_acpi_q35_viot(void) { test_data data = { @@ -2935,6 +2953,8 @@ int main(int argc, char *argv[]) } qtest_add_func("acpi/virt/acpi-watchdog", test_acpi_aarch64_virt_tcg_wdat); + qtest_add_func("acpi/virt/gwdt-watchdog", + test_acpi_aarch64_virt_tcg_gtdt_wd); } } else if (strcmp(arch, "riscv64") == 0) { if (has_tcg && qtest_has_device("virtio-blk-pci")) { From 7205b8ffb6566465af0628c2fd5258e26dca6b41 Mon Sep 17 00:00:00 2001 From: Igor Mammedov Date: Thu, 2 Jul 2026 16:58:56 +0200 Subject: [PATCH 39/44] tests: acpi: arm/virt: update expected GTDT blob Expected diff from base GTDT is an addition of watchdog timer block: [000h 0000 004h] Signature : "GTDT" [Generic Timer Description Table] -[004h 0004 004h] Table Length : 00000068 +[004h 0004 004h] Table Length : 00000084 [008h 0008 001h] Revision : 03 -[009h 0009 001h] Checksum : 93 +[009h 0009 001h] Checksum : 39 [00Ah 0010 006h] Oem ID : "BOCHS " [010h 0016 008h] Oem Table ID : "BXPC " [018h 0024 004h] Oem Revision : 00000001 @@ -48,17 +48,30 @@ Always On : 0 [050h 0080 008h] Counter Read Block Address : FFFFFFFFFFFFFFFF -[058h 0088 004h] Platform Timer Count : 00000000 -[05Ch 0092 004h] Platform Timer Offset : 00000000 +[058h 0088 004h] Platform Timer Count : 00000001 +[05Ch 0092 004h] Platform Timer Offset : 00000068 [060h 0096 004h] Virtual EL2 Timer GSIV : 00000000 [064h 0100 004h] Virtual EL2 Timer Flags : 00000000 -Raw Table Data: Length 104 (0x68) +[068h 0104 001h] Subtable Type : 01 [Generic Watchdog Timer] +[069h 0105 002h] Length : 001C +[06Bh 0107 001h] Reserved : 00 +[06Ch 0108 008h] Refresh Frame Address : 000000000C000000 +[074h 0116 008h] Control Frame Address : 000000000C001000 +[07Ch 0124 004h] Timer Interrupt : 00000090 +[080h 0128 004h] Timer Flags (decoded below) : 00000000 + Trigger Mode : 0 + Polarity : 0 + Security : 0 + +Raw Table Data: Length 132 (0x84) Signed-off-by: Igor Mammedov Reviewed-by: Eric Auger Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260702145856.1539572-14-imammedo@redhat.com> --- tests/data/acpi/aarch64/virt/GTDT.gwdt | Bin 0 -> 132 bytes tests/qtest/bios-tables-test-allowed-diff.h | 1 - 2 files changed, 1 deletion(-) create mode 100644 tests/data/acpi/aarch64/virt/GTDT.gwdt diff --git a/tests/data/acpi/aarch64/virt/GTDT.gwdt b/tests/data/acpi/aarch64/virt/GTDT.gwdt new file mode 100644 index 0000000000000000000000000000000000000000..44e6f2b18d2848f595a8c69bdb54a59e1becaffd GIT binary patch literal 132 zcmZ<{aS3T*U|?XL>E!S15v<@85#X$#prF9Wz`y`vgXsTIz`(%3APXWG7#QRj7#LU> k7#O6Xd?_dmRr(*KF9RwDq8MdBBm+N~6oAlLU=9NV0JuRI0RR91 literal 0 HcmV?d00001 diff --git a/tests/qtest/bios-tables-test-allowed-diff.h b/tests/qtest/bios-tables-test-allowed-diff.h index 3e9be28cc4..dfb8523c8b 100644 --- a/tests/qtest/bios-tables-test-allowed-diff.h +++ b/tests/qtest/bios-tables-test-allowed-diff.h @@ -1,2 +1 @@ /* List of comma-separated changed AML files to ignore */ -"tests/data/acpi/aarch64/virt/GTDT", From a63a6cd694cffa772e97bb37b0a304c8c7129397 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Fri, 22 May 2026 22:11:09 -0400 Subject: [PATCH 40/44] vhost-user: Guarantee that memory regions do not overlap Otherwise there would be an ambiguity problem. Suppose that: 1. There is a region from [0x40000, 0x50000) with mmap offset 0x500000. 2. There is a region from [0x48000, 0x58000) with mmap offset 0xA00000. A request has address 0x44000. Which mmap offset should be used? This problem appears with both guest and user addresses. Signed-off-by: Demi Marie Obenour Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260522-vhost-user-dev-v1-1-b31646cf19b8@gmail.com> --- docs/interop/vhost-user.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/interop/vhost-user.rst b/docs/interop/vhost-user.rst index a704f3cb53..ae8c7ed995 100644 --- a/docs/interop/vhost-user.rst +++ b/docs/interop/vhost-user.rst @@ -214,6 +214,18 @@ fields at the end. :domid: a 32-bit Xen hypervisor specific domain id. +For all memory regions active at a given time: + +- ``[guest address, guest address + size)`` of one memory region never overlaps + the ``[guest address, guest address + size)`` of another memory region. + +- ``[user address, user address + size)`` of one memory region never overlaps + the ``[user address, user address + size)`` of another memory region. + +Violating any of these is a bug in the front-end. This ensures that a guest +address or user address always refers to at most one location in memory. +The front-end must remove a region before it can add an overlapping one. + Single memory region description ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -745,6 +757,15 @@ The front-end sends a list of vhost memory regions to the back-end using the ``VHOST_USER_SET_MEM_TABLE`` message. Each region has two base addresses: a guest address and a user address. +Memory regions can be added via the ``VHOST_USER_ADD_MEM_REG`` message. They +can be removed via the ``VHOST_USER_REM_MEM_REG`` message. These messages can +only be used if the ``VHOST_USER_PROTOCOL_F_CONFIGURE_MEM_SLOTS`` protocol +feature has been successfully negotiated. + +Guest addresses are physical addresses in the guest. User addresses are +arbitrary opaque values, though they typically refer to userspace addresses in +the client process. + Messages contain guest addresses and/or user addresses to reference locations within the shared memory. The mapping of these addresses works as follows. From a2134e938a0c43d2f06f04689c59de4052d0b986 Mon Sep 17 00:00:00 2001 From: helei Date: Tue, 16 Jun 2026 20:32:51 +0800 Subject: [PATCH 41/44] hw/virtio-crypto: enforce max akcipher key length enforce the global max_size boundary for akcipher session creation Signed-off-by: helei Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260616123251.26446-2-lhestz@163.com> --- hw/virtio/virtio-crypto.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hw/virtio/virtio-crypto.c b/hw/virtio/virtio-crypto.c index 6fceb39681..79e2acb56c 100644 --- a/hw/virtio/virtio-crypto.c +++ b/hw/virtio/virtio-crypto.c @@ -216,6 +216,12 @@ virtio_crypto_create_asym_session(VirtIOCrypto *vcrypto, return -VIRTIO_CRYPTO_NOTSUPP; } + if (unlikely(keylen > vcrypto->conf.max_size)) { + error_report("virtio-crypto length of akcipher key is too large: %u", + keylen); + return -VIRTIO_CRYPTO_ERR; + } + if (keylen) { asym_info->key = g_malloc(keylen); if (iov_to_buf(iov, out_num, 0, asym_info->key, keylen) != keylen) { From cecd7a11df7b7fdfccc60fe50cc78e75635db886 Mon Sep 17 00:00:00 2001 From: GuoHan Zhao Date: Tue, 30 Jun 2026 15:27:28 +0800 Subject: [PATCH 42/44] vhost-user-scmi: free vhost virtqueue array on cleanup vhost-user-scmi allocates vhost_dev.vqs during realize, but the cleanup helper frees scmi->vhost_dev.vqs after vhost_dev_cleanup() has cleared struct vhost_dev. This turns the free into g_free(NULL), leaking the allocated vhost virtqueue array. Keep a copy of the vhost_dev.vqs pointer across vhost_dev_cleanup() and free that saved pointer from the common cleanup helper. Fixes: a5dab090e142 (hw/virtio: Add boilerplate for vhost-user-scmi device) Signed-off-by: GuoHan Zhao Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260630072728.3025097-1-zhaoguohan@kylinos.cn> --- hw/virtio/vhost-user-scmi.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/hw/virtio/vhost-user-scmi.c b/hw/virtio/vhost-user-scmi.c index 9470f68c1f..02dc088ea9 100644 --- a/hw/virtio/vhost-user-scmi.c +++ b/hw/virtio/vhost-user-scmi.c @@ -220,11 +220,12 @@ static void vu_scmi_event(void *opaque, QEMUChrEvent event) } } -static void do_vhost_user_cleanup(VirtIODevice *vdev, VHostUserSCMI *scmi) +static void do_vhost_user_cleanup(VirtIODevice *vdev, VHostUserSCMI *scmi, + struct vhost_virtqueue *vhost_vqs) { virtio_delete_queue(scmi->cmd_vq); virtio_delete_queue(scmi->event_vq); - g_free(scmi->vhost_dev.vqs); + g_free(vhost_vqs); virtio_cleanup(vdev); vhost_user_cleanup(&scmi->vhost_user); } @@ -233,6 +234,7 @@ static void vu_scmi_device_realize(DeviceState *dev, Error **errp) { VirtIODevice *vdev = VIRTIO_DEVICE(dev); VHostUserSCMI *scmi = VHOST_USER_SCMI(dev); + struct vhost_virtqueue *vhost_vqs; int ret; if (!scmi->chardev.chr) { @@ -252,13 +254,14 @@ static void vu_scmi_device_realize(DeviceState *dev, Error **errp) scmi->event_vq = virtio_add_queue(vdev, 256, vu_scmi_handle_output); scmi->vhost_dev.nvqs = 2; scmi->vhost_dev.vqs = g_new0(struct vhost_virtqueue, scmi->vhost_dev.nvqs); + vhost_vqs = scmi->vhost_dev.vqs; ret = vhost_dev_init(&scmi->vhost_dev, &scmi->vhost_user, VHOST_BACKEND_TYPE_USER, 0, errp); if (ret < 0) { error_setg_errno(errp, -ret, "vhost-user-scmi: vhost_dev_init() failed"); - do_vhost_user_cleanup(vdev, scmi); + do_vhost_user_cleanup(vdev, scmi, vhost_vqs); return; } @@ -270,10 +273,11 @@ static void vu_scmi_device_unrealize(DeviceState *dev) { VirtIODevice *vdev = VIRTIO_DEVICE(dev); VHostUserSCMI *scmi = VHOST_USER_SCMI(dev); + struct vhost_virtqueue *vhost_vqs = scmi->vhost_dev.vqs; vu_scmi_set_status(vdev, 0); vhost_dev_cleanup(&scmi->vhost_dev); - do_vhost_user_cleanup(vdev, scmi); + do_vhost_user_cleanup(vdev, scmi, vhost_vqs); } static const VMStateDescription vu_scmi_vmstate = { From 5d4d0f552bcbe784d74c405d205749384be16822 Mon Sep 17 00:00:00 2001 From: Sergei Heifetz Date: Thu, 2 Apr 2026 04:05:48 +0500 Subject: [PATCH 43/44] vhost-user-blk: add seg-max-adjust flag The virtio specification is not completely clear about seg_max and its relationship with queue_size. Some drivers (for example, the modern Linux kernel driver) rely on seg_max to set the maximum number of segments used in a request. If seg_max is set larger than queue_size, such a driver might overwhelm a virtqueue by trying to send more segments than it can handle. As a result, it either hangs or faults. One might argue that it is the vhost-user server's responsibility to set a valid seg_max value. However, due to the issue described in the previous paragraph, this value should generally depend on queue_size. That's why it may be necessary to control it on QEMU's side. This patch adds the seg-max-adjust flag. A flag with the same name already exists for virtio-blk (and it exists to solve the same problem, except that here we have a vhost-user server to consult). If seg-max-adjust is set, the final seg_max is the minimum of the value provided by the vhost-user server and (queue_size - 2). It is not enabled by default. Signed-off-by: Sergei Heifetz Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260401230548.136541-1-heifetz@yandex-team.com> --- hw/block/vhost-user-blk.c | 12 ++++++++++++ include/hw/virtio/vhost-user-blk.h | 1 + 2 files changed, 13 insertions(+) diff --git a/hw/block/vhost-user-blk.c b/hw/block/vhost-user-blk.c index b2e46eb3f9..2e5b3ae1b1 100644 --- a/hw/block/vhost-user-blk.c +++ b/hw/block/vhost-user-blk.c @@ -66,6 +66,12 @@ static void vhost_user_blk_update_config(VirtIODevice *vdev, uint8_t *config) /* Our num_queues overrides the device backend */ virtio_stw_p(vdev, &s->blkcfg.num_queues, s->num_queues); + if (s->seg_max_adjust) { + uint32_t seg_max = MIN(s->blkcfg.seg_max, s->queue_size - 2); + + virtio_stl_p(vdev, &s->blkcfg.seg_max, seg_max); + } + memcpy(config, &s->blkcfg, vdev->config_len); } @@ -489,6 +495,10 @@ static void vhost_user_blk_device_realize(DeviceState *dev, Error **errp) error_setg(errp, "queue size must be non-zero"); return; } + if (s->queue_size < 4 && s->seg_max_adjust) { + error_setg(errp, "queue size must be >= 4 when seg-max-adjust is set"); + return; + } if (s->queue_size > VIRTQUEUE_MAX_SIZE) { error_setg(errp, "queue size must not exceed %d", VIRTQUEUE_MAX_SIZE); @@ -624,6 +634,8 @@ static const Property vhost_user_blk_properties[] = { DEFINE_PROP_UINT16("num-queues", VHostUserBlk, num_queues, VHOST_USER_BLK_AUTO_NUM_QUEUES), DEFINE_PROP_UINT32("queue-size", VHostUserBlk, queue_size, 128), + DEFINE_PROP_BOOL("seg-max-adjust", VHostUserBlk, seg_max_adjust, + false), DEFINE_PROP_BIT64("config-wce", VHostUserBlk, parent_obj.host_features, VIRTIO_BLK_F_CONFIG_WCE, true), DEFINE_PROP_BIT64("discard", VHostUserBlk, parent_obj.host_features, diff --git a/include/hw/virtio/vhost-user-blk.h b/include/hw/virtio/vhost-user-blk.h index 1e41a2bcdf..dee848cfd8 100644 --- a/include/hw/virtio/vhost-user-blk.h +++ b/include/hw/virtio/vhost-user-blk.h @@ -34,6 +34,7 @@ struct VHostUserBlk { struct virtio_blk_config blkcfg; uint16_t num_queues; uint32_t queue_size; + bool seg_max_adjust; struct vhost_dev dev; struct vhost_inflight *inflight; VhostUserState vhost_user; From 32a90850e1e4222091df07b4b46d5f46a8b90d24 Mon Sep 17 00:00:00 2001 From: Junjie Cao Date: Tue, 24 Mar 2026 14:01:00 +0800 Subject: [PATCH 44/44] virtio-net: validate RSS indirections_len in post_load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit virtio_net_handle_rss() enforces that indirections_len is a non-zero power of two no larger than VIRTIO_NET_RSS_MAX_TABLE_LEN, but virtio_net_rss_post_load() applies none of these checks to values restored from the migration stream. A corrupted save file or crafted migration stream can set indirections_len to 0. Even if it also clears redirect, virtio_load() calls set_features_nocheck() after the device vmstate (including the RSS subsection and its post_load) has already been loaded, re-deriving redirect from the negotiated guest features. When VIRTIO_NET_F_RSS was negotiated, redirect is set back to true regardless of the migration stream value. The receive path then computes hash & (indirections_len - 1) /* wraps to 0xFFFFFFFF via int promotion */ and uses the result to index into indirections_table, which was not allocated by the VMState loader when the element count is zero (see vmstate_handle_alloc()), resulting in a NULL pointer dereference that crashes QEMU: #0 virtio_net_process_rss ../hw/net/virtio-net.c:1901 #1 virtio_net_receive_rcu ../hw/net/virtio-net.c:1921 #2 virtio_net_do_receive ../hw/net/virtio-net.c:2061 #3 nc_sendv_compat ../net/net.c:823 #4 qemu_deliver_packet_iov ../net/net.c:870 The RSS subsection is only loaded when rss_data.enabled is true (via virtio_net_rss_needed()), and the command path always produces indirections_len in {1, 2, 4, …, 128}, so an unconditional check cannot reject a legitimate migration stream. Factor the validation into virtio_net_rss_indirections_len_valid() and call it from both virtio_net_handle_rss() and virtio_net_rss_post_load(). Fixes: e41b711485e5 ("virtio-net: add migration support for RSS and hash report") Cc: qemu-stable@nongnu.org Signed-off-by: Junjie Cao Reviewed-by: Michael S. Tsirkin Signed-off-by: Michael S. Tsirkin Message-ID: <20260324060100.1997-1-junjie.cao@intel.com> --- hw/net/virtio-net.c | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/hw/net/virtio-net.c b/hw/net/virtio-net.c index 2a5d642a64..f0e3beb290 100644 --- a/hw/net/virtio-net.c +++ b/hw/net/virtio-net.c @@ -1374,6 +1374,11 @@ static void virtio_net_unload_ebpf(VirtIONet *n) ebpf_rss_unload(&n->ebpf_rss); } +static bool virtio_net_rss_indirections_len_valid(uint16_t len) +{ + return is_power_of_2(len) && len <= VIRTIO_NET_RSS_MAX_TABLE_LEN; +} + static uint16_t virtio_net_handle_rss(VirtIONet *n, struct iovec *iov, unsigned int iov_cnt, @@ -1411,14 +1416,9 @@ static uint16_t virtio_net_handle_rss(VirtIONet *n, if (!do_rss) { n->rss_data.indirections_len = 0; } - if (n->rss_data.indirections_len >= VIRTIO_NET_RSS_MAX_TABLE_LEN) { - err_msg = "Too large indirection table"; - err_value = n->rss_data.indirections_len; - goto error; - } n->rss_data.indirections_len++; - if (!is_power_of_2(n->rss_data.indirections_len)) { - err_msg = "Invalid size of indirection table"; + if (!virtio_net_rss_indirections_len_valid(n->rss_data.indirections_len)) { + err_msg = "Invalid indirection table length"; err_value = n->rss_data.indirections_len; goto error; } @@ -3427,6 +3427,13 @@ static int virtio_net_rss_post_load(void *opaque, int version_id) n->rss_data.supported_hash_types = VIRTIO_NET_RSS_SUPPORTED_HASHES; } + if (!virtio_net_rss_indirections_len_valid(n->rss_data.indirections_len)) { + error_report("virtio-net: saved image has invalid RSS " + "indirections_len: %u", + n->rss_data.indirections_len); + return -EINVAL; + } + return 0; }