On Tue, Jul 07, 2026 at 03:28:40PM -0700, Oliver Upton wrote:
> I've addressed all of this locally and pushed to my tree [*]. Untested,
> as always :) Would you be able to give it a spin?

Thank you so much for the review and code suggestions! I integrated
them and created v3 (with one extra change of wiring
KVM_CAP_ARM_PMU_V3_STRICT into kvm_vm_ioctl_check_extension() so
userspace can detect it). I tested the patches with kernel 7.2 and
patched QEMU 9.2.3, and the behaviors were as expected.

I'll send out the v3 patches shortly.

> Also, do you have VMM patches for using the new feature flag?

Below is the patch against QEMU 9.2.3 that I used for testing. The tests
were performed using different combinations of the four flags (host
PMMIR_EL1.SLOTS = 0xa):

- pmu-strict=on,pmu-set=on,pmmir-force=off,pmcr-n-force=off:
    new happy path where guest read the correct SLOTS (0xa).

- pmu-strict=off,pmu-set=off,pmmir-force=off,pmcr-n-force=off:
    old happy path where guest read SLOTS as 0x0.

- pmu-strict=off,pmu-set=on,pmmir-force=off,pmcr-n-force=off:
    non-strict with a PMU set, guest read SLOTS as 0x0 (SLOTS stays
    gated on the flag).

- pmu-strict=on,pmu-set=off,pmmir-force=off,pmcr-n-force=off:
    strict with no PMU, so KVM_ARM_VCPU_PMU_V3_INIT failed with -ENXIO
    and the guest did not boot.

- pmu-strict=on,pmu-set=on,pmmir-force=0,pmcr-n-force=off:
    strict with PMMIR forced to 0 (backward compatibility), where the
    KVM_SET_ONE_REG succeeded and guest read SLOTS as 0x0.

- pmu-strict=on,pmu-set=on,pmmir-force=7,pmcr-n-force=off:
    strict with PMMIR forced to a mismatching value, so KVM_SET_ONE_REG
    rejected with -EINVAL and guest still read SLOTS as 0xa.

- pmu-strict=off,pmu-set=on,pmmir-force=0,pmcr-n-force=off:
    non-strict with PMMIR forced to 0, KVM_SET_ONE_REG returned no error
    and guest read SLOTS as 0x0.

- pmu-strict=off,pmu-set=off,pmmir-force=10,pmcr-n-force=off:
    non-strict with PMMIR forced to a non-zero value, KVM_SET_ONE_REG
    rejected with -EINVAL and guest read SLOTS as 0x0.

- pmu-strict=off,pmu-set=off,pmmir-force=off,pmcr-n-force=3:
    non-strict with a PMCR_EL0.N write (original behavior), so the write
    was honored and N read back as 3.

- pmu-strict=on,pmu-set=on,pmmir-force=off,pmcr-n-force=3:
    strict with a PMCR_EL0.N write, so the write was ignored and N read
    back 6 (the PMU maximum).

---
Local test patch for the kernel "strict PMUv3 UAPI / PMMIR_EL1.SLOTS"
series.

Adds four 'virt' machine properties (all default off):

  pmu-strict=on   request KVM_ARM_VCPU_PMU_V3_STRICT at VCPU_INIT
  pmu-set=off     skip KVM_ARM_VCPU_PMU_V3_SET_PMU (exercise the no-PMU path)
  pmmir-force=N   write PMMIR_EL1=N via KVM_SET_ONE_REG (0 = set_pmmir reset)
  pmcr-n-force=N  write then read PMCR_EL0.N; strict ignores the write

SET_PMU is issued before PMU_V3_INIT (the kernel rejects it afterwards) and
is followed by a cpreg-cache refresh so the stale PMMIR_EL1=0 cached at init
is not written back.

Signed-off-by: Congkai Tan <[email protected]>
---
--- a/target/arm/kvm.c  2025-03-26 19:37:01.000000000 +0000
+++ b/target/arm/kvm.c  2026-07-10 04:05:16.082093995 +0000
@@ -18,11 +18,16 @@
 #include "qemu/timer.h"
 #include "qemu/error-report.h"
 #include "qemu/main-loop.h"
