* [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2
@ 2026-08-31 16:34 Fuad Tabba
2026-08-31 16:34 ` [PATCH 01/17] KVM: arm64: Sync HCR_EL2.VSE back to the host vCPU under pKVM Fuad Tabba
` (17 more replies)
0 siblings, 18 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
Hi folks,
Following the vCPU state-sync series [1], this series completes the
job for protected VMs: a protected guest's register state stays at
EL2, and the host sees only what handling each exit needs.
EL2 marshals a protected vCPU's state per exception class instead of
copying the whole context both ways. It owns the vCPU's trap
configuration, system register reset and HVC handling, and implements
PSCI itself: AFFINITY_INFO never reaches the host, and CPU_ON and
CPU_OFF are decided at EL2 with the host only scheduling or parking
the target. Host ioctls that would reach the state EL2 owns fail with
a clean errno, so a protected VM's state is not save/restorable. All
of this is scoped to KVM_VM_TYPE_ARM_PROTECTED, and pkvm.rst describes
the resulting API.
The kvmtool changes that go with this will be posted separately, and I
will reply here with a link.
Patch 1 is the HCR_EL2.VSE fix posted separately [2]. It is not part
of this series; it is carried so the series applies as is and Sashiko
can run on it.
The KVM_ARM_PREFERRED_TARGET documentation fix [3] went out just ahead
of this series. Nothing here needs it to apply, but patch 17 documents
vCPU feature availability as something the capabilities report, while
api.rst 4.83 still points userspace at a bitmap that has always been
empty.
The series is structured as follows:
01: The HCR_EL2.VSE fix, posted separately.
02-03: Capability allowlist and the PVTIME rejection.
04-05: Per-exception-class entry handlers; EL2 owns a protected
vCPU's trap configuration.
06-08: Timer state, system register reset and HVC handling at EL2.
09-10: PSCI at EL2, and the KVM_ARM_VCPU_INIT and PSCI version
restrictions.
11-14: Host PC adjustments blocked; an UNDEF at EL2 for exit
classes the host does not emulate; per-class state
marshalling; a protected guest's SError pended with
HCR_EL2.VSE.
15-16: Host access to private state, and host power-on of a vCPU
EL2 holds powered off, rejected.
17: Documentation.
Still to come: selftests, self-hosted debug, SVE for protected guests,
and much more, as separate series.
Based on v7.3-rc1 (cee9395acd804).
Cheers,
/fuad
P.S. Sashiko, bring it on!
[1] https://lore.kernel.org/all/20260729131823.2021516-1-fuad.tabba@linux.dev/
[2] https://lore.kernel.org/all/20260829071120.2522788-1-fuad.tabba@linux.dev/
[3] https://lore.kernel.org/all/20260831162815.269851-1-fuad.tabba@linux.dev/
Fuad Tabba (15):
KVM: arm64: Sync HCR_EL2.VSE back to the host vCPU under pKVM
KVM: arm64: Advertise the capabilities that protected VMs support
KVM: arm64: Reject the PVTIME vCPU attribute for protected VMs
KVM: arm64: Skip fixed-feature state flush for protected vCPUs
KVM: arm64: Add system register reset framework for protected VMs
KVM: arm64: Implement HVC handling for protected guests at EL2
KVM: arm64: Handle PSCI calls for protected VMs at EL2
KVM: arm64: Restrict KVM_ARM_VCPU_INIT and PSCI version for protected
VMs
KVM: arm64: Prevent host PC adjustments for protected vCPUs
KVM: arm64: Inject an UNDEF at EL2 for unhandled protected guest exits
KVM: arm64: Add per-EC entry/exit state marshalling for protected
guests
KVM: arm64: Pend a protected guest's SError with HCR_EL2.VSE only
KVM: arm64: Reject host access to protected VM private state
KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off
KVM: arm64: Document the protected VM userspace API
Marc Zyngier (2):
KVM: arm64: Introduce per-EC entry handlers for pKVM
KVM: arm64: Add {flush,sync}_hyp_timer_state() primitives
Documentation/virt/kvm/api.rst | 22 +-
.../virt/kvm/arm/fw-pseudo-registers.rst | 2 +
Documentation/virt/kvm/arm/pkvm.rst | 141 ++++-
Documentation/virt/kvm/devices/vcpu.rst | 4 +-
arch/arm64/include/asm/kvm_asm.h | 1 +
arch/arm64/include/asm/kvm_host.h | 21 +
arch/arm64/include/asm/kvm_pkvm.h | 34 +-
arch/arm64/kvm/arm.c | 40 ++
arch/arm64/kvm/guest.c | 29 +
arch/arm64/kvm/hyp/exception.c | 27 +-
arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 18 +
arch/arm64/kvm/hyp/nvhe/hyp-main.c | 564 +++++++++++++++++-
arch/arm64/kvm/hyp/nvhe/pkvm.c | 401 ++++++++++++-
arch/arm64/kvm/hyp/nvhe/switch.c | 29 +-
arch/arm64/kvm/hyp/nvhe/sys_regs.c | 91 ++-
arch/arm64/kvm/hypercalls.c | 7 +
arch/arm64/kvm/inject_fault.c | 5 +-
arch/arm64/kvm/pkvm.c | 21 +-
arch/arm64/kvm/psci.c | 3 +
19 files changed, 1378 insertions(+), 82 deletions(-)
base-commit: cee9395acd8043be0644b25c34bfa86623f2b935
--
2.39.5
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH 01/17] KVM: arm64: Sync HCR_EL2.VSE back to the host vCPU under pKVM
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 02/17] KVM: arm64: Advertise the capabilities that protected VMs support Fuad Tabba
` (16 subsequent siblings)
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
flush_hyp_vcpu() copies HCR_EL2.VSE from the host vCPU into the hyp
vCPU on every entry, and nothing copies it back. Once the guest takes
the vSError the hyp vCPU's copy clears with the hardware bit while the
host's stays set, so the next entry pends the same SError again,
KVM_GET_VCPU_EVENTS keeps reporting it, and kvm_arch_vcpu_runnable()
never lets the vCPU block in WFI.
Reflect VSE back on every exit, which the flush-side comment already
states.
Fixes: 734dc8c01c838 ("KVM: arm64: Implement lazy vCPU state sync for non-protected guests")
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/hyp/nvhe/hyp-main.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
index 9a3b92e626adb..b6bfe502bcd04 100644
--- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
+++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
@@ -276,6 +276,10 @@ static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
host_vcpu->arch.iflags = hyp_vcpu->vcpu.arch.iflags;
+ /* Cleared by hardware once the guest takes the vSError. */
+ host_vcpu->arch.hcr_el2 &= ~HCR_VSE;
+ host_vcpu->arch.hcr_el2 |= hyp_vcpu->vcpu.arch.hcr_el2 & HCR_VSE;
+
sync_hyp_vgic_state(hyp_vcpu);
}
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 02/17] KVM: arm64: Advertise the capabilities that protected VMs support
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
2026-08-31 16:34 ` [PATCH 01/17] KVM: arm64: Sync HCR_EL2.VSE back to the host vCPU under pKVM Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-09-02 13:22 ` Vincent Donnefort
2026-08-31 16:34 ` [PATCH 03/17] KVM: arm64: Reject the PVTIME vCPU attribute for protected VMs Fuad Tabba
` (15 subsequent siblings)
17 siblings, 1 reply; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
kvm_pkvm_ext_allowed() denies every capability it does not name, so a
protected VM reports 0 for interfaces it implements: KVM_CHECK_EXTENSION
denies KVM_CAP_ONE_REG while KVM_{GET,SET}_ONE_REG stage the guest's
boot state.
Allow the capabilities that work for a protected guest, the vGIC and
the I/O bus being host-managed, subject to the restrictions the rest of
this series adds.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/include/asm/kvm_pkvm.h | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/arch/arm64/include/asm/kvm_pkvm.h b/arch/arm64/include/asm/kvm_pkvm.h
index beea00e693a0a..c4c834d55e503 100644
--- a/arch/arm64/include/asm/kvm_pkvm.h
+++ b/arch/arm64/include/asm/kvm_pkvm.h
@@ -42,6 +42,15 @@ static inline bool kvm_pkvm_ext_allowed(struct kvm *kvm, long ext)
case KVM_CAP_ARM_VM_IPA_SIZE:
case KVM_CAP_ARM_PTRAUTH_ADDRESS:
case KVM_CAP_ARM_PTRAUTH_GENERIC:
+ case KVM_CAP_ONE_REG:
+ case KVM_CAP_MP_STATE:
+ case KVM_CAP_VCPU_EVENTS:
+ case KVM_CAP_VCPU_ATTRIBUTES:
+ case KVM_CAP_IMMEDIATE_EXIT:
+ case KVM_CAP_IOEVENTFD:
+ case KVM_CAP_IRQFD_RESAMPLE:
+ case KVM_CAP_ARM_IRQ_LINE_LAYOUT_2:
+ case KVM_CAP_ARM_INJECT_SERROR_ESR:
return true;
case KVM_CAP_ARM_MTE:
return false;
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 03/17] KVM: arm64: Reject the PVTIME vCPU attribute for protected VMs
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
2026-08-31 16:34 ` [PATCH 01/17] KVM: arm64: Sync HCR_EL2.VSE back to the host vCPU under pKVM Fuad Tabba
2026-08-31 16:34 ` [PATCH 02/17] KVM: arm64: Advertise the capabilities that protected VMs support Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-09-02 13:30 ` Vincent Donnefort
2026-08-31 16:34 ` [PATCH 04/17] KVM: arm64: Introduce per-EC entry handlers for pKVM Fuad Tabba
` (14 subsequent siblings)
17 siblings, 1 reply; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
pKVM currently does not support steal time for protected guests:
KVM_CAP_STEAL_TIME reports 0 for them. The host still accepts the
KVM_ARM_VCPU_PVTIME_CTRL attribute, whose IPA would point
kvm_update_stolen_time() at the guest's private memory on every vCPU
load. Return -EPERM for the attribute group instead.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/guest.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/arch/arm64/kvm/guest.c b/arch/arm64/kvm/guest.c
index b01d6622b8720..23f725e2cf745 100644
--- a/arch/arm64/kvm/guest.c
+++ b/arch/arm64/kvm/guest.c
@@ -916,6 +916,10 @@ int kvm_arm_vcpu_arch_set_attr(struct kvm_vcpu *vcpu,
ret = kvm_arm_timer_set_attr(vcpu, attr);
break;
case KVM_ARM_VCPU_PVTIME_CTRL:
+ /* Steal time is not offered to protected guests. */
+ if (kvm_vm_is_protected(vcpu->kvm))
+ return -EPERM;
+
ret = kvm_arm_pvtime_set_attr(vcpu, attr);
break;
default:
@@ -939,6 +943,9 @@ int kvm_arm_vcpu_arch_get_attr(struct kvm_vcpu *vcpu,
ret = kvm_arm_timer_get_attr(vcpu, attr);
break;
case KVM_ARM_VCPU_PVTIME_CTRL:
+ if (kvm_vm_is_protected(vcpu->kvm))
+ return -EPERM;
+
ret = kvm_arm_pvtime_get_attr(vcpu, attr);
break;
default:
@@ -962,6 +969,9 @@ int kvm_arm_vcpu_arch_has_attr(struct kvm_vcpu *vcpu,
ret = kvm_arm_timer_has_attr(vcpu, attr);
break;
case KVM_ARM_VCPU_PVTIME_CTRL:
+ if (kvm_vm_is_protected(vcpu->kvm))
+ return -EPERM;
+
ret = kvm_arm_pvtime_has_attr(vcpu, attr);
break;
default:
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 04/17] KVM: arm64: Introduce per-EC entry handlers for pKVM
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (2 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 03/17] KVM: arm64: Reject the PVTIME vCPU attribute for protected VMs Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-09-02 10:12 ` Joey Gouly
2026-08-31 16:34 ` [PATCH 05/17] KVM: arm64: Skip fixed-feature state flush for protected vCPUs Fuad Tabba
` (13 subsequent siblings)
17 siblings, 1 reply; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
From: Marc Zyngier <maz@kernel.org>
Add an ESR_EL2.EC-indexed handler table, entry_hyp_vm_handlers[],
consulted from flush_hyp_vcpu() on re-entry when the previous exit was
a trap: sync_hyp_vcpu() records the exit reason in the hyp vCPU as
exit_code, and the trap's exception class comes from its ESR_EL2.
Entry carries the host's requested PC updates to the hyp vCPU: add
vcpu_copy_flag() to copy a masked set of flags between the two vCPU
structures, with PC_UPDATE_REQ covering INCREMENT_PC and the
pending-exception flags. The wholesale iflags copy moves into the
non-protected branch; a protected vCPU takes only PC_UPDATE_REQ.
Signed-off-by: Marc Zyngier <maz@kernel.org>
Co-developed-by: Fuad Tabba <fuad.tabba@linux.dev>
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/include/asm/kvm_host.h | 19 +++++++++++++
arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 3 ++
arch/arm64/kvm/hyp/nvhe/hyp-main.c | 39 +++++++++++++++++++++++---
3 files changed, 57 insertions(+), 4 deletions(-)
diff --git a/arch/arm64/include/asm/kvm_host.h b/arch/arm64/include/asm/kvm_host.h
index 27fe0cd5b2d7a..78a4d387f9fd4 100644
--- a/arch/arm64/include/asm/kvm_host.h
+++ b/arch/arm64/include/asm/kvm_host.h
@@ -1033,11 +1033,28 @@ struct kvm_vcpu_arch {
set; \
})
+#define __vcpu_copy_flag(vt, vs, flagset, f, m) \
+ do { \
+ typeof(vs->arch.flagset) tmp, val; \
+ \
+ __build_check_flag(vs, flagset, f, m); \
+ \
+ val = READ_ONCE(vs->arch.flagset); \
+ val &= (m); \
+ __vcpu_flags_preempt_disable(); \
+ tmp = READ_ONCE(vt->arch.flagset); \
+ tmp &= ~(m); \
+ tmp |= val; \
+ WRITE_ONCE(vt->arch.flagset, tmp); \
+ __vcpu_flags_preempt_enable(); \
+ } while (0)
+
#define vcpu_get_flag(v, ...) __vcpu_get_flag((v), __VA_ARGS__)
#define vcpu_set_flag(v, ...) __vcpu_set_flag((v), __VA_ARGS__)
#define vcpu_clear_flag(v, ...) __vcpu_clear_flag((v), __VA_ARGS__)
#define vcpu_test_and_clear_flag(v, ...) \
__vcpu_test_and_clear_flag((v), __VA_ARGS__)
+#define vcpu_copy_flag(vt, vs, ...) __vcpu_copy_flag((vt), (vs), __VA_ARGS__)
/* KVM_ARM_VCPU_INIT completed */
#define VCPU_INITIALIZED __vcpu_single_flag(cflags, BIT(0))
@@ -1055,6 +1072,8 @@ struct kvm_vcpu_arch {
#define INCREMENT_PC __vcpu_single_flag(iflags, BIT(1))
/* Target EL/MODE (not a single flag, but let's abuse the macro) */
#define EXCEPT_MASK __vcpu_single_flag(iflags, GENMASK(3, 1))
+/* Cover both PENDING_EXCEPTION and EXCEPT_MASK for global operations */
+#define PC_UPDATE_REQ __vcpu_single_flag(iflags, GENMASK(3, 0))
/* Host-set: the hyp flushes the non-protected vCPU state in on entry */
#define PKVM_HOST_STATE_DIRTY __vcpu_single_flag(iflags, BIT(4))
diff --git a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
index c904647d2f760..49a0a992047ba 100644
--- a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
+++ b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
@@ -26,6 +26,9 @@ struct pkvm_hyp_vcpu {
* per-cpu pointer tracking us. Otherwise, NULL if not loaded.
*/
struct pkvm_hyp_vcpu **loaded_hyp_vcpu;
+
+ /* The previous exit's ARM_EXCEPTION_* code. */
+ u32 exit_code;
};
/*
diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
index b6bfe502bcd04..ca7122b0bccdd 100644
--- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
+++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
@@ -31,6 +31,17 @@ unsigned int hyp_gicv3_nr_lr;
void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt);
+typedef void (*hyp_entry_exit_handler_fn)(struct pkvm_hyp_vcpu *);
+
+static void handle_vm_entry_generic(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ vcpu_copy_flag(&hyp_vcpu->vcpu, hyp_vcpu->host_vcpu, PC_UPDATE_REQ);
+}
+
+static const hyp_entry_exit_handler_fn entry_hyp_vm_handlers[] = {
+ [0 ... ESR_ELx_EC_MAX] = handle_vm_entry_generic,
+};
+
static void __hyp_sve_save_guest(struct kvm_vcpu *vcpu)
{
__vcpu_assign_sys_reg(vcpu, ZCR_EL1, read_sysreg_el1(SYS_ZCR));
@@ -216,6 +227,8 @@ static void sync_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu)
static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
{
struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
+ hyp_entry_exit_handler_fn ec_handler;
+ u8 esr_ec;
fpsimd_sve_flush();
flush_debug_state(hyp_vcpu);
@@ -228,6 +241,7 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
if (vcpu_get_flag(host_vcpu, PKVM_HOST_STATE_DIRTY))
flush_hyp_vcpu_state(hyp_vcpu);
+ hyp_vcpu->vcpu.arch.iflags = host_vcpu->arch.iflags;
} else {
hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
}
@@ -245,16 +259,31 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) &
(HCR_TWI | HCR_TWE | HCR_VSE);
- hyp_vcpu->vcpu.arch.iflags = host_vcpu->arch.iflags;
-
hyp_vcpu->vcpu.arch.vsesr_el2 = host_vcpu->arch.vsesr_el2;
flush_hyp_vgic_state(hyp_vcpu);
hyp_vcpu->vcpu.arch.pid = host_vcpu->arch.pid;
+
+ switch (ARM_EXCEPTION_CODE(hyp_vcpu->exit_code)) {
+ case ARM_EXCEPTION_IRQ:
+ case ARM_EXCEPTION_EL1_SERROR:
+ case ARM_EXCEPTION_IL:
+ break;
+ case ARM_EXCEPTION_TRAP:
+ esr_ec = ESR_ELx_EC(kvm_vcpu_get_esr(&hyp_vcpu->vcpu));
+ ec_handler = entry_hyp_vm_handlers[esr_ec];
+ if (ec_handler)
+ ec_handler(hyp_vcpu);
+ break;
+ default:
+ BUG();
+ }
+
+ hyp_vcpu->exit_code = 0;
}
-static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
+static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, u32 exit_reason)
{
struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
@@ -281,6 +310,8 @@ static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
host_vcpu->arch.hcr_el2 |= hyp_vcpu->vcpu.arch.hcr_el2 & HCR_VSE;
sync_hyp_vgic_state(hyp_vcpu);
+
+ hyp_vcpu->exit_code = exit_reason;
}
static void handle___pkvm_vcpu_load(struct kvm_cpu_context *host_ctxt)
@@ -395,7 +426,7 @@ static void handle___kvm_vcpu_run(struct kvm_cpu_context *host_ctxt)
ret = __kvm_vcpu_run(&hyp_vcpu->vcpu);
- sync_hyp_vcpu(hyp_vcpu);
+ sync_hyp_vcpu(hyp_vcpu, ret);
} else {
/* The host is fully trusted, run its vCPU directly. */
fpsimd_lazy_switch_to_guest(host_vcpu);
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 05/17] KVM: arm64: Skip fixed-feature state flush for protected vCPUs
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (3 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 04/17] KVM: arm64: Introduce per-EC entry handlers for pKVM Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-09-02 15:05 ` Vincent Donnefort
2026-08-31 16:34 ` [PATCH 06/17] KVM: arm64: Add {flush,sync}_hyp_timer_state() primitives Fuad Tabba
` (12 subsequent siblings)
17 siblings, 1 reply; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
flush_hyp_vcpu() copies the host's mdcr_el2, TWI/TWE and debug state
into the hyp vCPU on every entry, so for a protected vCPU the host's
trap configuration overrides the one pkvm_vcpu_init_traps() computed
at EL2. Move those copies into the non-protected branch, and sync the
debug state back for non-protected vCPUs only: a protected guest's
debug registers are hypervisor-owned, and it takes the host's TWI/TWE
at vCPU load. MDCR_EL2.TDA traps only the guest's own accesses; it
does not stop the world switch from loading host-supplied breakpoints
and MDSCR_EL1.
EL2's mdcr_el2 starts from 0, and HPMN == 0 is reserved without
FEAT_HPMN0. Set HPMN to the CPU's event counter count at vCPU load,
the value the host's copy carried; a protected guest's PMU accesses
trap regardless.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/hyp/nvhe/hyp-main.c | 31 +++++++++++++++++++-----------
arch/arm64/kvm/hyp/nvhe/pkvm.c | 2 +-
2 files changed, 21 insertions(+), 12 deletions(-)
diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
index ca7122b0bccdd..14e847b122abf 100644
--- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
+++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
@@ -231,7 +231,6 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
u8 esr_ec;
fpsimd_sve_flush();
- flush_debug_state(hyp_vcpu);
/*
* If we deal with a non-protected guest and the state is potentially
@@ -241,7 +240,14 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
if (vcpu_get_flag(host_vcpu, PKVM_HOST_STATE_DIRTY))
flush_hyp_vcpu_state(hyp_vcpu);
+
+ hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWI | HCR_TWE);
+ hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) &
+ (HCR_TWI | HCR_TWE);
+
+ hyp_vcpu->vcpu.arch.mdcr_el2 = host_vcpu->arch.mdcr_el2;
hyp_vcpu->vcpu.arch.iflags = host_vcpu->arch.iflags;
+ flush_debug_state(hyp_vcpu);
} else {
hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
}
@@ -249,17 +255,13 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
/* __hyp_running_vcpu must be NULL in a guest context. */
hyp_vcpu->vcpu.arch.ctxt.__hyp_running_vcpu = NULL;
- hyp_vcpu->vcpu.arch.mdcr_el2 = host_vcpu->arch.mdcr_el2;
/*
- * HCR_EL2.VSE is host-owned (a pending virtual SError to inject), not a
- * trap-control bit, so it must flow to the hyp vCPU alongside TWI/TWE
- * for the vSError to be delivered. sync_hyp_vcpu() reflects it back.
+ * A host-injected vSError is masked by the guest's own PSTATE.A, so it
+ * applies to protected guests too.
*/
- hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWI | HCR_TWE | HCR_VSE);
- hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) &
- (HCR_TWI | HCR_TWE | HCR_VSE);
-
- hyp_vcpu->vcpu.arch.vsesr_el2 = host_vcpu->arch.vsesr_el2;
+ hyp_vcpu->vcpu.arch.hcr_el2 &= ~HCR_VSE;
+ hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) & HCR_VSE;
+ hyp_vcpu->vcpu.arch.vsesr_el2 = host_vcpu->arch.vsesr_el2;
flush_hyp_vgic_state(hyp_vcpu);
@@ -288,7 +290,8 @@ static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, u32 exit_reason)
struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
fpsimd_sve_sync(&hyp_vcpu->vcpu);
- sync_debug_state(hyp_vcpu);
+ if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu))
+ sync_debug_state(hyp_vcpu);
if (pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
host_vcpu->arch.ctxt = hyp_vcpu->vcpu.arch.ctxt;
@@ -329,6 +332,12 @@ static void handle___pkvm_vcpu_load(struct kvm_cpu_context *host_ctxt)
/* Propagate WFx trapping flags */
hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWE | HCR_TWI);
hyp_vcpu->vcpu.arch.hcr_el2 |= hcr_el2 & (HCR_TWE | HCR_TWI);
+
+ /* HPMN == 0 is reserved without FEAT_HPMN0. */
+ if (system_supports_pmuv3())
+ u64p_replace_bits(&hyp_vcpu->vcpu.arch.mdcr_el2,
+ FIELD_GET(ARMV8_PMU_PMCR_N, read_sysreg(pmcr_el0)),
+ MDCR_EL2_HPMN);
} else {
memcpy(&hyp_vcpu->vcpu.arch.fgt, hyp_vcpu->host_vcpu->arch.fgt,
sizeof(hyp_vcpu->vcpu.arch.fgt));
diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
index 459bd9eb7e4bc..e85f13233da08 100644
--- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
+++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
@@ -102,7 +102,7 @@ static void pvm_init_traps_mdcr(struct kvm_vcpu *vcpu)
if (!kvm_has_feat(kvm, ID_AA64DFR0_EL1, PMUVer, IMP)) {
val |= MDCR_EL2_TPM | MDCR_EL2_TPMCR;
- val &= ~(MDCR_EL2_HPME | MDCR_EL2_MTPME | MDCR_EL2_HPMN_MASK);
+ val &= ~(MDCR_EL2_HPME | MDCR_EL2_MTPME);
}
if (!kvm_has_feat(kvm, ID_AA64DFR0_EL1, DebugVer, IMP))
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 06/17] KVM: arm64: Add {flush,sync}_hyp_timer_state() primitives
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (4 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 05/17] KVM: arm64: Skip fixed-feature state flush for protected vCPUs Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 07/17] KVM: arm64: Add system register reset framework for protected VMs Fuad Tabba
` (11 subsequent siblings)
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
From: Marc Zyngier <maz@kernel.org>
A protected guest's virtual timer is programmed from EL2's own copy of
the state and read back there, so the host cannot change the guest's
deadline. cntvoff_el2 is zeroed, so the guest's virtual time is the
physical time.
The flush also preserves the guest's CNTV and CNTP CVAL/CTL across the
host's full-context copy into the hyp vCPU.
Signed-off-by: Marc Zyngier <maz@kernel.org>
Co-developed-by: Fuad Tabba <fuad.tabba@linux.dev>
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/hyp/nvhe/hyp-main.c | 39 ++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
index 14e847b122abf..d4b0f69ff130c 100644
--- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
+++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
@@ -154,6 +154,33 @@ static void sync_hyp_vgic_state(struct pkvm_hyp_vcpu *hyp_vcpu)
host_cpu_if->vgic_lr[i] = hyp_cpu_if->vgic_lr[i];
}
+static void flush_hyp_timer_state(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu))
+ return;
+
+ /* A hyp vcpu has no offset, and sees vtime == ptime. */
+ write_sysreg(0, cntvoff_el2);
+ write_sysreg_el0(__vcpu_sys_reg(&hyp_vcpu->vcpu, CNTV_CVAL_EL0),
+ SYS_CNTV_CVAL);
+ isb();
+ write_sysreg_el0(__vcpu_sys_reg(&hyp_vcpu->vcpu, CNTV_CTL_EL0),
+ SYS_CNTV_CTL);
+}
+
+static void sync_hyp_timer_state(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu))
+ return;
+
+ /*
+ * Preserve the vtimer state so that it is always correct,
+ * even if the host tries to make a mess.
+ */
+ __vcpu_assign_sys_reg(&hyp_vcpu->vcpu, CNTV_CVAL_EL0, read_sysreg_el0(SYS_CNTV_CVAL));
+ __vcpu_assign_sys_reg(&hyp_vcpu->vcpu, CNTV_CTL_EL0, read_sysreg_el0(SYS_CNTV_CTL));
+}
+
static void __copy_vcpu_state(const struct kvm_vcpu *from_vcpu,
struct kvm_vcpu *to_vcpu)
{
@@ -249,7 +276,17 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
hyp_vcpu->vcpu.arch.iflags = host_vcpu->arch.iflags;
flush_debug_state(hyp_vcpu);
} else {
+ u64 v_cval = hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTV_CVAL_EL0];
+ u64 v_ctl = hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTV_CTL_EL0];
+ u64 p_cval = hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTP_CVAL_EL0];
+ u64 p_ctl = hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTP_CTL_EL0];
+
hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
+
+ hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTV_CVAL_EL0] = v_cval;
+ hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTV_CTL_EL0] = v_ctl;
+ hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTP_CVAL_EL0] = p_cval;
+ hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTP_CTL_EL0] = p_ctl;
}
/* __hyp_running_vcpu must be NULL in a guest context. */
@@ -264,6 +301,7 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
hyp_vcpu->vcpu.arch.vsesr_el2 = host_vcpu->arch.vsesr_el2;
flush_hyp_vgic_state(hyp_vcpu);
+ flush_hyp_timer_state(hyp_vcpu);
hyp_vcpu->vcpu.arch.pid = host_vcpu->arch.pid;
@@ -313,6 +351,7 @@ static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, u32 exit_reason)
host_vcpu->arch.hcr_el2 |= hyp_vcpu->vcpu.arch.hcr_el2 & HCR_VSE;
sync_hyp_vgic_state(hyp_vcpu);
+ sync_hyp_timer_state(hyp_vcpu);
hyp_vcpu->exit_code = exit_reason;
}
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 07/17] KVM: arm64: Add system register reset framework for protected VMs
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (5 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 06/17] KVM: arm64: Add {flush,sync}_hyp_timer_state() primitives Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-09-02 15:14 ` Joey Gouly
2026-08-31 16:34 ` [PATCH 08/17] KVM: arm64: Implement HVC handling for protected guests at EL2 Fuad Tabba
` (10 subsequent siblings)
17 siblings, 1 reply; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
Reset a protected VM's system registers at EL2 rather than taking the
host's values: add kvm_reset_pvm_sys_regs() and the
pvm_sys_reg_reset_vals[] table that drives it, and call it from
init_pkvm_hyp_vcpu() for protected vCPUs. The values follow the
host-side reset in sys_regs.c, with a poison value where it resets to
UNKNOWN, and for VBAR_EL1 and CONTEXTIDR_EL1 in place of the 0 it
resets them to. MPIDR_EL1 is derived from vcpu_id, as for any KVM
guest.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 1 +
arch/arm64/kvm/hyp/nvhe/pkvm.c | 5 ++
arch/arm64/kvm/hyp/nvhe/sys_regs.c | 91 ++++++++++++++++++++++++--
3 files changed, 93 insertions(+), 4 deletions(-)
diff --git a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
index 49a0a992047ba..a04b7c04d5135 100644
--- a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
+++ b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
@@ -95,6 +95,7 @@ bool kvm_handle_pvm_hvc64(struct kvm_vcpu *vcpu, u64 *exit_code);
bool kvm_handle_pvm_sysreg(struct kvm_vcpu *vcpu, u64 *exit_code);
bool kvm_handle_pvm_restricted(struct kvm_vcpu *vcpu, u64 *exit_code);
void kvm_init_pvm_id_regs(struct kvm_vcpu *vcpu);
+void kvm_reset_pvm_sys_regs(struct kvm_vcpu *vcpu);
int kvm_check_pvm_sysreg_table(void);
#endif /* __ARM64_KVM_NVHE_PKVM_H__ */
diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
index e85f13233da08..af334318d0a03 100644
--- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
+++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
@@ -551,6 +551,11 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu,
goto done;
ret = pkvm_vcpu_init_sve(hyp_vcpu, host_vcpu);
+ if (ret)
+ goto done;
+
+ if (pkvm_hyp_vcpu_is_protected(hyp_vcpu))
+ kvm_reset_pvm_sys_regs(&hyp_vcpu->vcpu);
done:
if (ret)
unpin_host_vcpu(host_vcpu);
diff --git a/arch/arm64/kvm/hyp/nvhe/sys_regs.c b/arch/arm64/kvm/hyp/nvhe/sys_regs.c
index 8758c68017765..ebfd48aa15b56 100644
--- a/arch/arm64/kvm/hyp/nvhe/sys_regs.c
+++ b/arch/arm64/kvm/hyp/nvhe/sys_regs.c
@@ -525,6 +525,84 @@ static const struct sys_reg_desc pvm_sys_reg_descs[] = {
/* Performance Monitoring Registers are restricted. */
};
+struct sys_reg_desc_reset {
+ int reg;
+ void (*reset)(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *rd);
+ u64 value;
+};
+
+/* Hardware value, as sys_regs.c's reset_actlr()/reset_amair_el1(). */
+static void reset_actlr(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
+{
+ __vcpu_assign_sys_reg(vcpu, r->reg, read_sysreg(actlr_el1));
+}
+
+static void reset_amair_el1(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
+{
+ __vcpu_assign_sys_reg(vcpu, r->reg, read_sysreg(amair_el1));
+}
+
+static void reset_mpidr(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
+{
+ __vcpu_assign_sys_reg(vcpu, r->reg, kvm_calculate_mpidr(vcpu));
+}
+
+static void reset_value(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
+{
+ __vcpu_assign_sys_reg(vcpu, r->reg, r->value);
+}
+
+#define RESET_VAL(REG, RESET_VAL) { REG, reset_value, RESET_VAL }
+
+#define RESET_ZERO(REG) RESET_VAL(REG, 0)
+
+#define RESET_UNKNOWN(REG) RESET_VAL(REG, 0x1de7ec7edbadc0deULL)
+
+#define RESET_FUNC(REG, RESET_FUNC) { REG, RESET_FUNC, 0 }
+
+/* Sorted ascending by reg; kvm_check_pvm_sysreg_table() enforces it. */
+static const struct sys_reg_desc_reset pvm_sys_reg_reset_vals[] = {
+ RESET_FUNC(MPIDR_EL1, reset_mpidr),
+ RESET_UNKNOWN(TPIDR_EL0),
+ RESET_UNKNOWN(TPIDRRO_EL0),
+ RESET_UNKNOWN(TPIDR_EL1),
+ RESET_ZERO(CNTKCTL_EL1),
+ RESET_UNKNOWN(PAR_EL1),
+ RESET_ZERO(MDCCINT_EL1),
+ RESET_ZERO(DISR_EL1),
+ RESET_ZERO(PMCCFILTR_EL0),
+ RESET_ZERO(PMUSERENR_EL0),
+ RESET_ZERO(CPACR_EL1),
+ RESET_VAL(CONTEXTIDR_EL1, 0x00000000dbadc0deULL),
+ RESET_VAL(SCTLR_EL1, 0x00C50078ULL),
+ RESET_FUNC(ACTLR_EL1, reset_actlr),
+ RESET_ZERO(TCR_EL1),
+ RESET_UNKNOWN(AFSR0_EL1),
+ RESET_UNKNOWN(AFSR1_EL1),
+ RESET_UNKNOWN(ESR_EL1),
+ RESET_UNKNOWN(MAIR_EL1),
+ RESET_FUNC(AMAIR_EL1, reset_amair_el1),
+ RESET_ZERO(MDSCR_EL1),
+ RESET_ZERO(ZCR_EL1),
+ RESET_UNKNOWN(TTBR0_EL1),
+ RESET_UNKNOWN(TTBR1_EL1),
+ RESET_UNKNOWN(FAR_EL1),
+ RESET_VAL(VBAR_EL1, 0x1de7ec7edbadc000ULL),
+ RESET_UNKNOWN(PIRE0_EL1),
+ RESET_UNKNOWN(PIR_EL1),
+};
+
+void kvm_reset_pvm_sys_regs(struct kvm_vcpu *vcpu)
+{
+ unsigned long i;
+
+ for (i = 0; i < ARRAY_SIZE(pvm_sys_reg_reset_vals); i++) {
+ const struct sys_reg_desc_reset *r = &pvm_sys_reg_reset_vals[i];
+
+ r->reset(vcpu, r);
+ }
+}
+
/*
* Initializes feature registers for protected vms.
*/
@@ -550,16 +628,21 @@ void kvm_init_pvm_id_regs(struct kvm_vcpu *vcpu)
}
/*
- * Checks that the sysreg table is unique and in-order.
- *
- * Returns 0 if the table is consistent, or 1 otherwise.
+ * Both tables must be unique and sorted ascending. pvm_sys_reg_descs.reg is the
+ * sys_reg() encoding, pvm_sys_reg_reset_vals.reg the vcpu_sysreg index, so they
+ * compare differently. BUG_ON() at __pkvm_init: fatal at boot.
*/
int kvm_check_pvm_sysreg_table(void)
{
unsigned int i;
for (i = 1; i < ARRAY_SIZE(pvm_sys_reg_descs); i++) {
- if (cmp_sys_reg(&pvm_sys_reg_descs[i-1], &pvm_sys_reg_descs[i]) >= 0)
+ if (cmp_sys_reg(&pvm_sys_reg_descs[i - 1], &pvm_sys_reg_descs[i]) >= 0)
+ return 1;
+ }
+
+ for (i = 1; i < ARRAY_SIZE(pvm_sys_reg_reset_vals); i++) {
+ if (pvm_sys_reg_reset_vals[i - 1].reg >= pvm_sys_reg_reset_vals[i].reg)
return 1;
}
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 08/17] KVM: arm64: Implement HVC handling for protected guests at EL2
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (6 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 07/17] KVM: arm64: Add system register reset framework for protected VMs Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-09-03 15:12 ` Joey Gouly
2026-08-31 16:34 ` [PATCH 09/17] KVM: arm64: Handle PSCI calls for protected VMs " Fuad Tabba
` (9 subsequent siblings)
17 siblings, 1 reply; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
Extend kvm_handle_pvm_hvc64() to handle SMCCC_VERSION,
SMCCC_ARCH_FEATURES and the vendor hypervisor call UID at EL2, so
these queries do not reach the host. ARCH_FEATURES is mandatory from
SMCCC 1.1, the version EL2 reports: it returns SUCCESS for itself and
for SMCCC_VERSION, and NOT_SUPPORTED for anything else.
Add handle_pvm_entry_hvc64() and handle_pvm_exit_hvc64(), which
forward a protected guest's HVCs to the host and return the reply; a
later patch wires them into the per-EC dispatch tables.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/hyp/nvhe/hyp-main.c | 28 ++++++++++++++++++++++++++++
arch/arm64/kvm/hyp/nvhe/pkvm.c | 29 +++++++++++++++++++++++++++++
2 files changed, 57 insertions(+)
diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
index d4b0f69ff130c..62864db1e099a 100644
--- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
+++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
@@ -4,6 +4,8 @@
* Author: Andrew Scull <ascull@google.com>
*/
+#include <kvm/arm_hypercalls.h>
+
#include <hyp/adjust_pc.h>
#include <hyp/switch.h>
@@ -33,6 +35,32 @@ void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt);
typedef void (*hyp_entry_exit_handler_fn)(struct pkvm_hyp_vcpu *);
+static void __maybe_unused handle_pvm_entry_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ int i;
+
+ for (i = 0; i < 4; i++) {
+ u64 ret =
+ READ_ONCE(hyp_vcpu->host_vcpu->arch.ctxt.regs.regs[i]);
+ vcpu_set_reg(&hyp_vcpu->vcpu, i, ret);
+ }
+}
+
+static void __maybe_unused handle_pvm_exit_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
+ int i;
+
+ WRITE_ONCE(host_vcpu->arch.fault.esr_el2,
+ hyp_vcpu->vcpu.arch.fault.esr_el2);
+
+ /* Pass the HVC function id (r0) and its arguments. */
+ for (i = 0; i < 8; i++) {
+ WRITE_ONCE(host_vcpu->arch.ctxt.regs.regs[i],
+ vcpu_get_reg(&hyp_vcpu->vcpu, i));
+ }
+}
+
static void handle_vm_entry_generic(struct pkvm_hyp_vcpu *hyp_vcpu)
{
vcpu_copy_flag(&hyp_vcpu->vcpu, hyp_vcpu->host_vcpu, PC_UPDATE_REQ);
diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
index af334318d0a03..0fe11f95e2e26 100644
--- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
+++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
@@ -1144,8 +1144,37 @@ bool kvm_handle_pvm_hvc64(struct kvm_vcpu *vcpu, u64 *exit_code)
{
u64 val[4] = { SMCCC_RET_INVALID_PARAMETER };
bool handled = true;
+ u32 feature;
+ uuid_t uuid;
switch (smccc_get_function(vcpu)) {
+ case ARM_SMCCC_VERSION_FUNC_ID:
+ /* Nothing to be handled by the host. Go back to the guest. */
+ val[0] = ARM_SMCCC_VERSION_1_1;
+ val[1] = 0;
+ val[2] = 0;
+ val[3] = 0;
+ break;
+ case ARM_SMCCC_ARCH_FEATURES_FUNC_ID:
+ /* SUCCESS only for the architecture calls EL2 implements. */
+ feature = smccc_get_arg1(vcpu);
+ switch (feature) {
+ case ARM_SMCCC_VERSION_FUNC_ID:
+ case ARM_SMCCC_ARCH_FEATURES_FUNC_ID:
+ val[0] = SMCCC_RET_SUCCESS;
+ break;
+ default:
+ val[0] = SMCCC_RET_NOT_SUPPORTED;
+ break;
+ }
+ break;
+ case ARM_SMCCC_VENDOR_HYP_CALL_UID_FUNC_ID:
+ uuid = ARM_SMCCC_VENDOR_HYP_UID_KVM;
+ val[0] = smccc_uuid_to_reg(&uuid, 0);
+ val[1] = smccc_uuid_to_reg(&uuid, 1);
+ val[2] = smccc_uuid_to_reg(&uuid, 2);
+ val[3] = smccc_uuid_to_reg(&uuid, 3);
+ break;
case ARM_SMCCC_VENDOR_HYP_KVM_FEATURES_FUNC_ID:
val[0] = BIT(ARM_SMCCC_KVM_FUNC_FEATURES);
val[0] |= BIT(ARM_SMCCC_KVM_FUNC_HYP_MEMINFO);
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 09/17] KVM: arm64: Handle PSCI calls for protected VMs at EL2
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (7 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 08/17] KVM: arm64: Implement HVC handling for protected guests at EL2 Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 10/17] KVM: arm64: Restrict KVM_ARM_VCPU_INIT and PSCI version for protected VMs Fuad Tabba
` (8 subsequent siblings)
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
EL2 implements PSCI 1.1 for protected VMs: CPU_ON, CPU_OFF,
AFFINITY_INFO, PSCI_VERSION and PSCI_FEATURES are decided at EL2
(CPU_ON and CPU_OFF still exit to the host, which only schedules or
parks the target), CPU_SUSPEND and the platform power operations are
forwarded to the host, and anything else returns NOT_SUPPORTED.
Three consequences follow:
- A protected VM has one primary vCPU, the first whose hyp vCPU is
created with mp_state RUNNABLE. A second one, or an mp_state other
than RUNNABLE or STOPPED, fails that vCPU's first KVM_RUN with
-EINVAL.
- CPU_ON finds its target among the hyp vCPUs, which exist from the
target's first KVM_RUN; before that the guest gets
INVALID_PARAMETERS.
- A vCPU EL2 holds powered off does not run: handle___kvm_vcpu_run()
returns ARM_EXCEPTION_IL, reported as KVM_EXIT_FAIL_ENTRY. Its
existing bail-outs return the same code instead of an -EINVAL that
handle_exit() did not recognise, for every hyp vCPU.
Non-protected VMs keep power_state ON and accept any mp_state.
Each protected vCPU is OFF, ON_PENDING or ON. CPU_ON moves the target
to ON_PENDING, the target's next run resets it and moves it to ON, and
a CPU_ON that fails in the host rolls it back to OFF. The racing
transitions are cmpxchg, and the reset state is published with a
release/acquire pair, documented at each site.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 14 +
arch/arm64/kvm/hyp/nvhe/hyp-main.c | 125 +++++++--
arch/arm64/kvm/hyp/nvhe/pkvm.c | 348 ++++++++++++++++++++++++-
3 files changed, 465 insertions(+), 22 deletions(-)
diff --git a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
index a04b7c04d5135..63b368baf0e72 100644
--- a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
+++ b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
@@ -29,6 +29,12 @@ struct pkvm_hyp_vcpu {
/* The previous exit's ARM_EXCEPTION_* code. */
u32 exit_code;
+
+ /*
+ * PSCI_0_2_AFFINITY_LEVEL_{OFF, ON_PENDING, ON}. A non-protected
+ * vCPU is always ON.
+ */
+ int power_state;
};
/*
@@ -46,6 +52,12 @@ struct pkvm_hyp_vm {
struct hyp_pool pool;
hyp_spinlock_t lock;
+ /*
+ * The vCPU initialised RUNNABLE: claimed under vm_table_lock,
+ * released only if its own init fails.
+ */
+ struct pkvm_hyp_vcpu *primary_vcpu;
+
/* Array of the hyp vCPU structures for this VM. */
struct pkvm_hyp_vcpu *vcpus[];
};
@@ -98,4 +110,6 @@ void kvm_init_pvm_id_regs(struct kvm_vcpu *vcpu);
void kvm_reset_pvm_sys_regs(struct kvm_vcpu *vcpu);
int kvm_check_pvm_sysreg_table(void);
+int pkvm_reset_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu);
+struct pkvm_hyp_vcpu *pkvm_mpidr_to_hyp_vcpu(struct pkvm_hyp_vm *vm, u64 mpidr);
#endif /* __ARM64_KVM_NVHE_PKVM_H__ */
diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
index 62864db1e099a..2fdb861abb22d 100644
--- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
+++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
@@ -10,6 +10,7 @@
#include <hyp/switch.h>
#include <linux/irqchip/arm-gic-v3.h>
+#include <uapi/linux/psci.h>
#include <asm/pgtable-types.h>
#include <asm/kvm_asm.h>
@@ -26,6 +27,8 @@
#include <nvhe/trace.h>
#include <nvhe/trap_handler.h>
+#include "../../sys_regs.h"
+
DEFINE_PER_CPU(struct kvm_nvhe_init_params, kvm_init_params);
/* Number of implemented GICv3 LRs. Used by flush_hyp_vcpu(). */
@@ -37,27 +40,103 @@ typedef void (*hyp_entry_exit_handler_fn)(struct pkvm_hyp_vcpu *);
static void __maybe_unused handle_pvm_entry_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
{
- int i;
+ u64 ret = READ_ONCE(hyp_vcpu->host_vcpu->arch.ctxt.regs.regs[0]);
+ u32 psci_fn = smccc_get_function(&hyp_vcpu->vcpu);
- for (i = 0; i < 4; i++) {
- u64 ret =
- READ_ONCE(hyp_vcpu->host_vcpu->arch.ctxt.regs.regs[i]);
- vcpu_set_reg(&hyp_vcpu->vcpu, i, ret);
+ switch (psci_fn) {
+ case PSCI_0_2_FN_CPU_ON:
+ case PSCI_0_2_FN64_CPU_ON:
+ /*
+ * Roll back a CPU_ON the host failed, unless the target
+ * already reached ON: it is running, and the guest sees
+ * SUCCESS.
+ */
+ if (ret != PSCI_RET_SUCCESS) {
+ unsigned long cpu_id = smccc_get_arg1(&hyp_vcpu->vcpu);
+ struct pkvm_hyp_vcpu *target_vcpu;
+ struct pkvm_hyp_vm *hyp_vm;
+ int prev;
+
+ hyp_vm = pkvm_hyp_vcpu_to_hyp_vm(hyp_vcpu);
+ target_vcpu = pkvm_mpidr_to_hyp_vcpu(hyp_vm, cpu_id);
+
+ /*
+ * pvm_psci_vcpu_on() resolved this MPIDR and vcpus[]
+ * entries are never removed, so the lookup cannot miss.
+ */
+ if (WARN_ON(!target_vcpu)) {
+ ret = PSCI_RET_INTERNAL_FAILURE;
+ break;
+ }
+
+ prev = cmpxchg_relaxed(&target_vcpu->power_state,
+ PSCI_0_2_AFFINITY_LEVEL_ON_PENDING,
+ PSCI_0_2_AFFINITY_LEVEL_OFF);
+ switch (prev) {
+ case PSCI_0_2_AFFINITY_LEVEL_ON_PENDING:
+ /*
+ * Leave reset_state.reset set: clearing it
+ * races a concurrent CPU_ON's re-publish and
+ * wedges the target at ON_PENDING. The stale
+ * pc/r0/be are the guest's own.
+ */
+ ret = PSCI_RET_INTERNAL_FAILURE;
+ break;
+ case PSCI_0_2_AFFINITY_LEVEL_ON:
+ case PSCI_0_2_AFFINITY_LEVEL_OFF:
+ /* Target already ran (and may have stopped). */
+ ret = PSCI_RET_SUCCESS;
+ break;
+ default:
+ ret = PSCI_RET_INTERNAL_FAILURE;
+ break;
+ }
+ }
+
+ break;
+ default:
+ break;
}
+
+ vcpu_set_reg(&hyp_vcpu->vcpu, 0, ret);
}
static void __maybe_unused handle_pvm_exit_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
{
struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
- int i;
+ int n, i;
- WRITE_ONCE(host_vcpu->arch.fault.esr_el2,
- hyp_vcpu->vcpu.arch.fault.esr_el2);
+ switch (smccc_get_function(&hyp_vcpu->vcpu)) {
+ /* CPU_ON: the host needs only the target MPIDR (x1). */
+ case PSCI_0_2_FN_CPU_ON:
+ case PSCI_0_2_FN64_CPU_ON:
+ n = 2;
+ break;
+
+ case PSCI_0_2_FN_CPU_OFF:
+ case PSCI_0_2_FN_SYSTEM_OFF:
+ case PSCI_0_2_FN_SYSTEM_RESET:
+ case PSCI_0_2_FN_CPU_SUSPEND:
+ case PSCI_0_2_FN64_CPU_SUSPEND:
+ n = 1;
+ break;
+
+ case PSCI_1_1_FN_SYSTEM_RESET2:
+ case PSCI_1_1_FN64_SYSTEM_RESET2:
+ n = 3;
+ break;
+
+ /* Unreachable: kvm_handle_pvm_hvc64() forwards only the calls above. */
+ default:
+ hyp_panic();
+ }
+
+ host_vcpu->arch.fault.esr_el2 = hyp_vcpu->vcpu.arch.fault.esr_el2;
/* Pass the HVC function id (r0) and its arguments. */
- for (i = 0; i < 8; i++) {
- WRITE_ONCE(host_vcpu->arch.ctxt.regs.regs[i],
- vcpu_get_reg(&hyp_vcpu->vcpu, i));
+ for (i = 0; i < n; i++) {
+ host_vcpu->arch.ctxt.regs.regs[i] =
+ vcpu_get_reg(&hyp_vcpu->vcpu, i);
}
}
@@ -477,14 +556,12 @@ static void handle___kvm_vcpu_run(struct kvm_cpu_context *host_ctxt)
{
struct pkvm_hyp_vcpu *hyp_vcpu;
struct kvm_vcpu *host_vcpu;
- int ret;
+ int ret = ARM_EXCEPTION_IL;
host_vcpu = get_host_hyp_vcpus(host_ctxt, 1, &hyp_vcpu);
- if (!host_vcpu) {
- ret = -EINVAL;
+ if (!host_vcpu)
goto out;
- }
if (unlikely(hyp_vcpu)) {
/*
@@ -493,8 +570,22 @@ static void handle___kvm_vcpu_run(struct kvm_cpu_context *host_ctxt)
* loading a vcpu. Therefore, if SME features enabled the host
* is misbehaving.
*/
- if (unlikely(system_supports_sme() && read_sysreg_s(SYS_SVCR))) {
- ret = -EINVAL;
+ if (unlikely(system_supports_sme() && read_sysreg_s(SYS_SVCR)))
+ goto out;
+
+ /*
+ * ON has a single writer, pkvm_reset_vcpu() on this CPU, so
+ * READ_ONCE suffices. ON_PENDING takes the reset; -ECANCELED
+ * is a rollback that raced it.
+ */
+ switch (READ_ONCE(hyp_vcpu->power_state)) {
+ case PSCI_0_2_AFFINITY_LEVEL_ON:
+ break;
+ case PSCI_0_2_AFFINITY_LEVEL_ON_PENDING:
+ if (pkvm_reset_vcpu(hyp_vcpu))
+ goto out;
+ break;
+ default:
goto out;
}
diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
index 0fe11f95e2e26..4dce489837cd2 100644
--- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
+++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
@@ -5,6 +5,7 @@
*/
#include <kvm/arm_hypercalls.h>
+#include <kvm/arm_psci.h>
#include <linux/kvm_host.h>
#include <linux/mm.h>
@@ -388,6 +389,40 @@ static void pkvm_init_features_from_host(struct pkvm_hyp_vm *hyp_vm, const struc
allowed_features, KVM_VCPU_MAX_FEATURES);
}
+static int pkvm_vcpu_init_psci(struct pkvm_hyp_vcpu *hyp_vcpu, u32 mp_state)
+{
+ struct vcpu_reset_state *reset_state = &hyp_vcpu->vcpu.arch.reset_state;
+ struct pkvm_hyp_vm *hyp_vm = pkvm_hyp_vcpu_to_hyp_vm(hyp_vcpu);
+ struct kvm_vcpu *host_vcpu;
+
+ if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
+ /* The host manages a non-protected vCPU: always ON at EL2. */
+ hyp_vcpu->power_state = PSCI_0_2_AFFINITY_LEVEL_ON;
+ return 0;
+ }
+
+ if (mp_state != KVM_MP_STATE_RUNNABLE && mp_state != KVM_MP_STATE_STOPPED)
+ return -EINVAL;
+
+ if (mp_state == KVM_MP_STATE_STOPPED) {
+ reset_state->reset = false;
+ hyp_vcpu->power_state = PSCI_0_2_AFFINITY_LEVEL_OFF;
+ return 0;
+ }
+
+ hyp_assert_lock_held(&vm_table_lock);
+ if (hyp_vm->primary_vcpu)
+ return -EINVAL;
+ hyp_vm->primary_vcpu = hyp_vcpu;
+
+ host_vcpu = hyp_vcpu->host_vcpu;
+ reset_state->pc = READ_ONCE(host_vcpu->arch.ctxt.regs.pc);
+ reset_state->r0 = READ_ONCE(host_vcpu->arch.ctxt.regs.regs[0]);
+ reset_state->reset = true;
+ hyp_vcpu->power_state = PSCI_0_2_AFFINITY_LEVEL_ON_PENDING;
+ return 0;
+}
+
static void unpin_host_vcpu(struct kvm_vcpu *host_vcpu)
{
if (host_vcpu)
@@ -402,6 +437,9 @@ static void unpin_host_sve_state(struct pkvm_hyp_vcpu *hyp_vcpu)
return;
sve_state = hyp_vcpu->vcpu.arch.sve_state;
+ if (!sve_state)
+ return;
+
hyp_unpin_shared_mem(sve_state,
sve_state + vcpu_sve_state_size(&hyp_vcpu->vcpu));
}
@@ -514,10 +552,12 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu,
struct kvm_vcpu *host_vcpu)
{
int ret = 0;
+ u32 mp_state;
if (hyp_pin_shared_mem(host_vcpu, host_vcpu + 1))
return -EBUSY;
+ mp_state = READ_ONCE(host_vcpu->arch.mp_state.mp_state);
hyp_vcpu->host_vcpu = host_vcpu;
hyp_vcpu->vcpu.kvm = &hyp_vm->kvm;
@@ -526,7 +566,6 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu,
hyp_vcpu->vcpu.arch.hw_mmu = &hyp_vm->kvm.arch.mmu;
hyp_vcpu->vcpu.arch.cflags = READ_ONCE(host_vcpu->arch.cflags);
- hyp_vcpu->vcpu.arch.mp_state.mp_state = KVM_MP_STATE_STOPPED;
if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
/*
@@ -556,9 +595,12 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu,
if (pkvm_hyp_vcpu_is_protected(hyp_vcpu))
kvm_reset_pvm_sys_regs(&hyp_vcpu->vcpu);
+ ret = pkvm_vcpu_init_psci(hyp_vcpu, mp_state);
done:
- if (ret)
+ if (ret) {
unpin_host_vcpu(host_vcpu);
+ unpin_host_sve_state(hyp_vcpu);
+ }
return ret;
}
@@ -935,13 +977,22 @@ int __pkvm_init_vcpu(pkvm_handle_t handle, struct kvm_vcpu *host_vcpu,
ret = init_pkvm_hyp_vcpu(hyp_vcpu, hyp_vm, host_vcpu);
if (ret)
- goto unlock;
+ goto unclaim;
ret = register_hyp_vcpu(hyp_vm, hyp_vcpu);
if (ret) {
unpin_host_vcpu(host_vcpu);
unpin_host_sve_state(hyp_vcpu);
+ goto unclaim;
}
+ goto unlock;
+unclaim:
+ /*
+ * Under vm_table_lock, so no other claim can have landed: undo
+ * this one.
+ */
+ if (hyp_vm->primary_vcpu == hyp_vcpu)
+ hyp_vm->primary_vcpu = NULL;
unlock:
hyp_spin_unlock(&vm_table_lock);
@@ -1133,6 +1184,293 @@ static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu)
ret[0] = SMCCC_RET_SUCCESS;
}
+/*
+ * Reset the vCPU to its power-on state and commit ON_PENDING -> ON, on the
+ * target's own CPU. Returns -ECANCELED, with no side effects, if a rollback
+ * raced the reset.
+ */
+int pkvm_reset_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ struct vcpu_reset_state *reset_state = &hyp_vcpu->vcpu.arch.reset_state;
+ int prev;
+
+ /*
+ * Pairs with smp_store_release(&reset_state->reset, true) in
+ * pvm_psci_vcpu_on(). The acquire must precede the cmpxchg: reversed, a
+ * winning cmpxchg with a false acquire would leave power_state == ON
+ * with the reset skipped.
+ */
+ if (!smp_load_acquire(&reset_state->reset))
+ return -ECANCELED;
+
+ prev = cmpxchg_relaxed(&hyp_vcpu->power_state,
+ PSCI_0_2_AFFINITY_LEVEL_ON_PENDING,
+ PSCI_0_2_AFFINITY_LEVEL_ON);
+ if (prev != PSCI_0_2_AFFINITY_LEVEL_ON_PENDING) {
+ /* The only other writer of ON_PENDING is the rollback. */
+ WARN_ON(prev != PSCI_0_2_AFFINITY_LEVEL_OFF);
+ return -ECANCELED;
+ }
+
+ kvm_reset_vcpu_core(&hyp_vcpu->vcpu);
+ kvm_reset_pvm_sys_regs(&hyp_vcpu->vcpu);
+
+ /* Must be done after resetting sys registers. */
+ kvm_reset_vcpu_psci(&hyp_vcpu->vcpu, reset_state);
+
+ hyp_vcpu->exit_code = 0;
+ /*
+ * power_state == ON rules out the other two writers, the source
+ * cmpxchg(OFF, ON_PENDING) and the rollback cmpxchg(ON_PENDING, OFF).
+ */
+ reset_state->reset = false;
+ return 0;
+}
+
+struct pkvm_hyp_vcpu *pkvm_mpidr_to_hyp_vcpu(struct pkvm_hyp_vm *hyp_vm,
+ u64 mpidr)
+{
+ struct pkvm_hyp_vcpu *hyp_vcpu;
+ int i;
+
+ mpidr &= MPIDR_HWID_BITMASK;
+
+ for (i = 0; i < hyp_vm->kvm.created_vcpus; i++) {
+ /* Pairs with smp_store_release() in register_hyp_vcpu(). */
+ hyp_vcpu = smp_load_acquire(&hyp_vm->vcpus[i]);
+
+ if (!hyp_vcpu)
+ continue;
+
+ if (mpidr == kvm_vcpu_get_mpidr_aff(&hyp_vcpu->vcpu))
+ return hyp_vcpu;
+ }
+
+ return NULL;
+}
+
+/*
+ * Returns true when handled at EL2, false when the host must wake the target
+ * vCPU.
+ */
+static bool pvm_psci_vcpu_on(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ struct pkvm_hyp_vm *hyp_vm = pkvm_hyp_vcpu_to_hyp_vm(hyp_vcpu);
+ struct vcpu_reset_state *reset_state;
+ struct pkvm_hyp_vcpu *target;
+ unsigned long cpu_id, ret;
+ int power_state;
+
+ cpu_id = smccc_get_arg1(&hyp_vcpu->vcpu);
+ if (!kvm_psci_valid_affinity(&hyp_vcpu->vcpu, cpu_id)) {
+ ret = PSCI_RET_INVALID_PARAMS;
+ goto error;
+ }
+
+ target = pkvm_mpidr_to_hyp_vcpu(hyp_vm, cpu_id);
+ if (!target) {
+ ret = PSCI_RET_INVALID_PARAMS;
+ goto error;
+ }
+
+ /*
+ * vCPUs race to power on the same target. Relaxed: reset_state
+ * is published by the release on reset_state.reset below.
+ */
+ power_state = cmpxchg_relaxed(&target->power_state,
+ PSCI_0_2_AFFINITY_LEVEL_OFF,
+ PSCI_0_2_AFFINITY_LEVEL_ON_PENDING);
+ switch (power_state) {
+ case PSCI_0_2_AFFINITY_LEVEL_ON_PENDING:
+ ret = PSCI_RET_ON_PENDING;
+ goto error;
+ case PSCI_0_2_AFFINITY_LEVEL_ON:
+ ret = PSCI_RET_ALREADY_ON;
+ goto error;
+ case PSCI_0_2_AFFINITY_LEVEL_OFF:
+ break;
+ default:
+ ret = PSCI_RET_INTERNAL_FAILURE;
+ goto error;
+ }
+
+ reset_state = &target->vcpu.arch.reset_state;
+ reset_state->pc = smccc_get_arg2(&hyp_vcpu->vcpu);
+ reset_state->r0 = smccc_get_arg3(&hyp_vcpu->vcpu);
+ reset_state->be = kvm_vcpu_is_be(&hyp_vcpu->vcpu);
+ /*
+ * Publish reset_state.{pc, r0, be} to the target vCPU. Pairs with
+ * smp_load_acquire(&reset_state->reset) in pkvm_reset_vcpu().
+ */
+ smp_store_release(&reset_state->reset, true);
+
+ /* The host requests KVM_REQ_VCPU_RESET and wakes the target. */
+ return false;
+
+error:
+ smccc_set_retval(&hyp_vcpu->vcpu, ret, 0, 0, 0);
+ return true;
+}
+
+static bool pvm_psci_vcpu_affinity_info(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ unsigned long target_affinity_mask, target_affinity, lowest_affinity_level;
+ struct pkvm_hyp_vm *hyp_vm = pkvm_hyp_vcpu_to_hyp_vm(hyp_vcpu);
+ struct kvm_vcpu *vcpu = &hyp_vcpu->vcpu;
+ unsigned long mpidr, ret;
+ int i, matching_cpus = 0;
+
+ target_affinity = smccc_get_arg1(vcpu);
+ lowest_affinity_level = smccc_get_arg2(vcpu);
+ if (!kvm_psci_valid_affinity(vcpu, target_affinity)) {
+ ret = PSCI_RET_INVALID_PARAMS;
+ goto done;
+ }
+
+ target_affinity_mask = kvm_psci_affinity_mask(lowest_affinity_level);
+ if (!target_affinity_mask) {
+ ret = PSCI_RET_INVALID_PARAMS;
+ goto done;
+ }
+
+ target_affinity &= target_affinity_mask;
+ ret = PSCI_0_2_AFFINITY_LEVEL_OFF;
+
+ /*
+ * If at least one vcpu matching target affinity is ON then return ON,
+ * then if at least one is PENDING_ON then return PENDING_ON.
+ * Otherwise, return OFF.
+ */
+ for (i = 0; i < hyp_vm->kvm.created_vcpus; i++) {
+ /* Pairs with smp_store_release() in register_hyp_vcpu(). */
+ struct pkvm_hyp_vcpu *target = smp_load_acquire(&hyp_vm->vcpus[i]);
+
+ if (!target)
+ continue;
+
+ mpidr = kvm_vcpu_get_mpidr_aff(&target->vcpu);
+
+ if ((mpidr & target_affinity_mask) == target_affinity) {
+ int power_state;
+
+ matching_cpus++;
+ power_state = READ_ONCE(target->power_state);
+ switch (power_state) {
+ case PSCI_0_2_AFFINITY_LEVEL_ON_PENDING:
+ ret = PSCI_0_2_AFFINITY_LEVEL_ON_PENDING;
+ break;
+ case PSCI_0_2_AFFINITY_LEVEL_ON:
+ ret = PSCI_0_2_AFFINITY_LEVEL_ON;
+ goto done;
+ case PSCI_0_2_AFFINITY_LEVEL_OFF:
+ break;
+ default:
+ ret = PSCI_RET_INTERNAL_FAILURE;
+ goto done;
+ }
+ }
+ }
+
+ if (!matching_cpus)
+ ret = PSCI_RET_INVALID_PARAMS;
+
+done:
+ /* Nothing to be handled by the host. Go back to the guest. */
+ smccc_set_retval(vcpu, ret, 0, 0, 0);
+ return true;
+}
+
+/*
+ * Returns true when handled at EL2, false when the host must stop scheduling
+ * the vCPU.
+ */
+static bool pvm_psci_vcpu_off(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ /* No other writer runs while this vCPU is ON and executing. */
+ WARN_ON(READ_ONCE(hyp_vcpu->power_state) != PSCI_0_2_AFFINITY_LEVEL_ON);
+
+ WRITE_ONCE(hyp_vcpu->power_state, PSCI_0_2_AFFINITY_LEVEL_OFF);
+
+ /* Return to the host so that it can finish powering off the vcpu. */
+ return false;
+}
+
+static bool pvm_psci_version(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ /* Nothing to be handled by the host. Go back to the guest. */
+ smccc_set_retval(&hyp_vcpu->vcpu, KVM_ARM_PSCI_1_1, 0, 0, 0);
+ return true;
+}
+
+static bool pvm_psci_features(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ struct kvm_vcpu *vcpu = &hyp_vcpu->vcpu;
+ u32 feature = smccc_get_arg1(vcpu);
+ unsigned long val;
+
+ switch (feature) {
+ case PSCI_0_2_FN_PSCI_VERSION:
+ case PSCI_0_2_FN_CPU_SUSPEND:
+ case PSCI_0_2_FN64_CPU_SUSPEND:
+ case PSCI_0_2_FN_CPU_OFF:
+ case PSCI_0_2_FN_CPU_ON:
+ case PSCI_0_2_FN64_CPU_ON:
+ case PSCI_0_2_FN_AFFINITY_INFO:
+ case PSCI_0_2_FN64_AFFINITY_INFO:
+ case PSCI_0_2_FN_SYSTEM_OFF:
+ case PSCI_0_2_FN_SYSTEM_RESET:
+ case PSCI_1_0_FN_PSCI_FEATURES:
+ case PSCI_1_1_FN_SYSTEM_RESET2:
+ case PSCI_1_1_FN64_SYSTEM_RESET2:
+ case ARM_SMCCC_VERSION_FUNC_ID:
+ val = PSCI_RET_SUCCESS;
+ break;
+ default:
+ val = PSCI_RET_NOT_SUPPORTED;
+ break;
+ }
+
+ /* Nothing to be handled by the host. Go back to the guest. */
+ smccc_set_retval(vcpu, val, 0, 0, 0);
+ return true;
+}
+
+static bool pkvm_handle_psci(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ struct kvm_vcpu *vcpu = &hyp_vcpu->vcpu;
+ u32 psci_fn = smccc_get_function(vcpu);
+
+ switch (psci_fn) {
+ case PSCI_0_2_FN_CPU_ON:
+ kvm_psci_narrow_to_32bit(vcpu);
+ fallthrough;
+ case PSCI_0_2_FN64_CPU_ON:
+ return pvm_psci_vcpu_on(hyp_vcpu);
+ case PSCI_0_2_FN_CPU_OFF:
+ return pvm_psci_vcpu_off(hyp_vcpu);
+ case PSCI_0_2_FN_AFFINITY_INFO:
+ kvm_psci_narrow_to_32bit(vcpu);
+ fallthrough;
+ case PSCI_0_2_FN64_AFFINITY_INFO:
+ return pvm_psci_vcpu_affinity_info(hyp_vcpu);
+ case PSCI_0_2_FN_PSCI_VERSION:
+ return pvm_psci_version(hyp_vcpu);
+ case PSCI_1_0_FN_PSCI_FEATURES:
+ return pvm_psci_features(hyp_vcpu);
+ case PSCI_0_2_FN_SYSTEM_RESET:
+ case PSCI_0_2_FN_CPU_SUSPEND:
+ case PSCI_0_2_FN64_CPU_SUSPEND:
+ case PSCI_0_2_FN_SYSTEM_OFF:
+ case PSCI_1_1_FN_SYSTEM_RESET2:
+ case PSCI_1_1_FN64_SYSTEM_RESET2:
+ return false; /* Handled by the host. */
+ default:
+ /* Unknown PSCI calls are answered here, not forwarded. */
+ smccc_set_retval(vcpu, PSCI_RET_NOT_SUPPORTED, 0, 0, 0);
+ return true;
+ }
+}
+
/*
* Handler for protected VM HVC calls.
*
@@ -1142,6 +1480,7 @@ static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu)
*/
bool kvm_handle_pvm_hvc64(struct kvm_vcpu *vcpu, u64 *exit_code)
{
+ struct pkvm_hyp_vcpu *hyp_vcpu = container_of(vcpu, struct pkvm_hyp_vcpu, vcpu);
u64 val[4] = { SMCCC_RET_INVALID_PARAMETER };
bool handled = true;
u32 feature;
@@ -1207,8 +1546,7 @@ bool kvm_handle_pvm_hvc64(struct kvm_vcpu *vcpu, u64 *exit_code)
pkvm_memunshare_call(val, vcpu);
break;
default:
- /* Punt everything else back to the host, for now. */
- handled = false;
+ return pkvm_handle_psci(hyp_vcpu);
}
if (handled)
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 10/17] KVM: arm64: Restrict KVM_ARM_VCPU_INIT and PSCI version for protected VMs
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (8 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 09/17] KVM: arm64: Handle PSCI calls for protected VMs " Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 11/17] KVM: arm64: Prevent host PC adjustments for protected vCPUs Fuad Tabba
` (7 subsequent siblings)
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
EL2 masks a protected VM's vCPU features down to the set pKVM allows,
so a KVM_ARM_VCPU_INIT that requests more succeeds and produces a
guest without them. Reject such an init with -EINVAL. The host and
EL2 both take the allowed set from kvm_pkvm_vcpu_allowed_features().
EL2 returns 1.1 for a protected guest's PSCI_VERSION whatever the
host is configured for, and forwards the platform calls to the host.
Require KVM_ARM_VCPU_PSCI_0_2: without it the host dispatches the
forwarded calls as PSCI 0.1. Reject a KVM_REG_ARM_PSCI_VERSION write
below 1.1: below it the host rejects the SYSTEM_RESET2 that EL2
advertises. KVM_CAP_ARM_PSCI is no longer advertised to protected VMs.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/include/asm/kvm_pkvm.h | 25 ++++++++++++++++++++++++-
arch/arm64/kvm/arm.c | 13 +++++++++++++
arch/arm64/kvm/hyp/nvhe/pkvm.c | 17 ++---------------
arch/arm64/kvm/hypercalls.c | 7 +++++++
4 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/arch/arm64/include/asm/kvm_pkvm.h b/arch/arm64/include/asm/kvm_pkvm.h
index c4c834d55e503..ee3ab505b01f7 100644
--- a/arch/arm64/include/asm/kvm_pkvm.h
+++ b/arch/arm64/include/asm/kvm_pkvm.h
@@ -33,7 +33,6 @@ static inline bool kvm_pkvm_ext_allowed(struct kvm *kvm, long ext)
{
switch (ext) {
case KVM_CAP_IRQCHIP:
- case KVM_CAP_ARM_PSCI:
case KVM_CAP_ARM_PSCI_0_2:
case KVM_CAP_NR_VCPUS:
case KVM_CAP_MAX_VCPUS:
@@ -62,6 +61,30 @@ static inline bool kvm_pkvm_ext_allowed(struct kvm *kvm, long ext)
}
}
+/*
+ * The vCPU features a protected VM may use: checked by the host at
+ * KVM_ARM_VCPU_INIT, applied by EL2 when the hyp VM is created.
+ */
+static inline void kvm_pkvm_vcpu_allowed_features(struct kvm *kvm,
+ unsigned long *allowed)
+{
+ bitmap_zero(allowed, KVM_VCPU_MAX_FEATURES);
+
+ set_bit(KVM_ARM_VCPU_PSCI_0_2, allowed);
+
+ if (kvm_pkvm_ext_allowed(kvm, KVM_CAP_ARM_PMU_V3))
+ set_bit(KVM_ARM_VCPU_PMU_V3, allowed);
+
+ if (kvm_pkvm_ext_allowed(kvm, KVM_CAP_ARM_PTRAUTH_ADDRESS))
+ set_bit(KVM_ARM_VCPU_PTRAUTH_ADDRESS, allowed);
+
+ if (kvm_pkvm_ext_allowed(kvm, KVM_CAP_ARM_PTRAUTH_GENERIC))
+ set_bit(KVM_ARM_VCPU_PTRAUTH_GENERIC, allowed);
+
+ if (kvm_pkvm_ext_allowed(kvm, KVM_CAP_ARM_SVE))
+ set_bit(KVM_ARM_VCPU_SVE, allowed);
+}
+
/*
* Check whether the KVM VM IOCTL is allowed in pKVM.
*
diff --git a/arch/arm64/kvm/arm.c b/arch/arm64/kvm/arm.c
index 8b080804bc90b..3e00edce23a99 100644
--- a/arch/arm64/kvm/arm.c
+++ b/arch/arm64/kvm/arm.c
@@ -1620,6 +1620,19 @@ static int kvm_vcpu_init_check_features(struct kvm_vcpu *vcpu,
if (features & ~system_supported_vcpu_features())
return -EINVAL;
+ /* Reject features EL2 would drop when it creates the hyp VM. */
+ if (kvm_vm_is_protected(vcpu->kvm)) {
+ DECLARE_BITMAP(allowed, KVM_VCPU_MAX_FEATURES);
+
+ kvm_pkvm_vcpu_allowed_features(vcpu->kvm, allowed);
+ if (!bitmap_subset(&features, allowed, KVM_VCPU_MAX_FEATURES))
+ return -EINVAL;
+
+ /* EL2 implements PSCI 1.1; the host must not dispatch as 0.1. */
+ if (!test_bit(KVM_ARM_VCPU_PSCI_0_2, &features))
+ return -EINVAL;
+ }
+
/*
* For now make sure that both address/generic pointer authentication
* features are requested by the userspace together.
diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
index 4dce489837cd2..4fb4c3c6ee92a 100644
--- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
+++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
@@ -367,23 +367,10 @@ static void pkvm_init_features_from_host(struct pkvm_hyp_vm *hyp_vm, const struc
if (kvm_pkvm_ext_allowed(kvm, KVM_CAP_ARM_MTE))
kvm->arch.flags |= host_arch_flags & BIT(KVM_ARCH_FLAG_MTE_ENABLED);
- bitmap_zero(allowed_features, KVM_VCPU_MAX_FEATURES);
+ kvm_pkvm_vcpu_allowed_features(kvm, allowed_features);
- set_bit(KVM_ARM_VCPU_PSCI_0_2, allowed_features);
-
- if (kvm_pkvm_ext_allowed(kvm, KVM_CAP_ARM_PMU_V3))
- set_bit(KVM_ARM_VCPU_PMU_V3, allowed_features);
-
- if (kvm_pkvm_ext_allowed(kvm, KVM_CAP_ARM_PTRAUTH_ADDRESS))
- set_bit(KVM_ARM_VCPU_PTRAUTH_ADDRESS, allowed_features);
-
- if (kvm_pkvm_ext_allowed(kvm, KVM_CAP_ARM_PTRAUTH_GENERIC))
- set_bit(KVM_ARM_VCPU_PTRAUTH_GENERIC, allowed_features);
-
- if (kvm_pkvm_ext_allowed(kvm, KVM_CAP_ARM_SVE)) {
- set_bit(KVM_ARM_VCPU_SVE, allowed_features);
+ if (kvm_pkvm_ext_allowed(kvm, KVM_CAP_ARM_SVE))
kvm->arch.flags |= host_arch_flags & BIT(KVM_ARCH_FLAG_GUEST_HAS_SVE);
- }
bitmap_and(kvm->arch.vcpu_features, host_kvm->arch.vcpu_features,
allowed_features, KVM_VCPU_MAX_FEATURES);
diff --git a/arch/arm64/kvm/hypercalls.c b/arch/arm64/kvm/hypercalls.c
index b11b8821c9fbc..121199bda1add 100644
--- a/arch/arm64/kvm/hypercalls.c
+++ b/arch/arm64/kvm/hypercalls.c
@@ -580,6 +580,13 @@ int kvm_arm_set_fw_reg(struct kvm_vcpu *vcpu, const struct kvm_one_reg *reg)
wants_02 = vcpu_has_feature(vcpu, KVM_ARM_VCPU_PSCI_0_2);
+ /*
+ * EL2 advertises PSCI 1.1; the host handles the forwarded calls
+ * by this version.
+ */
+ if (kvm_vm_is_protected(vcpu->kvm) && val < KVM_ARM_PSCI_1_1)
+ return -EINVAL;
+
switch (val) {
case KVM_ARM_PSCI_0_1:
if (wants_02)
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 11/17] KVM: arm64: Prevent host PC adjustments for protected vCPUs
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (9 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 10/17] KVM: arm64: Restrict KVM_ARM_VCPU_INIT and PSCI version for protected VMs Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 12/17] KVM: arm64: Inject an UNDEF at EL2 for unhandled protected guest exits Fuad Tabba
` (6 subsequent siblings)
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
__kvm_adjust_pc() lets the host advance a vCPU's PC or inject an
exception, which for a protected vCPU would let the host redirect
guest execution. Drop the request there: the entry handlers apply the
host's PC_UPDATE_REQ on re-entry, where EL2 allows it.
For a non-protected vCPU, adjusting the hyp vCPU while
PKVM_HOST_STATE_DIRTY is set loses the update at the next flush and
the guest re-executes an emulated MMIO access, so the flag selects
which copy to adjust. Adjusting the hyp vCPU copies PC_UPDATE_REQ in
and back out again. Without the copy back, INCREMENT_PC outlives the
adjustment and the next KVM_SET_VCPU_EVENTS trips
WARN_ON(INCREMENT_PC) in kvm_pend_exception().
Unloaded, the host copy is host-writable, so pin it and its VM before
adjusting. Only commit_pending_events(), under KVM_SET_VCPU_EVENTS,
reaches EL2 with no hyp vCPU loaded. KVM_RUN's call always has one.
enter_exception64() reads the VM's MTE flag and a host copy's
vcpu->kvm is host-written, so __kvm_adjust_pc_vm() takes the VM as a
parameter: the pinned host struct kvm when unloaded, the hyp VM
otherwise.
Suggested-by: Marc Zyngier <maz@kernel.org>
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/include/asm/kvm_asm.h | 1 +
arch/arm64/kvm/hyp/exception.c | 27 ++++++++++-------
arch/arm64/kvm/hyp/nvhe/hyp-main.c | 47 +++++++++++++++++++++++++++++-
3 files changed, 63 insertions(+), 12 deletions(-)
diff --git a/arch/arm64/include/asm/kvm_asm.h b/arch/arm64/include/asm/kvm_asm.h
index e5b92ac09e69e..d149afee7b4bd 100644
--- a/arch/arm64/include/asm/kvm_asm.h
+++ b/arch/arm64/include/asm/kvm_asm.h
@@ -280,6 +280,7 @@ extern int __kvm_at_s12(struct kvm_vcpu *vcpu, u32 op, u64 vaddr);
extern int __kvm_vcpu_run(struct kvm_vcpu *vcpu);
extern void __kvm_adjust_pc(struct kvm_vcpu *vcpu);
+extern void __kvm_adjust_pc_vm(struct kvm_vcpu *vcpu, struct kvm *kvm);
extern bool __vgic_v3_get_gic_config(void);
extern void __vgic_v3_init_lrs(void);
diff --git a/arch/arm64/kvm/hyp/exception.c b/arch/arm64/kvm/hyp/exception.c
index 754e2dc1df54a..bf9d8efce9984 100644
--- a/arch/arm64/kvm/hyp/exception.c
+++ b/arch/arm64/kvm/hyp/exception.c
@@ -66,8 +66,8 @@ static void __vcpu_write_spsr_und(struct kvm_vcpu *vcpu, u64 val)
* Here we manipulate the fields in order of the AArch64 SPSR_ELx layout, from
* MSB to LSB.
*/
-static void enter_exception64(struct kvm_vcpu *vcpu, unsigned long target_mode,
- enum exception_type type)
+static void enter_exception64(struct kvm_vcpu *vcpu, struct kvm *kvm,
+ unsigned long target_mode, enum exception_type type)
{
unsigned long sctlr, vbar, old, new, mode;
u64 exc_offset;
@@ -109,7 +109,7 @@ static void enter_exception64(struct kvm_vcpu *vcpu, unsigned long target_mode,
new |= (old & PSR_C_BIT);
new |= (old & PSR_V_BIT);
- if (kvm_has_mte(kern_hyp_va(vcpu->kvm)))
+ if (kvm_has_mte(kvm))
new |= PSR_TCO_BIT;
new |= (old & PSR_DIT_BIT);
@@ -294,7 +294,7 @@ static void enter_exception32(struct kvm_vcpu *vcpu, u32 mode, u32 vect_offset)
*vcpu_pc(vcpu) = vect_offset;
}
-static void kvm_inject_exception(struct kvm_vcpu *vcpu)
+static void kvm_inject_exception(struct kvm_vcpu *vcpu, struct kvm *kvm)
{
if (vcpu_el1_is_32bit(vcpu)) {
switch (vcpu_get_flag(vcpu, EXCEPT_MASK)) {
@@ -314,23 +314,23 @@ static void kvm_inject_exception(struct kvm_vcpu *vcpu)
} else {
switch (vcpu_get_flag(vcpu, EXCEPT_MASK)) {
case unpack_vcpu_flag(EXCEPT_AA64_EL1_SYNC):
- enter_exception64(vcpu, PSR_MODE_EL1h, except_type_sync);
+ enter_exception64(vcpu, kvm, PSR_MODE_EL1h, except_type_sync);
break;
case unpack_vcpu_flag(EXCEPT_AA64_EL1_SERR):
- enter_exception64(vcpu, PSR_MODE_EL1h, except_type_serror);
+ enter_exception64(vcpu, kvm, PSR_MODE_EL1h, except_type_serror);
break;
case unpack_vcpu_flag(EXCEPT_AA64_EL2_SYNC):
- enter_exception64(vcpu, PSR_MODE_EL2h, except_type_sync);
+ enter_exception64(vcpu, kvm, PSR_MODE_EL2h, except_type_sync);
break;
case unpack_vcpu_flag(EXCEPT_AA64_EL2_IRQ):
- enter_exception64(vcpu, PSR_MODE_EL2h, except_type_irq);
+ enter_exception64(vcpu, kvm, PSR_MODE_EL2h, except_type_irq);
break;
case unpack_vcpu_flag(EXCEPT_AA64_EL2_SERR):
- enter_exception64(vcpu, PSR_MODE_EL2h, except_type_serror);
+ enter_exception64(vcpu, kvm, PSR_MODE_EL2h, except_type_serror);
break;
default:
@@ -348,10 +348,10 @@ static void kvm_inject_exception(struct kvm_vcpu *vcpu)
* Adjust the guest PC (and potentially exception state) depending on
* flags provided by the emulation code.
*/
-void __kvm_adjust_pc(struct kvm_vcpu *vcpu)
+void __kvm_adjust_pc_vm(struct kvm_vcpu *vcpu, struct kvm *kvm)
{
if (vcpu_get_flag(vcpu, PENDING_EXCEPTION)) {
- kvm_inject_exception(vcpu);
+ kvm_inject_exception(vcpu, kvm);
vcpu_clear_flag(vcpu, PENDING_EXCEPTION);
vcpu_clear_flag(vcpu, EXCEPT_MASK);
} else if (vcpu_get_flag(vcpu, INCREMENT_PC)) {
@@ -359,3 +359,8 @@ void __kvm_adjust_pc(struct kvm_vcpu *vcpu)
vcpu_clear_flag(vcpu, INCREMENT_PC);
}
}
+
+void __kvm_adjust_pc(struct kvm_vcpu *vcpu)
+{
+ __kvm_adjust_pc_vm(vcpu, kern_hyp_va(vcpu->kvm));
+}
diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
index 2fdb861abb22d..5925d35abba8c 100644
--- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
+++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
@@ -741,11 +741,56 @@ static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt)
cpu_reg(host_ctxt, 1) = ret;
}
+static void adjust_pc_loaded(struct pkvm_hyp_vcpu *hyp_vcpu,
+ struct kvm_vcpu *host_vcpu)
+{
+ /*
+ * PKVM_HOST_STATE_DIRTY names the authoritative copy: the host's
+ * when set, the hyp vCPU's otherwise. Adjust that one.
+ */
+ if (vcpu_get_flag(host_vcpu, PKVM_HOST_STATE_DIRTY)) {
+ __kvm_adjust_pc_vm(host_vcpu, hyp_vcpu->vcpu.kvm);
+ return;
+ }
+
+ /* Reflect the consumed request back, otherwise it stays pending. */
+ vcpu_copy_flag(&hyp_vcpu->vcpu, host_vcpu, PC_UPDATE_REQ);
+ __kvm_adjust_pc(&hyp_vcpu->vcpu);
+ vcpu_copy_flag(host_vcpu, &hyp_vcpu->vcpu, PC_UPDATE_REQ);
+}
+
+static void adjust_pc_unloaded(struct kvm_vcpu *host_vcpu)
+{
+ struct kvm *host_kvm;
+
+ if (!is_protected_kvm_enabled()) {
+ __kvm_adjust_pc(host_vcpu);
+ return;
+ }
+
+ /* The host copy is authoritative, used only while pinned. */
+ if (hyp_pin_shared_mem(host_vcpu, host_vcpu + 1))
+ return;
+
+ host_kvm = kern_hyp_va(READ_ONCE(host_vcpu->kvm));
+ if (!hyp_pin_shared_mem(host_kvm, host_kvm + 1)) {
+ __kvm_adjust_pc_vm(host_vcpu, host_kvm);
+ hyp_unpin_shared_mem(host_kvm, host_kvm + 1);
+ }
+ hyp_unpin_shared_mem(host_vcpu, host_vcpu + 1);
+}
+
static void handle___kvm_adjust_pc(struct kvm_cpu_context *host_ctxt)
{
DECLARE_REG(struct kvm_vcpu *, vcpu, host_ctxt, 1);
+ struct pkvm_hyp_vcpu *hyp_vcpu;
+ struct kvm_vcpu *host_vcpu;
- __kvm_adjust_pc(kern_hyp_va(vcpu));
+ host_vcpu = __get_host_hyp_vcpus(vcpu, &hyp_vcpu);
+ if (!hyp_vcpu)
+ adjust_pc_unloaded(kern_hyp_va(vcpu));
+ else if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu))
+ adjust_pc_loaded(hyp_vcpu, host_vcpu);
}
static void handle___kvm_flush_vm_context(struct kvm_cpu_context *host_ctxt)
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 12/17] KVM: arm64: Inject an UNDEF at EL2 for unhandled protected guest exits
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (10 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 11/17] KVM: arm64: Prevent host PC adjustments for protected vCPUs Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 13/17] KVM: arm64: Add per-EC entry/exit state marshalling for protected guests Fuad Tabba
` (5 subsequent siblings)
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
Make pvm_exit_handlers[] an allow-list: a protected guest's exit class
is handled at EL2, forwarded where the host emulates it, or results in
an UNDEF at EL2. Any other class, once forwarded, would re-execute
forever. The host resolves such an exit by injecting an exception or
incrementing the PC. The marshalling that follows allows neither: a
protected vCPU takes only the UNDEF on a SYS64 trap and the external
abort on a forwarded IABT or DABT that EL2 builds for it.
WFxT takes the UNDEF, as it is not offered to protected guests.
FP/SIMD keeps the lazy switch, and the watchpoint entry is dropped: a
protected vCPU runs with TDE clear.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/hyp/nvhe/switch.c | 29 +++++++++++++++++++++++++----
1 file changed, 25 insertions(+), 4 deletions(-)
diff --git a/arch/arm64/kvm/hyp/nvhe/switch.c b/arch/arm64/kvm/hyp/nvhe/switch.c
index 7318e3e6a5f36..afddcf14f366e 100644
--- a/arch/arm64/kvm/hyp/nvhe/switch.c
+++ b/arch/arm64/kvm/hyp/nvhe/switch.c
@@ -203,15 +203,36 @@ static const exit_handler_fn hyp_exit_handlers[] = {
[ESR_ELx_EC_MOPS] = kvm_hyp_handle_mops,
};
+/* WFI/WFE exit to the host, which emulates them. WFxT is not offered. */
+static bool kvm_handle_pvm_wfx(struct kvm_vcpu *vcpu, u64 *exit_code)
+{
+ if (kvm_vcpu_get_esr(vcpu) & ESR_ELx_WFx_ISS_WFxT)
+ return kvm_handle_pvm_restricted(vcpu, exit_code);
+
+ return false;
+}
+
+/* Lazy FP/SIMD switch, or the UNDEF the host would otherwise be asked for. */
+static bool kvm_handle_pvm_fpsimd(struct kvm_vcpu *vcpu, u64 *exit_code)
+{
+ if (kvm_hyp_handle_fpsimd(vcpu, exit_code))
+ return true;
+
+ return kvm_handle_pvm_restricted(vcpu, exit_code);
+}
+
+/*
+ * A class not listed takes an UNDEF at EL2: the host has no way to
+ * inject one into a protected vCPU.
+ */
static const exit_handler_fn pvm_exit_handlers[] = {
- [0 ... ESR_ELx_EC_MAX] = NULL,
+ [0 ... ESR_ELx_EC_MAX] = kvm_handle_pvm_restricted,
+ [ESR_ELx_EC_WFx] = kvm_handle_pvm_wfx,
[ESR_ELx_EC_HVC64] = kvm_handle_pvm_hvc64,
[ESR_ELx_EC_SYS64] = kvm_handle_pvm_sys64,
- [ESR_ELx_EC_SVE] = kvm_handle_pvm_restricted,
- [ESR_ELx_EC_FP_ASIMD] = kvm_hyp_handle_fpsimd,
+ [ESR_ELx_EC_FP_ASIMD] = kvm_handle_pvm_fpsimd,
[ESR_ELx_EC_IABT_LOW] = kvm_hyp_handle_iabt_low,
[ESR_ELx_EC_DABT_LOW] = kvm_hyp_handle_dabt_low,
- [ESR_ELx_EC_WATCHPT_LOW] = kvm_hyp_handle_watchpt_low,
[ESR_ELx_EC_MOPS] = kvm_hyp_handle_mops,
};
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 13/17] KVM: arm64: Add per-EC entry/exit state marshalling for protected guests
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (11 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 12/17] KVM: arm64: Inject an UNDEF at EL2 for unhandled protected guest exits Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 14/17] KVM: arm64: Pend a protected guest's SError with HCR_EL2.VSE only Fuad Tabba
` (4 subsequent siblings)
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
Move a protected guest's state between the hyp vCPU and the host per
exception class instead of copying the whole context. Add
entry_hyp_pvm_handlers[] and exit_hyp_pvm_handlers[] for WFx, SYS64,
IABT, DABT and HVC64, and route protected guests through them: on exit
each handler copies out only what its class needs, and on re-entry
only what the host may have changed, once the host has completed the
access (INCREMENT_PC). entry_hyp_vm_handlers[] is removed: a
non-protected vCPU's iflags are copied wholesale.
The host's fault view is EL2's own syndrome with the guest register
index withheld, plus the addresses each class needs. The value of a
written register is passed in r0. MMIO data is clamped to the access
width and, for a load, sign-extended at EL2 from EL2's syndrome.
Endianness stays with the host.
Neither dispatch runs for a trap taken with an SError pending: EL2
does not handle it, and the guest replays it once the host has
injected the SError. The exit handlers would otherwise marshal a trap
EL2 never handled, and handle_pvm_exit_hvc64() would panic on an
unfiltered function id.
Wire in the HVC64 entry and exit handlers added earlier in the series.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/hyp/nvhe/hyp-main.c | 319 ++++++++++++++++++++++++++---
1 file changed, 290 insertions(+), 29 deletions(-)
diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
index 5925d35abba8c..461e15cab1b92 100644
--- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
+++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
@@ -38,7 +38,7 @@ void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt);
typedef void (*hyp_entry_exit_handler_fn)(struct pkvm_hyp_vcpu *);
-static void __maybe_unused handle_pvm_entry_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
+static void handle_pvm_entry_psci(struct pkvm_hyp_vcpu *hyp_vcpu)
{
u64 ret = READ_ONCE(hyp_vcpu->host_vcpu->arch.ctxt.regs.regs[0]);
u32 psci_fn = smccc_get_function(&hyp_vcpu->vcpu);
@@ -101,7 +101,12 @@ static void __maybe_unused handle_pvm_entry_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu
vcpu_set_reg(&hyp_vcpu->vcpu, 0, ret);
}
-static void __maybe_unused handle_pvm_exit_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
+static void handle_pvm_entry_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ handle_pvm_entry_psci(hyp_vcpu);
+}
+
+static void handle_pvm_exit_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
{
struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
int n, i;
@@ -131,8 +136,6 @@ static void __maybe_unused handle_pvm_exit_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
hyp_panic();
}
- host_vcpu->arch.fault.esr_el2 = hyp_vcpu->vcpu.arch.fault.esr_el2;
-
/* Pass the HVC function id (r0) and its arguments. */
for (i = 0; i < n; i++) {
host_vcpu->arch.ctxt.regs.regs[i] =
@@ -140,13 +143,242 @@ static void __maybe_unused handle_pvm_exit_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
}
}
-static void handle_vm_entry_generic(struct pkvm_hyp_vcpu *hyp_vcpu)
+static void handle_pvm_entry_wfx(struct pkvm_hyp_vcpu *hyp_vcpu)
{
- vcpu_copy_flag(&hyp_vcpu->vcpu, hyp_vcpu->host_vcpu, PC_UPDATE_REQ);
+ if (vcpu_get_flag(hyp_vcpu->host_vcpu, INCREMENT_PC)) {
+ vcpu_clear_flag(&hyp_vcpu->vcpu, PC_UPDATE_REQ);
+ kvm_incr_pc(&hyp_vcpu->vcpu);
+ }
}
-static const hyp_entry_exit_handler_fn entry_hyp_vm_handlers[] = {
- [0 ... ESR_ELx_EC_MAX] = handle_vm_entry_generic,
+static void handle_pvm_entry_sys64(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
+ bool pc_update;
+
+ /* Exceptions have priority over anything else */
+ if (vcpu_get_flag(host_vcpu, PENDING_EXCEPTION)) {
+ /* A host-requested exception on SYS64 is always an UNDEF. */
+ u32 esr = (ESR_ELx_EC_UNKNOWN << ESR_ELx_EC_SHIFT) | ESR_ELx_IL;
+
+ __vcpu_assign_sys_reg(&hyp_vcpu->vcpu, ESR_EL1, esr);
+ kvm_pend_exception(&hyp_vcpu->vcpu, EXCEPT_AA64_EL1_SYNC);
+ return;
+ }
+
+ /* Handle PC increment on a host-emulated access */
+ pc_update = vcpu_get_flag(host_vcpu, INCREMENT_PC);
+ if (pc_update) {
+ vcpu_clear_flag(&hyp_vcpu->vcpu, PC_UPDATE_REQ);
+ kvm_incr_pc(&hyp_vcpu->vcpu);
+ }
+
+ /* If the host emulated a read access, update the register */
+ if (pc_update &&
+ !esr_sys64_to_params(hyp_vcpu->vcpu.arch.fault.esr_el2).is_write) {
+ /* r0 as transfer register between the guest and the host. */
+ u64 rt_val = READ_ONCE(host_vcpu->arch.ctxt.regs.regs[0]);
+ int rt = kvm_vcpu_sys_get_rt(&hyp_vcpu->vcpu);
+
+ vcpu_set_reg(&hyp_vcpu->vcpu, rt, rt_val);
+ }
+}
+
+static void handle_pvm_entry_iabt(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ unsigned long cpsr = *vcpu_cpsr(&hyp_vcpu->vcpu);
+ u32 esr = ESR_ELx_IL;
+
+ if (!vcpu_get_flag(hyp_vcpu->host_vcpu, PENDING_EXCEPTION))
+ return;
+
+ /* The host's only IABT injection: an external abort. */
+ if ((cpsr & PSR_MODE_MASK) == PSR_MODE_EL0t)
+ esr |= (ESR_ELx_EC_IABT_LOW << ESR_ELx_EC_SHIFT);
+ else
+ esr |= (ESR_ELx_EC_IABT_CUR << ESR_ELx_EC_SHIFT);
+
+ esr |= ESR_ELx_FSC_EXTABT;
+
+ __vcpu_assign_sys_reg(&hyp_vcpu->vcpu, ESR_EL1, esr);
+ __vcpu_assign_sys_reg(&hyp_vcpu->vcpu, FAR_EL1,
+ kvm_vcpu_get_hfar(&hyp_vcpu->vcpu));
+
+ /* Injected by __kvm_adjust_pc() on entry. */
+ kvm_pend_exception(&hyp_vcpu->vcpu, EXCEPT_AA64_EL1_SYNC);
+}
+
+/*
+ * Clamp MMIO data to the access width, so a write does not leak the
+ * register's upper bits and a read takes no bits beyond the load. The
+ * host applies endianness.
+ */
+static inline u64 kvm_mmio_clamp_data(struct kvm_vcpu *vcpu, u64 val)
+{
+ unsigned int len = kvm_vcpu_dabt_get_as(vcpu);
+
+ return val & GENMASK_U64(len * 8 - 1, 0);
+}
+
+/*
+ * Complete an MMIO load: sign-extend from EL2's own syndrome, as the
+ * architecture does.
+ */
+static inline u64 kvm_mmio_read_data(struct kvm_vcpu *vcpu, u64 val)
+{
+ val = kvm_mmio_clamp_data(vcpu, val);
+
+ if (kvm_vcpu_dabt_issext(vcpu))
+ val = sign_extend64(val, kvm_vcpu_dabt_get_as(vcpu) * 8 - 1);
+
+ if (!kvm_vcpu_dabt_issf(vcpu))
+ val &= GENMASK_U64(31, 0);
+
+ return val;
+}
+
+static void handle_pvm_entry_dabt(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
+ bool pc_update;
+
+ /* Exceptions have priority over anything else */
+ if (vcpu_get_flag(host_vcpu, PENDING_EXCEPTION)) {
+ unsigned long cpsr = *vcpu_cpsr(&hyp_vcpu->vcpu);
+ u32 esr = ESR_ELx_IL;
+
+ if ((cpsr & PSR_MODE_MASK) == PSR_MODE_EL0t)
+ esr |= (ESR_ELx_EC_DABT_LOW << ESR_ELx_EC_SHIFT);
+ else
+ esr |= (ESR_ELx_EC_DABT_CUR << ESR_ELx_EC_SHIFT);
+
+ esr |= ESR_ELx_FSC_EXTABT;
+
+ __vcpu_assign_sys_reg(&hyp_vcpu->vcpu, ESR_EL1, esr);
+ __vcpu_assign_sys_reg(&hyp_vcpu->vcpu, FAR_EL1,
+ kvm_vcpu_get_hfar(&hyp_vcpu->vcpu));
+
+ /* Injected by __kvm_adjust_pc() on entry. */
+ kvm_pend_exception(&hyp_vcpu->vcpu, EXCEPT_AA64_EL1_SYNC);
+
+ /* Cancel any in-flight MMIO */
+ hyp_vcpu->vcpu.mmio_needed = false;
+ return;
+ }
+
+ /* Handle PC increment on MMIO */
+ pc_update = (hyp_vcpu->vcpu.mmio_needed &&
+ vcpu_get_flag(host_vcpu, INCREMENT_PC));
+ if (pc_update) {
+ vcpu_clear_flag(&hyp_vcpu->vcpu, PC_UPDATE_REQ);
+ kvm_incr_pc(&hyp_vcpu->vcpu);
+ }
+
+ /* If the host emulated an MMIO read, update the register */
+ if (pc_update && !kvm_vcpu_dabt_iswrite(&hyp_vcpu->vcpu)) {
+ /* r0 as transfer register between the guest and the host. */
+ u64 rd_val = READ_ONCE(host_vcpu->arch.ctxt.regs.regs[0]);
+ int rd = kvm_vcpu_dabt_get_rd(&hyp_vcpu->vcpu);
+
+ rd_val = kvm_mmio_read_data(&hyp_vcpu->vcpu, rd_val);
+ vcpu_set_reg(&hyp_vcpu->vcpu, rd, rd_val);
+ }
+
+ hyp_vcpu->vcpu.mmio_needed = false;
+}
+
+/* The host's view of a syndrome: the guest register index is withheld. */
+static u64 pvm_host_esr(u64 esr)
+{
+ switch (ESR_ELx_EC(esr)) {
+ case ESR_ELx_EC_WFx:
+ return esr & ~ESR_ELx_WFx_ISS_RN;
+ case ESR_ELx_EC_SYS64:
+ return esr & ~ESR_ELx_SYS64_ISS_RT_MASK;
+ case ESR_ELx_EC_DABT_LOW:
+ return esr & ~ESR_ELx_SRT_MASK;
+ default:
+ return esr;
+ }
+}
+
+static void handle_pvm_exit_wfx(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ hyp_vcpu->host_vcpu->arch.ctxt.regs.pstate =
+ hyp_vcpu->vcpu.arch.ctxt.regs.pstate & PSR_MODE_MASK;
+}
+
+static void handle_pvm_exit_sys64(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
+ u32 esr_el2 = hyp_vcpu->vcpu.arch.fault.esr_el2;
+
+ /* The mode is required for the host to emulate some sysregs */
+ host_vcpu->arch.ctxt.regs.pstate =
+ hyp_vcpu->vcpu.arch.ctxt.regs.pstate & PSR_MODE_MASK;
+
+ /* r0 as transfer register between the guest and the host. */
+ if (esr_sys64_to_params(esr_el2).is_write) {
+ int rt = kvm_vcpu_sys_get_rt(&hyp_vcpu->vcpu);
+ u64 rt_val = vcpu_get_reg(&hyp_vcpu->vcpu, rt);
+
+ host_vcpu->arch.ctxt.regs.regs[0] = rt_val;
+ }
+}
+
+static void handle_pvm_exit_iabt(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ hyp_vcpu->host_vcpu->arch.fault.hpfar_el2 =
+ hyp_vcpu->vcpu.arch.fault.hpfar_el2;
+}
+
+static void handle_pvm_exit_dabt(struct pkvm_hyp_vcpu *hyp_vcpu)
+{
+ struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
+
+ /*
+ * EL2 has no memslot view: a decodable data abort is prepared as MMIO
+ * for the host to resolve. One with ISV clear (LDP/STP, atomics) on
+ * unbacked memory gets an SEA from the host; EL2 does not decode it.
+ */
+ hyp_vcpu->vcpu.mmio_needed = kvm_vcpu_dabt_isvalid(&hyp_vcpu->vcpu);
+
+ /* r0 as transfer register between the guest and the host. */
+ if (hyp_vcpu->vcpu.mmio_needed &&
+ kvm_vcpu_dabt_iswrite(&hyp_vcpu->vcpu)) {
+ int rt = kvm_vcpu_dabt_get_rd(&hyp_vcpu->vcpu);
+ u64 rt_val = vcpu_get_reg(&hyp_vcpu->vcpu, rt);
+
+ rt_val = kvm_mmio_clamp_data(&hyp_vcpu->vcpu, rt_val);
+ host_vcpu->arch.ctxt.regs.regs[0] = rt_val;
+ }
+
+ host_vcpu->arch.ctxt.regs.pstate =
+ hyp_vcpu->vcpu.arch.ctxt.regs.pstate & PSR_MODE_MASK;
+ host_vcpu->arch.fault.far_el2 =
+ hyp_vcpu->vcpu.arch.fault.far_el2 & GENMASK(11, 0);
+ host_vcpu->arch.fault.hpfar_el2 = hyp_vcpu->vcpu.arch.fault.hpfar_el2;
+ __vcpu_assign_sys_reg(host_vcpu, SCTLR_EL1,
+ __vcpu_sys_reg(&hyp_vcpu->vcpu, SCTLR_EL1) &
+ (SCTLR_ELx_EE | SCTLR_EL1_E0E));
+}
+
+static const hyp_entry_exit_handler_fn entry_hyp_pvm_handlers[] = {
+ [0 ... ESR_ELx_EC_MAX] = NULL,
+ [ESR_ELx_EC_WFx] = handle_pvm_entry_wfx,
+ [ESR_ELx_EC_SYS64] = handle_pvm_entry_sys64,
+ [ESR_ELx_EC_IABT_LOW] = handle_pvm_entry_iabt,
+ [ESR_ELx_EC_DABT_LOW] = handle_pvm_entry_dabt,
+ [ESR_ELx_EC_HVC64] = handle_pvm_entry_hvc64,
+};
+
+static const hyp_entry_exit_handler_fn exit_hyp_pvm_handlers[] = {
+ [0 ... ESR_ELx_EC_MAX] = NULL,
+ [ESR_ELx_EC_WFx] = handle_pvm_exit_wfx,
+ [ESR_ELx_EC_SYS64] = handle_pvm_exit_sys64,
+ [ESR_ELx_EC_IABT_LOW] = handle_pvm_exit_iabt,
+ [ESR_ELx_EC_DABT_LOW] = handle_pvm_exit_dabt,
+ [ESR_ELx_EC_HVC64] = handle_pvm_exit_hvc64,
};
static void __hyp_sve_save_guest(struct kvm_vcpu *vcpu)
@@ -382,18 +614,6 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
hyp_vcpu->vcpu.arch.mdcr_el2 = host_vcpu->arch.mdcr_el2;
hyp_vcpu->vcpu.arch.iflags = host_vcpu->arch.iflags;
flush_debug_state(hyp_vcpu);
- } else {
- u64 v_cval = hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTV_CVAL_EL0];
- u64 v_ctl = hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTV_CTL_EL0];
- u64 p_cval = hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTP_CVAL_EL0];
- u64 p_ctl = hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTP_CTL_EL0];
-
- hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
-
- hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTV_CVAL_EL0] = v_cval;
- hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTV_CTL_EL0] = v_ctl;
- hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTP_CVAL_EL0] = p_cval;
- hyp_vcpu->vcpu.arch.ctxt.sys_regs[CNTP_CTL_EL0] = p_ctl;
}
/* __hyp_running_vcpu must be NULL in a guest context. */
@@ -418,10 +638,16 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
case ARM_EXCEPTION_IL:
break;
case ARM_EXCEPTION_TRAP:
- esr_ec = ESR_ELx_EC(kvm_vcpu_get_esr(&hyp_vcpu->vcpu));
- ec_handler = entry_hyp_vm_handlers[esr_ec];
- if (ec_handler)
- ec_handler(hyp_vcpu);
+ /* Nothing was marshalled for this trap, see sync_hyp_vcpu(). */
+ if (ARM_SERROR_PENDING(hyp_vcpu->exit_code))
+ break;
+
+ if (pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
+ esr_ec = ESR_ELx_EC(kvm_vcpu_get_esr(&hyp_vcpu->vcpu));
+ ec_handler = entry_hyp_pvm_handlers[esr_ec];
+ if (ec_handler)
+ ec_handler(hyp_vcpu);
+ }
break;
default:
BUG();
@@ -433,14 +659,30 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, u32 exit_reason)
{
struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
+ hyp_entry_exit_handler_fn ec_handler;
+ u8 esr_ec;
fpsimd_sve_sync(&hyp_vcpu->vcpu);
if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu))
sync_debug_state(hyp_vcpu);
+ sync_hyp_vgic_state(hyp_vcpu);
+ sync_hyp_timer_state(hyp_vcpu);
+
if (pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
- host_vcpu->arch.ctxt = hyp_vcpu->vcpu.arch.ctxt;
+ /*
+ * Protected: the host sees ESR_EL2 as EL2 took it, register
+ * index withheld; the fault addresses stay withheld unless the
+ * EC handler below adds them.
+ */
+ host_vcpu->arch.fault = (struct kvm_vcpu_fault_info) {
+ .esr_el2 = pvm_host_esr(hyp_vcpu->vcpu.arch.fault.esr_el2),
+ .disr_el1 = hyp_vcpu->vcpu.arch.fault.disr_el1,
+ };
} else {
+ /* Non-protected: the host gets the full fault. */
+ host_vcpu->arch.fault = hyp_vcpu->vcpu.arch.fault;
+ host_vcpu->arch.iflags = hyp_vcpu->vcpu.arch.iflags;
/*
* PC feeds trace_kvm_exit(), PSTATE.SS the host software-step
* machine, and both run before the next on-demand ctxt sync.
@@ -449,16 +691,35 @@ static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, u32 exit_reason)
host_vcpu->arch.ctxt.regs.pstate = hyp_vcpu->vcpu.arch.ctxt.regs.pstate;
}
- host_vcpu->arch.fault = hyp_vcpu->vcpu.arch.fault;
+ switch (ARM_EXCEPTION_CODE(exit_reason)) {
+ case ARM_EXCEPTION_IRQ:
+ break;
+ case ARM_EXCEPTION_TRAP:
+ /* SError pending: not handled at EL2, the guest replays it. */
+ if (ARM_SERROR_PENDING(exit_reason))
+ break;
- host_vcpu->arch.iflags = hyp_vcpu->vcpu.arch.iflags;
+ /* Per-EC marshalling is for protected guests only. */
+ if (pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
+ esr_ec = ESR_ELx_EC(kvm_vcpu_get_esr(&hyp_vcpu->vcpu));
+ ec_handler = exit_hyp_pvm_handlers[esr_ec];
+ if (ec_handler)
+ ec_handler(hyp_vcpu);
+ }
+ break;
+ case ARM_EXCEPTION_EL1_SERROR:
+ case ARM_EXCEPTION_IL:
+ break;
+ default:
+ BUG();
+ }
/* Cleared by hardware once the guest takes the vSError. */
host_vcpu->arch.hcr_el2 &= ~HCR_VSE;
host_vcpu->arch.hcr_el2 |= hyp_vcpu->vcpu.arch.hcr_el2 & HCR_VSE;
- sync_hyp_vgic_state(hyp_vcpu);
- sync_hyp_timer_state(hyp_vcpu);
+ if (pkvm_hyp_vcpu_is_protected(hyp_vcpu))
+ vcpu_clear_flag(host_vcpu, PC_UPDATE_REQ);
hyp_vcpu->exit_code = exit_reason;
}
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 14/17] KVM: arm64: Pend a protected guest's SError with HCR_EL2.VSE only
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (12 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 13/17] KVM: arm64: Add per-EC entry/exit state marshalling for protected guests Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 15/17] KVM: arm64: Reject host access to protected VM private state Fuad Tabba
` (3 subsequent siblings)
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
kvm_inject_serror_esr() emulates the exception entry when
serror_is_masked() reads PSTATE.A clear, but the host has no view of a
protected guest's PSTATE.A, and an emulated entry never reaches the
guest: the SError is lost. Set VSE instead and let the guest's own
PSTATE.A mask it.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/inject_fault.c | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/arch/arm64/kvm/inject_fault.c b/arch/arm64/kvm/inject_fault.c
index d6c4fc16f8795..b88098a7e756c 100644
--- a/arch/arm64/kvm/inject_fault.c
+++ b/arch/arm64/kvm/inject_fault.c
@@ -378,8 +378,11 @@ int kvm_inject_serror_esr(struct kvm_vcpu *vcpu, u64 esr)
*
* As we're emulating the SError injection we need to explicitly populate
* ESR_ELx.EC because hardware will not do it on our behalf.
+ *
+ * The host does not see a protected guest's PSTATE.A: leave the
+ * vSError to HCR_EL2.VSE below, which the guest masks itself.
*/
- if (!serror_is_masked(vcpu)) {
+ if (!vcpu_is_protected(vcpu) && !serror_is_masked(vcpu)) {
pend_serror_exception(vcpu);
esr |= FIELD_PREP(ESR_ELx_EC_MASK, ESR_ELx_EC_SERROR) | ESR_ELx_IL;
vcpu_write_sys_reg(vcpu, esr, exception_esr_elx(vcpu));
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 15/17] KVM: arm64: Reject host access to protected VM private state
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (13 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 14/17] KVM: arm64: Pend a protected guest's SError with HCR_EL2.VSE only Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 16/17] KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off Fuad Tabba
` (2 subsequent siblings)
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
A protected vCPU's register and debug state is no longer exposed to
the host. Host ioctls that would reach that state now fail rather
than operate on a copy that is not the guest's:
- KVM_GET_ONE_REG and KVM_SET_ONE_REG return -EPERM once the vCPU has
run: the copy then holds reset values plus what the exit handlers
marshal out. Pre-run access still builds the guest's boot state.
- KVM_ARM_VCPU_INIT returns -EPERM once the vCPU has run: it would
reset the host copy alone and rewrite mp_state, which EL2 reads
only at hyp vCPU creation, so a vCPU the guest powered off would
come back RUNNABLE.
- KVM_SET_VCPU_EVENTS rejects external-abort injection with -EPERM;
SError injection is forwarded and stays permitted.
- KVM_SET_GUEST_DEBUG returns -EPERM: a protected guest's debug state
is hypervisor-owned.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/kvm/arm.c | 7 +++++++
arch/arm64/kvm/guest.c | 19 +++++++++++++++++++
2 files changed, 26 insertions(+)
diff --git a/arch/arm64/kvm/arm.c b/arch/arm64/kvm/arm.c
index 3e00edce23a99..8caacdd4de020 100644
--- a/arch/arm64/kvm/arm.c
+++ b/arch/arm64/kvm/arm.c
@@ -1750,6 +1750,13 @@ static int kvm_arch_vcpu_ioctl_vcpu_init(struct kvm_vcpu *vcpu,
bool power_off = false;
int ret;
+ /*
+ * Re-init would reset the host copy alone and rewrite mp_state
+ * after EL2 has read it. Pre-run init stays allowed.
+ */
+ if (kvm_vm_is_protected(vcpu->kvm) && vcpu_has_run_once(vcpu))
+ return -EPERM;
+
/*
* Treat the power-off vCPU feature as ephemeral. Clear the bit to avoid
* reflecting it in the finalized feature set, thus limiting its scope
diff --git a/arch/arm64/kvm/guest.c b/arch/arm64/kvm/guest.c
index 23f725e2cf745..3a0ea2ae4a307 100644
--- a/arch/arm64/kvm/guest.c
+++ b/arch/arm64/kvm/guest.c
@@ -701,6 +701,10 @@ int kvm_arm_copy_reg_indices(struct kvm_vcpu *vcpu, u64 __user *uindices)
int kvm_arm_get_reg(struct kvm_vcpu *vcpu, const struct kvm_one_reg *reg)
{
+ /* Once the vCPU has run, the host copy is not the guest's state. */
+ if (kvm_vm_is_protected(vcpu->kvm) && vcpu_has_run_once(vcpu))
+ return -EPERM;
+
/* We currently use nothing arch-specific in upper 32 bits */
if ((reg->id & ~KVM_REG_SIZE_MASK) >> 32 != KVM_REG_ARM64 >> 32)
return -EINVAL;
@@ -718,6 +722,10 @@ int kvm_arm_get_reg(struct kvm_vcpu *vcpu, const struct kvm_one_reg *reg)
int kvm_arm_set_reg(struct kvm_vcpu *vcpu, const struct kvm_one_reg *reg)
{
+ /* Writes build the boot state; once run, EL2 owns the registers. */
+ if (kvm_vm_is_protected(vcpu->kvm) && vcpu_has_run_once(vcpu))
+ return -EPERM;
+
/* We currently use nothing arch-specific in upper 32 bits */
if ((reg->id & ~KVM_REG_SIZE_MASK) >> 32 != KVM_REG_ARM64 >> 32)
return -EINVAL;
@@ -786,6 +794,13 @@ int __kvm_arm_vcpu_set_events(struct kvm_vcpu *vcpu,
u64 esr = events->exception.serror_esr;
int ret = 0;
+ /*
+ * EL2 injects an external abort only to complete a forwarded abort.
+ * SError injection is forwarded.
+ */
+ if (kvm_vm_is_protected(vcpu->kvm) && ext_dabt_pending)
+ return -EPERM;
+
/*
* Immediately commit the pending SEA to the vCPU's architectural
* state which is necessary since we do not return a pending SEA
@@ -883,6 +898,10 @@ int kvm_arch_vcpu_ioctl_set_guest_debug(struct kvm_vcpu *vcpu,
{
trace_kvm_set_guest_debug(vcpu, dbg->control);
+ /* A protected guest's debug state is not exposed to the host. */
+ if (kvm_vm_is_protected(vcpu->kvm))
+ return -EPERM;
+
if (dbg->control & ~KVM_GUESTDBG_VALID_MASK)
return -EINVAL;
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 16/17] KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (14 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 15/17] KVM: arm64: Reject host access to protected VM private state Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 17/17] KVM: arm64: Document the protected VM userspace API Fuad Tabba
2026-08-31 19:27 ` [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
A protected vCPU's power state belongs to EL2, which changes it only
on the guest's own PSCI calls. KVM_SET_MP_STATE(RUNNABLE) on a vCPU
EL2 holds powered off changes the host's copy alone, and the guest's
next CPU_ON of that vCPU then fails: mp_state is no longer STOPPED, so
the host returns ALREADY_ON, which EL2 turns into INTERNAL_FAILURE.
SUSPENDED has the same effect.
Track at the host whether EL2 holds a protected vCPU powered off and
return -EPERM for both writes in that state. STOPPED stays permitted,
so a VMM can pause a vCPU, and RUNNABLE on a vCPU EL2 has powered on,
so it can resume one.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
arch/arm64/include/asm/kvm_host.h | 2 ++
arch/arm64/kvm/arm.c | 20 ++++++++++++++++++++
arch/arm64/kvm/pkvm.c | 21 +++++++++++++++++----
arch/arm64/kvm/psci.c | 3 +++
4 files changed, 42 insertions(+), 4 deletions(-)
diff --git a/arch/arm64/include/asm/kvm_host.h b/arch/arm64/include/asm/kvm_host.h
index 78a4d387f9fd4..39788c4331260 100644
--- a/arch/arm64/include/asm/kvm_host.h
+++ b/arch/arm64/include/asm/kvm_host.h
@@ -922,6 +922,8 @@ struct kvm_vcpu_arch {
/* vcpu power state */
struct kvm_mp_state mp_state;
spinlock_t mp_state_lock;
+ /* EL2 holds the protected vCPU powered off. Under mp_state_lock. */
+ bool pkvm_powered_off;
/* Cache some mmu pages needed inside spinlock regions */
struct kvm_mmu_memory_cache mmu_page_cache;
diff --git a/arch/arm64/kvm/arm.c b/arch/arm64/kvm/arm.c
index 8caacdd4de020..a96d01b2c5e86 100644
--- a/arch/arm64/kvm/arm.c
+++ b/arch/arm64/kvm/arm.c
@@ -777,9 +777,12 @@ static void __kvm_arm_vcpu_power_off(struct kvm_vcpu *vcpu)
kvm_vcpu_kick(vcpu);
}
+/* The guest's own CPU_OFF: EL2 has already powered the vCPU off. */
void kvm_arm_vcpu_power_off(struct kvm_vcpu *vcpu)
{
spin_lock(&vcpu->arch.mp_state_lock);
+ if (kvm_vm_is_protected(vcpu->kvm))
+ vcpu->arch.pkvm_powered_off = true;
__kvm_arm_vcpu_power_off(vcpu);
spin_unlock(&vcpu->arch.mp_state_lock);
}
@@ -801,6 +804,13 @@ static bool kvm_arm_vcpu_suspended(struct kvm_vcpu *vcpu)
return READ_ONCE(vcpu->arch.mp_state.mp_state) == KVM_MP_STATE_SUSPENDED;
}
+/* Only the guest's CPU_ON may start a vCPU EL2 holds powered off. */
+static bool kvm_pkvm_vcpu_is_powered_off(struct kvm_vcpu *vcpu)
+{
+ return kvm_vm_is_protected(vcpu->kvm) &&
+ vcpu->arch.pkvm_powered_off;
+}
+
int kvm_arch_vcpu_ioctl_get_mpstate(struct kvm_vcpu *vcpu,
struct kvm_mp_state *mp_state)
{
@@ -818,12 +828,22 @@ int kvm_arch_vcpu_ioctl_set_mpstate(struct kvm_vcpu *vcpu,
switch (mp_state->mp_state) {
case KVM_MP_STATE_RUNNABLE:
+ if (kvm_pkvm_vcpu_is_powered_off(vcpu)) {
+ ret = -EPERM;
+ break;
+ }
+
WRITE_ONCE(vcpu->arch.mp_state, *mp_state);
break;
case KVM_MP_STATE_STOPPED:
__kvm_arm_vcpu_power_off(vcpu);
break;
case KVM_MP_STATE_SUSPENDED:
+ if (kvm_pkvm_vcpu_is_powered_off(vcpu)) {
+ ret = -EPERM;
+ break;
+ }
+
kvm_arm_vcpu_suspend(vcpu);
break;
default:
diff --git a/arch/arm64/kvm/pkvm.c b/arch/arm64/kvm/pkvm.c
index 8e4c6e4bec123..8ae974fec42ba 100644
--- a/arch/arm64/kvm/pkvm.c
+++ b/arch/arm64/kvm/pkvm.c
@@ -118,12 +118,25 @@ static int __pkvm_create_hyp_vcpu(struct kvm_vcpu *vcpu)
return -ENOMEM;
ret = kvm_call_hyp_nvhe(__pkvm_init_vcpu, handle, vcpu, hyp_vcpu);
- if (!ret)
- vcpu_set_flag(vcpu, VCPU_PKVM_FINALIZED);
- else
+ if (ret) {
free_pages_exact(hyp_vcpu, hyp_vcpu_sz);
+ return ret;
+ }
- return ret;
+ /*
+ * Mirror EL2's seeding of power_state from mp_state. The hyp vCPU is
+ * published, so take mp_state_lock against kvm_psci_vcpu_on().
+ */
+ if (kvm_vm_is_protected(vcpu->kvm)) {
+ spin_lock(&vcpu->arch.mp_state_lock);
+ if (kvm_arm_vcpu_stopped(vcpu))
+ vcpu->arch.pkvm_powered_off = true;
+ spin_unlock(&vcpu->arch.mp_state_lock);
+ }
+
+ vcpu_set_flag(vcpu, VCPU_PKVM_FINALIZED);
+
+ return 0;
}
/*
diff --git a/arch/arm64/kvm/psci.c b/arch/arm64/kvm/psci.c
index e3db84400d1f8..208289a08099f 100644
--- a/arch/arm64/kvm/psci.c
+++ b/arch/arm64/kvm/psci.c
@@ -94,6 +94,9 @@ static unsigned long kvm_psci_vcpu_on(struct kvm_vcpu *source_vcpu)
*/
smp_wmb();
+ /* EL2 has committed the protected vCPU to ON_PENDING to get here. */
+ vcpu->arch.pkvm_powered_off = false;
+
WRITE_ONCE(vcpu->arch.mp_state.mp_state, KVM_MP_STATE_RUNNABLE);
kvm_vcpu_wake_up(vcpu);
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH 17/17] KVM: arm64: Document the protected VM userspace API
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (15 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 16/17] KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off Fuad Tabba
@ 2026-08-31 16:34 ` Fuad Tabba
2026-08-31 19:27 ` [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 16:34 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
EL2 owns a protected vCPU's state once it has run, and the ioctls that
would access it fail rather than silently operate on a copy that is not
the guest's. Describe the resulting API in pkvm.rst, point api.rst at
it from each ioctl that behaves differently, and note the errnos beside
the PVTIME attribute in devices/vcpu.rst and the PSCI version register
in fw-pseudo-registers.rst.
No functional change intended.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
Documentation/virt/kvm/api.rst | 22 ++-
.../virt/kvm/arm/fw-pseudo-registers.rst | 2 +
Documentation/virt/kvm/arm/pkvm.rst | 141 +++++++++++++++++-
Documentation/virt/kvm/devices/vcpu.rst | 4 +-
4 files changed, 161 insertions(+), 8 deletions(-)
diff --git a/Documentation/virt/kvm/api.rst b/Documentation/virt/kvm/api.rst
index e0430cc750c9e..212ac2aa5c6bd 100644
--- a/Documentation/virt/kvm/api.rst
+++ b/Documentation/virt/kvm/api.rst
@@ -1322,6 +1322,9 @@ guests, across different userspace implementations. Nevertheless, userspace
can still emulate all Arm exceptions by manipulating individual registers
using the KVM_SET_ONE_REG API.
+For a protected VM, setting ext_dabt_pending returns -EPERM; see
+Documentation/virt/kvm/arm/pkvm.rst. Injecting an SError is unaffected.
+
See KVM_GET_VCPU_EVENTS for the data structure.
Calling this ioctl on a vCPU that hasn't been initialized will return
@@ -1624,6 +1627,11 @@ For arm64/riscv:
The only states that are valid are KVM_MP_STATE_STOPPED and
KVM_MP_STATE_RUNNABLE which reflect if the vcpu should be paused or not.
+On arm64, once a protected VM's vcpu has run, KVM_MP_STATE_RUNNABLE and
+KVM_MP_STATE_SUSPENDED return -EPERM if the guest has powered it off with
+CPU_OFF or has not yet brought it online with CPU_ON: only the guest can
+power it on. See Documentation/virt/kvm/arm/pkvm.rst.
+
On LoongArch, only the KVM_MP_STATE_RUNNABLE state is used to reflect
whether the vcpu is runnable.
@@ -2324,7 +2332,8 @@ Errors:
ENOENT no such register
EINVAL invalid register ID, or no such register or used with VMs in
protected virtualization mode on s390
- EPERM (arm64) register access not allowed before vcpu finalization
+ EPERM (arm64) register access not allowed before vcpu
+ finalization, or after a protected VM's vcpu has run
EBUSY (riscv) changing register value not allowed after the vcpu
has run at least once
====== ============================================================
@@ -2949,7 +2958,8 @@ Errors include:
ENOENT no such register
EINVAL invalid register ID, or no such register or used with VMs in
protected virtualization mode on s390
- EPERM (arm64) register access not allowed before vcpu finalization
+ EPERM (arm64) register access not allowed before vcpu
+ finalization, or after a protected VM's vcpu has run
======== ============================================================
(These error codes are indicative only: do not rely on a specific error
@@ -3484,6 +3494,7 @@ Errors:
====== =================================================================
EINVAL the target is unknown, or the combination of features is invalid.
ENOENT a features bit specified is unknown.
+ EPERM the vcpu belongs to a protected VM and has already run.
====== =================================================================
This tells KVM what type of CPU to present to the guest, and what
@@ -3512,6 +3523,10 @@ after the vcpu has been run. This will reset the vcpu to its initial
state. All calls to this function after the initial call must use the same
target and same set of feature flags, otherwise EINVAL will be returned.
+For a protected VM this ioctl returns EPERM once the vcpu has run, and the
+features it accepts are restricted: KVM_ARM_VCPU_PSCI_0_2 is required. See
+Documentation/virt/kvm/arm/pkvm.rst.
+
Possible features:
- KVM_ARM_VCPU_POWER_OFF: Starts the CPU in a power-off state.
@@ -3790,6 +3805,9 @@ can be determined by querying the KVM_CAP_GUEST_DEBUG_HW_BPS and
KVM_CAP_GUEST_DEBUG_HW_WPS capabilities which return a positive number
indicating the number of supported registers.
+On arm64, this ioctl returns -EPERM for a protected VM: debugging a
+protected guest is not supported. See Documentation/virt/kvm/arm/pkvm.rst.
+
For ppc, the KVM_CAP_PPC_GUEST_DEBUG_SSTEP capability indicates whether
the single-step debug event (KVM_GUESTDBG_SINGLESTEP) is supported.
diff --git a/Documentation/virt/kvm/arm/fw-pseudo-registers.rst b/Documentation/virt/kvm/arm/fw-pseudo-registers.rst
index d78b53b05dfcf..c07471e258ea2 100644
--- a/Documentation/virt/kvm/arm/fw-pseudo-registers.rst
+++ b/Documentation/virt/kvm/arm/fw-pseudo-registers.rst
@@ -33,6 +33,8 @@ The following registers are defined:
highest PSCI version implemented by KVM and compatible with v0.2)
- Allows any PSCI version implemented by KVM and compatible with
v0.2 to be set with SET_ONE_REG
+ - Returns -EINVAL for a version below v1.1 on a protected VM (see
+ Documentation/virt/kvm/arm/pkvm.rst)
- Affects the whole VM (even if the register view is per-vcpu)
* KVM_REG_ARM_SMCCC_ARCH_WORKAROUND_1:
diff --git a/Documentation/virt/kvm/arm/pkvm.rst b/Documentation/virt/kvm/arm/pkvm.rst
index 514992a79a833..4411599fd508f 100644
--- a/Documentation/virt/kvm/arm/pkvm.rst
+++ b/Documentation/virt/kvm/arm/pkvm.rst
@@ -15,7 +15,8 @@ Overview
Booting a host kernel with '``kvm-arm.mode=protected``' enables
"Protected KVM" (pKVM). During boot, pKVM installs a stage-2 identity
map page-table for the host and uses it to isolate the hypervisor
-running at EL2 from the rest of the host running at EL1/0.
+running at EL2 from the rest of the host running at EL1/0. pKVM requires
+a GICv3 interrupt controller.
pKVM permits creation of protected virtual machines (pVMs) by passing
the ``KVM_VM_TYPE_ARM_PROTECTED`` machine type identifier to the
@@ -31,7 +32,7 @@ See hypercalls.rst for more details.
Isolation mechanisms
====================
-pKVM relies on a number of mechanisms to isolate PVMs from the host:
+pKVM relies on a number of mechanisms to isolate pVMs from the host:
CPU memory isolation
--------------------
@@ -67,12 +68,20 @@ largely due to the lack of MMU notifiers:
then it will either return ``-EFAULT`` or forcefully reclaim the
memory pages. Reclaimed memory is zeroed by the hypervisor and a
subsequent attempt to access it in the pVM will return ``-EFAULT``
- from the ``VCPU_RUN`` ioctl().
+ from the ``KVM_RUN`` ioctl().
CPU state isolation
-------------------
-Status: **Unimplemented.**
+Status: CPU register state of protected vCPUs is managed entirely at EL2.
+
+pKVM performs the complete context switch for protected vCPUs within the
+hypervisor. Protected vCPU state is initialised by the hypervisor to
+architecturally defined reset values, and only what each exit needs is
+synchronised back to the host.
+
+The user-visible consequences are described under `API behaviour for
+protected VMs`_.
DMA isolation using an IOMMU
----------------------------
@@ -89,13 +98,135 @@ The FF-A proxy ensures that the host cannot share pVM or hypervisor
memory with Trustzone as part of a "confused deputy" attack.
The PSCI proxy ensures that CPUs always have the stage-2 identity map
-installed when they are executing in the host.
+installed when they are executing in the host. This proxy is distinct
+from the PSCI handling provided to protected guests, which is described
+under `API behaviour for protected VMs`_.
Protected VM firmware (pvmfw)
-----------------------------
Status: **Unimplemented.**
+API behaviour for protected VMs
+===============================
+
+Protected vCPU state is owned by EL2 (see `CPU state isolation`_). The VMM
+configures a vCPU before its first ``KVM_RUN``; afterwards the state is
+private to the guest and the ioctls that access it return ``-EPERM``. The
+errors follow one rule: ``-EPERM`` means the host asked for state that the
+guest owns, and ``-EINVAL`` means the request is not valid for a protected
+VM. The ioctls themselves are described in Documentation/virt/kvm/api.rst.
+
+Boot
+----
+
+A protected VM boots from a single primary vCPU. Before the first
+``KVM_RUN``, the VMM prepares the boot state:
+
+* Set ``KVM_MP_STATE_RUNNABLE`` on the primary vCPU and
+ ``KVM_MP_STATE_STOPPED`` on every other vCPU. EL2 allows only one
+ RUNNABLE primary per protected VM. A second RUNNABLE vCPU fails at its
+ first ``KVM_RUN``.
+* Set the primary vCPU's boot state with ``KVM_SET_ONE_REG``: the kernel
+ entry address in ``PC`` and the DTB pointer in ``x0``.
+
+``PC`` and ``x0`` are the only registers EL2 takes from the host. Other
+pre-run writes are accepted, but the guest starts from the architectural
+reset values.
+
+Secondary vCPUs are started by the guest itself through PSCI ``CPU_ON``
+(see `Power state`_), which supplies their entry point and context ID.
+The VMM cannot choose where they boot.
+
+vCPU state
+----------
+
+* ``KVM_GET_ONE_REG`` and ``KVM_SET_ONE_REG`` return ``-EPERM`` once the
+ vCPU has run. Before that, they access the host-side copy from which
+ EL2 builds the guest's boot state (see `Boot`_).
+* ``KVM_ARM_VCPU_INIT`` accepts only the vCPU features that a protected
+ guest supports and returns ``-EINVAL`` otherwise.
+ ``KVM_ARM_VCPU_PSCI_0_2`` is required, as EL2 implements PSCI 1.1 for
+ the guest (see `Power state`_). ``KVM_ARM_VCPU_EL1_32BIT`` is not
+ supported: protected guests run in AArch64 only and see no AArch32
+ support in ``ID_AA64PFR0_EL1``. Once the vCPU has run,
+ ``KVM_ARM_VCPU_INIT`` returns ``-EPERM``, as re-initialising it would
+ reset the host-side copy alone.
+* ``KVM_SET_VCPU_EVENTS`` returns ``-EPERM`` for external-abort injection
+ (``ext_dabt_pending``). SError injection is unaffected.
+* ``KVM_SET_GUEST_DEBUG`` returns ``-EPERM`` (see `Debug`_).
+
+Power state
+-----------
+
+EL2 implements PSCI 1.1 for a protected guest. The calls that move a
+vCPU's power state, ``CPU_ON``, ``CPU_OFF`` and ``AFFINITY_INFO``, are
+handled at EL2, and the host cannot change the outcome: for ``CPU_ON``
+the host only schedules the target, which EL2 has already reset to the
+entry point the guest chose, and for ``CPU_OFF`` it only stops
+scheduling it. A vCPU becomes a valid ``CPU_ON`` target at its first
+``KVM_RUN``, and before that EL2 returns ``INVALID_PARAMETERS``. The
+platform calls, ``CPU_SUSPEND``, ``SYSTEM_OFF``, ``SYSTEM_RESET`` and
+``SYSTEM_RESET2``, are forwarded to the host and behave as for a
+non-protected VM, with the ``SYSTEM_*`` calls exiting to the VMM as
+``KVM_EXIT_SYSTEM_EVENT``; the ``SYSTEM_RESET2`` reset type and cookie
+are in the guest's registers, which ``KVM_GET_ONE_REG`` rejects once
+the vCPU has run. Any other function returns ``NOT_SUPPORTED``, and
+``PSCI_FEATURES`` reports the same set. Because the host handles those
+forwarded calls, ``KVM_SET_ONE_REG`` on ``KVM_REG_ARM_PSCI_VERSION``
+returns ``-EINVAL`` for a version below 1.1.
+
+Once a vCPU has run, its power state follows the guest's PSCI calls, not
+the VMM's. ``KVM_SET_MP_STATE`` with ``KVM_MP_STATE_STOPPED`` still stops
+the vCPU, so the VMM can pause it. ``KVM_MP_STATE_RUNNABLE`` and
+``KVM_MP_STATE_SUSPENDED`` return ``-EPERM`` for a vCPU that the guest has
+powered off with ``CPU_OFF``, or has not yet brought online with ``CPU_ON``:
+only an in-guest ``CPU_ON`` can power it on.
+
+Other interface differences
+---------------------------
+
+* ``KVM_CHECK_EXTENSION`` reports only the capabilities that pKVM supports
+ for protected guests, and ``KVM_ENABLE_CAP`` accepts only those. Query
+ them on the VM file descriptor: the system file descriptor has no VM to
+ filter against. The filter does not cover every interface either:
+ device-fd configuration (for example the VGIC after
+ ``KVM_CREATE_DEVICE``) and vCPU attributes are unfiltered, and can refuse
+ what a capability reported as available. ``KVM_ARM_VCPU_PVTIME_CTRL``
+ returns ``-EPERM``, for example, since steal time cannot work for a
+ protected guest.
+* The vGIC of a protected VM remains host-managed: device creation,
+ configuration and interrupt injection all work as they do for a
+ non-protected VM.
+* ``KVM_ARM_SET_COUNTER_OFFSET`` and ``KVM_ARM_GET_REG_WRITABLE_MASKS``
+ return ``-EINVAL``: their capabilities are not offered to a protected
+ VM, whose counter offset and ID registers are set by EL2. A protected
+ guest sees the physical timebase.
+* A protected guest's first access to each page of memory exits to the
+ host, since the hypervisor cannot tell memory from a device before the
+ page is mapped. For a store, the host sees the value of the register
+ the syndrome names, clamped to the access width, and nothing else from
+ the register file.
+* A protected guest that uses a feature it was not given, or executes an
+ ``SMC``, takes an undefined instruction exception from the hypervisor;
+ the host is not involved.
+* The hypervisor handles a protected guest's SMCCC calls itself and does
+ not involve the host. A function it does not implement returns
+ ``NOT_SUPPORTED``.
+* The hypervisor can decode a trapped guest access only from the CPU's
+ instruction syndrome, which is provided only for a load or store of a
+ single general-purpose register. An access without one (for example a
+ load/store pair or a SIMD/FP access) cannot be decoded. For a
+ non-protected VM it can exit to the VMM as ``KVM_EXIT_ARM_NISV``. For a
+ protected VM it cannot be emulated by the VMM, so the guest takes a
+ synchronous external abort instead.
+
+Debug
+-----
+
+Hardware-assisted debugging is not available to protected guests: their
+debug registers are RAZ/WI.
+
Resources
=========
diff --git a/Documentation/virt/kvm/devices/vcpu.rst b/Documentation/virt/kvm/devices/vcpu.rst
index deb5c51bc00c8..5e571d43e54ca 100644
--- a/Documentation/virt/kvm/devices/vcpu.rst
+++ b/Documentation/virt/kvm/devices/vcpu.rst
@@ -226,12 +226,14 @@ Returns:
-ENXIO Stolen time not implemented
-EEXIST Base address already set for this VCPU
-EINVAL Base address not 64 byte aligned
+ -EPERM The VCPU belongs to a protected VM
======= ======================================
Specifies the base address of the stolen time structure for this VCPU. The
base address must be 64 byte aligned and exist within a valid guest memory
region. See Documentation/virt/kvm/arm/pvtime.rst for more information
-including the layout of the stolen time structure.
+including the layout of the stolen time structure. Stolen time is not
+supported for protected VMs (see Documentation/virt/kvm/arm/pkvm.rst).
4. GROUP: KVM_VCPU_TSC_CTRL
===========================
--
2.39.5
^ permalink raw reply related [flat|nested] 32+ messages in thread
* Re: [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
` (16 preceding siblings ...)
2026-08-31 16:34 ` [PATCH 17/17] KVM: arm64: Document the protected VM userspace API Fuad Tabba
@ 2026-08-31 19:27 ` Fuad Tabba
17 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-08-31 19:27 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel
Cc: Catalin Marinas, Will Deacon, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret
On Mon, 31 Aug 2026 at 17:34, Fuad Tabba <fuad.tabba@linux.dev> wrote:
...
>
> The kvmtool changes that go with this will be posted separately, and I
> will reply here with a link.
... and here they are:
https://lore.kernel.org/all/20260831192406.1341841-1-fuad.tabba@linux.dev/
Cheers,
/fuad
>
> Patch 1 is the HCR_EL2.VSE fix posted separately [2]. It is not part
> of this series; it is carried so the series applies as is and Sashiko
> can run on it.
>
> The KVM_ARM_PREFERRED_TARGET documentation fix [3] went out just ahead
> of this series. Nothing here needs it to apply, but patch 17 documents
> vCPU feature availability as something the capabilities report, while
> api.rst 4.83 still points userspace at a bitmap that has always been
> empty.
>
> The series is structured as follows:
>
> 01: The HCR_EL2.VSE fix, posted separately.
> 02-03: Capability allowlist and the PVTIME rejection.
> 04-05: Per-exception-class entry handlers; EL2 owns a protected
> vCPU's trap configuration.
> 06-08: Timer state, system register reset and HVC handling at EL2.
> 09-10: PSCI at EL2, and the KVM_ARM_VCPU_INIT and PSCI version
> restrictions.
> 11-14: Host PC adjustments blocked; an UNDEF at EL2 for exit
> classes the host does not emulate; per-class state
> marshalling; a protected guest's SError pended with
> HCR_EL2.VSE.
> 15-16: Host access to private state, and host power-on of a vCPU
> EL2 holds powered off, rejected.
> 17: Documentation.
>
> Still to come: selftests, self-hosted debug, SVE for protected guests,
> and much more, as separate series.
>
> Based on v7.3-rc1 (cee9395acd804).
>
> Cheers,
> /fuad
>
> P.S. Sashiko, bring it on!
>
> [1] https://lore.kernel.org/all/20260729131823.2021516-1-fuad.tabba@linux.dev/
> [2] https://lore.kernel.org/all/20260829071120.2522788-1-fuad.tabba@linux.dev/
> [3] https://lore.kernel.org/all/20260831162815.269851-1-fuad.tabba@linux.dev/
>
> Fuad Tabba (15):
> KVM: arm64: Sync HCR_EL2.VSE back to the host vCPU under pKVM
> KVM: arm64: Advertise the capabilities that protected VMs support
> KVM: arm64: Reject the PVTIME vCPU attribute for protected VMs
> KVM: arm64: Skip fixed-feature state flush for protected vCPUs
> KVM: arm64: Add system register reset framework for protected VMs
> KVM: arm64: Implement HVC handling for protected guests at EL2
> KVM: arm64: Handle PSCI calls for protected VMs at EL2
> KVM: arm64: Restrict KVM_ARM_VCPU_INIT and PSCI version for protected
> VMs
> KVM: arm64: Prevent host PC adjustments for protected vCPUs
> KVM: arm64: Inject an UNDEF at EL2 for unhandled protected guest exits
> KVM: arm64: Add per-EC entry/exit state marshalling for protected
> guests
> KVM: arm64: Pend a protected guest's SError with HCR_EL2.VSE only
> KVM: arm64: Reject host access to protected VM private state
> KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off
> KVM: arm64: Document the protected VM userspace API
>
> Marc Zyngier (2):
> KVM: arm64: Introduce per-EC entry handlers for pKVM
> KVM: arm64: Add {flush,sync}_hyp_timer_state() primitives
>
> Documentation/virt/kvm/api.rst | 22 +-
> .../virt/kvm/arm/fw-pseudo-registers.rst | 2 +
> Documentation/virt/kvm/arm/pkvm.rst | 141 ++++-
> Documentation/virt/kvm/devices/vcpu.rst | 4 +-
> arch/arm64/include/asm/kvm_asm.h | 1 +
> arch/arm64/include/asm/kvm_host.h | 21 +
> arch/arm64/include/asm/kvm_pkvm.h | 34 +-
> arch/arm64/kvm/arm.c | 40 ++
> arch/arm64/kvm/guest.c | 29 +
> arch/arm64/kvm/hyp/exception.c | 27 +-
> arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 18 +
> arch/arm64/kvm/hyp/nvhe/hyp-main.c | 564 +++++++++++++++++-
> arch/arm64/kvm/hyp/nvhe/pkvm.c | 401 ++++++++++++-
> arch/arm64/kvm/hyp/nvhe/switch.c | 29 +-
> arch/arm64/kvm/hyp/nvhe/sys_regs.c | 91 ++-
> arch/arm64/kvm/hypercalls.c | 7 +
> arch/arm64/kvm/inject_fault.c | 5 +-
> arch/arm64/kvm/pkvm.c | 21 +-
> arch/arm64/kvm/psci.c | 3 +
> 19 files changed, 1378 insertions(+), 82 deletions(-)
>
>
> base-commit: cee9395acd8043be0644b25c34bfa86623f2b935
> --
> 2.39.5
>
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 04/17] KVM: arm64: Introduce per-EC entry handlers for pKVM
2026-08-31 16:34 ` [PATCH 04/17] KVM: arm64: Introduce per-EC entry handlers for pKVM Fuad Tabba
@ 2026-09-02 10:12 ` Joey Gouly
2026-09-02 11:35 ` Fuad Tabba
0 siblings, 1 reply; 32+ messages in thread
From: Joey Gouly @ 2026-09-02 10:12 UTC (permalink / raw)
To: Fuad Tabba
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
On Mon, Aug 31, 2026 at 05:34:08PM +0100, Fuad Tabba wrote:
> From: Marc Zyngier <maz@kernel.org>
>
> Add an ESR_EL2.EC-indexed handler table, entry_hyp_vm_handlers[],
> consulted from flush_hyp_vcpu() on re-entry when the previous exit was
> a trap: sync_hyp_vcpu() records the exit reason in the hyp vCPU as
> exit_code, and the trap's exception class comes from its ESR_EL2.
>
> Entry carries the host's requested PC updates to the hyp vCPU: add
> vcpu_copy_flag() to copy a masked set of flags between the two vCPU
> structures, with PC_UPDATE_REQ covering INCREMENT_PC and the
> pending-exception flags. The wholesale iflags copy moves into the
> non-protected branch; a protected vCPU takes only PC_UPDATE_REQ.
>
> Signed-off-by: Marc Zyngier <maz@kernel.org>
> Co-developed-by: Fuad Tabba <fuad.tabba@linux.dev>
> Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
Not really looked at nVHE much (or pkvm ever), but:
Reviewed-by: Joey Gouly <joey.gouly@arm.com>
> ---
> arch/arm64/include/asm/kvm_host.h | 19 +++++++++++++
> arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 3 ++
> arch/arm64/kvm/hyp/nvhe/hyp-main.c | 39 +++++++++++++++++++++++---
> 3 files changed, 57 insertions(+), 4 deletions(-)
>
> diff --git a/arch/arm64/include/asm/kvm_host.h b/arch/arm64/include/asm/kvm_host.h
> index 27fe0cd5b2d7a..78a4d387f9fd4 100644
> --- a/arch/arm64/include/asm/kvm_host.h
> +++ b/arch/arm64/include/asm/kvm_host.h
> @@ -1033,11 +1033,28 @@ struct kvm_vcpu_arch {
> set; \
> })
>
> +#define __vcpu_copy_flag(vt, vs, flagset, f, m) \
> + do { \
> + typeof(vs->arch.flagset) tmp, val; \
> + \
> + __build_check_flag(vs, flagset, f, m); \
> + \
> + val = READ_ONCE(vs->arch.flagset); \
> + val &= (m); \
> + __vcpu_flags_preempt_disable(); \
> + tmp = READ_ONCE(vt->arch.flagset); \
> + tmp &= ~(m); \
> + tmp |= val; \
> + WRITE_ONCE(vt->arch.flagset, tmp); \
> + __vcpu_flags_preempt_enable(); \
> + } while (0)
> +
> #define vcpu_get_flag(v, ...) __vcpu_get_flag((v), __VA_ARGS__)
> #define vcpu_set_flag(v, ...) __vcpu_set_flag((v), __VA_ARGS__)
> #define vcpu_clear_flag(v, ...) __vcpu_clear_flag((v), __VA_ARGS__)
> #define vcpu_test_and_clear_flag(v, ...) \
> __vcpu_test_and_clear_flag((v), __VA_ARGS__)
> +#define vcpu_copy_flag(vt, vs, ...) __vcpu_copy_flag((vt), (vs), __VA_ARGS__)
>
> /* KVM_ARM_VCPU_INIT completed */
> #define VCPU_INITIALIZED __vcpu_single_flag(cflags, BIT(0))
> @@ -1055,6 +1072,8 @@ struct kvm_vcpu_arch {
> #define INCREMENT_PC __vcpu_single_flag(iflags, BIT(1))
> /* Target EL/MODE (not a single flag, but let's abuse the macro) */
> #define EXCEPT_MASK __vcpu_single_flag(iflags, GENMASK(3, 1))
> +/* Cover both PENDING_EXCEPTION and EXCEPT_MASK for global operations */
> +#define PC_UPDATE_REQ __vcpu_single_flag(iflags, GENMASK(3, 0))
> /* Host-set: the hyp flushes the non-protected vCPU state in on entry */
> #define PKVM_HOST_STATE_DIRTY __vcpu_single_flag(iflags, BIT(4))
>
> diff --git a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> index c904647d2f760..49a0a992047ba 100644
> --- a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> +++ b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> @@ -26,6 +26,9 @@ struct pkvm_hyp_vcpu {
> * per-cpu pointer tracking us. Otherwise, NULL if not loaded.
> */
> struct pkvm_hyp_vcpu **loaded_hyp_vcpu;
> +
> + /* The previous exit's ARM_EXCEPTION_* code. */
> + u32 exit_code;
> };
>
> /*
> diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> index b6bfe502bcd04..ca7122b0bccdd 100644
> --- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> +++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> @@ -31,6 +31,17 @@ unsigned int hyp_gicv3_nr_lr;
>
> void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt);
>
> +typedef void (*hyp_entry_exit_handler_fn)(struct pkvm_hyp_vcpu *);
> +
> +static void handle_vm_entry_generic(struct pkvm_hyp_vcpu *hyp_vcpu)
> +{
> + vcpu_copy_flag(&hyp_vcpu->vcpu, hyp_vcpu->host_vcpu, PC_UPDATE_REQ);
> +}
This is added here because the full copy of iflags was moved into the
!pkvm_hyp_vcpu_is_protected() section, so is needed temporarily until
each EC has its own handler.
Thanks,
Joey
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 04/17] KVM: arm64: Introduce per-EC entry handlers for pKVM
2026-09-02 10:12 ` Joey Gouly
@ 2026-09-02 11:35 ` Fuad Tabba
0 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-09-02 11:35 UTC (permalink / raw)
To: Joey Gouly
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret
On Wed, 2 Sept 2026 at 11:12, Joey Gouly <joey.gouly@arm.com> wrote:
>
> On Mon, Aug 31, 2026 at 05:34:08PM +0100, Fuad Tabba wrote:
> > From: Marc Zyngier <maz@kernel.org>
> >
> > Add an ESR_EL2.EC-indexed handler table, entry_hyp_vm_handlers[],
> > consulted from flush_hyp_vcpu() on re-entry when the previous exit was
> > a trap: sync_hyp_vcpu() records the exit reason in the hyp vCPU as
> > exit_code, and the trap's exception class comes from its ESR_EL2.
> >
> > Entry carries the host's requested PC updates to the hyp vCPU: add
> > vcpu_copy_flag() to copy a masked set of flags between the two vCPU
> > structures, with PC_UPDATE_REQ covering INCREMENT_PC and the
> > pending-exception flags. The wholesale iflags copy moves into the
> > non-protected branch; a protected vCPU takes only PC_UPDATE_REQ.
> >
> > Signed-off-by: Marc Zyngier <maz@kernel.org>
> > Co-developed-by: Fuad Tabba <fuad.tabba@linux.dev>
> > Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
>
> Not really looked at nVHE much (or pkvm ever), but:
That's a plus. Good to get a fresh perspective on the code.
> Reviewed-by: Joey Gouly <joey.gouly@arm.com>
Thanks for the review!
/fuad
>
> > ---
> > arch/arm64/include/asm/kvm_host.h | 19 +++++++++++++
> > arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 3 ++
> > arch/arm64/kvm/hyp/nvhe/hyp-main.c | 39 +++++++++++++++++++++++---
> > 3 files changed, 57 insertions(+), 4 deletions(-)
> >
> > diff --git a/arch/arm64/include/asm/kvm_host.h b/arch/arm64/include/asm/kvm_host.h
> > index 27fe0cd5b2d7a..78a4d387f9fd4 100644
> > --- a/arch/arm64/include/asm/kvm_host.h
> > +++ b/arch/arm64/include/asm/kvm_host.h
> > @@ -1033,11 +1033,28 @@ struct kvm_vcpu_arch {
> > set; \
> > })
> >
> > +#define __vcpu_copy_flag(vt, vs, flagset, f, m) \
> > + do { \
> > + typeof(vs->arch.flagset) tmp, val; \
> > + \
> > + __build_check_flag(vs, flagset, f, m); \
> > + \
> > + val = READ_ONCE(vs->arch.flagset); \
> > + val &= (m); \
> > + __vcpu_flags_preempt_disable(); \
> > + tmp = READ_ONCE(vt->arch.flagset); \
> > + tmp &= ~(m); \
> > + tmp |= val; \
> > + WRITE_ONCE(vt->arch.flagset, tmp); \
> > + __vcpu_flags_preempt_enable(); \
> > + } while (0)
> > +
> > #define vcpu_get_flag(v, ...) __vcpu_get_flag((v), __VA_ARGS__)
> > #define vcpu_set_flag(v, ...) __vcpu_set_flag((v), __VA_ARGS__)
> > #define vcpu_clear_flag(v, ...) __vcpu_clear_flag((v), __VA_ARGS__)
> > #define vcpu_test_and_clear_flag(v, ...) \
> > __vcpu_test_and_clear_flag((v), __VA_ARGS__)
> > +#define vcpu_copy_flag(vt, vs, ...) __vcpu_copy_flag((vt), (vs), __VA_ARGS__)
> >
> > /* KVM_ARM_VCPU_INIT completed */
> > #define VCPU_INITIALIZED __vcpu_single_flag(cflags, BIT(0))
> > @@ -1055,6 +1072,8 @@ struct kvm_vcpu_arch {
> > #define INCREMENT_PC __vcpu_single_flag(iflags, BIT(1))
> > /* Target EL/MODE (not a single flag, but let's abuse the macro) */
> > #define EXCEPT_MASK __vcpu_single_flag(iflags, GENMASK(3, 1))
> > +/* Cover both PENDING_EXCEPTION and EXCEPT_MASK for global operations */
> > +#define PC_UPDATE_REQ __vcpu_single_flag(iflags, GENMASK(3, 0))
> > /* Host-set: the hyp flushes the non-protected vCPU state in on entry */
> > #define PKVM_HOST_STATE_DIRTY __vcpu_single_flag(iflags, BIT(4))
> >
> > diff --git a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> > index c904647d2f760..49a0a992047ba 100644
> > --- a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> > +++ b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> > @@ -26,6 +26,9 @@ struct pkvm_hyp_vcpu {
> > * per-cpu pointer tracking us. Otherwise, NULL if not loaded.
> > */
> > struct pkvm_hyp_vcpu **loaded_hyp_vcpu;
> > +
> > + /* The previous exit's ARM_EXCEPTION_* code. */
> > + u32 exit_code;
> > };
> >
> > /*
> > diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > index b6bfe502bcd04..ca7122b0bccdd 100644
> > --- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > +++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > @@ -31,6 +31,17 @@ unsigned int hyp_gicv3_nr_lr;
> >
> > void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt);
> >
> > +typedef void (*hyp_entry_exit_handler_fn)(struct pkvm_hyp_vcpu *);
> > +
> > +static void handle_vm_entry_generic(struct pkvm_hyp_vcpu *hyp_vcpu)
> > +{
> > + vcpu_copy_flag(&hyp_vcpu->vcpu, hyp_vcpu->host_vcpu, PC_UPDATE_REQ);
> > +}
>
> This is added here because the full copy of iflags was moved into the
> !pkvm_hyp_vcpu_is_protected() section, so is needed temporarily until
> each EC has its own handler.
>
> Thanks,
> Joey
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 02/17] KVM: arm64: Advertise the capabilities that protected VMs support
2026-08-31 16:34 ` [PATCH 02/17] KVM: arm64: Advertise the capabilities that protected VMs support Fuad Tabba
@ 2026-09-02 13:22 ` Vincent Donnefort
2026-09-03 16:20 ` Fuad Tabba
0 siblings, 1 reply; 32+ messages in thread
From: Vincent Donnefort @ 2026-09-02 13:22 UTC (permalink / raw)
To: Fuad Tabba
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Joey Gouly,
Steffen Eiden, Suzuki K Poulose, Zenghui Yu, Quentin Perret,
Fuad Tabba
On Mon, Aug 31, 2026 at 05:34:06PM +0100, Fuad Tabba wrote:
> kvm_pkvm_ext_allowed() denies every capability it does not name, so a
> protected VM reports 0 for interfaces it implements: KVM_CHECK_EXTENSION
> denies KVM_CAP_ONE_REG while KVM_{GET,SET}_ONE_REG stage the guest's
> boot state.
>
> Allow the capabilities that work for a protected guest, the vGIC and
> the I/O bus being host-managed, subject to the restrictions the rest of
> this series adds.
>
> Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
> ---
> arch/arm64/include/asm/kvm_pkvm.h | 9 +++++++++
> 1 file changed, 9 insertions(+)
>
> diff --git a/arch/arm64/include/asm/kvm_pkvm.h b/arch/arm64/include/asm/kvm_pkvm.h
> index beea00e693a0a..c4c834d55e503 100644
> --- a/arch/arm64/include/asm/kvm_pkvm.h
> +++ b/arch/arm64/include/asm/kvm_pkvm.h
> @@ -42,6 +42,15 @@ static inline bool kvm_pkvm_ext_allowed(struct kvm *kvm, long ext)
> case KVM_CAP_ARM_VM_IPA_SIZE:
> case KVM_CAP_ARM_PTRAUTH_ADDRESS:
> case KVM_CAP_ARM_PTRAUTH_GENERIC:
> + case KVM_CAP_ONE_REG:
Shouldn't we bring the VCPU_PKVM_FINALIZED restriction in kvm_arch_vcpu_ioctl()
first before we allow SET_ONE_REG/GET_ONE_REG?
> + case KVM_CAP_MP_STATE:
> + case KVM_CAP_VCPU_EVENTS:
> + case KVM_CAP_VCPU_ATTRIBUTES:
> + case KVM_CAP_IMMEDIATE_EXIT:
> + case KVM_CAP_IOEVENTFD:
> + case KVM_CAP_IRQFD_RESAMPLE:
> + case KVM_CAP_ARM_IRQ_LINE_LAYOUT_2:
> + case KVM_CAP_ARM_INJECT_SERROR_ESR:
> return true;
> case KVM_CAP_ARM_MTE:
> return false;
> --
> 2.39.5
>
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 03/17] KVM: arm64: Reject the PVTIME vCPU attribute for protected VMs
2026-08-31 16:34 ` [PATCH 03/17] KVM: arm64: Reject the PVTIME vCPU attribute for protected VMs Fuad Tabba
@ 2026-09-02 13:30 ` Vincent Donnefort
2026-09-03 16:21 ` Fuad Tabba
0 siblings, 1 reply; 32+ messages in thread
From: Vincent Donnefort @ 2026-09-02 13:30 UTC (permalink / raw)
To: Fuad Tabba
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Joey Gouly,
Steffen Eiden, Suzuki K Poulose, Zenghui Yu, Quentin Perret,
Fuad Tabba
On Mon, Aug 31, 2026 at 05:34:07PM +0100, Fuad Tabba wrote:
> pKVM currently does not support steal time for protected guests:
> KVM_CAP_STEAL_TIME reports 0 for them. The host still accepts the
> KVM_ARM_VCPU_PVTIME_CTRL attribute, whose IPA would point
> kvm_update_stolen_time() at the guest's private memory on every vCPU
> load. Return -EPERM for the attribute group instead.
>
> Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
> ---
> arch/arm64/kvm/guest.c | 10 ++++++++++
> 1 file changed, 10 insertions(+)
>
> diff --git a/arch/arm64/kvm/guest.c b/arch/arm64/kvm/guest.c
> index b01d6622b8720..23f725e2cf745 100644
> --- a/arch/arm64/kvm/guest.c
> +++ b/arch/arm64/kvm/guest.c
> @@ -916,6 +916,10 @@ int kvm_arm_vcpu_arch_set_attr(struct kvm_vcpu *vcpu,
> ret = kvm_arm_timer_set_attr(vcpu, attr);
> break;
> case KVM_ARM_VCPU_PVTIME_CTRL:
> + /* Steal time is not offered to protected guests. */
> + if (kvm_vm_is_protected(vcpu->kvm))
> + return -EPERM;
> +
nit: We have vcpu_is_protected()
Also, would that make more sense to put that check in kvm_arm_pvtime_supported()?
> ret = kvm_arm_pvtime_set_attr(vcpu, attr);
> break;
> default:
> @@ -939,6 +943,9 @@ int kvm_arm_vcpu_arch_get_attr(struct kvm_vcpu *vcpu,
> ret = kvm_arm_timer_get_attr(vcpu, attr);
> break;
> case KVM_ARM_VCPU_PVTIME_CTRL:
> + if (kvm_vm_is_protected(vcpu->kvm))
> + return -EPERM;
> +
> ret = kvm_arm_pvtime_get_attr(vcpu, attr);
> break;
> default:
> @@ -962,6 +969,9 @@ int kvm_arm_vcpu_arch_has_attr(struct kvm_vcpu *vcpu,
> ret = kvm_arm_timer_has_attr(vcpu, attr);
> break;
> case KVM_ARM_VCPU_PVTIME_CTRL:
> + if (kvm_vm_is_protected(vcpu->kvm))
> + return -EPERM;
> +
> ret = kvm_arm_pvtime_has_attr(vcpu, attr);
> break;
> default:
> --
> 2.39.5
>
--
Vincent
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 05/17] KVM: arm64: Skip fixed-feature state flush for protected vCPUs
2026-08-31 16:34 ` [PATCH 05/17] KVM: arm64: Skip fixed-feature state flush for protected vCPUs Fuad Tabba
@ 2026-09-02 15:05 ` Vincent Donnefort
2026-09-02 15:26 ` Vincent Donnefort
0 siblings, 1 reply; 32+ messages in thread
From: Vincent Donnefort @ 2026-09-02 15:05 UTC (permalink / raw)
To: Fuad Tabba
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Joey Gouly,
Steffen Eiden, Suzuki K Poulose, Zenghui Yu, Quentin Perret,
Fuad Tabba
On Mon, Aug 31, 2026 at 05:34:09PM +0100, Fuad Tabba wrote:
> flush_hyp_vcpu() copies the host's mdcr_el2, TWI/TWE and debug state
> into the hyp vCPU on every entry, so for a protected vCPU the host's
> trap configuration overrides the one pkvm_vcpu_init_traps() computed
> at EL2. Move those copies into the non-protected branch, and sync the
> debug state back for non-protected vCPUs only: a protected guest's
> debug registers are hypervisor-owned, and it takes the host's TWI/TWE
> at vCPU load. MDCR_EL2.TDA traps only the guest's own accesses; it
> does not stop the world switch from loading host-supplied breakpoints
> and MDSCR_EL1.
>
> EL2's mdcr_el2 starts from 0, and HPMN == 0 is reserved without
> FEAT_HPMN0. Set HPMN to the CPU's event counter count at vCPU load,
> the value the host's copy carried; a protected guest's PMU accesses
> trap regardless.
>
> Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
> ---
> arch/arm64/kvm/hyp/nvhe/hyp-main.c | 31 +++++++++++++++++++-----------
> arch/arm64/kvm/hyp/nvhe/pkvm.c | 2 +-
> 2 files changed, 21 insertions(+), 12 deletions(-)
>
> diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> index ca7122b0bccdd..14e847b122abf 100644
> --- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> +++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> @@ -231,7 +231,6 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> u8 esr_ec;
>
> fpsimd_sve_flush();
> - flush_debug_state(hyp_vcpu);
>
> /*
> * If we deal with a non-protected guest and the state is potentially
> @@ -241,7 +240,14 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
> if (vcpu_get_flag(host_vcpu, PKVM_HOST_STATE_DIRTY))
> flush_hyp_vcpu_state(hyp_vcpu);
> +
> + hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWI | HCR_TWE);
> + hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) &
> + (HCR_TWI | HCR_TWE);
> +
> + hyp_vcpu->vcpu.arch.mdcr_el2 = host_vcpu->arch.mdcr_el2;
> hyp_vcpu->vcpu.arch.iflags = host_vcpu->arch.iflags;
> + flush_debug_state(hyp_vcpu);
> } else {
> hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
> }
> @@ -249,17 +255,13 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> /* __hyp_running_vcpu must be NULL in a guest context. */
> hyp_vcpu->vcpu.arch.ctxt.__hyp_running_vcpu = NULL;
>
> - hyp_vcpu->vcpu.arch.mdcr_el2 = host_vcpu->arch.mdcr_el2;
> /*
> - * HCR_EL2.VSE is host-owned (a pending virtual SError to inject), not a
> - * trap-control bit, so it must flow to the hyp vCPU alongside TWI/TWE
> - * for the vSError to be delivered. sync_hyp_vcpu() reflects it back.
> + * A host-injected vSError is masked by the guest's own PSTATE.A, so it
> + * applies to protected guests too.
> */
> - hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWI | HCR_TWE | HCR_VSE);
> - hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) &
> - (HCR_TWI | HCR_TWE | HCR_VSE);
> -
> - hyp_vcpu->vcpu.arch.vsesr_el2 = host_vcpu->arch.vsesr_el2;
> + hyp_vcpu->vcpu.arch.hcr_el2 &= ~HCR_VSE;
> + hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) & HCR_VSE;
> + hyp_vcpu->vcpu.arch.vsesr_el2 = host_vcpu->arch.vsesr_el2;
>
> flush_hyp_vgic_state(hyp_vcpu);
>
> @@ -288,7 +290,8 @@ static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, u32 exit_reason)
> struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
>
> fpsimd_sve_sync(&hyp_vcpu->vcpu);
> - sync_debug_state(hyp_vcpu);
> + if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu))
> + sync_debug_state(hyp_vcpu);
nit: We can't use the else arm below?
>
> if (pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
> host_vcpu->arch.ctxt = hyp_vcpu->vcpu.arch.ctxt;
> @@ -329,6 +332,12 @@ static void handle___pkvm_vcpu_load(struct kvm_cpu_context *host_ctxt)
> /* Propagate WFx trapping flags */
> hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWE | HCR_TWI);
> hyp_vcpu->vcpu.arch.hcr_el2 |= hcr_el2 & (HCR_TWE | HCR_TWI);
> +
> + /* HPMN == 0 is reserved without FEAT_HPMN0. */
> + if (system_supports_pmuv3())
> + u64p_replace_bits(&hyp_vcpu->vcpu.arch.mdcr_el2,
> + FIELD_GET(ARMV8_PMU_PMCR_N, read_sysreg(pmcr_el0)),
> + MDCR_EL2_HPMN);
> } else {
> memcpy(&hyp_vcpu->vcpu.arch.fgt, hyp_vcpu->host_vcpu->arch.fgt,
> sizeof(hyp_vcpu->vcpu.arch.fgt));
> diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> index 459bd9eb7e4bc..e85f13233da08 100644
> --- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
> +++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> @@ -102,7 +102,7 @@ static void pvm_init_traps_mdcr(struct kvm_vcpu *vcpu)
>
> if (!kvm_has_feat(kvm, ID_AA64DFR0_EL1, PMUVer, IMP)) {
> val |= MDCR_EL2_TPM | MDCR_EL2_TPMCR;
> - val &= ~(MDCR_EL2_HPME | MDCR_EL2_MTPME | MDCR_EL2_HPMN_MASK);
> + val &= ~(MDCR_EL2_HPME | MDCR_EL2_MTPME);
> }
>
> if (!kvm_has_feat(kvm, ID_AA64DFR0_EL1, DebugVer, IMP))
> --
> 2.39.5
>
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 07/17] KVM: arm64: Add system register reset framework for protected VMs
2026-08-31 16:34 ` [PATCH 07/17] KVM: arm64: Add system register reset framework for protected VMs Fuad Tabba
@ 2026-09-02 15:14 ` Joey Gouly
2026-09-03 16:24 ` Fuad Tabba
0 siblings, 1 reply; 32+ messages in thread
From: Joey Gouly @ 2026-09-02 15:14 UTC (permalink / raw)
To: Fuad Tabba
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
On Mon, Aug 31, 2026 at 05:34:11PM +0100, Fuad Tabba wrote:
> Reset a protected VM's system registers at EL2 rather than taking the
> host's values: add kvm_reset_pvm_sys_regs() and the
> pvm_sys_reg_reset_vals[] table that drives it, and call it from
> init_pkvm_hyp_vcpu() for protected vCPUs. The values follow the
> host-side reset in sys_regs.c, with a poison value where it resets to
> UNKNOWN, and for VBAR_EL1 and CONTEXTIDR_EL1 in place of the 0 it
> resets them to. MPIDR_EL1 is derived from vcpu_id, as for any KVM
> guest.
>
> Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
The commit message says "rather than taking the host's values", but if I
understand correctly, at this point in the series, they still take the
hosts values because this is still present:
hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
Not necessarily saying anything needs to change, just to write down what
I noticed while reviewing / reading this code!
The only other thing I was a bit unsure of was the actual list of
registers pvm_sys_reg_reset_vals. It's the EL1 registers, minus
unsupported FEATs (MTE, debug(?), GCS, etc)
Otherwise:
Acked-by: Joey Gouly <joey.gouly@arm.com>
Thanks,
Joey
> ---
> arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 1 +
> arch/arm64/kvm/hyp/nvhe/pkvm.c | 5 ++
> arch/arm64/kvm/hyp/nvhe/sys_regs.c | 91 ++++++++++++++++++++++++--
> 3 files changed, 93 insertions(+), 4 deletions(-)
>
> diff --git a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> index 49a0a992047ba..a04b7c04d5135 100644
> --- a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> +++ b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> @@ -95,6 +95,7 @@ bool kvm_handle_pvm_hvc64(struct kvm_vcpu *vcpu, u64 *exit_code);
> bool kvm_handle_pvm_sysreg(struct kvm_vcpu *vcpu, u64 *exit_code);
> bool kvm_handle_pvm_restricted(struct kvm_vcpu *vcpu, u64 *exit_code);
> void kvm_init_pvm_id_regs(struct kvm_vcpu *vcpu);
> +void kvm_reset_pvm_sys_regs(struct kvm_vcpu *vcpu);
> int kvm_check_pvm_sysreg_table(void);
>
> #endif /* __ARM64_KVM_NVHE_PKVM_H__ */
> diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> index e85f13233da08..af334318d0a03 100644
> --- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
> +++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> @@ -551,6 +551,11 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu,
> goto done;
>
> ret = pkvm_vcpu_init_sve(hyp_vcpu, host_vcpu);
> + if (ret)
> + goto done;
> +
> + if (pkvm_hyp_vcpu_is_protected(hyp_vcpu))
> + kvm_reset_pvm_sys_regs(&hyp_vcpu->vcpu);
> done:
> if (ret)
> unpin_host_vcpu(host_vcpu);
> diff --git a/arch/arm64/kvm/hyp/nvhe/sys_regs.c b/arch/arm64/kvm/hyp/nvhe/sys_regs.c
> index 8758c68017765..ebfd48aa15b56 100644
> --- a/arch/arm64/kvm/hyp/nvhe/sys_regs.c
> +++ b/arch/arm64/kvm/hyp/nvhe/sys_regs.c
> @@ -525,6 +525,84 @@ static const struct sys_reg_desc pvm_sys_reg_descs[] = {
> /* Performance Monitoring Registers are restricted. */
> };
>
> +struct sys_reg_desc_reset {
> + int reg;
> + void (*reset)(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *rd);
> + u64 value;
> +};
> +
> +/* Hardware value, as sys_regs.c's reset_actlr()/reset_amair_el1(). */
> +static void reset_actlr(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
> +{
> + __vcpu_assign_sys_reg(vcpu, r->reg, read_sysreg(actlr_el1));
> +}
> +
> +static void reset_amair_el1(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
> +{
> + __vcpu_assign_sys_reg(vcpu, r->reg, read_sysreg(amair_el1));
> +}
> +
> +static void reset_mpidr(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
> +{
> + __vcpu_assign_sys_reg(vcpu, r->reg, kvm_calculate_mpidr(vcpu));
> +}
> +
> +static void reset_value(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
> +{
> + __vcpu_assign_sys_reg(vcpu, r->reg, r->value);
> +}
> +
> +#define RESET_VAL(REG, RESET_VAL) { REG, reset_value, RESET_VAL }
> +
> +#define RESET_ZERO(REG) RESET_VAL(REG, 0)
> +
> +#define RESET_UNKNOWN(REG) RESET_VAL(REG, 0x1de7ec7edbadc0deULL)
> +
> +#define RESET_FUNC(REG, RESET_FUNC) { REG, RESET_FUNC, 0 }
> +
> +/* Sorted ascending by reg; kvm_check_pvm_sysreg_table() enforces it. */
> +static const struct sys_reg_desc_reset pvm_sys_reg_reset_vals[] = {
> + RESET_FUNC(MPIDR_EL1, reset_mpidr),
> + RESET_UNKNOWN(TPIDR_EL0),
> + RESET_UNKNOWN(TPIDRRO_EL0),
> + RESET_UNKNOWN(TPIDR_EL1),
> + RESET_ZERO(CNTKCTL_EL1),
> + RESET_UNKNOWN(PAR_EL1),
> + RESET_ZERO(MDCCINT_EL1),
> + RESET_ZERO(DISR_EL1),
> + RESET_ZERO(PMCCFILTR_EL0),
> + RESET_ZERO(PMUSERENR_EL0),
> + RESET_ZERO(CPACR_EL1),
> + RESET_VAL(CONTEXTIDR_EL1, 0x00000000dbadc0deULL),
> + RESET_VAL(SCTLR_EL1, 0x00C50078ULL),
> + RESET_FUNC(ACTLR_EL1, reset_actlr),
> + RESET_ZERO(TCR_EL1),
> + RESET_UNKNOWN(AFSR0_EL1),
> + RESET_UNKNOWN(AFSR1_EL1),
> + RESET_UNKNOWN(ESR_EL1),
> + RESET_UNKNOWN(MAIR_EL1),
> + RESET_FUNC(AMAIR_EL1, reset_amair_el1),
> + RESET_ZERO(MDSCR_EL1),
> + RESET_ZERO(ZCR_EL1),
> + RESET_UNKNOWN(TTBR0_EL1),
> + RESET_UNKNOWN(TTBR1_EL1),
> + RESET_UNKNOWN(FAR_EL1),
> + RESET_VAL(VBAR_EL1, 0x1de7ec7edbadc000ULL),
> + RESET_UNKNOWN(PIRE0_EL1),
> + RESET_UNKNOWN(PIR_EL1),
> +};
> +
> +void kvm_reset_pvm_sys_regs(struct kvm_vcpu *vcpu)
> +{
> + unsigned long i;
> +
> + for (i = 0; i < ARRAY_SIZE(pvm_sys_reg_reset_vals); i++) {
> + const struct sys_reg_desc_reset *r = &pvm_sys_reg_reset_vals[i];
> +
> + r->reset(vcpu, r);
> + }
> +}
> +
> /*
> * Initializes feature registers for protected vms.
> */
> @@ -550,16 +628,21 @@ void kvm_init_pvm_id_regs(struct kvm_vcpu *vcpu)
> }
>
> /*
> - * Checks that the sysreg table is unique and in-order.
> - *
> - * Returns 0 if the table is consistent, or 1 otherwise.
> + * Both tables must be unique and sorted ascending. pvm_sys_reg_descs.reg is the
> + * sys_reg() encoding, pvm_sys_reg_reset_vals.reg the vcpu_sysreg index, so they
> + * compare differently. BUG_ON() at __pkvm_init: fatal at boot.
> */
> int kvm_check_pvm_sysreg_table(void)
> {
> unsigned int i;
>
> for (i = 1; i < ARRAY_SIZE(pvm_sys_reg_descs); i++) {
> - if (cmp_sys_reg(&pvm_sys_reg_descs[i-1], &pvm_sys_reg_descs[i]) >= 0)
> + if (cmp_sys_reg(&pvm_sys_reg_descs[i - 1], &pvm_sys_reg_descs[i]) >= 0)
> + return 1;
> + }
> +
> + for (i = 1; i < ARRAY_SIZE(pvm_sys_reg_reset_vals); i++) {
> + if (pvm_sys_reg_reset_vals[i - 1].reg >= pvm_sys_reg_reset_vals[i].reg)
> return 1;
> }
>
> --
> 2.39.5
>
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 05/17] KVM: arm64: Skip fixed-feature state flush for protected vCPUs
2026-09-02 15:05 ` Vincent Donnefort
@ 2026-09-02 15:26 ` Vincent Donnefort
2026-09-03 16:22 ` Fuad Tabba
0 siblings, 1 reply; 32+ messages in thread
From: Vincent Donnefort @ 2026-09-02 15:26 UTC (permalink / raw)
To: Fuad Tabba
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Joey Gouly,
Steffen Eiden, Suzuki K Poulose, Zenghui Yu, Quentin Perret,
Fuad Tabba
On Wed, Sep 02, 2026 at 04:05:12PM +0100, Vincent Donnefort wrote:
> On Mon, Aug 31, 2026 at 05:34:09PM +0100, Fuad Tabba wrote:
> > flush_hyp_vcpu() copies the host's mdcr_el2, TWI/TWE and debug state
> > into the hyp vCPU on every entry, so for a protected vCPU the host's
> > trap configuration overrides the one pkvm_vcpu_init_traps() computed
> > at EL2. Move those copies into the non-protected branch, and sync the
> > debug state back for non-protected vCPUs only: a protected guest's
> > debug registers are hypervisor-owned, and it takes the host's TWI/TWE
> > at vCPU load. MDCR_EL2.TDA traps only the guest's own accesses; it
> > does not stop the world switch from loading host-supplied breakpoints
> > and MDSCR_EL1.
> >
> > EL2's mdcr_el2 starts from 0, and HPMN == 0 is reserved without
> > FEAT_HPMN0. Set HPMN to the CPU's event counter count at vCPU load,
> > the value the host's copy carried; a protected guest's PMU accesses
> > trap regardless.
> >
> > Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
> > ---
> > arch/arm64/kvm/hyp/nvhe/hyp-main.c | 31 +++++++++++++++++++-----------
> > arch/arm64/kvm/hyp/nvhe/pkvm.c | 2 +-
> > 2 files changed, 21 insertions(+), 12 deletions(-)
> >
> > diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > index ca7122b0bccdd..14e847b122abf 100644
> > --- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > +++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > @@ -231,7 +231,6 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> > u8 esr_ec;
> >
> > fpsimd_sve_flush();
> > - flush_debug_state(hyp_vcpu);
> >
> > /*
> > * If we deal with a non-protected guest and the state is potentially
> > @@ -241,7 +240,14 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> > if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
> > if (vcpu_get_flag(host_vcpu, PKVM_HOST_STATE_DIRTY))
> > flush_hyp_vcpu_state(hyp_vcpu);
> > +
> > + hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWI | HCR_TWE);
> > + hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) &
> > + (HCR_TWI | HCR_TWE);
> > +
> > + hyp_vcpu->vcpu.arch.mdcr_el2 = host_vcpu->arch.mdcr_el2;
> > hyp_vcpu->vcpu.arch.iflags = host_vcpu->arch.iflags;
> > + flush_debug_state(hyp_vcpu);
> > } else {
> > hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
> > }
> > @@ -249,17 +255,13 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> > /* __hyp_running_vcpu must be NULL in a guest context. */
> > hyp_vcpu->vcpu.arch.ctxt.__hyp_running_vcpu = NULL;
> >
> > - hyp_vcpu->vcpu.arch.mdcr_el2 = host_vcpu->arch.mdcr_el2;
> > /*
> > - * HCR_EL2.VSE is host-owned (a pending virtual SError to inject), not a
> > - * trap-control bit, so it must flow to the hyp vCPU alongside TWI/TWE
> > - * for the vSError to be delivered. sync_hyp_vcpu() reflects it back.
> > + * A host-injected vSError is masked by the guest's own PSTATE.A, so it
> > + * applies to protected guests too.
> > */
> > - hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWI | HCR_TWE | HCR_VSE);
> > - hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) &
> > - (HCR_TWI | HCR_TWE | HCR_VSE);
> > -
> > - hyp_vcpu->vcpu.arch.vsesr_el2 = host_vcpu->arch.vsesr_el2;
> > + hyp_vcpu->vcpu.arch.hcr_el2 &= ~HCR_VSE;
> > + hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) & HCR_VSE;
> > + hyp_vcpu->vcpu.arch.vsesr_el2 = host_vcpu->arch.vsesr_el2;
> >
> > flush_hyp_vgic_state(hyp_vcpu);
> >
> > @@ -288,7 +290,8 @@ static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, u32 exit_reason)
> > struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
> >
> > fpsimd_sve_sync(&hyp_vcpu->vcpu);
> > - sync_debug_state(hyp_vcpu);
> > + if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu))
> > + sync_debug_state(hyp_vcpu);
>
> nit: We can't use the else arm below?
or actually could we move the check into sync_debug_state() just like
sync_hyp_timer_state() does?
>
> >
> > if (pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
> > host_vcpu->arch.ctxt = hyp_vcpu->vcpu.arch.ctxt;
> > @@ -329,6 +332,12 @@ static void handle___pkvm_vcpu_load(struct kvm_cpu_context *host_ctxt)
> > /* Propagate WFx trapping flags */
> > hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWE | HCR_TWI);
> > hyp_vcpu->vcpu.arch.hcr_el2 |= hcr_el2 & (HCR_TWE | HCR_TWI);
> > +
> > + /* HPMN == 0 is reserved without FEAT_HPMN0. */
> > + if (system_supports_pmuv3())
> > + u64p_replace_bits(&hyp_vcpu->vcpu.arch.mdcr_el2,
> > + FIELD_GET(ARMV8_PMU_PMCR_N, read_sysreg(pmcr_el0)),
> > + MDCR_EL2_HPMN);
> > } else {
> > memcpy(&hyp_vcpu->vcpu.arch.fgt, hyp_vcpu->host_vcpu->arch.fgt,
> > sizeof(hyp_vcpu->vcpu.arch.fgt));
> > diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > index 459bd9eb7e4bc..e85f13233da08 100644
> > --- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > +++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > @@ -102,7 +102,7 @@ static void pvm_init_traps_mdcr(struct kvm_vcpu *vcpu)
> >
> > if (!kvm_has_feat(kvm, ID_AA64DFR0_EL1, PMUVer, IMP)) {
> > val |= MDCR_EL2_TPM | MDCR_EL2_TPMCR;
> > - val &= ~(MDCR_EL2_HPME | MDCR_EL2_MTPME | MDCR_EL2_HPMN_MASK);
> > + val &= ~(MDCR_EL2_HPME | MDCR_EL2_MTPME);
> > }
> >
> > if (!kvm_has_feat(kvm, ID_AA64DFR0_EL1, DebugVer, IMP))
> > --
> > 2.39.5
> >
--
Vincent
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 08/17] KVM: arm64: Implement HVC handling for protected guests at EL2
2026-08-31 16:34 ` [PATCH 08/17] KVM: arm64: Implement HVC handling for protected guests at EL2 Fuad Tabba
@ 2026-09-03 15:12 ` Joey Gouly
2026-09-03 16:25 ` Fuad Tabba
0 siblings, 1 reply; 32+ messages in thread
From: Joey Gouly @ 2026-09-03 15:12 UTC (permalink / raw)
To: Fuad Tabba
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret,
Fuad Tabba
On Mon, Aug 31, 2026 at 05:34:12PM +0100, Fuad Tabba wrote:
> Extend kvm_handle_pvm_hvc64() to handle SMCCC_VERSION,
> SMCCC_ARCH_FEATURES and the vendor hypervisor call UID at EL2, so
> these queries do not reach the host. ARCH_FEATURES is mandatory from
> SMCCC 1.1, the version EL2 reports: it returns SUCCESS for itself and
> for SMCCC_VERSION, and NOT_SUPPORTED for anything else.
>
> Add handle_pvm_entry_hvc64() and handle_pvm_exit_hvc64(), which
> forward a protected guest's HVCs to the host and return the reply; a
> later patch wires them into the per-EC dispatch tables.
These two functions are unused in this commit, and are modified in the
next commit, while still being unused. Then handle_pvm_entry_hvc64()
becomes a wrapper for handle_pvm_entry_psci() (which was the whole body
of handle_pvm_entry_hvc64). It's quite hard to follow. If these
functions are really unused and then modified, maybe it's just better to
remove them from this commit?
Thanks,
Joey
>
> Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
> ---
> arch/arm64/kvm/hyp/nvhe/hyp-main.c | 28 ++++++++++++++++++++++++++++
> arch/arm64/kvm/hyp/nvhe/pkvm.c | 29 +++++++++++++++++++++++++++++
> 2 files changed, 57 insertions(+)
>
> diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> index d4b0f69ff130c..62864db1e099a 100644
> --- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> +++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> @@ -4,6 +4,8 @@
> * Author: Andrew Scull <ascull@google.com>
> */
>
> +#include <kvm/arm_hypercalls.h>
> +
> #include <hyp/adjust_pc.h>
> #include <hyp/switch.h>
>
> @@ -33,6 +35,32 @@ void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt);
>
> typedef void (*hyp_entry_exit_handler_fn)(struct pkvm_hyp_vcpu *);
>
> +static void __maybe_unused handle_pvm_entry_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
> +{
> + int i;
> +
> + for (i = 0; i < 4; i++) {
> + u64 ret =
> + READ_ONCE(hyp_vcpu->host_vcpu->arch.ctxt.regs.regs[i]);
> + vcpu_set_reg(&hyp_vcpu->vcpu, i, ret);
> + }
> +}
> +
> +static void __maybe_unused handle_pvm_exit_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
> +{
> + struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
> + int i;
> +
> + WRITE_ONCE(host_vcpu->arch.fault.esr_el2,
> + hyp_vcpu->vcpu.arch.fault.esr_el2);
> +
> + /* Pass the HVC function id (r0) and its arguments. */
> + for (i = 0; i < 8; i++) {
> + WRITE_ONCE(host_vcpu->arch.ctxt.regs.regs[i],
> + vcpu_get_reg(&hyp_vcpu->vcpu, i));
> + }
> +}
> +
> static void handle_vm_entry_generic(struct pkvm_hyp_vcpu *hyp_vcpu)
> {
> vcpu_copy_flag(&hyp_vcpu->vcpu, hyp_vcpu->host_vcpu, PC_UPDATE_REQ);
> diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> index af334318d0a03..0fe11f95e2e26 100644
> --- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
> +++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> @@ -1144,8 +1144,37 @@ bool kvm_handle_pvm_hvc64(struct kvm_vcpu *vcpu, u64 *exit_code)
> {
> u64 val[4] = { SMCCC_RET_INVALID_PARAMETER };
> bool handled = true;
> + u32 feature;
> + uuid_t uuid;
>
> switch (smccc_get_function(vcpu)) {
> + case ARM_SMCCC_VERSION_FUNC_ID:
> + /* Nothing to be handled by the host. Go back to the guest. */
> + val[0] = ARM_SMCCC_VERSION_1_1;
> + val[1] = 0;
> + val[2] = 0;
> + val[3] = 0;
> + break;
> + case ARM_SMCCC_ARCH_FEATURES_FUNC_ID:
> + /* SUCCESS only for the architecture calls EL2 implements. */
> + feature = smccc_get_arg1(vcpu);
> + switch (feature) {
> + case ARM_SMCCC_VERSION_FUNC_ID:
> + case ARM_SMCCC_ARCH_FEATURES_FUNC_ID:
> + val[0] = SMCCC_RET_SUCCESS;
> + break;
> + default:
> + val[0] = SMCCC_RET_NOT_SUPPORTED;
> + break;
> + }
> + break;
> + case ARM_SMCCC_VENDOR_HYP_CALL_UID_FUNC_ID:
> + uuid = ARM_SMCCC_VENDOR_HYP_UID_KVM;
> + val[0] = smccc_uuid_to_reg(&uuid, 0);
> + val[1] = smccc_uuid_to_reg(&uuid, 1);
> + val[2] = smccc_uuid_to_reg(&uuid, 2);
> + val[3] = smccc_uuid_to_reg(&uuid, 3);
> + break;
> case ARM_SMCCC_VENDOR_HYP_KVM_FEATURES_FUNC_ID:
> val[0] = BIT(ARM_SMCCC_KVM_FUNC_FEATURES);
> val[0] |= BIT(ARM_SMCCC_KVM_FUNC_HYP_MEMINFO);
> --
> 2.39.5
>
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 02/17] KVM: arm64: Advertise the capabilities that protected VMs support
2026-09-02 13:22 ` Vincent Donnefort
@ 2026-09-03 16:20 ` Fuad Tabba
0 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-09-03 16:20 UTC (permalink / raw)
To: Vincent Donnefort
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Joey Gouly,
Steffen Eiden, Suzuki K Poulose, Zenghui Yu, Quentin Perret
On Wed, 2 Sept 2026 at 14:22, Vincent Donnefort <vdonnefort@google.com> wrote:
>
> On Mon, Aug 31, 2026 at 05:34:06PM +0100, Fuad Tabba wrote:
> > kvm_pkvm_ext_allowed() denies every capability it does not name, so a
> > protected VM reports 0 for interfaces it implements: KVM_CHECK_EXTENSION
> > denies KVM_CAP_ONE_REG while KVM_{GET,SET}_ONE_REG stage the guest's
> > boot state.
> >
> > Allow the capabilities that work for a protected guest, the vGIC and
> > the I/O bus being host-managed, subject to the restrictions the rest of
> > this series adds.
> >
> > Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
> > ---
> > arch/arm64/include/asm/kvm_pkvm.h | 9 +++++++++
> > 1 file changed, 9 insertions(+)
> >
> > diff --git a/arch/arm64/include/asm/kvm_pkvm.h b/arch/arm64/include/asm/kvm_pkvm.h
> > index beea00e693a0a..c4c834d55e503 100644
> > --- a/arch/arm64/include/asm/kvm_pkvm.h
> > +++ b/arch/arm64/include/asm/kvm_pkvm.h
> > @@ -42,6 +42,15 @@ static inline bool kvm_pkvm_ext_allowed(struct kvm *kvm, long ext)
> > case KVM_CAP_ARM_VM_IPA_SIZE:
> > case KVM_CAP_ARM_PTRAUTH_ADDRESS:
> > case KVM_CAP_ARM_PTRAUTH_GENERIC:
> > + case KVM_CAP_ONE_REG:
>
> Shouldn't we bring the VCPU_PKVM_FINALIZED restriction in kvm_arch_vcpu_ioctl()
> first before we allow SET_ONE_REG/GET_ONE_REG?
KVM_{GET,SET}_ONE_REG don't go through kvm_pkvm_ext_allowed(), so
they're as reachable before this patch as after it.
The ordering is wrong though, and there are a few more capabilities
advertised here and restricted later. I'll fix it for all of them in
v2.
Thanks,
/fuad
>
> > + case KVM_CAP_MP_STATE:
> > + case KVM_CAP_VCPU_EVENTS:
> > + case KVM_CAP_VCPU_ATTRIBUTES:
> > + case KVM_CAP_IMMEDIATE_EXIT:
> > + case KVM_CAP_IOEVENTFD:
> > + case KVM_CAP_IRQFD_RESAMPLE:
> > + case KVM_CAP_ARM_IRQ_LINE_LAYOUT_2:
> > + case KVM_CAP_ARM_INJECT_SERROR_ESR:
> > return true;
> > case KVM_CAP_ARM_MTE:
> > return false;
> > --
> > 2.39.5
> >
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 03/17] KVM: arm64: Reject the PVTIME vCPU attribute for protected VMs
2026-09-02 13:30 ` Vincent Donnefort
@ 2026-09-03 16:21 ` Fuad Tabba
0 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-09-03 16:21 UTC (permalink / raw)
To: Vincent Donnefort
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Joey Gouly,
Steffen Eiden, Suzuki K Poulose, Zenghui Yu, Quentin Perret
On Wed, 2 Sept 2026 at 14:30, Vincent Donnefort <vdonnefort@google.com> wrote:
>
> On Mon, Aug 31, 2026 at 05:34:07PM +0100, Fuad Tabba wrote:
> > pKVM currently does not support steal time for protected guests:
> > KVM_CAP_STEAL_TIME reports 0 for them. The host still accepts the
> > KVM_ARM_VCPU_PVTIME_CTRL attribute, whose IPA would point
> > kvm_update_stolen_time() at the guest's private memory on every vCPU
> > load. Return -EPERM for the attribute group instead.
> >
> > Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
> > ---
> > arch/arm64/kvm/guest.c | 10 ++++++++++
> > 1 file changed, 10 insertions(+)
> >
> > diff --git a/arch/arm64/kvm/guest.c b/arch/arm64/kvm/guest.c
> > index b01d6622b8720..23f725e2cf745 100644
> > --- a/arch/arm64/kvm/guest.c
> > +++ b/arch/arm64/kvm/guest.c
> > @@ -916,6 +916,10 @@ int kvm_arm_vcpu_arch_set_attr(struct kvm_vcpu *vcpu,
> > ret = kvm_arm_timer_set_attr(vcpu, attr);
> > break;
> > case KVM_ARM_VCPU_PVTIME_CTRL:
> > + /* Steal time is not offered to protected guests. */
> > + if (kvm_vm_is_protected(vcpu->kvm))
> > + return -EPERM;
> > +
>
> nit: We have vcpu_is_protected()
Will do, and I found others in this series to fix with it.
> Also, would that make more sense to put that check in kvm_arm_pvtime_supported()?
kvm_arm_pvtime_supported() takes no vcpu, and its other caller reports
KVM_CAP_STEAL_TIME, which is already 0 for a protected VM. More
important, moving them would turn these three into -ENXIO, where
-EPERM is the right errno: steal time is implemented, it's just not
permitted for a protected VM.
Cheers,
/fuad
>
> > ret = kvm_arm_pvtime_set_attr(vcpu, attr);
> > break;
> > default:
> > @@ -939,6 +943,9 @@ int kvm_arm_vcpu_arch_get_attr(struct kvm_vcpu *vcpu,
> > ret = kvm_arm_timer_get_attr(vcpu, attr);
> > break;
> > case KVM_ARM_VCPU_PVTIME_CTRL:
> > + if (kvm_vm_is_protected(vcpu->kvm))
> > + return -EPERM;
> > +
> > ret = kvm_arm_pvtime_get_attr(vcpu, attr);
> > break;
> > default:
> > @@ -962,6 +969,9 @@ int kvm_arm_vcpu_arch_has_attr(struct kvm_vcpu *vcpu,
> > ret = kvm_arm_timer_has_attr(vcpu, attr);
> > break;
> > case KVM_ARM_VCPU_PVTIME_CTRL:
> > + if (kvm_vm_is_protected(vcpu->kvm))
> > + return -EPERM;
> > +
> > ret = kvm_arm_pvtime_has_attr(vcpu, attr);
> > break;
> > default:
> > --
> > 2.39.5
> >
>
> --
> Vincent
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 05/17] KVM: arm64: Skip fixed-feature state flush for protected vCPUs
2026-09-02 15:26 ` Vincent Donnefort
@ 2026-09-03 16:22 ` Fuad Tabba
0 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-09-03 16:22 UTC (permalink / raw)
To: Vincent Donnefort
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Joey Gouly,
Steffen Eiden, Suzuki K Poulose, Zenghui Yu, Quentin Perret
On Wed, 2 Sept 2026 at 16:26, Vincent Donnefort <vdonnefort@google.com> wrote:
>
> On Wed, Sep 02, 2026 at 04:05:12PM +0100, Vincent Donnefort wrote:
> > On Mon, Aug 31, 2026 at 05:34:09PM +0100, Fuad Tabba wrote:
> > > flush_hyp_vcpu() copies the host's mdcr_el2, TWI/TWE and debug state
> > > into the hyp vCPU on every entry, so for a protected vCPU the host's
> > > trap configuration overrides the one pkvm_vcpu_init_traps() computed
> > > at EL2. Move those copies into the non-protected branch, and sync the
> > > debug state back for non-protected vCPUs only: a protected guest's
> > > debug registers are hypervisor-owned, and it takes the host's TWI/TWE
> > > at vCPU load. MDCR_EL2.TDA traps only the guest's own accesses; it
> > > does not stop the world switch from loading host-supplied breakpoints
> > > and MDSCR_EL1.
> > >
> > > EL2's mdcr_el2 starts from 0, and HPMN == 0 is reserved without
> > > FEAT_HPMN0. Set HPMN to the CPU's event counter count at vCPU load,
> > > the value the host's copy carried; a protected guest's PMU accesses
> > > trap regardless.
> > >
> > > Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
> > > ---
> > > arch/arm64/kvm/hyp/nvhe/hyp-main.c | 31 +++++++++++++++++++-----------
> > > arch/arm64/kvm/hyp/nvhe/pkvm.c | 2 +-
> > > 2 files changed, 21 insertions(+), 12 deletions(-)
> > >
> > > diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > > index ca7122b0bccdd..14e847b122abf 100644
> > > --- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > > +++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > > @@ -231,7 +231,6 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> > > u8 esr_ec;
> > >
> > > fpsimd_sve_flush();
> > > - flush_debug_state(hyp_vcpu);
> > >
> > > /*
> > > * If we deal with a non-protected guest and the state is potentially
> > > @@ -241,7 +240,14 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> > > if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
> > > if (vcpu_get_flag(host_vcpu, PKVM_HOST_STATE_DIRTY))
> > > flush_hyp_vcpu_state(hyp_vcpu);
> > > +
> > > + hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWI | HCR_TWE);
> > > + hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) &
> > > + (HCR_TWI | HCR_TWE);
> > > +
> > > + hyp_vcpu->vcpu.arch.mdcr_el2 = host_vcpu->arch.mdcr_el2;
> > > hyp_vcpu->vcpu.arch.iflags = host_vcpu->arch.iflags;
> > > + flush_debug_state(hyp_vcpu);
> > > } else {
> > > hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
> > > }
> > > @@ -249,17 +255,13 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> > > /* __hyp_running_vcpu must be NULL in a guest context. */
> > > hyp_vcpu->vcpu.arch.ctxt.__hyp_running_vcpu = NULL;
> > >
> > > - hyp_vcpu->vcpu.arch.mdcr_el2 = host_vcpu->arch.mdcr_el2;
> > > /*
> > > - * HCR_EL2.VSE is host-owned (a pending virtual SError to inject), not a
> > > - * trap-control bit, so it must flow to the hyp vCPU alongside TWI/TWE
> > > - * for the vSError to be delivered. sync_hyp_vcpu() reflects it back.
> > > + * A host-injected vSError is masked by the guest's own PSTATE.A, so it
> > > + * applies to protected guests too.
> > > */
> > > - hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWI | HCR_TWE | HCR_VSE);
> > > - hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) &
> > > - (HCR_TWI | HCR_TWE | HCR_VSE);
> > > -
> > > - hyp_vcpu->vcpu.arch.vsesr_el2 = host_vcpu->arch.vsesr_el2;
> > > + hyp_vcpu->vcpu.arch.hcr_el2 &= ~HCR_VSE;
> > > + hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) & HCR_VSE;
> > > + hyp_vcpu->vcpu.arch.vsesr_el2 = host_vcpu->arch.vsesr_el2;
> > >
> > > flush_hyp_vgic_state(hyp_vcpu);
> > >
> > > @@ -288,7 +290,8 @@ static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, u32 exit_reason)
> > > struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
> > >
> > > fpsimd_sve_sync(&hyp_vcpu->vcpu);
> > > - sync_debug_state(hyp_vcpu);
> > > + if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu))
> > > + sync_debug_state(hyp_vcpu);
> >
> > nit: We can't use the else arm below?
>
> or actually could we move the check into sync_debug_state() just like
> sync_hyp_timer_state() does?
Will do, and there's another case of this in the series that gets fixed with it.
/fuad
>
> >
> > >
> > > if (pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
> > > host_vcpu->arch.ctxt = hyp_vcpu->vcpu.arch.ctxt;
> > > @@ -329,6 +332,12 @@ static void handle___pkvm_vcpu_load(struct kvm_cpu_context *host_ctxt)
> > > /* Propagate WFx trapping flags */
> > > hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWE | HCR_TWI);
> > > hyp_vcpu->vcpu.arch.hcr_el2 |= hcr_el2 & (HCR_TWE | HCR_TWI);
> > > +
> > > + /* HPMN == 0 is reserved without FEAT_HPMN0. */
> > > + if (system_supports_pmuv3())
> > > + u64p_replace_bits(&hyp_vcpu->vcpu.arch.mdcr_el2,
> > > + FIELD_GET(ARMV8_PMU_PMCR_N, read_sysreg(pmcr_el0)),
> > > + MDCR_EL2_HPMN);
> > > } else {
> > > memcpy(&hyp_vcpu->vcpu.arch.fgt, hyp_vcpu->host_vcpu->arch.fgt,
> > > sizeof(hyp_vcpu->vcpu.arch.fgt));
> > > diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > > index 459bd9eb7e4bc..e85f13233da08 100644
> > > --- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > > +++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > > @@ -102,7 +102,7 @@ static void pvm_init_traps_mdcr(struct kvm_vcpu *vcpu)
> > >
> > > if (!kvm_has_feat(kvm, ID_AA64DFR0_EL1, PMUVer, IMP)) {
> > > val |= MDCR_EL2_TPM | MDCR_EL2_TPMCR;
> > > - val &= ~(MDCR_EL2_HPME | MDCR_EL2_MTPME | MDCR_EL2_HPMN_MASK);
> > > + val &= ~(MDCR_EL2_HPME | MDCR_EL2_MTPME);
> > > }
> > >
> > > if (!kvm_has_feat(kvm, ID_AA64DFR0_EL1, DebugVer, IMP))
> > > --
> > > 2.39.5
> > >
>
> --
> Vincent
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 07/17] KVM: arm64: Add system register reset framework for protected VMs
2026-09-02 15:14 ` Joey Gouly
@ 2026-09-03 16:24 ` Fuad Tabba
0 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-09-03 16:24 UTC (permalink / raw)
To: Joey Gouly
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret
On Wed, 2 Sept 2026 at 16:14, Joey Gouly <joey.gouly@arm.com> wrote:
>
> On Mon, Aug 31, 2026 at 05:34:11PM +0100, Fuad Tabba wrote:
> > Reset a protected VM's system registers at EL2 rather than taking the
> > host's values: add kvm_reset_pvm_sys_regs() and the
> > pvm_sys_reg_reset_vals[] table that drives it, and call it from
> > init_pkvm_hyp_vcpu() for protected vCPUs. The values follow the
> > host-side reset in sys_regs.c, with a poison value where it resets to
> > UNKNOWN, and for VBAR_EL1 and CONTEXTIDR_EL1 in place of the 0 it
> > resets them to. MPIDR_EL1 is derived from vcpu_id, as for any KVM
> > guest.
> >
> > Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
>
> The commit message says "rather than taking the host's values", but if I
> understand correctly, at this point in the series, they still take the
> hosts values because this is still present:
>
> hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
>
> Not necessarily saying anything needs to change, just to write down what
> I noticed while reviewing / reading this code!
Actually, you're right. This should change. That copy runs on every
entry and is removed only in patch 13, so until then the reset values
are overwritten and this patch has no observable effect for a
protected VM. The message will say so for v2. Patch 6 hits the same
copy and works around it, preserving the timer registers across it.
>
> The only other thing I was a bit unsure of was the actual list of
> registers pvm_sys_reg_reset_vals. It's the EL1 registers, minus
> unsupported FEATs (MTE, debug(?), GCS, etc)
Not quite: some EL0 registers are in it, and debug isn't excluded.
I'll state the rule in the commit message. And there's the Sashiko fix
too.
Thanks!
/fuad
>
> Otherwise:
> Acked-by: Joey Gouly <joey.gouly@arm.com>
>
> Thanks,
> Joey
>
> > ---
> > arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 1 +
> > arch/arm64/kvm/hyp/nvhe/pkvm.c | 5 ++
> > arch/arm64/kvm/hyp/nvhe/sys_regs.c | 91 ++++++++++++++++++++++++--
> > 3 files changed, 93 insertions(+), 4 deletions(-)
> >
> > diff --git a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> > index 49a0a992047ba..a04b7c04d5135 100644
> > --- a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> > +++ b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
> > @@ -95,6 +95,7 @@ bool kvm_handle_pvm_hvc64(struct kvm_vcpu *vcpu, u64 *exit_code);
> > bool kvm_handle_pvm_sysreg(struct kvm_vcpu *vcpu, u64 *exit_code);
> > bool kvm_handle_pvm_restricted(struct kvm_vcpu *vcpu, u64 *exit_code);
> > void kvm_init_pvm_id_regs(struct kvm_vcpu *vcpu);
> > +void kvm_reset_pvm_sys_regs(struct kvm_vcpu *vcpu);
> > int kvm_check_pvm_sysreg_table(void);
> >
> > #endif /* __ARM64_KVM_NVHE_PKVM_H__ */
> > diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > index e85f13233da08..af334318d0a03 100644
> > --- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > +++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > @@ -551,6 +551,11 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu,
> > goto done;
> >
> > ret = pkvm_vcpu_init_sve(hyp_vcpu, host_vcpu);
> > + if (ret)
> > + goto done;
> > +
> > + if (pkvm_hyp_vcpu_is_protected(hyp_vcpu))
> > + kvm_reset_pvm_sys_regs(&hyp_vcpu->vcpu);
> > done:
> > if (ret)
> > unpin_host_vcpu(host_vcpu);
> > diff --git a/arch/arm64/kvm/hyp/nvhe/sys_regs.c b/arch/arm64/kvm/hyp/nvhe/sys_regs.c
> > index 8758c68017765..ebfd48aa15b56 100644
> > --- a/arch/arm64/kvm/hyp/nvhe/sys_regs.c
> > +++ b/arch/arm64/kvm/hyp/nvhe/sys_regs.c
> > @@ -525,6 +525,84 @@ static const struct sys_reg_desc pvm_sys_reg_descs[] = {
> > /* Performance Monitoring Registers are restricted. */
> > };
> >
> > +struct sys_reg_desc_reset {
> > + int reg;
> > + void (*reset)(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *rd);
> > + u64 value;
> > +};
> > +
> > +/* Hardware value, as sys_regs.c's reset_actlr()/reset_amair_el1(). */
> > +static void reset_actlr(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
> > +{
> > + __vcpu_assign_sys_reg(vcpu, r->reg, read_sysreg(actlr_el1));
> > +}
> > +
> > +static void reset_amair_el1(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
> > +{
> > + __vcpu_assign_sys_reg(vcpu, r->reg, read_sysreg(amair_el1));
> > +}
> > +
> > +static void reset_mpidr(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
> > +{
> > + __vcpu_assign_sys_reg(vcpu, r->reg, kvm_calculate_mpidr(vcpu));
> > +}
> > +
> > +static void reset_value(struct kvm_vcpu *vcpu, const struct sys_reg_desc_reset *r)
> > +{
> > + __vcpu_assign_sys_reg(vcpu, r->reg, r->value);
> > +}
> > +
> > +#define RESET_VAL(REG, RESET_VAL) { REG, reset_value, RESET_VAL }
> > +
> > +#define RESET_ZERO(REG) RESET_VAL(REG, 0)
> > +
> > +#define RESET_UNKNOWN(REG) RESET_VAL(REG, 0x1de7ec7edbadc0deULL)
> > +
> > +#define RESET_FUNC(REG, RESET_FUNC) { REG, RESET_FUNC, 0 }
> > +
> > +/* Sorted ascending by reg; kvm_check_pvm_sysreg_table() enforces it. */
> > +static const struct sys_reg_desc_reset pvm_sys_reg_reset_vals[] = {
> > + RESET_FUNC(MPIDR_EL1, reset_mpidr),
> > + RESET_UNKNOWN(TPIDR_EL0),
> > + RESET_UNKNOWN(TPIDRRO_EL0),
> > + RESET_UNKNOWN(TPIDR_EL1),
> > + RESET_ZERO(CNTKCTL_EL1),
> > + RESET_UNKNOWN(PAR_EL1),
> > + RESET_ZERO(MDCCINT_EL1),
> > + RESET_ZERO(DISR_EL1),
> > + RESET_ZERO(PMCCFILTR_EL0),
> > + RESET_ZERO(PMUSERENR_EL0),
> > + RESET_ZERO(CPACR_EL1),
> > + RESET_VAL(CONTEXTIDR_EL1, 0x00000000dbadc0deULL),
> > + RESET_VAL(SCTLR_EL1, 0x00C50078ULL),
> > + RESET_FUNC(ACTLR_EL1, reset_actlr),
> > + RESET_ZERO(TCR_EL1),
> > + RESET_UNKNOWN(AFSR0_EL1),
> > + RESET_UNKNOWN(AFSR1_EL1),
> > + RESET_UNKNOWN(ESR_EL1),
> > + RESET_UNKNOWN(MAIR_EL1),
> > + RESET_FUNC(AMAIR_EL1, reset_amair_el1),
> > + RESET_ZERO(MDSCR_EL1),
> > + RESET_ZERO(ZCR_EL1),
> > + RESET_UNKNOWN(TTBR0_EL1),
> > + RESET_UNKNOWN(TTBR1_EL1),
> > + RESET_UNKNOWN(FAR_EL1),
> > + RESET_VAL(VBAR_EL1, 0x1de7ec7edbadc000ULL),
> > + RESET_UNKNOWN(PIRE0_EL1),
> > + RESET_UNKNOWN(PIR_EL1),
> > +};
> > +
> > +void kvm_reset_pvm_sys_regs(struct kvm_vcpu *vcpu)
> > +{
> > + unsigned long i;
> > +
> > + for (i = 0; i < ARRAY_SIZE(pvm_sys_reg_reset_vals); i++) {
> > + const struct sys_reg_desc_reset *r = &pvm_sys_reg_reset_vals[i];
> > +
> > + r->reset(vcpu, r);
> > + }
> > +}
> > +
> > /*
> > * Initializes feature registers for protected vms.
> > */
> > @@ -550,16 +628,21 @@ void kvm_init_pvm_id_regs(struct kvm_vcpu *vcpu)
> > }
> >
> > /*
> > - * Checks that the sysreg table is unique and in-order.
> > - *
> > - * Returns 0 if the table is consistent, or 1 otherwise.
> > + * Both tables must be unique and sorted ascending. pvm_sys_reg_descs.reg is the
> > + * sys_reg() encoding, pvm_sys_reg_reset_vals.reg the vcpu_sysreg index, so they
> > + * compare differently. BUG_ON() at __pkvm_init: fatal at boot.
> > */
> > int kvm_check_pvm_sysreg_table(void)
> > {
> > unsigned int i;
> >
> > for (i = 1; i < ARRAY_SIZE(pvm_sys_reg_descs); i++) {
> > - if (cmp_sys_reg(&pvm_sys_reg_descs[i-1], &pvm_sys_reg_descs[i]) >= 0)
> > + if (cmp_sys_reg(&pvm_sys_reg_descs[i - 1], &pvm_sys_reg_descs[i]) >= 0)
> > + return 1;
> > + }
> > +
> > + for (i = 1; i < ARRAY_SIZE(pvm_sys_reg_reset_vals); i++) {
> > + if (pvm_sys_reg_reset_vals[i - 1].reg >= pvm_sys_reg_reset_vals[i].reg)
> > return 1;
> > }
> >
> > --
> > 2.39.5
> >
^ permalink raw reply [flat|nested] 32+ messages in thread
* Re: [PATCH 08/17] KVM: arm64: Implement HVC handling for protected guests at EL2
2026-09-03 15:12 ` Joey Gouly
@ 2026-09-03 16:25 ` Fuad Tabba
0 siblings, 0 replies; 32+ messages in thread
From: Fuad Tabba @ 2026-09-03 16:25 UTC (permalink / raw)
To: Joey Gouly
Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel,
linux-kernel, Catalin Marinas, Will Deacon, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Vincent Donnefort, Quentin Perret
On Thu, 3 Sept 2026 at 16:13, Joey Gouly <joey.gouly@arm.com> wrote:
>
> On Mon, Aug 31, 2026 at 05:34:12PM +0100, Fuad Tabba wrote:
> > Extend kvm_handle_pvm_hvc64() to handle SMCCC_VERSION,
> > SMCCC_ARCH_FEATURES and the vendor hypervisor call UID at EL2, so
> > these queries do not reach the host. ARCH_FEATURES is mandatory from
> > SMCCC 1.1, the version EL2 reports: it returns SUCCESS for itself and
> > for SMCCC_VERSION, and NOT_SUPPORTED for anything else.
> >
> > Add handle_pvm_entry_hvc64() and handle_pvm_exit_hvc64(), which
> > forward a protected guest's HVCs to the host and return the reply; a
> > later patch wires them into the per-EC dispatch tables.
>
> These two functions are unused in this commit, and are modified in the
> next commit, while still being unused. Then handle_pvm_entry_hvc64()
> becomes a wrapper for handle_pvm_entry_psci() (which was the whole body
> of handle_pvm_entry_hvc64). It's quite hard to follow. If these
> functions are really unused and then modified, maybe it's just better to
> remove them from this commit?
Both move to the patch that first calls them, so each appears once, in
its final form, beside its caller.
The wrapper should go with it: handle_pvm_entry_hvc64() only calls
handle_pvm_entry_psci(), and PSCI calls are the only HVCs forwarded to
the host, so the split has no non-PSCI case to handle.
Thanks for the reviews!
/fuad
>
> Thanks,
> Joey
>
> >
> > Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
> > ---
> > arch/arm64/kvm/hyp/nvhe/hyp-main.c | 28 ++++++++++++++++++++++++++++
> > arch/arm64/kvm/hyp/nvhe/pkvm.c | 29 +++++++++++++++++++++++++++++
> > 2 files changed, 57 insertions(+)
> >
> > diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > index d4b0f69ff130c..62864db1e099a 100644
> > --- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > +++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > @@ -4,6 +4,8 @@
> > * Author: Andrew Scull <ascull@google.com>
> > */
> >
> > +#include <kvm/arm_hypercalls.h>
> > +
> > #include <hyp/adjust_pc.h>
> > #include <hyp/switch.h>
> >
> > @@ -33,6 +35,32 @@ void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt);
> >
> > typedef void (*hyp_entry_exit_handler_fn)(struct pkvm_hyp_vcpu *);
> >
> > +static void __maybe_unused handle_pvm_entry_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
> > +{
> > + int i;
> > +
> > + for (i = 0; i < 4; i++) {
> > + u64 ret =
> > + READ_ONCE(hyp_vcpu->host_vcpu->arch.ctxt.regs.regs[i]);
> > + vcpu_set_reg(&hyp_vcpu->vcpu, i, ret);
> > + }
> > +}
> > +
> > +static void __maybe_unused handle_pvm_exit_hvc64(struct pkvm_hyp_vcpu *hyp_vcpu)
> > +{
> > + struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
> > + int i;
> > +
> > + WRITE_ONCE(host_vcpu->arch.fault.esr_el2,
> > + hyp_vcpu->vcpu.arch.fault.esr_el2);
> > +
> > + /* Pass the HVC function id (r0) and its arguments. */
> > + for (i = 0; i < 8; i++) {
> > + WRITE_ONCE(host_vcpu->arch.ctxt.regs.regs[i],
> > + vcpu_get_reg(&hyp_vcpu->vcpu, i));
> > + }
> > +}
> > +
> > static void handle_vm_entry_generic(struct pkvm_hyp_vcpu *hyp_vcpu)
> > {
> > vcpu_copy_flag(&hyp_vcpu->vcpu, hyp_vcpu->host_vcpu, PC_UPDATE_REQ);
> > diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > index af334318d0a03..0fe11f95e2e26 100644
> > --- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > +++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
> > @@ -1144,8 +1144,37 @@ bool kvm_handle_pvm_hvc64(struct kvm_vcpu *vcpu, u64 *exit_code)
> > {
> > u64 val[4] = { SMCCC_RET_INVALID_PARAMETER };
> > bool handled = true;
> > + u32 feature;
> > + uuid_t uuid;
> >
> > switch (smccc_get_function(vcpu)) {
> > + case ARM_SMCCC_VERSION_FUNC_ID:
> > + /* Nothing to be handled by the host. Go back to the guest. */
> > + val[0] = ARM_SMCCC_VERSION_1_1;
> > + val[1] = 0;
> > + val[2] = 0;
> > + val[3] = 0;
> > + break;
> > + case ARM_SMCCC_ARCH_FEATURES_FUNC_ID:
> > + /* SUCCESS only for the architecture calls EL2 implements. */
> > + feature = smccc_get_arg1(vcpu);
> > + switch (feature) {
> > + case ARM_SMCCC_VERSION_FUNC_ID:
> > + case ARM_SMCCC_ARCH_FEATURES_FUNC_ID:
> > + val[0] = SMCCC_RET_SUCCESS;
> > + break;
> > + default:
> > + val[0] = SMCCC_RET_NOT_SUPPORTED;
> > + break;
> > + }
> > + break;
> > + case ARM_SMCCC_VENDOR_HYP_CALL_UID_FUNC_ID:
> > + uuid = ARM_SMCCC_VENDOR_HYP_UID_KVM;
> > + val[0] = smccc_uuid_to_reg(&uuid, 0);
> > + val[1] = smccc_uuid_to_reg(&uuid, 1);
> > + val[2] = smccc_uuid_to_reg(&uuid, 2);
> > + val[3] = smccc_uuid_to_reg(&uuid, 3);
> > + break;
> > case ARM_SMCCC_VENDOR_HYP_KVM_FEATURES_FUNC_ID:
> > val[0] = BIT(ARM_SMCCC_KVM_FUNC_FEATURES);
> > val[0] |= BIT(ARM_SMCCC_KVM_FUNC_HYP_MEMINFO);
> > --
> > 2.39.5
> >
^ permalink raw reply [flat|nested] 32+ messages in thread
end of thread, other threads:[~2026-09-03 16:26 UTC | newest]
Thread overview: 32+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-31 16:34 [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
2026-08-31 16:34 ` [PATCH 01/17] KVM: arm64: Sync HCR_EL2.VSE back to the host vCPU under pKVM Fuad Tabba
2026-08-31 16:34 ` [PATCH 02/17] KVM: arm64: Advertise the capabilities that protected VMs support Fuad Tabba
2026-09-02 13:22 ` Vincent Donnefort
2026-09-03 16:20 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 03/17] KVM: arm64: Reject the PVTIME vCPU attribute for protected VMs Fuad Tabba
2026-09-02 13:30 ` Vincent Donnefort
2026-09-03 16:21 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 04/17] KVM: arm64: Introduce per-EC entry handlers for pKVM Fuad Tabba
2026-09-02 10:12 ` Joey Gouly
2026-09-02 11:35 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 05/17] KVM: arm64: Skip fixed-feature state flush for protected vCPUs Fuad Tabba
2026-09-02 15:05 ` Vincent Donnefort
2026-09-02 15:26 ` Vincent Donnefort
2026-09-03 16:22 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 06/17] KVM: arm64: Add {flush,sync}_hyp_timer_state() primitives Fuad Tabba
2026-08-31 16:34 ` [PATCH 07/17] KVM: arm64: Add system register reset framework for protected VMs Fuad Tabba
2026-09-02 15:14 ` Joey Gouly
2026-09-03 16:24 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 08/17] KVM: arm64: Implement HVC handling for protected guests at EL2 Fuad Tabba
2026-09-03 15:12 ` Joey Gouly
2026-09-03 16:25 ` Fuad Tabba
2026-08-31 16:34 ` [PATCH 09/17] KVM: arm64: Handle PSCI calls for protected VMs " Fuad Tabba
2026-08-31 16:34 ` [PATCH 10/17] KVM: arm64: Restrict KVM_ARM_VCPU_INIT and PSCI version for protected VMs Fuad Tabba
2026-08-31 16:34 ` [PATCH 11/17] KVM: arm64: Prevent host PC adjustments for protected vCPUs Fuad Tabba
2026-08-31 16:34 ` [PATCH 12/17] KVM: arm64: Inject an UNDEF at EL2 for unhandled protected guest exits Fuad Tabba
2026-08-31 16:34 ` [PATCH 13/17] KVM: arm64: Add per-EC entry/exit state marshalling for protected guests Fuad Tabba
2026-08-31 16:34 ` [PATCH 14/17] KVM: arm64: Pend a protected guest's SError with HCR_EL2.VSE only Fuad Tabba
2026-08-31 16:34 ` [PATCH 15/17] KVM: arm64: Reject host access to protected VM private state Fuad Tabba
2026-08-31 16:34 ` [PATCH 16/17] KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off Fuad Tabba
2026-08-31 16:34 ` [PATCH 17/17] KVM: arm64: Document the protected VM userspace API Fuad Tabba
2026-08-31 19:27 ` [PATCH 00/17] KVM: arm64: Confine protected VM vCPU state to EL2 Fuad Tabba
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).