[PATCH] KVM: arm64: CCA: Handle GICv3 CPU interface traps for realms
From: Steven Price
Date: Thu Jul 23 2026 - 06:49:36 EST
On CPUs that lack ICH_HCR_EL2.TDIR, KVM works around the missing DIR
trap by setting ICH_HCR_EL2.TC and trapping the GICv3 common CPU
interface registers. Normal guests handle these traps in hyp via the
VGIC CPU interface emulation, but realm guests exit through the RMM and
reach KVM's host-side sysreg emulation instead.
That path only handles ICC_DIR_EL1 and treats the rest of the trapped
common group as undefined. A realm guest can therefore take an
unexpected UNDEF when accessing registers such as ICC_PMR_EL1 or
ICC_CTLR_EL1 on systems that require the TC workaround.
Handle the GICv3 common CPU interface registers directly from the realm
sysreg exit path. Reads are satisfied from the saved VGIC state and
writes update the VGIC shadow state. For PMR and CTLR writes, also
restore the VMCR/APR state to the live GIC CPU interface before
re-entering the REC, matching the effect a direct guest sysreg write
would have had.
Signed-off-by: Steven Price <steven.price@xxxxxxx>
---
arch/arm64/kvm/rmi-exit.c | 119 ++++++++++++++++++++++++++++++++++++++
1 file changed, 119 insertions(+)
diff --git a/arch/arm64/kvm/rmi-exit.c b/arch/arm64/kvm/rmi-exit.c
index 78d9189fd5ca..0fad8e68d920 100644
--- a/arch/arm64/kvm/rmi-exit.c
+++ b/arch/arm64/kvm/rmi-exit.c
@@ -3,17 +3,25 @@
* Copyright (C) 2023-2026 ARM Ltd.
*/
+#include <linux/bitfield.h>
+#include <linux/irqchip/arm-gic-v3.h>
#include <linux/kvm_host.h>
#include <kvm/arm_hypercalls.h>
#include <kvm/arm_psci.h>
#include <linux/arm-smccc-rmi.h>
#include <asm/kvm_emulate.h>
+#include <asm/kvm_hyp.h>
#include <asm/kvm_rmi.h>
#include <asm/kvm_mmu.h>
+#include <asm/sysreg.h>
+
+#include "vgic/vgic.h"
typedef int (*exit_handler_fn)(struct kvm_vcpu *vcpu);
+#define GICV3_IDLE_PRIORITY 0xff
+
static int rec_exit_reason_notimpl(struct kvm_vcpu *vcpu)
{
vcpu_err(vcpu, "Unhandled exit reason from realm (ESR: %#llx)\n",
@@ -48,6 +56,114 @@ static int rec_exit_sync_iabt(struct kvm_vcpu *vcpu)
return -ENXIO;
}
+static int rec_get_gicv3_bpr_min(void)
+{
+ return 8 - (FIELD_GET(ICH_VTR_EL2_PREbits,
+ kvm_vgic_global_state.ich_vtr_el2) + 1);
+}
+
+static u8 rec_get_gicv3_active_priority(struct kvm_vcpu *vcpu)
+{
+ struct vgic_v3_cpu_if *cpuif = &vcpu->arch.vgic_cpu.vgic_v3;
+ u8 prio = 0;
+
+ for (int i = 0; i <= vgic_v3_max_apr_idx(vcpu); i++) {
+ u32 val = cpuif->vgic_ap0r[i] | cpuif->vgic_ap1r[i];
+
+ if (!val) {
+ prio += 32;
+ continue;
+ }
+
+ return (prio + __ffs(val)) << rec_get_gicv3_bpr_min();
+ }
+
+ return GICV3_IDLE_PRIORITY;
+}
+
+static void rec_restore_gicv3_vmcr_aprs(struct kvm_vcpu *vcpu)
+{
+ preempt_disable();
+ kvm_call_hyp(__vgic_v3_restore_vmcr_aprs,
+ &vcpu->arch.vgic_cpu.vgic_v3);
+ preempt_enable();
+}
+
+/*
+ * Realm exits are handled after the RMM has returned to the host, so use the
+ * saved VGIC shadow state instead of the hyp-side live sysreg helpers.
+ */
+static bool rec_exit_gicv3_sys_reg(struct kvm_vcpu *vcpu, bool is_write)
+{
+ struct realm_rec *rec = &vcpu->arch.rec;
+ struct vgic_cpu *vgic_cpu = &vcpu->arch.vgic_cpu;
+ struct vgic_vmcr vmcr;
+ unsigned long esr = kvm_vcpu_get_esr(vcpu);
+ int rt = kvm_vcpu_sys_get_rt(vcpu);
+ u32 sysreg = esr_sys64_to_sysreg(esr);
+ u64 val;
+
+ if (!kvm_has_gicv3(vcpu->kvm))
+ return false;
+
+ vgic_get_vmcr(vcpu, &vmcr);
+
+ switch (sysreg) {
+ case SYS_ICC_DIR_EL1:
+ if (!is_write)
+ return false;
+
+ vgic_v3_deactivate(vcpu, rec->run->exit.gprs[rt]);
+ return true;
+ case SYS_ICC_PMR_EL1:
+ if (is_write) {
+ vmcr.pmr = FIELD_GET(ICC_PMR_EL1_MASK,
+ rec->run->exit.gprs[rt]);
+ vgic_set_vmcr(vcpu, &vmcr);
+ rec_restore_gicv3_vmcr_aprs(vcpu);
+ } else {
+ rec->run->enter.gprs[rt] = FIELD_PREP(ICC_PMR_EL1_MASK,
+ vmcr.pmr);
+ }
+
+ return true;
+ case SYS_ICC_CTLR_EL1:
+ if (is_write) {
+ val = rec->run->exit.gprs[rt];
+ vmcr.cbpr = FIELD_GET(ICC_CTLR_EL1_CBPR_MASK, val);
+ vmcr.eoim = FIELD_GET(ICC_CTLR_EL1_EOImode_MASK, val);
+ vgic_set_vmcr(vcpu, &vmcr);
+ rec_restore_gicv3_vmcr_aprs(vcpu);
+ } else {
+ val = FIELD_PREP(ICC_CTLR_EL1_PRI_BITS_MASK,
+ vgic_cpu->num_pri_bits - 1);
+ val |= FIELD_PREP(ICC_CTLR_EL1_ID_BITS_MASK,
+ vgic_cpu->num_id_bits);
+ val |= FIELD_PREP(ICC_CTLR_EL1_SEIS_MASK,
+ FIELD_GET(ICH_VTR_EL2_SEIS,
+ kvm_vgic_global_state.ich_vtr_el2));
+ val |= FIELD_PREP(ICC_CTLR_EL1_A3V_MASK,
+ FIELD_GET(ICH_VTR_EL2_A3V,
+ kvm_vgic_global_state.ich_vtr_el2));
+ val |= FIELD_PREP(ICC_CTLR_EL1_CBPR_MASK,
+ vmcr.cbpr);
+ val |= FIELD_PREP(ICC_CTLR_EL1_EOImode_MASK,
+ vmcr.eoim);
+ rec->run->enter.gprs[rt] = val;
+ }
+
+ return true;
+ case SYS_ICC_RPR_EL1:
+ if (is_write)
+ return false;
+
+ rec->run->enter.gprs[rt] = rec_get_gicv3_active_priority(vcpu);
+ return true;
+ default:
+ return false;
+ }
+}
+
static int rec_exit_sys_reg(struct kvm_vcpu *vcpu)
{
struct realm_rec *rec = &vcpu->arch.rec;
@@ -59,6 +175,9 @@ static int rec_exit_sys_reg(struct kvm_vcpu *vcpu)
if (is_write)
vcpu_set_reg(vcpu, rt, rec->run->exit.gprs[rt]);
+ if (rec_exit_gicv3_sys_reg(vcpu, is_write))
+ return 1;
+
ret = kvm_handle_sys_reg(vcpu);
if (!is_write)
rec->run->enter.gprs[rt] = vcpu_get_reg(vcpu, rt);
--
2.43.0