Skip to contentSkip to conversation

KVM: arm64: Trap guest MPAM accesses whenever MPAM is implemented

PATCH Superseded Fuad Tabba 4 participants 6 messages Sep 8 – Sep 11

Subsystems (2)

v2 of 5 Superseded Latest is v5 →

lore
Share & tools
lore

Fuad Tabba

author reviewer
#0 lore raw

PATCH v2 1/1 KVM: arm64: Trap guest MPAM accesses whenever MPAM is implemented

finalise_el2_state() clears the EL2 MPAM traps whenever the ID registers advertise MPAM, while KVM sets them only under ARM64_MPAM, which also requires MPAMEN. Without EL3 nothing sets that enable, so a guest reaches the MPAM registers while ID_AA64PFR0_EL1.MPAM reads 0 for it.

Gate the traps on the ID registers alone: MPAMEN isn't a term in any MPAM accessor, so they take effect without it. The cap tests the sanitised ID fields, which never exceed a CPU's own, so it's set only where finalise_el2_state already wrote MPAM2_EL2. MPAM3_EL3.TRAPLOWER is clear there, and arm64.nompam turns the cap off too.

Fixes 23b33d1e168c ("arm64: head.S: Initialise MPAM EL2 registers and disable traps")

---

Notes:
Changes since v1:
- Commit message only. The code is unchanged, so Yuan Yao's Reviewed-by is carried.
- Fixes: names the commit that cleared the traps, rather than the one that added KVM's trapping.
- Says why the sanitised ID fields are the safe gate: the cap is set only where finalise_el2_state already wrote MPAM2_EL2.

Not covered, and answered on the v1 thread: a machine whose CPUs differ on MPAM, raised by Ben and by sashiko.

v1: https://lore.kernel.org/all/[email protected]/

 arch/arm64/include/asm/cpufeature.h     |  5 +++++
 arch/arm64/kernel/cpufeature.c          | 13 +++++++++++++
 arch/arm64/kvm/hyp/include/hyp/switch.h |  4 ++--
 arch/arm64/tools/cpucaps                |  1 +
 4 files changed, 21 insertions(+), 2 deletions(-)
arch/arm64/include/asm/cpufeature.h +5 -0
index 7404a6e83a930..8863ae99596bc 100644
@@ -873,6 +873,11 @@ static __always_inline bool system_supports_mpam_hcr(void)
 	return alternative_has_cap_unlikely(ARM64_MPAM_HCR);
 }
 
+static __always_inline bool system_supports_mpam_sysregs(void)
+{
+	return alternative_has_cap_unlikely(ARM64_MPAM_SYSREGS);
+}
+
 static inline bool system_supports_pmuv3(void)
 {
 	return cpus_have_final_cap(ARM64_HAS_PMUV3);
arch/arm64/kernel/cpufeature.c +13 -0
index 17b83a2518a8f..36a27692e5cf7 100644
@@ -2501,6 +2501,13 @@ test_has_mpam(const struct arm64_cpu_capabilities *entry, int scope)
 	return (read_sysreg_s(SYS_MPAM1_EL1) & MPAM1_EL1_MPAMEN);
 }
 