+#include "qemu/cutils.h"
 #include "qom/object.h"
 #include "qapi/error.h"
 #include "sysemu/sysemu.h"
 #include "sysemu/runstate.h"
 #include "sysemu/kvm.h"
+
+#ifndef KVM_ARM_VCPU_PMU_V3_STRICT
+#define KVM_ARM_VCPU_PMU_V3_STRICT 9
+#endif
 #include "sysemu/kvm_int.h"
 #include "kvm_arm.h"
 #include "cpu.h"
@@ -1728,6 +1733,82 @@
     }
 }
 
+/* Test knob: 'pmu-strict' machine property (request 
KVM_ARM_VCPU_PMU_V3_STRICT). */
+bool kvm_arm_pmu_strict_enabled(void)
+{
+    Object *m = qdev_get_machine();
+
+    if (m && object_property_find(m, "pmu-strict")) {
+        return object_property_get_bool(m, "pmu-strict", NULL);
+    }
+    return false;
+}
+
+/* Test knob: 'pmu-set' machine property (issue SET_PMU; off = no-PMU path). */
+static bool kvm_arm_pmu_set_enabled(void)
+{
+    Object *m = qdev_get_machine();
+
+    if (m && object_property_find(m, "pmu-set")) {
+        return object_property_get_bool(m, "pmu-set", NULL);
+    }
+    return false;
+}
+
+/* Read the host PMUv3 perf "type" id from sysfs; -1 on failure. */
+static int kvm_arm_pmu_read_host_pmu_type(void)
+{
+    const char *path = "/sys/bus/event_source/devices/armv8_pmuv3_0/type";
+    g_autofree char *contents = NULL;
+    const char *ep;
+    gsize len;
+    int type;
+
+    if (!g_file_get_contents(path, &contents, &len, NULL)) {
+        return -1;
+    }
+    if (qemu_strtoi(contents, &ep, 10, &type) < 0 || (*ep && *ep != '\n')) {
+        return -1;
+    }
+    return type;
+}
+
+/*
+ * Select the host PMU (SET_PMU). Under the strict UAPI this is mandatory and
+ * makes the kernel snapshot PMMIR_EL1.SLOTS. Call after INIT, before 
PMU_V3_INIT.
+ */
+void kvm_arm_pmu_set_pmu(ARMCPU *cpu)
+{
+    int pmu_type;
+    struct kvm_device_attr attr = {
+        .group = KVM_ARM_VCPU_PMU_V3_CTRL,
+        .attr = KVM_ARM_VCPU_PMU_V3_SET_PMU,
+    };
+
+    if (!cpu->has_pmu || !kvm_arm_pmu_set_enabled()) {
+        return;
+    }
+
+    pmu_type = kvm_arm_pmu_read_host_pmu_type();
+    if (pmu_type < 0) {
+        warn_report("PMU: could not read host PMU type; "
+                    "PMMIR_EL1.SLOTS will read 0 in the guest");
+        return;
+    }
+    attr.addr = (intptr_t)&pmu_type;
+
+    if (!kvm_arm_set_device_attr(cpu, &attr, "PMU SET_PMU")) {
+        warn_report("PMU: KVM_ARM_VCPU_PMU_V3_SET_PMU unsupported or failed; "
+                    "PMMIR_EL1.SLOTS will read 0 in the guest");
+        return;
+    }
+
+    /* Refresh the cpreg cache so the stale PMMIR_EL1=0 isn't written back. */
+    if (!write_kvmstate_to_list(cpu)) {
+        warn_report("PMU: failed to refresh cpreg cache after SET_PMU");
+    }
+}
+
 void kvm_arm_pmu_set_irq(ARMCPU *cpu, int irq)
 {
     struct kvm_device_attr attr = {
@@ -1904,6 +1985,10 @@
     }
     if (cpu->has_pmu) {
         cpu->kvm_init_features[0] |= 1 << KVM_ARM_VCPU_PMU_V3;
+        /* Test knob: opt in to the strict PMUv3 UAPI. */
+        if (kvm_arm_pmu_strict_enabled()) {
+            cpu->kvm_init_features[0] |= 1 << KVM_ARM_VCPU_PMU_V3_STRICT;
+        }
     }
     if (cpu_isar_feature(aa64_sve, cpu)) {
         assert(kvm_arm_sve_supported());
--- a/target/arm/kvm_arm.h      2025-03-26 19:37:01.000000000 +0000
+++ b/target/arm/kvm_arm.h      2026-07-10 04:05:25.331964805 +0000
@@ -207,6 +207,10 @@
 
 void kvm_arm_pmu_init(ARMCPU *cpu);
 void kvm_arm_pmu_set_irq(ARMCPU *cpu, int irq);
+/* Test knob: select the host PMU (SET_PMU). Call after INIT, before 
PMU_V3_INIT. */
+void kvm_arm_pmu_set_pmu(ARMCPU *cpu);
+/* Test knob: true if the 'pmu-strict' machine property is set. */
+bool kvm_arm_pmu_strict_enabled(void);
 
 /**
  * kvm_arm_pvtime_init:
@@ -280,6 +284,16 @@
     g_assert_not_reached();
 }
 
+static inline void kvm_arm_pmu_set_pmu(ARMCPU *cpu)
+{
+    g_assert_not_reached();
+}
+
+static inline bool kvm_arm_pmu_strict_enabled(void)
+{
+    return false;
+}
+
 static inline void kvm_arm_pvtime_init(ARMCPU *cpu, uint64_t ipa)
 {
     g_assert_not_reached();
--- a/hw/arm/virt.c     2025-03-26 19:37:00.000000000 +0000
+++ b/hw/arm/virt.c     2026-07-10 04:05:51.051606007 +0000
@@ -53,6 +53,7 @@
 #include "hw/loader.h"
 #include "qapi/error.h"
 #include "qemu/bitops.h"
+#include "qemu/cutils.h"
 #include "qemu/error-report.h"
 #include "qemu/module.h"
 #include "hw/pci-host/gpex.h"
@@ -2070,7 +2071,64 @@
                 if (kvm_irqchip_in_kernel()) {
                     kvm_arm_pmu_set_irq(ARM_CPU(cpu), VIRTUAL_PMU_IRQ);
                 }
+                /* Test knob: SET_PMU before PMU_V3_INIT (see 
kvm_arm_pmu_set_pmu). */
+                kvm_arm_pmu_set_pmu(ARM_CPU(cpu));
                 kvm_arm_pmu_init(ARM_CPU(cpu));
+                /* Test knob: force PMMIR_EL1 (0 = set_pmmir() back-compat 
reset). */
+                if (vms->pmmir_force >= 0) {
+                    uint64_t v = vms->pmmir_force;
+                    int r = kvm_set_one_reg(cpu, ARM64_SYS_REG(3, 0, 9, 14, 6),
+                                            &v);
+                    if (r) {
+                        warn_report("pmmir-force: KVM_SET_ONE_REG(PMMIR_EL1) "
+                                    "returned %d", r);
+                    }
+                    /* Refresh the cpreg cache so the forced value sticks. */
+                    if (!write_kvmstate_to_list(ARM_CPU(cpu))) {
+                        warn_report("pmmir-force: failed to refresh cpreg "
+                                    "cache after KVM_SET_ONE_REG");
+                    }
+                }
+                /*
+                 * Test knob: SET then GET PMCR_EL0.N. strict=on -> write 
ignored
+                 * (N stays at max); strict=off -> read-back matches the write.
+                 */
+                if (vms->pmcr_n_force >= 0) {
+                    uint64_t pmcr = 0;
+                    int r = kvm_get_one_reg(cpu, ARM64_SYS_REG(3, 3, 9, 12, 0),
+                                            &pmcr);
+                    if (r) {
+                        warn_report("pmcr-n-force: KVM_GET_ONE_REG(PMCR_EL0) "
+                                    "returned %d", r);
+                    } else {
+                        uint64_t n = vms->pmcr_n_force;
+
+                        pmcr &= ~(uint64_t)PMCRN_MASK;
+                        pmcr |= (n << PMCRN_SHIFT) & PMCRN_MASK;
+                        r = kvm_set_one_reg(cpu,
+                                            ARM64_SYS_REG(3, 3, 9, 12, 0),
+                                            &pmcr);
+                        if (r) {
+                            warn_report("pmcr-n-force: KVM_SET_ONE_REG("
+                                        "PMCR_EL0) returned %d", r);
+                        }
+                        if (!kvm_get_one_reg(cpu,
+                                             ARM64_SYS_REG(3, 3, 9, 12, 0),
+                                             &pmcr)) {
+                            uint64_t got = (pmcr & PMCRN_MASK)
+                                           >> PMCRN_SHIFT;
+                            info_report("pmcr-n-force: wrote N=%" PRIu64
+                                        ", read back N=%" PRIu64 " 
(strict=%s)",
+                                        n, got,
+                                        vms->pmu_strict ? "on" : "off");
+                        }
+                        /* Keep QEMU's cpreg cache consistent with the kernel. 
*/
+                        if (!write_kvmstate_to_list(ARM_CPU(cpu))) {
+                            warn_report("pmcr-n-force: failed to refresh cpreg 
"
+                                        "cache after KVM_SET_ONE_REG");
+                        }
+                    }
+                }
             }
             if (steal_time) {
                 kvm_arm_pvtime_init(ARM_CPU(cpu), pvtime_reg_base
@@ -2676,6 +2734,88 @@
     vms->mte = value;
 }
 
+static bool virt_get_pmu_strict(Object *obj, Error **errp)
+{
+    VirtMachineState *vms = VIRT_MACHINE(obj);
+
+    return vms->pmu_strict;
+}
+
+static void virt_set_pmu_strict(Object *obj, bool value, Error **errp)
+{
+    VirtMachineState *vms = VIRT_MACHINE(obj);
+
+    vms->pmu_strict = value;
+}
+
+static bool virt_get_pmu_set(Object *obj, Error **errp)
+{
+    VirtMachineState *vms = VIRT_MACHINE(obj);
+
+    return vms->pmu_set;
+}
+
+static void virt_set_pmu_set(Object *obj, bool value, Error **errp)
+{
+    VirtMachineState *vms = VIRT_MACHINE(obj);
+
+    vms->pmu_set = value;
+}
+
+static char *virt_get_pmmir_force(Object *obj, Error **errp)
+{
+    VirtMachineState *vms = VIRT_MACHINE(obj);
+
+    if (vms->pmmir_force < 0) {
+        return g_strdup("off");
+    }
+    return g_strdup_printf("%" PRId64, vms->pmmir_force);
+}
+
+static void virt_set_pmmir_force(Object *obj, const char *value, Error **errp)
+{
+    VirtMachineState *vms = VIRT_MACHINE(obj);
+    uint64_t v;
+
+    if (!strcmp(value, "off")) {
+        vms->pmmir_force = -1;
+        return;
+    }
+    if (qemu_strtou64(value, NULL, 0, &v) < 0) {
+        error_setg(errp, "Invalid pmmir-force value '%s' "
+                   "(expected 'off' or an integer)", value);
+        return;
+    }
+    vms->pmmir_force = (int64_t)v;
+}
+
+static char *virt_get_pmcr_n_force(Object *obj, Error **errp)
+{
+    VirtMachineState *vms = VIRT_MACHINE(obj);
+
+    if (vms->pmcr_n_force < 0) {
+        return g_strdup("off");
+    }
+    return g_strdup_printf("%" PRId64, vms->pmcr_n_force);
+}
+
+static void virt_set_pmcr_n_force(Object *obj, const char *value, Error **errp)
+{
+    VirtMachineState *vms = VIRT_MACHINE(obj);
+    uint64_t v;
+
+    if (!strcmp(value, "off")) {
+        vms->pmcr_n_force = -1;
+        return;
+    }
+    if (qemu_strtou64(value, NULL, 0, &v) < 0) {
+        error_setg(errp, "Invalid pmcr-n-force value '%s' "
+                   "(expected 'off' or an integer)", value);
+        return;
+    }
+    vms->pmcr_n_force = (int64_t)v;
+}
+
 static char *virt_get_gic_version(Object *obj, Error **errp)
 {
     VirtMachineState *vms = VIRT_MACHINE(obj);
@@ -3233,6 +3373,41 @@
                                           "to a KVM guest using ACPI and guest 
external abort exceptions");
 
     object_class_property_add_bool(oc, "mte", virt_get_mte, virt_set_mte);
+
+    object_class_property_add_bool(oc, "pmu-strict", virt_get_pmu_strict,
+                                   virt_set_pmu_strict);
+    object_class_property_set_description(oc, "pmu-strict",
+                                          "Test knob: request the strict "
+                                          "PMUv3 UAPI (KVM_ARM_VCPU_PMU_V3_"
+                                          "STRICT) for each vCPU");
+
+    object_class_property_add_bool(oc, "pmu-set", virt_get_pmu_set,
+                                   virt_set_pmu_set);
+    object_class_property_set_description(oc, "pmu-set",
+                                          "Test knob: issue "
+                                          "KVM_ARM_VCPU_PMU_V3_SET_PMU "
+                                          "(default on); off exercises the "
+                                          "strict no-PMU path");
+
+    object_class_property_add_str(oc, "pmmir-force", virt_get_pmmir_force,
+                                  virt_set_pmmir_force);
+    object_class_property_set_description(oc, "pmmir-force",
+                                          "Test knob: force PMMIR_EL1 to this "
+                                          "value via KVM_SET_ONE_REG after the 
"
+                                          "PMU is selected. 'off' (default) "
+                                          "leaves the kernel snapshot intact; "
+                                          "0 exercises the set_pmmir() "
+                                          "back-compat reset");
+
+    object_class_property_add_str(oc, "pmcr-n-force", virt_get_pmcr_n_force,
+                                  virt_set_pmcr_n_force);
+    object_class_property_set_description(oc, "pmcr-n-force",
+                                          "Test knob: write PMCR_EL0.N with "
+                                          "this value via KVM_SET_ONE_REG, 
then "
+                                          "read it back and log the result. "
+                                          "Under pmu-strict=on the kernel "
+                                          "ignores the write (N stays at max); 
"
+                                          "off (default) disables the knob");
     object_class_property_set_description(oc, "mte",
                                           "Set on/off to enable/disable 
emulating a "
                                           "guest CPU which implements the ARM "
@@ -3290,6 +3465,12 @@
     /* EL2 is also disabled by default, for similar reasons */
     vms->virt = false;
 
+    /* Test knobs default: select a PMU, no strict flag, no pmmir override */
+    vms->pmu_strict = false;
+    vms->pmu_set = true;
+    vms->pmmir_force = -1;
+    vms->pmcr_n_force = -1;
+
     /* High memory is enabled by default */
     vms->highmem = true;
     vms->highmem_compact = !vmc->no_highmem_compact;
--- a/include/hw/arm/virt.h     2025-03-26 19:37:00.000000000 +0000
+++ b/include/hw/arm/virt.h     2026-07-10 04:06:12.091312904 +0000
@@ -180,6 +180,11 @@
     char *oem_id;
     char *oem_table_id;
     bool ns_el2_virt_timer_irq;
+    /* Test knobs for the strict PMUv3 series (-1 / false = off). */
+    bool pmu_strict;      /* request KVM_ARM_VCPU_PMU_V3_STRICT */
+    bool pmu_set;         /* issue SET_PMU (default on) */
+    int64_t pmmir_force;  /* force PMMIR_EL1 via KVM_SET_ONE_REG */
+    int64_t pmcr_n_force; /* write+read PMCR_EL0.N via KVM_SET_ONE_REG */
 };
 
 #define VIRT_ECAM_ID(high) (high ? VIRT_HIGH_PCIE_ECAM : VIRT_PCIE_ECAM)

Reply via email to