+static bool
+test_has_mpam_sysregs(const struct arm64_cpu_capabilities *entry, int __unused)
+{
+	/* The registers exist whether or not firmware enabled MPAM. */
+	return detect_ftr_has_mpam();
+}
+
 static void
 cpu_enable_mpam(const struct arm64_cpu_capabilities *entry)
 {
@@ -3116,6 +3123,12 @@ static const struct arm64_cpu_capabilities arm64_features[] = {
 		.matches = test_has_mpam,
 		.cpu_enable = cpu_enable_mpam,
 	},
+	{
+		.desc = "Memory Partitioning And Monitoring system registers",
+		.type = ARM64_CPUCAP_SYSTEM_FEATURE,
+		.capability = ARM64_MPAM_SYSREGS,
+		.matches = test_has_mpam_sysregs,
+	},
 	{
 		.desc = "Memory Partitioning And Monitoring Virtualisation",
 		.type = ARM64_CPUCAP_SYSTEM_FEATURE,
arch/arm64/kvm/hyp/include/hyp/switch.h +2 -2
index 1ce7130e25490..8941335724f6b 100644
@@ -298,7 +298,7 @@ static inline void  __activate_traps_mpam(struct kvm_vcpu *vcpu)
 	u64 clr = MPAM2_EL2_EnMPAMSM;
 	u64 set = MPAM2_EL2_TRAPMPAM0EL1 | MPAM2_EL2_TRAPMPAM1EL1;
 
-	if (!system_supports_mpam())
+	if (!system_supports_mpam_sysregs())
 		return;
 
 	/* trap guest access to MPAMIDR_EL1 */
@@ -317,7 +317,7 @@ static inline void __deactivate_traps_mpam(void)
 	u64 clr = MPAM2_EL2_TRAPMPAM0EL1 | MPAM2_EL2_TRAPMPAM1EL1 | MPAM2_EL2_TIDR;
 	u64 set = MPAM2_EL2_EnMPAMSM;
 
-	if (!system_supports_mpam())
+	if (!system_supports_mpam_sysregs())
 		return;
 
 	sysreg_clear_set_s(SYS_MPAM2_EL2, clr, set);
arch/arm64/tools/cpucaps +1 -0
index 2775ba3359cfe..aa5be51385f68 100644
@@ -78,6 +78,7 @@ KVM_PROTECTED_MODE
 MISMATCHED_CACHE_TYPE
 MPAM
 MPAM_HCR
+MPAM_SYSREGS
 MTE
 MTE_ASYMM
 MTE_FAR
Signature · 1 line
2.39.5
#1 lore raw
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [High] Guests can bypass EL2 traps and directly access physical MPAM registers if the host has asymmetric CPUs or boots with arm64.nompam. --

commit e23ed2fd702c6c43a8b21feffe356931e0f7eb5a
Author: Fuad Tabba <[email protected]>

KVM: arm64: Trap guest MPAM accesses whenever MPAM is implemented

This commit modifies the EL2 MPAM trap activation to gate on the sanitized ID registers rather than the MPAMEN bit. This ensures that guest MPAM accesses are trapped whenever MPAM registers are implemented, even if firmware hasn't enabled MPAM.

Quoted 4 lines
arch/arm64/kvm/hyp/include/hyp/switch.h +0 -0
index 1ce7130e25490..8941335724f6b 100644

[ ... ]

arch/arm64/kvm/hyp/include/hyp/switch.h static inline void __activate_traps_mpam(struct kvm_vcpu *vcpu) · line 301

Quoted 6 lines
arch/arm64/kvm/hyp/include/hyp/switch.h +1 -1
@@ -298,7 +298,7 @@ static inline void  __activate_traps_mpam(struct kvm_vcpu *vcpu)
 	u64 clr = MPAM2_EL2_EnMPAMSM;
 	u64 set = MPAM2_EL2_TRAPMPAM0EL1 | MPAM2_EL2_TRAPMPAM1EL1;
 
-	if (!system_supports_mpam())
+	if (!system_supports_mpam_sysregs())

[Severity: High]
This is a pre-existing issue, but does gating this early return on the sanitized system-wide ID registers allow the guest direct access to physical MPAM registers in asymmetric or overridden configurations?

During early boot, head.S (in finalise_el2_state) reads the physical CPU ID registers and clears the EL2 MPAM traps. If the system has asymmetric MPAM CPUs, or if the kernel is booted with arm64.nompam, system_supports_mpam_sysregs() evaluates to false.

When that happens, __activate_traps_mpam() takes this early return and skips setting TRAPMPAM0EL1 and TRAPMPAM1EL1. Since the hardware traps were already cleared by head.S (or left clear by firmware), the guest's MPAM register accesses execute natively without trapping to EL2. This could allow an untrusted guest to manipulate host cache and memory bandwidth partitioning.

This patch swaps one system-wide capability (ARM64_MPAM) for another (ARM64_MPAM_SYSREGS), which inherits the exact same bypass. Should KVM base its trap activation on the physical ID registers to safely isolate the guest instead?

Quoted 3 lines
arch/arm64/kvm/hyp/include/hyp/switch.h +0 -0
 		return;
 
 	/* trap guest access to MPAMIDR_EL1 */
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
maintainer
#2 lore raw
In reply to Fuad Tabba
Quoted 72 lines · Fuad Tabba

finalise_el2_state() clears the EL2 MPAM traps whenever the ID registers advertise MPAM, while KVM sets them only under ARM64_MPAM, which also requires MPAMEN. Without EL3 nothing sets that enable, so a guest reaches the MPAM registers while ID_AA64PFR0_EL1.MPAM reads 0 for it.

Gate the traps on the ID registers alone: MPAMEN isn't a term in any MPAM accessor, so they take effect without it. The cap tests the sanitised ID fields, which never exceed a CPU's own, so it's set only where finalise_el2_state already wrote MPAM2_EL2. MPAM3_EL3.TRAPLOWER is clear there, and arm64.nompam turns the cap off too.

Fixes 23b33d1e168c ("arm64: head.S: Initialise MPAM EL2 registers and disable traps")

---

Notes:
Changes since v1:
- Commit message only. The code is unchanged, so Yuan Yao's Reviewed-by is carried.
- Fixes: names the commit that cleared the traps, rather than the one that added KVM's trapping.
- Says why the sanitised ID fields are the safe gate: the cap is set only where finalise_el2_state already wrote MPAM2_EL2.

Not covered, and answered on the v1 thread: a machine whose CPUs differ on MPAM, raised by Ben and by sashiko.

v1: https://lore.kernel.org/all/[email protected]/

 arch/arm64/include/asm/cpufeature.h     |  5 +++++
 arch/arm64/kernel/cpufeature.c          | 13 +++++++++++++
 arch/arm64/kvm/hyp/include/hyp/switch.h |  4 ++--
 arch/arm64/tools/cpucaps                |  1 +
 4 files changed, 21 insertions(+), 2 deletions(-)
arch/arm64/include/asm/cpufeature.h +5 -0
index 7404a6e83a930..8863ae99596bc 100644
@@ -873,6 +873,11 @@ static __always_inline bool system_supports_mpam_hcr(void)
 	return alternative_has_cap_unlikely(ARM64_MPAM_HCR);
 }
 
+static __always_inline bool system_supports_mpam_sysregs(void)
+{
+	return alternative_has_cap_unlikely(ARM64_MPAM_SYSREGS);
+}
+
 static inline bool system_supports_pmuv3(void)
 {
 	return cpus_have_final_cap(ARM64_HAS_PMUV3);
arch/arm64/kernel/cpufeature.c +7 -0
index 17b83a2518a8f..36a27692e5cf7 100644
@@ -2501,6 +2501,13 @@ test_has_mpam(const struct arm64_cpu_capabilities *entry, int scope)
 	return (read_sysreg_s(SYS_MPAM1_EL1) & MPAM1_EL1_MPAMEN);
 }
 
+static bool
+test_has_mpam_sysregs(const struct arm64_cpu_capabilities *entry, int __unused)
+{
+	/* The registers exist whether or not firmware enabled MPAM. */
+	return detect_ftr_has_mpam();
+}
+
 static void
 cpu_enable_mpam(const struct arm64_cpu_capabilities *entry)
 {

arch/arm64/kernel/cpufeature.c static const struct arm64_cpu_capabilities arm64_features[] = { · line 3131

Quoted 9 lines
arch/arm64/kernel/cpufeature.c +6 -0
@@ -3116,6 +3123,12 @@ static const struct arm64_cpu_capabilities arm64_features[] = {
 		.matches = test_has_mpam,
 		.cpu_enable = cpu_enable_mpam,
 	},
+	{
+		.desc = "Memory Partitioning And Monitoring system registers",
+		.type = ARM64_CPUCAP_SYSTEM_FEATURE,
+		.capability = ARM64_MPAM_SYSREGS,
+		.matches = test_has_mpam_sysregs,
+	},

We already have two system capabilities for MPAM so I'm not overly keen to add a third, especially as the MPAM code is largely confined to the resctrl driver. In fact, this feels a bit similar to things like TRBE and SPE in the sense that (a) we have to probe it per-cpu (b) it can be disabled by a higher EL and (c) most of the code is in a driver, but KVM needs to know how to switch it.

Neither TRBE nor SPE need entries in arm64_features[] at all, so something isn't right here...

Will

author reviewer
#3 lore raw

On Thu, 10 Sept 2026 at 11:40, Will Deacon <[email protected]> wrote:
...

Quoted 28 lines
Quoted 11 lines
arch/arm64/kernel/cpufeature.c +7 -0
+static bool
+test_has_mpam_sysregs(const struct arm64_cpu_capabilities *entry, int __unused)
+{
+     /* The registers exist whether or not firmware enabled MPAM. */
+     return detect_ftr_has_mpam();
+}
+
 static void
 cpu_enable_mpam(const struct arm64_cpu_capabilities *entry)
 {

arch/arm64/kernel/cpufeature.c static const struct arm64_cpu_capabilities arm64_features[] = { · line 3131

Quoted 9 lines
arch/arm64/kernel/cpufeature.c +6 -0
@@ -3116,6 +3123,12 @@ static const struct arm64_cpu_capabilities arm64_features[] = {
              .matches = test_has_mpam,
              .cpu_enable = cpu_enable_mpam,
      },
+     {
+             .desc = "Memory Partitioning And Monitoring system registers",
+             .type = ARM64_CPUCAP_SYSTEM_FEATURE,
+             .capability = ARM64_MPAM_SYSREGS,
+             .matches = test_has_mpam_sysregs,
+     },

We already have two system capabilities for MPAM so I'm not overly keen to add a third, especially as the MPAM code is largely confined to the resctrl driver. In fact, this feels a bit similar to things like TRBE and SPE in the sense that (a) we have to probe it per-cpu (b) it can be disabled by a higher EL and (c) most of the code is in a driver, but KVM needs to know how to switch it.

Quoted 2 lines

Neither TRBE nor SPE need entries in arm64_features[] at all, so something isn't right here...

I think I've overthunk it. The problem isn't in what the host is doing, but that KVM is basing its traps on whether MPAM support exists and whether that support is enabled.

V3 should be more in line with what you're suggesting...

Cheers,
/fuad

Quoted 2 lines

Will

maintainer
#4 lore raw
In reply to Fuad Tabba
Quoted 43 lines · Fuad Tabba

finalise_el2_state() clears the EL2 MPAM traps whenever the ID registers advertise MPAM, while KVM sets them only under ARM64_MPAM, which also requires MPAMEN. Without EL3 nothing sets that enable, so a guest reaches the MPAM registers while ID_AA64PFR0_EL1.MPAM reads 0 for it.

Gate the traps on the ID registers alone: MPAMEN isn't a term in any MPAM accessor, so they take effect without it. The cap tests the sanitised ID fields, which never exceed a CPU's own, so it's set only where finalise_el2_state already wrote MPAM2_EL2. MPAM3_EL3.TRAPLOWER is clear there, and arm64.nompam turns the cap off too.

Fixes 23b33d1e168c ("arm64: head.S: Initialise MPAM EL2 registers and disable traps")

---

Notes:
Changes since v1:
- Commit message only. The code is unchanged, so Yuan Yao's Reviewed-by is carried.
- Fixes: names the commit that cleared the traps, rather than the one that added KVM's trapping.
- Says why the sanitised ID fields are the safe gate: the cap is set only where finalise_el2_state already wrote MPAM2_EL2.

Not covered, and answered on the v1 thread: a machine whose CPUs differ on MPAM, raised by Ben and by sashiko.

v1: https://lore.kernel.org/all/[email protected]/

 arch/arm64/include/asm/cpufeature.h     |  5 +++++
 arch/arm64/kernel/cpufeature.c          | 13 +++++++++++++
 arch/arm64/kvm/hyp/include/hyp/switch.h |  4 ++--
 arch/arm64/tools/cpucaps                |  1 +
 4 files changed, 21 insertions(+), 2 deletions(-)
arch/arm64/include/asm/cpufeature.h +0 -0
index 7404a6e83a930..8863ae99596bc 100644

arch/arm64/include/asm/cpufeature.h static __always_inline bool system_supports_mpam_hcr(void) · line 880

Quoted 8 lines
arch/arm64/include/asm/cpufeature.h +5 -0
@@ -873,6 +873,11 @@ static __always_inline bool system_supports_mpam_hcr(void)
 	return alternative_has_cap_unlikely(ARM64_MPAM_HCR);
 }
 
+static __always_inline bool system_supports_mpam_sysregs(void)
+{
+	return alternative_has_cap_unlikely(ARM64_MPAM_SYSREGS);
+}
+

What is the rationale for choosing alternative_has_cap_unlikely() instead of cpus_have_final_cap(), which is more wildly used?

	M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
author reviewer
#5 lore raw

On Thu, 10 Sept 2026 at 12:08, Marc Zyngier <[email protected]> wrote:
...

Quoted 6 lines
Quoted 6 lines
arch/arm64/include/asm/cpufeature.h +0 -0
@@ -873,6 +873,11 @@ static __always_inline bool system_supports_mpam_hcr(void)
      return alternative_has_cap_unlikely(ARM64_MPAM_HCR);
 }
 
Quoted 8 lines

arch/arm64/include/asm/cpufeature.h static __always_inline bool system_supports_mpam_hcr(void) · line 880

Quoted 5 lines
arch/arm64/include/asm/cpufeature.h +5 -0
+static __always_inline bool system_supports_mpam_sysregs(void)
+{
+     return alternative_has_cap_unlikely(ARM64_MPAM_SYSREGS);
+}
+

What is the rationale for choosing alternative_has_cap_unlikely() instead of cpus_have_final_cap(), which is more wildly used?

Not a good one to be honest, and it's gone in V3.

Cheers,
/fuad

Quoted 5 lines
        M.

--
Without deviation from the norm, progress is not possible.