KVM: arm64: Confine protected VM vCPU state to EL2
Subsystems (4)
Fuad Tabba
reviewerv2 cover KVM: arm64: Confine protected VM vCPU state to EL2
Hi folks,
Changes since v1 [1]:
- Handle the SMCCC_ARCH_WORKAROUND_1/2/3 queries at EL2 from the host's mitigation state, instead of returning NOT_SUPPORTED. (Sashiko)
- Reset ACTLR_EL1 and AMAIR_EL1 to 0, and trim the reset table to the registers the world switch loads. (Sashiko, Joey)
- Move the capability allowlist to the end, after the restrictions it's subject to. (Vincent)
- Use vcpu_is_protected() throughout, and move the protected-vCPU check inside the debug flush and sync helpers. (Vincent)
- Add the HVC64 entry and exit handlers in the marshalling patch that first uses them, without the wrapper. (Joey)
- Rebased onto v7.3-rc2.
- Collected Joey's Reviewed-by on the per-EC entry handler patch. His Acked-by on the reset framework patch isn't carried, since its code and message changed. (thanks!)
Following the vCPU state-sync series [2], 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 isn't 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 are posted separately [3].
Patch 1 is the HCR_EL2.VSE fix posted separately [4]. It isn't part of this series. It's carried so the series applies as is and Sashiko can run on it.
The KVM_ARM_PREFERRED_TARGET documentation fix [5] went out just ahead of v1. 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: The PVTIME rejection. 03-04: Per-exception-class entry handlers; EL2 owns a protected
vCPU's trap configuration.
05-07: Timer state, system register reset and HVC handling at EL2. 08-09: PSCI at EL2, and the KVM_ARM_VCPU_INIT and PSCI version
restrictions.
10-13: Host PC adjustments blocked; an UNDEF at EL2 for exit
classes the host doesn't emulate; per-class state marshalling; a protected guest's SError pended with HCR_EL2.VSE.
14-15: Host access to private state, and host power-on of a vCPU
EL2 holds powered off, rejected.
16: Capability allowlist.
17: Documentation.
Still to come: selftests, TRNG, self-hosted debug and SVE for protected guests, and much more, as separate series.
Based on v7.3-rc2 (df2908090cda3).
Cheers,
/fuad
[1] https://lore.kernel.org/all/[email protected]/
[2] https://lore.kernel.org/all/[email protected]/
[3] https://lore.kernel.org/all/[email protected]/
[4] https://lore.kernel.org/all/[email protected]/
[5] https://lore.kernel.org/all/[email protected]/
Fuad Tabba (15):
KVM: arm64: Sync HCR_EL2.VSE back to the host vCPU under pKVM
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: Advertise the capabilities that protected VMs support
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_hyp.h | 4 + arch/arm64/include/asm/kvm_pkvm.h | 34 +- arch/arm64/kvm/arm.c | 43 ++ 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 | 560 +++++++++++++++++- arch/arm64/kvm/hyp/nvhe/pkvm.c | 442 +++++++++++++- arch/arm64/kvm/hyp/nvhe/switch.c | 29 +- arch/arm64/kvm/hyp/nvhe/sys_regs.c | 73 ++- 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 + 20 files changed, 1406 insertions(+), 80 deletions(-)
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v2 1/17 KVM: arm64: Sync HCR_EL2.VSE back to the host vCPU under pKVM
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.
--- arch/arm64/kvm/hyp/nvhe/hyp-main.c | 4 ++++ 1 file changed, 4 insertions(+)
arch/arm64/kvm/hyp/nvhe/hyp-main.c +4 -0
@@ -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); }
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v2 2/17 KVM: arm64: Reject the PVTIME vCPU attribute for protected VMs
In reply to Fuad TabbapKVM currently doesn't 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.
--- arch/arm64/kvm/guest.c | 10 ++++++++++ 1 file changed, 10 insertions(+)
arch/arm64/kvm/guest.c +10 -0
@@ -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 advertised to protected guests. */ +if (vcpu_is_protected (vcpu)) +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 (vcpu_is_protected (vcpu)) +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 (vcpu_is_protected (vcpu)) +return -EPERM ; + ret =kvm_arm_pvtime_has_attr (vcpu, attr);break ;default :
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v2 3/17 KVM: arm64: Introduce per-EC entry handlers for pKVM
In reply to Fuad TabbaFrom: Marc Zyngier <[email protected]>
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. Its single generic handler stands in until the per-EC marshalling patch replaces the table with protected-only ones.
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.
--- 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(-)
arch/arm64/include/asm/kvm_host.h +19 -0
@@ -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))
arch/arm64/kvm/hyp/include/nvhe/pkvm.h +3 -0
@@ -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; };/*
arch/arm64/kvm/hyp/nvhe/hyp-main.c +35 -4
@@ -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);
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v2 4/17 KVM: arm64: Skip fixed-feature state flush for protected vCPUs
In reply to Fuad Tabbaflush_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 the mdcr_el2 and TWI/TWE copies into the non-protected branch, and return early from flush_debug_state() and sync_debug_state() for a protected vCPU: its 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 doesn't 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.
--- arch/arm64/kvm/hyp/nvhe/hyp-main.c | 32 +++++++++++++++++++++--------- arch/arm64/kvm/hyp/nvhe/pkvm.c | 2 +- 2 files changed, 24 insertions(+), 10 deletions(-)
arch/arm64/kvm/hyp/nvhe/hyp-main.c +23 -9
@@ -200,6 +200,9 @@ static void flush_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu) {struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu; +if (pkvm_hyp_vcpu_is_protected (hyp_vcpu)) +return ; + hyp_vcpu->vcpu.arch.debug_owner = host_vcpu->arch.debug_owner;if (kvm_guest_owns_debug_regs (&hyp_vcpu->vcpu)) {
@@ -218,6 +221,9 @@ static void sync_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu) {struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu; +if (pkvm_hyp_vcpu_is_protected (hyp_vcpu)) +return ; +if (kvm_guest_owns_debug_regs (&hyp_vcpu->vcpu)) host_vcpu->arch.vcpu_debug_state = hyp_vcpu->vcpu.arch.vcpu_debug_state;else if (kvm_host_owns_debug_regs (&hyp_vcpu->vcpu))
@@ -241,6 +247,12 @@ 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; }else { hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
@@ -249,17 +261,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);
@@ -329,6 +337,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));
arch/arm64/kvm/hyp/nvhe/pkvm.c +1 -1
@@ -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 ))
Signature · 1 line
2.39.5
[email protected]
AI reviewRe: [PATCH v2 04/17] KVM: arm64: Skip fixed-feature state flush for protected vCPUs
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [High] State Leak and Host Integrity Bypass via Unswitched Debug Registers for Protected vCPUs --
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] State Leak and Host Integrity Bypass via Unswitched Debug Registers for Protected vCPUs --
commit d142f7027c7a95ffce6ea110097ca91e27e8ccea
Author: Fuad Tabba <[email protected]>
KVM: arm64: Skip fixed-feature state flush for protected vCPUs
This commit modifies flush_hyp_vcpu() and related debug state flushing functions to skip copying fixed-feature states, such as mdcr_el2, TWI/TWE, and debug state, for protected vCPUs. The host's debug registers are considered hypervisor-owned for protected guests, and the debug state flush now returns early in this case.
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void flush_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu) · line 205
Quoted 11 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +3
-0@@ -200,6 +200,9 @@ static void flush_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu) {struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu; +if (pkvm_hyp_vcpu_is_protected (hyp_vcpu)) +return ; +
[Severity: High]
Does this early return in flush_debug_state() in arch/arm64/kvm/hyp/nvhe/hyp-main.c bypass the debug register context switch for protected guests?
By returning early here, hyp_vcpu->vcpu.arch.debug_owner is never updated from the host and remains at its default VCPU_DEBUG_FREE. During the world switch, __debug_switch_to_guest_common() and __debug_switch_to_host_common() return early if debug_owner == FREE. Consequently, KVM bypasses saving the host's live hardware debug registers and loading the guest's debug registers. The guest enters execution with the host's actual hardware breakpoint registers still active in the pipeline.
Normally, this is mitigated because KVM sets MDCR_EL2.TDA to trap debug accesses if the guest doesn't own the registers. However, for protected VMs, pvm_init_traps_mdcr() only sets TDA if DebugVer is unimplemented. With TDA unset, the protected guest can freely read the host's debug registers (leaking host state) and overwrite them when it explicitly reads or writes DBGBVRn_EL1, DBGBCRn_EL1, or MDSCR_EL1.
Quoted 3 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0hyp_vcpu->vcpu.arch.debug_owner = host_vcpu->arch.debug_owner;if (kvm_guest_owns_debug_regs (&hyp_vcpu->vcpu)) {
[ ... ]
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void sync_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu) · line 226
Quoted 7 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +3
-0@@ -218,6 +221,9 @@ static void sync_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu) {struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu; +if (pkvm_hyp_vcpu_is_protected (hyp_vcpu)) +return ; +
[Severity: High]
Does this early return in sync_debug_state() in arch/arm64/kvm/hyp/nvhe/hyp-main.c skip restoring the host's debug registers upon guest exit?
Because the context switch back to the host returns early due to the bypassed flush, any guest modifications to the hardware debug registers persist into the host execution context. When the guest exits, the corrupted debug registers remain active when the host resumes execution, allowing an untrusted protected guest to mount an attack against the host kernel.
Quoted 2 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0if (kvm_guest_owns_debug_regs (&hyp_vcpu->vcpu)) host_vcpu->arch.vcpu_debug_state = hyp_vcpu->vcpu.arch.vcpu_debug_state;
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=4
Fuad Tabba
reviewerRe: [PATCH v2 04/17] KVM: arm64: Skip fixed-feature state flush for protected vCPUs
Quoted 3 lines · On Mon, 7 Sept 2026 at 08:23, <[email protected]> wrote:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] State Leak and Host Integrity Bypass via Unswitched Debug Registers for Protected vCPUs
Not this one, Sashiko.
...
Quoted 13 lines
[Severity: High]
Does this early return inflush_debug_state()inarch/arm64/kvm/hyp/nvhe/hyp-main.cbypass the debug register context switch for protected guests?By returning early here,
hyp_vcpu->vcpu.arch.debug_owneris never updated from the host and remains at its defaultVCPU_DEBUG_FREE. During the world switch,__debug_switch_to_guest_common()and__debug_switch_to_host_common()return early ifdebug_owner== FREE. Consequently, KVM bypasses saving the host's live hardware debug registers and loading the guest's debug registers. The guest enters execution with the host's actual hardware breakpoint registers still active in the pipeline.
Quoted 6 lines
Normally, this is mitigated because KVM sets MDCR_EL2.TDA to trap debug accesses if the guest doesn't own the registers. However, for protected VMs,
pvm_init_traps_mdcr()only sets TDA if DebugVer is unimplemented. With TDA unset, the protected guest can freely read the host's debug registers (leaking host state) and overwrite them when it explicitly reads or writesDBGBVRn_EL1,DBGBCRn_EL1, orMDSCR_EL1.
Self-hosted debug for protected VMs is still to come, as the cover says. Until then DebugVer is NI, so pvm_init_traps_mdcr() sets TDA and TDRA and the guest's debug register accesses trap, RAZ/WI or UNDEF.
Quoted 11 lines
Quoted 4 lines
hyp_vcpu->vcpu.arch.debug_owner = host_vcpu->arch.debug_owner; if (kvm_guest_owns_debug_regs(&hyp_vcpu->vcpu)) {[ ... ]
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0@@ -218,6 +221,9 @@ static void sync_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu) {struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
Quoted 8 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void flush_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu) · line 205
Quoted 3 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +3
-0+if (pkvm_hyp_vcpu_is_protected (hyp_vcpu)) +return ; +[Severity: High]
Does this early return insync_debug_state()inarch/arm64/kvm/hyp/nvhe/hyp-main.cskip restoring the host's debug registers upon guest exit?
Same answer: the guest can't write them.
Thanks,
/fuad
/fuad
Quoted 12 lines
Because the context switch back to the host returns early due to the bypassed flush, any guest modifications to the hardware debug registers persist into the host execution context. When the guest exits, the corrupted debug registers remain active when the host resumes execution, allowing an untrusted protected guest to mount an attack against the host kernel.
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void sync_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu) · line 228
Quoted 2 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0if (kvm_guest_owns_debug_regs (&hyp_vcpu->vcpu)) host_vcpu->arch.vcpu_debug_state = hyp_vcpu->vcpu.arch.vcpu_debug_state;--
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=4
Fuad Tabba
reviewerPATCH v2 5/17 KVM: arm64: Add {flush,sync}_hyp_timer_state() primitives
In reply to Fuad TabbaFrom: Marc Zyngier <[email protected]>
A protected guest's virtual timer is programmed from EL2's own copy of the state and read back there, so the host can't 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, until the per-EC marshalling patch removes that copy.
--- arch/arm64/kvm/hyp/nvhe/hyp-main.c | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+)
arch/arm64/kvm/hyp/nvhe/hyp-main.c +39 -0
@@ -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) {
@@ -255,7 +282,17 @@ 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; }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. */
@@ -270,6 +307,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;
@@ -318,6 +356,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; }
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v2 6/17 KVM: arm64: Add system register reset framework for protected VMs
In reply to Fuad TabbaAdd 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 table holds the registers sys_regs.c resets that __sysreg_restore_state_nvhe() loads for a protected vCPU. The rest of the context keeps the zero the donated hyp vCPU page is cleared to. A poison value stands in where sys_regs.c resets to UNKNOWN, and for VBAR_EL1 and CONTEXTIDR_EL1 in place of the 0 it uses. MPIDR_EL1 is derived from vcpu_id, as for any KVM guest. AMAIR_EL1 resets to 0 where sys_regs.c reads the hardware: the host writes it freely before the hypercall, and it's loaded on every guest entry.
Until the per-EC marshalling patch removes the host's full-context copy from flush_hyp_vcpu(), that copy overwrites these values on every entry, so this patch has no observable effect on its own.
--- 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 | 73 ++++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 4 deletions(-)
arch/arm64/kvm/hyp/include/nvhe/pkvm.h +1 -0
@@ -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__ */
arch/arm64/kvm/hyp/nvhe/pkvm.c +5 -0
@@ -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);
arch/arm64/kvm/hyp/nvhe/sys_regs.c +69 -4
91 lines not on this page · raw message
Signature · 1 line
2.39.5
[email protected]
AI reviewRe: [PATCH v2 06/17] KVM: arm64: Add system register reset framework for protected VMs
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Medium] Missing EL1 system registers from the pKVM reset array `pvm_sys_reg_reset_vals`. - [Medium]…
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Missing EL1 system registers from the pKVM reset array pvm_sys_reg_reset_vals.
- [Medium] Invalid numeric comparison (>=) over sparse enum vcpu_sysreg values to enforce fragile array sorting. --
commit 7a5dc29475a7c4550659db2df410ae56a785635e
Author: Fuad Tabba <[email protected]>
Subject: KVM: arm64: Add system register reset framework for protected VMs
This patch introduces the system register reset framework for protected VMs, adding kvm_reset_pvm_sys_regs() and its driving array pvm_sys_reg_reset_vals. It populates reset values for sysregs that need to be reset to UNKNOWN or explicit values for pKVM guests.
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/sys_regs.c +0
-0
[ ... ]
arch/arm64/kvm/hyp/nvhe/sys_regs.c static const struct sys_reg_desc pvm_sys_reg_descs[] = { · line 557
Quoted 6 lines
arch/arm64/kvm/hyp/nvhe/sys_regs.c +6
-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 ),
[Severity: Medium]
Are we missing several EL1 system registers from this array? The commit message mentions this table is supposed to hold the registers that sys_regs.c resets to UNKNOWN. However, it seems to omit several newer architectural registers (like PIR_EL1, PIRE0_EL1, POR_EL1, POR_EL0, TFSR_EL1, TFSRE0_EL1) which __sysreg_restore_state_nvhe() explicitly restores for a protected vCPU.
Because the hyp vCPU page is zeroed, these omitted registers will be zero-initialized instead of receiving the intended poison value (0x1de7ec7edbadc0deULL) when kvm_reset_pvm_sys_regs() is called for a protected VM.
[ ... ]
Quoted 1 line
arch/arm64/kvm/hyp/nvhe/sys_regs.c +0
-0@@ -550,16 +610,21 @@ void kvm_init_pvm_id_regs(struct kvm_vcpu *vcpu)
[ ... ]
Quoted 4 lines
int
kvm_check_pvm_sysreg_table(void)
{arch/arm64/kvm/hyp/nvhe/sys_regs.c +0
-0unsigned int i;
arch/arm64/kvm/hyp/nvhe/sys_regs.c void kvm_init_pvm_id_regs(struct kvm_vcpu *vcpu) · line 629
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/sys_regs.c +6
-1for (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 ; }
[Severity: Medium]
Is it safe to use a numeric comparison (>=) over the .reg field here? Since pvm_sys_reg_reset_vals[].reg contains enum vcpu_sysreg values, and these are indexed by their VNCR-page byte offset rather than in declaration order, a layout change or new enum value could reorder their integer values.
If that happens, the static array's manually sorted order would break, triggering a fatal BUG_ON() at boot during hypervisor initialization. Should this enforce an explicit allowlist or a different sorting mechanism instead of a numeric range check?
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=6
Fuad Tabba
reviewerRe: [PATCH v2 06/17] KVM: arm64: Add system register reset framework for protected VMs
Quoted 4 lines · On Mon, 7 Sept 2026 at 08:16, <[email protected]> wrote:
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Missing EL1 system registers from the pKVM reset arraypvm_sys_reg_reset_vals.
- [Medium] Invalid numeric comparison (>=) over sparseenum vcpu_sysregvalues to enforce fragile array sorting.
Nope...
Quoted 18 lines
--
commit
7a5dc29475a7c4550659db2df410ae56a785635e
Author: Fuad Tabba <[email protected]>
Subject: KVM: arm64: Add system register reset framework for protected VMsThis patch introduces the system register reset framework for protected VMs, adding
kvm_reset_pvm_sys_regs()and its driving arraypvm_sys_reg_reset_vals. It populates reset values for sysregs that need to be reset to UNKNOWN or explicit values for pKVM guests.Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/sys_regs.c +0
-0[ ... ]
Quoted 14 lines
arch/arm64/kvm/hyp/nvhe/sys_regs.c static const struct sys_reg_desc pvm_sys_reg_descs[] = { · line 557
Quoted 6 lines
arch/arm64/kvm/hyp/nvhe/sys_regs.c +6
-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 ),[Severity: Medium]
Are we missing several EL1 system registers from this array? The commit message mentions this table is supposed to hold the registers thatsys_regs.cresets to UNKNOWN. However, it seems to omit several newer architectural registers (likePIR_EL1,PIRE0_EL1,POR_EL1,POR_EL0,TFSR_EL1,TFSRE0_EL1) which__sysreg_restore_state_nvhe()explicitly restores for a protected vCPU.
It doesn't restore any of them for a protected vCPU. PIR_EL1, PIRE0_EL1 and POR_EL[01] are loaded only when the VM has S1PIE or S1POE, which pvm_calc_id_reg() never sets (it returns 0 for ID_AA64MMFR3_EL1), and TFSR_EL1 and TFSRE0_EL1 only with MTE, which kvm_pkvm_ext_allowed() doesn't allow. The table holds what the switch loads, as the message says.
...
Quoted 11 lines
[Severity: Medium]
Is it safe to use a numeric comparison (>=) over the .reg field here? Since pvm_sys_reg_reset_vals[].reg contains enumvcpu_sysregvalues, and these are indexed by their VNCR-page byte offset rather than in declaration order, a layout change or new enum value could reorder their integer values.If that happens, the static array's manually sorted order would break, triggering a fatal
BUG_ON()at boot during hypervisor initialization. Should this enforce an explicit allowlist or a different sorting mechanism instead of a numeric range check?
Nothing searches the reset table: kvm_reset_pvm_sys_regs() walks it linearly, only pvm_sys_reg_descs[] is searched. The check on it buys nothing, and I'll drop it in v3.
/fuad
/fuad
Quoted 3 lines
--
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=6
Re: [PATCH v2 06/17] KVM: arm64: Add system register reset framework for protected VMs
In reply to Fuad TabbaHi Fuad,
Quoted 5 lines · Fuad Tabba
Add
kvm_reset_pvm_sys_regs()and the pvm_sys_reg_reset_vals[] table that drives it, and call it frominit_pkvm_hyp_vcpu()for protected vCPUs. The table holds the registerssys_regs.cresets that__sysreg_restore_state_nvhe()loads for a protected vCPU. The rest of the context keeps the zero the donated hyp vCPU page is cleared to. A
If the context is zero'd, why is there RESET_ZERO()? If it's just for 'completeness', worth noting here.
Quoted 5 lines
poison value stands in where
sys_regs.cresets to UNKNOWN, and forVBAR_EL1andCONTEXTIDR_EL1in place of the 0 it uses.MPIDR_EL1is derived fromvcpu_id, as for any KVM guest.AMAIR_EL1resets to 0 wheresys_regs.creads the hardware: the host writes it freely before the hypercall, and it's loaded on every guest entry.
Also if you can just put a new line for each 'category' here would make it simpler to read.
Quoted 4 lines
Until the per-EC marshalling patch removes the host's full-context copy from
flush_hyp_vcpu(), that copy overwrites these values on every entry, so this patch has no observable effect on its own.
Thanks for adding this!
Quoted 123 lines
Signed-off-by Fuad Tabba <[email protected]>--- 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 | 73 ++++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 4 deletions(-)arch/arm64/kvm/hyp/include/nvhe/pkvm.h +1
-0@@ -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__ */ arch/arm64/kvm/hyp/nvhe/pkvm.c +5
-0@@ -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);arch/arm64/kvm/hyp/nvhe/sys_regs.c +63
-3@@ -525,6 +525,66 @@ 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; +}; + +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 (DISR_EL1 ), +RESET_ZERO (CPACR_EL1 ), +RESET_VAL (CONTEXTIDR_EL1 ,0x00000000dbadc0deULL ), +RESET_VAL (SCTLR_EL1 ,0x00C50078ULL ), +RESET_ZERO (TCR_EL1 ), +RESET_UNKNOWN (AFSR0_EL1 ), +RESET_UNKNOWN (AFSR1_EL1 ), +RESET_UNKNOWN (ESR_EL1 ), +RESET_UNKNOWN (MAIR_EL1 ), +RESET_ZERO (AMAIR_EL1 ), +RESET_ZERO (MDSCR_EL1 ), +RESET_UNKNOWN (TTBR0_EL1 ), +RESET_UNKNOWN (TTBR1_EL1 ), +RESET_UNKNOWN (FAR_EL1 ), +RESET_VAL (VBAR_EL1 ,0x1de7ec7edbadc000ULL ), +}; + +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 +610,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++) {
arch/arm64/kvm/hyp/nvhe/sys_regs.c void kvm_init_pvm_id_regs(struct kvm_vcpu *vcpu) · line 630
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/sys_regs.c +6
-1-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 ; }
Some minor commit message nits, and I see you're going to remove this sorting check.
Thanks,
Joey
Fuad Tabba
reviewerRe: [PATCH v2 06/17] KVM: arm64: Add system register reset framework for protected VMs
Hi Joey,
Quoted 4 lines · Joey Gouly
Hi Fuad,
On Mon, Sep 07, 2026 at 07:59:51AM +0100, Fuad Tabba wrote:
Quoted 8 lines
Quoted 5 lines
Add
kvm_reset_pvm_sys_regs()and the pvm_sys_reg_reset_vals[] table that drives it, and call it frominit_pkvm_hyp_vcpu()for protected vCPUs. The table holds the registerssys_regs.cresets that__sysreg_restore_state_nvhe()loads for a protected vCPU. The rest of the context keeps the zero the donated hyp vCPU page is cleared to. AIf the context is zero'd, why is there
RESET_ZERO()? If it's just for 'completeness', worth noting here.
I'll answer your question with part of the updated commit message for V3:
A register that resets to 0 still needs an entry: a later patch calls kvm_reset_pvm_sys_regs() again for PSCI CPU_ON at EL2, on a context that has run.
Quoted 9 lines
Quoted 6 lines
poison value stands in where
sys_regs.cresets to UNKNOWN, and forVBAR_EL1andCONTEXTIDR_EL1in place of the 0 it uses.MPIDR_EL1is derived fromvcpu_id, as for any KVM guest.AMAIR_EL1resets to 0 wheresys_regs.creads the hardware: the host writes it freely before the hypercall, and it's loaded on every guest entry.Also if you can just put a new line for each 'category' here would make it simpler to read.
Done for V3.
Quoted 7 lines
Quoted 5 lines
Until the per-EC marshalling patch removes the host's full-context copy from
flush_hyp_vcpu(), that copy overwrites these values on every entry, so this patch has no observable effect on its own.Thanks for adding this!
Thank you for your reviews. They've been very helpful!
Cheers,
/fuad
Quoted 142 lines
Quoted 124 lines
Signed-off-by Fuad Tabba <[email protected]>--- 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 | 73 ++++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 4 deletions(-)arch/arm64/kvm/hyp/include/nvhe/pkvm.h +1
-0@@ -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__ */ arch/arm64/kvm/hyp/nvhe/pkvm.c +5
-0@@ -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);arch/arm64/kvm/hyp/nvhe/sys_regs.c +63
-3@@ -525,6 +525,66 @@ 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; +}; + +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 (DISR_EL1 ), +RESET_ZERO (CPACR_EL1 ), +RESET_VAL (CONTEXTIDR_EL1 ,0x00000000dbadc0deULL ), +RESET_VAL (SCTLR_EL1 ,0x00C50078ULL ), +RESET_ZERO (TCR_EL1 ), +RESET_UNKNOWN (AFSR0_EL1 ), +RESET_UNKNOWN (AFSR1_EL1 ), +RESET_UNKNOWN (ESR_EL1 ), +RESET_UNKNOWN (MAIR_EL1 ), +RESET_ZERO (AMAIR_EL1 ), +RESET_ZERO (MDSCR_EL1 ), +RESET_UNKNOWN (TTBR0_EL1 ), +RESET_UNKNOWN (TTBR1_EL1 ), +RESET_UNKNOWN (FAR_EL1 ), +RESET_VAL (VBAR_EL1 ,0x1de7ec7edbadc000ULL ), +}; + +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 +610,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++) {arch/arm64/kvm/hyp/nvhe/sys_regs.c void kvm_init_pvm_id_regs(struct kvm_vcpu *vcpu) · line 630
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/sys_regs.c +6
-1-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 ; }Some minor commit message nits, and I see you're going to remove this sorting check.
Reviewed-by Joey Gouly <[email protected]>Thanks,
Joey
Fuad Tabba
reviewerPATCH v2 7/17 KVM: arm64: Implement HVC handling for protected guests at EL2
In reply to Fuad TabbaExtend kvm_handle_pvm_hvc64() to handle SMCCC_VERSION, SMCCC_ARCH_FEATURES and the vendor hypervisor call UID at EL2, so these queries don't reach the host. ARCH_FEATURES is mandatory from SMCCC 1.1, the version EL2 reports: it returns SUCCESS for itself and for SMCCC_VERSION, NOT_SUPPORTED for anything else, and handles the ARCH_WORKAROUND_1/2/3 queries as the host does, from a copy of the host's mitigation state taken at init with the sanitised ID registers. The workaround calls themselves are already handled on hyp entry.
--- arch/arm64/include/asm/kvm_hyp.h | 4 ++ arch/arm64/kvm/arm.c | 4 ++ arch/arm64/kvm/hyp/nvhe/pkvm.c | 70 ++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+)
arch/arm64/include/asm/kvm_hyp.h +4 -0
@@ -10,6 +10,7 @@#include <linux/compiler.h> #include <linux/kvm_host.h> #include <asm/alternative.h> +#include <asm/spectre.h> #include <asm/sysreg.h> DECLARE_PER_CPU(struct kvm_cpu_context, kvm_hyp_ctxt);
@@ -150,6 +151,9 @@ extern u64 kvm_nvhe_sym(id_aa64smfr0_el1_sys_val);extern unsigned long kvm_nvhe_sym (__icache_flags );extern unsigned int kvm_nvhe_sym (kvm_arm_vmid_bits ); +extern enum mitigation_state kvm_nvhe_sym (spectre_v2_state ); +extern enum mitigation_state kvm_nvhe_sym (spectre_v4_state ); +extern enum mitigation_state kvm_nvhe_sym (spectre_bhb_state );extern unsigned int kvm_nvhe_sym (kvm_host_sve_max_vl );extern unsigned long kvm_nvhe_sym (hyp_nr_cpus );extern unsigned int kvm_nvhe_sym (hyp_gicv3_nr_lr );
arch/arm64/kvm/arm.c +4 -0
@@ -2622,6 +2622,10 @@ static void kvm_hyp_init_symbols(void)kvm_nvhe_sym (__icache_flags) = __icache_flags;kvm_nvhe_sym (kvm_arm_vmid_bits) = kvm_arm_vmid_bits; +kvm_nvhe_sym (spectre_v2_state) =arm64_get_spectre_v2_state (); +kvm_nvhe_sym (spectre_v4_state) =arm64_get_spectre_v4_state (); +kvm_nvhe_sym (spectre_bhb_state) =arm64_get_spectre_bhb_state (); +/* Propagate the FGT state to the nVHE side */ kvm_nvhe_sym (hfgrtr_masks) = hfgrtr_masks;kvm_nvhe_sym (hfgwtr_masks) = hfgwtr_masks;
arch/arm64/kvm/hyp/nvhe/pkvm.c +70 -0
84 lines not on this page · raw message
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v2 8/17 KVM: arm64: Handle PSCI calls for protected VMs at EL2
In reply to Fuad TabbaEL2 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, including the TRNG calls the host handled for a protected guest until now. TRNG for protected guests is a follow-up.
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 doesn't 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() didn't 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, and the target's next run resets it and moves it to ON. The racing transitions are cmpxchg, and the reset state is published with a release/acquire pair, documented at each site. Rolling a CPU_ON the host failed back to OFF needs the host's return value, which the per-EC marshalling patch delivers along with the rollback. Until then such a target stays ON_PENDING.
--- arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 14 + arch/arm64/kvm/hyp/nvhe/hyp-main.c | 25 +- arch/arm64/kvm/hyp/nvhe/pkvm.c | 348 ++++++++++++++++++++++++- 3 files changed, 376 insertions(+), 11 deletions(-)
arch/arm64/kvm/hyp/include/nvhe/pkvm.h +14 -0
@@ -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__ */
arch/arm64/kvm/hyp/nvhe/hyp-main.c +19 -6
@@ -8,6 +8,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>
@@ -454,14 +455,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)) {/*
@@ -470,8 +469,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; }
arch/arm64/kvm/hyp/nvhe/pkvm.c +343 -5
420 lines not on this page · raw message
Signature · 1 line
2.39.5
[email protected]
AI reviewRe: [PATCH v2 08/17] KVM: arm64: Handle PSCI calls for protected VMs at EL2
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [High] The error path in pkvm_reset_vcpu() leaves reset_state->reset = true, breaking the synchronization barrier for subsequent CPU_ON calls.
Pre-existing issues:
- [High] Memory leak of SVE state pages when VM-wide initialization fails, due to clearing KVM_ARM_VCPU_SVE. --
commit c2e0971411fd802ad76348d4eef69923169c10e5
Author: Fuad Tabba <[email protected]>
KVM: arm64: Handle PSCI calls for protected VMs at EL2
This commit implements PSCI 1.1 handling at EL2 for protected VMs. It manages the vCPU power state transitions between OFF, ON_PENDING, and ON, and intercepts PSCI calls like CPU_ON, CPU_OFF, and AFFINITY_INFO. Calls that require host intervention, such as scheduling, are safely forwarded.
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +0
-0
[ ... ]
arch/arm64/kvm/hyp/nvhe/pkvm.c static void unpin_host_sve_state(struct pkvm_hyp_vcpu *hyp_vcpu) · line 447
Quoted 7 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +3
-0@@ -407,6 +442,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 ; +
[Severity: High]
This isn't a bug introduced by this patch, but does the early return in unpin_host_sve_state() cause a memory leak of SVE state pages when VM-wide initialization fails?
If pkvm_vcpu_init_sve() fails on a secondary vCPU (for example, due to an invalid configuration from host userspace), the error path in pkvm_vcpu_init_sve() clears the KVM_ARM_VCPU_SVE bit in the VM-wide vcpu_features bitmap.
During the subsequent teardown of the VM, unpin_host_vcpus() calls unpin_host_sve_state() for all vCPUs. Because the feature bit was cleared, unpin_host_sve_state() returns early and fails to unpin the SVE state memory that was successfully pinned for previously initialized vCPUs. This allows for a denial of service via resource exhaustion from host userspace.
Quoted 3 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +0
-0hyp_unpin_shared_mem (sve_state, sve_state +vcpu_sve_state_size (&hyp_vcpu->vcpu)); }
[ ... ]
Quoted 11 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +11
-0+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. +*/
arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu) · line 1217
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +10
-0+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 ;
[Severity: High]
Does the -ECANCELED error path in pkvm_reset_vcpu() leave reset_state->reset set to true, breaking the synchronization barrier for subsequent CPU_ON calls?
If a CPU_ON hypercall is followed by a host rollback (which sets power_state back to OFF but intentionally leaves reset_state.reset = true), a subsequent CPU_ON from the guest sets power_state to ON_PENDING, writes the new pc/r0, and sets reset_state->reset = true.
However, because reset_state->reset was already left as true, if the target vCPU executes pkvm_reset_vcpu() concurrently, its smp_load_acquire(&reset_state->reset) will immediately succeed. The target vCPU then reads pc and r0 before the CPU_ON call has finished writing them, resulting in a data race and the guest booting with corrupted state.
Quoted 16 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +16
-0+ } + +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 ; +}
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=8
Fuad Tabba
reviewerRe: [PATCH v2 08/17] KVM: arm64: Handle PSCI calls for protected VMs at EL2
Quoted 5 lines · On Mon, 7 Sept 2026 at 08:16, <[email protected]> wrote:
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [High] The error path inpkvm_reset_vcpu()leavesreset_state->reset = true, breaking the synchronization barrier for subsequentCPU_ONcalls.
Same as v1's rollback finding [1], so no.
Quoted 3 lines
Pre-existing issues:
- [High] Memory leak of SVE state pages when VM-wide initialization fails, due to clearingKVM_ARM_VCPU_SVE.
Fix is here [2].
/fuad
[1] https://lore.kernel.org/all/CA+EHjTyXKua0jbC-9AF4L19ikXopa5ELWPoQV18QojZy_e6F7Q@mail.gmail.com/
[2] https://lore.kernel.org/all/[email protected]/
Quoted 106 lines
--
commit
c2e0971411fd802ad76348d4eef69923169c10e5
Author: Fuad Tabba <[email protected]>KVM: arm64: Handle PSCI calls for protected VMs at EL2
This commit implements PSCI 1.1 handling at EL2 for protected VMs. It manages the vCPU power state transitions between OFF,
ON_PENDING, and ON, and intercepts PSCI calls likeCPU_ON,CPU_OFF, andAFFINITY_INFO. Calls that require host intervention, such as scheduling, are safely forwarded.Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +0
-0[ ... ]
arch/arm64/kvm/hyp/nvhe/pkvm.c static void unpin_host_sve_state(struct pkvm_hyp_vcpu *hyp_vcpu) · line 447
Quoted 7 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +3
-0@@ -407,6 +442,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 ; +[Severity: High]
This isn't a bug introduced by this patch, but does the early return inunpin_host_sve_state()cause a memory leak of SVE state pages when VM-wide initialization fails?If
pkvm_vcpu_init_sve()fails on a secondary vCPU (for example, due to an invalid configuration from host userspace), the error path inpkvm_vcpu_init_sve()clears theKVM_ARM_VCPU_SVEbit in the VM-widevcpu_featuresbitmap.During the subsequent teardown of the VM,
unpin_host_vcpus()callsunpin_host_sve_state()for all vCPUs. Because the feature bit was cleared,unpin_host_sve_state()returns early and fails to unpin the SVE state memory that was successfully pinned for previously initialized vCPUs. This allows for a denial of service via resource exhaustion from host userspace.Quoted 3 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +0
-0hyp_unpin_shared_mem (sve_state, sve_state +vcpu_sve_state_size (&hyp_vcpu->vcpu)); }[ ... ]
Quoted 11 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +11
-0+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. +*/ arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu) · line 1217
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +10
-0+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 ;[Severity: High]
Does the-ECANCELEDerror path inpkvm_reset_vcpu()leavereset_state->resetset to true, breaking the synchronization barrier for subsequentCPU_ONcalls?If a
CPU_ONhypercall is followed by a host rollback (which setspower_stateback to OFF but intentionally leaves reset_state.reset = true), a subsequentCPU_ONfrom the guest setspower_statetoON_PENDING, writes the new pc/r0, and setsreset_state->reset= true.However, because
reset_state->resetwas already left as true, if the target vCPU executespkvm_reset_vcpu()concurrently, itssmp_load_acquire(&reset_state->reset)will immediately succeed. The target vCPU then reads pc and r0 before theCPU_ONcall has finished writing them, resulting in a data race and the guest booting with corrupted state.Quoted 6 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +6
-0+ } + +kvm_reset_vcpu_core (&hyp_vcpu->vcpu); +kvm_reset_pvm_sys_regs (&hyp_vcpu->vcpu); + +/* Must be done after resetting sys registers. */ arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu) · line 1233
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +10
-0+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 ; +}--
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=8
Fuad Tabba
reviewerPATCH v2 9/17 KVM: arm64: Restrict KVM_ARM_VCPU_INIT and PSCI version for protected VMs
In reply to Fuad TabbaEL2 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.
--- 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(-)
arch/arm64/include/asm/kvm_pkvm.h +24 -1
@@ -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 :
@@ -53,6 +52,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. *
arch/arm64/kvm/arm.c +13 -0
@@ -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 (vcpu_is_protected (vcpu)) { +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.
arch/arm64/kvm/hyp/nvhe/pkvm.c +2 -15
@@ -372,23 +372,10 @@ static void pkvm_init_features_from_host(struct pkvm_hyp_vm *hyp_vm, const strucif (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 );
arch/arm64/kvm/hypercalls.c +7 -0
@@ -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 (vcpu_is_protected (vcpu) && val <KVM_ARM_PSCI_1_1 ) +return -EINVAL ; +switch (val) {case KVM_ARM_PSCI_0_1 :if (wants_02)
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v2 10/17 KVM: arm64: Prevent host PC adjustments for protected vCPUs
In reply to 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.
--- 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(-)
arch/arm64/include/asm/kvm_asm.h +1 -0
@@ -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 );
arch/arm64/kvm/hyp/exception.c +16 -11
@@ -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)); +}
arch/arm64/kvm/hyp/nvhe/hyp-main.c +46 -1
57 lines not on this page · raw message
Signature · 1 line
2.39.5
Re: [PATCH v2 10/17] KVM: arm64: Prevent host PC adjustments for protected vCPUs
Hello!
Quoted 4 lines · 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'sPC_UPDATE_REQon re-entry, where EL2 allows it.
I wrote a comment about how "doesn't this lose the PC update status because for a loaded-protected vCPU we do nothing and the entry handlers are in a later commit", but I realised it was wrong because:
a) the handle_vm_entry_generic() handler copies PC_UPDATE_REQ from
host_vcpu to hpy->vcpu
b) adjust_pc() is called in __kvm_vcpu_run() so applies the update
Quoted 8 lines
For a non-protected vCPU, adjusting the hyp vCPU while
PKVM_HOST_STATE_DIRTYis 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 copiesPC_UPDATE_REQin and back out again. Without the copy back,INCREMENT_PCoutlives the adjustment and the nextKVM_SET_VCPU_EVENTStripsWARN_ON(INCREMENT_PC)inkvm_pend_exception().
Maybe a slight rewording:
For a non-protected vCPU, use PKVM_HOST_STATE_DIRTY to determine whether to update the host vCPU or hyp vCPU.
If the host vCPU is the authoritative vCPU, the PC can be adjusted directly.
If the hyp vCPU is the authoritative vCPU, copy PC_UPDATE_REQ from the host vCPU, adjust the PC and copy the flags back to the host vCPU. 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().
Quoted 8 lines
Unloaded, the host copy is host-writable, so pin it and its VM before adjusting. Only
commit_pending_events(), underKVM_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'svcpu->kvmis host-written, so__kvm_adjust_pc_vm()takes the VM as a
parameter: the pinned host struct kvm when unloaded, the hyp VM otherwise.
Rewording:
A hyp vCPU is not loaded when __kvm_adjust_pc() is called from commit_pending_events(), through KVM_SET_VCPU_EVENTS. In that case, pin the host vCPU and its VM before updating the host copy. The KVM_RUN call always has a hyp vCPU loaded.
enter_exception64() reads the VM's MTE flag. Since host_vcpu->kvm is host-controlled, add __kvm_adjust_pc_vm() and pass it the already pinned host VM for an unloaded vCPU, or the trusted hyp VM for a loaded vCPU.
Feel free to not take the rewording, but it helped me understand a bit, and felt clearer.
Quoted 112 lines
Suggested-by Marc Zyngier <[email protected]>Signed-off-by Fuad Tabba <[email protected]>--- 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(-)arch/arm64/include/asm/kvm_asm.h +1
-0@@ -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 );arch/arm64/kvm/hyp/exception.c +16
-11@@ -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)); +}arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0@@ -640,11 +640,56 @@ static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt)cpu_reg (host_ctxt,1 ) = ret; }
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 651
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +9
-0+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);
Not sure here why we use hyp_vcpu->vcpu.kvm over host_vcpu->kvm, in non-protected pKVM do we still not trust it?
Quoted 32 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +30
-0+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) {
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 692
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +7
-1DECLARE_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);
Possibly a comment at the end as the "else" clause.
/* Loaded protected vCPU: do nothing. */
(replace with "do nothing" with "handled by entry handlers" or anything else that you think makes sense!)
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 695
Quoted 3 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0}static void handle___kvm_flush_vm_context (struct kvm_cpu_context *host_ctxt)
Overall looks good to me.
Thanks,
Joey
Fuad Tabba
reviewerRe: [PATCH v2 10/17] KVM: arm64: Prevent host PC adjustments for protected vCPUs
Hi Joey,
[...]
Quoted 2 lines
For a non-protected vCPU, use
PKVM_HOST_STATE_DIRTYto determine whether to update the host vCPU or hyp vCPU.
[...]
Quoted 2 lines
Feel free to not take the rewording, but it helped me understand a bit, and felt clearer.
Marc had a second look at this patch and it came out reworked: the copy selection moved into __kvm_adjust_pc() behind a get/put pair, and the pinning moved out into its own fix. The pinning paragraph you reworded is gone from this patch, and the PKVM_HOST_STATE_DIRTY one is reworded along your lines.
Quoted 2 lines
Not sure here why we use
hyp_vcpu->vcpu.kvmoverhost_vcpu->kvm, in non-protected pKVM do we still not trust it?
It was the pointer, not the VM: EL2 wasn't dereferencing a host-written pointer without pinning what it points at. Moot now, the host copy is adjusted with its own struct kvm, which the fix pins.
Quoted 1 line
/* Loaded protected vCPU: do nothing. */
The else clause went with the rework. The drop is a NULL from the get helper, with a comment there.
Quoted 1 line
Acked-by Joey Gouly <[email protected]>
Thanks! I've left it off v3 since the code you acked has changed under you, so I'd appreciate a second look there.
Cheers,
/fuad
Marc Zyngier
maintainerRe: [PATCH v2 10/17] KVM: arm64: Prevent host PC adjustments for protected vCPUs
In reply to Fuad TabbaQuoted 22 lines · 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'sPC_UPDATE_REQon re-entry, where EL2 allows it.For a non-protected vCPU, adjusting the hyp vCPU while
PKVM_HOST_STATE_DIRTYis 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 copiesPC_UPDATE_REQin and back out again. Without the copy back,INCREMENT_PCoutlives the adjustment and the nextKVM_SET_VCPU_EVENTStripsWARN_ON(INCREMENT_PC)inkvm_pend_exception().Unloaded, the host copy is host-writable, so pin it and its VM before adjusting. Only
commit_pending_events(), underKVM_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'svcpu->kvmis host-written, so__kvm_adjust_pc_vm()takes the VM as a
parameter: the pinned host struct kvm when unloaded, the hyp VM otherwise.
Quoted 7 lines
Suggested-by Marc Zyngier <[email protected]>Signed-off-by Fuad Tabba <[email protected]>--- 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(-)
I keep coming to this patch because I really dislike how invasive it is. Suggestions below.
Quoted 29 lines
arch/arm64/include/asm/kvm_asm.h +1
-0@@ -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 );arch/arm64/kvm/hyp/exception.c +2
-2@@ -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;
arch/arm64/kvm/hyp/exception.c static void enter_exception64(struct kvm_vcpu *vcpu, unsigned long target_mode, · line 113
Quoted 6 lines
arch/arm64/kvm/hyp/exception.c +1
-1@@ -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 ;
So this is the sole reason why we need to access the kvm structure, and the cause for all of this churn. Can't this be written as some form of:
kvm = vcpu_get_kvm(vcpu); [do whatever is needed] vcpu_put_kvm(vcpu, kvm);
where get/put do the pinning/unpinning as required? These helpers would become NOPs on VHE, for example.
Quoted 76 lines
arch/arm64/kvm/hyp/exception.c +13
-8new |= (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)); +}arch/arm64/kvm/hyp/nvhe/hyp-main.c +6
-0@@ -640,11 +640,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.
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 658
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +10
-0+*/ +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 );
And this stuff could simply be a similar set of helpers in __kvm_adjust_pc(), the original one.
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +4
-0+} + +static void adjust_pc_unloaded (struct kvm_vcpu *host_vcpu) +{
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 672
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +10
-0+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 ;
Huh. Really?
Quoted 14 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +11
-0+ + 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;
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 695
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +5
-1-__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)
Thanks,
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Fuad Tabba
reviewerRe: [PATCH v2 10/17] KVM: arm64: Prevent host PC adjustments for protected vCPUs
Hi Marc,
...
Quoted 4 lines
So this is the sole reason why we need to access the kvm structure, and the cause for all of this churn. Can't this be written as some form of:
Quoted 8 lines
kvm = vcpu_get_kvm(vcpu); [do whatever is needed] vcpu_put_kvm(vcpu, kvm);where get/put do the pinning/unpinning as required? These helpers would become NOPs on VHE, for example.
I've put the get/put pair one level up instead, in __kvm_adjust_pc()
itself: pkvm_adjust_pc_get() returns the vCPU to adjust and pkvm_adjust_pc_put() copies PC_UPDATE_REQ back, both no-ops outside the nVHE build and outside protected mode. The kvm read goes back to the base's kern_hyp_va(vcpu->kvm), and exception.c keeps its signatures.
Quoted 2 lines
And this stuff could simply be a similar set of helpers in
__kvm_adjust_pc(), the original one.
That's where it lives now, and this patch no longer touches handle___kvm_adjust_pc().
Quoted 5 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 672
Quoted 3 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +3
-0+/* The host copy is authoritative, used only while pinned. */ +if (hyp_pin_shared_mem (host_vcpu, host_vcpu +1 )) +return ;Huh. Really?
After taking a closer look I realized that we have a live bug, and this is actually a fix. The base dereferences the host vCPU unpinned in this handler, and the vCPU and its VM are mapped at EL2 only once pinned, at the first KVM_RUN. KVM_SET_VCPU_EVENTS with ext_dabt_pending gets to __kvm_adjust_pc() before that, and the hypervisor panics on the read. Any process with /dev/kvm on a pKVM host can trigger it, since 6.17 (efa1368ba9f4b). I'll send it as a separate fix before sending V3 (later today).
Cheers,
/fuad
Fuad Tabba
reviewerPATCH v2 11/17 KVM: arm64: Inject an UNDEF at EL2 for unhandled protected guest exits
In reply to Fuad TabbaMake 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 isn't advertised to protected guests. FP/SIMD keeps the lazy switch, and the watchpoint entry is dropped: a protected vCPU runs with TDE clear.
--- arch/arm64/kvm/hyp/nvhe/switch.c | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-)
arch/arm64/kvm/hyp/nvhe/switch.c +25 -4
@@ -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, };
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v2 12/17 KVM: arm64: Add per-EC entry/exit state marshalling for protected guests
In reply to Fuad TabbaMove 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 doesn't 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.
For HVC64, only the PSCI calls EL2 forwards reach the host: the exit handler passes the function id and the arguments each call needs, and the entry handler returns the host's result, rolling a CPU_ON the host failed back to OFF unless the target already reached ON.
--- arch/arm64/kvm/hyp/nvhe/hyp-main.c | 412 +++++++++++++++++++++++++++-- 1 file changed, 387 insertions(+), 25 deletions(-)
arch/arm64/kvm/hyp/nvhe/hyp-main.c +387 -25
@@ -4,6 +4,8 @@* Author: Andrew Scull <[email protected]> */ +#include <kvm/arm_hypercalls.h> +#include <hyp/adjust_pc.h> #include <hyp/switch.h>
@@ -25,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(). */
@@ -34,13 +38,342 @@ 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) +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; +} + +static void handle_pvm_entry_hvc64 (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); + +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); +} + +/* 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 void handle_pvm_exit_hvc64 (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu; +int n, i; + +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 (); + } + +/* Pass the HVC function id (r0) and its arguments. */ +for (i =0 ; i < n; i++) { + host_vcpu->arch.ctxt.regs.regs[i] = +vcpu_get_reg (&hyp_vcpu->vcpu, i); + } +} + +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)
@@ -282,18 +615,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; - }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. */
@@ -318,10 +639,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 ();
@@ -333,13 +660,29 @@ 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);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.
@@ -348,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; }
Signature · 1 line
2.39.5
[email protected]
AI reviewRe: [PATCH v2 12/17] KVM: arm64: Add per-EC entry/exit state marshalling for protected guests
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Low] Dead fallback code due to EL2 WARN_ON panic semantics --
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Dead fallback code due to EL2 WARN_ON panic semantics
--
commit e6a23fd729ce9b1a6ab9652bdfcd06dd180d6ce8
Author: Fuad Tabba <[email protected]>
KVM: arm64: Add per-EC entry/exit state marshalling for protected guests
This commit introduces per-EC entry and exit state marshalling for protected guests. It routes protected guests through specific entry and exit handlers to copy only the necessary context rather than the entire context between the host and the hyp vCPU.
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0
[ ... ]
Quoted 1 line
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0@@ -34,13 +38,342 @@ void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt);
[ ... ]
Quoted 20 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +20
-0+static void handle_pvm_entry_hvc64 (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); + +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);
arch/arm64/kvm/hyp/nvhe/hyp-main.c void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt); · line 214
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +10
-0+ 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 ; + }
[Severity: Low]
Is the fallback error handling unreachable here?
At EL2 in nVHE/pKVM, WARN_ON evaluates to a BRK instruction which panics the hypervisor. There is no warn-and-continue behavior in this context. If target_vcpu evaluates to NULL, the hypervisor will panic immediately, leaving the fallback assignment to PSCI_RET_INTERNAL_FAILURE and the break statement entirely dead code.
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +4
-0+ + prev =cmpxchg_relaxed (&target_vcpu->power_state, +PSCI_0_2_AFFINITY_LEVEL_ON_PENDING , +PSCI_0_2_AFFINITY_LEVEL_OFF );
[ ... ]
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=12
Fuad Tabba
reviewerRe: [PATCH v2 12/17] KVM: arm64: Add per-EC entry/exit state marshalling for protected guests
Quoted 3 lines · On Mon, 7 Sept 2026 at 08:26, <[email protected]> wrote:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Dead fallback code due to EL2WARN_ONpanic semantics
Yes: a WARN_ON() at EL2 is a hyp_panic(), and _I_ taught you that one. Now the student has become the master. I'll drop the fallback in v3.
/fuad
Quoted 72 lines
--
commit
e6a23fd729ce9b1a6ab9652bdfcd06dd180d6ce8
Author: Fuad Tabba <[email protected]>KVM: arm64: Add per-EC entry/exit state marshalling for protected guests
This commit introduces per-EC entry and exit state marshalling for protected guests. It routes protected guests through specific entry and exit handlers to copy only the necessary context rather than the entire context between the host and the hyp vCPU.
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0[ ... ]
Quoted 1 line
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0@@ -34,13 +38,342 @@ void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt);[ ... ]
Quoted 20 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +20
-0+static void handle_pvm_entry_hvc64 (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); + +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);arch/arm64/kvm/hyp/nvhe/hyp-main.c void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt); · line 214
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +10
-0+ 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 ; + }[Severity: Low]
Is the fallback error handling unreachable here?At EL2 in nVHE/pKVM,
WARN_ONevaluates to a BRK instruction which panics the hypervisor. There is no warn-and-continue behavior in this context. Iftarget_vcpuevaluates to NULL, the hypervisor will panic immediately, leaving the fallback assignment toPSCI_RET_INTERNAL_FAILUREand the break statement entirely dead code.arch/arm64/kvm/hyp/nvhe/hyp-main.c void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt); · line 218
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +4
-0+ + prev =cmpxchg_relaxed (&target_vcpu->power_state, +PSCI_0_2_AFFINITY_LEVEL_ON_PENDING , +PSCI_0_2_AFFINITY_LEVEL_OFF );[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=12
Fuad Tabba
reviewerPATCH v2 13/17 KVM: arm64: Pend a protected guest's SError with HCR_EL2.VSE only
In reply to Fuad Tabbakvm_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.
--- arch/arm64/kvm/inject_fault.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-)
arch/arm64/kvm/inject_fault.c +4 -1
@@ -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));
Signature · 1 line
2.39.5
Marc Zyngier
maintainerRe: [PATCH v2 13/17] KVM: arm64: Pend a protected guest's SError with HCR_EL2.VSE only
Quoted 19 lines · Fuad Tabba
kvm_inject_serror_esr()emulates the exception entry whenserror_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 <[email protected]>--- arch/arm64/kvm/inject_fault.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-)arch/arm64/kvm/inject_fault.c +0
-0@@ -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
arch/arm64/kvm/inject_fault.c int kvm_inject_serror_esr(struct kvm_vcpu *vcpu, u64 esr) · line 388
Quoted 10 lines
arch/arm64/kvm/inject_fault.c +4
-1* 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));
I can't say I like this very much.
The fundamental premise is that as far as the host is concerned, PSTATE.A is always set. So why can't we give the illusion of this at all times?
That'd be a much cleaner approach.
Thanks,
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Fuad Tabba
reviewerRe: [PATCH v2 13/17] KVM: arm64: Pend a protected guest's SError with HCR_EL2.VSE only
Hi Marc,
On Fri, 11 Sept 2026 at 11:29, Marc Zyngier <[email protected]> wrote:
...
Quoted 20 lines
Quoted 7 lines
arch/arm64/kvm/inject_fault.c +0
-0@@ -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 arch/arm64/kvm/inject_fault.c int kvm_inject_serror_esr(struct kvm_vcpu *vcpu, u64 esr) · line 388
Quoted 10 lines
arch/arm64/kvm/inject_fault.c +4
-1* 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));I can't say I like this very much.
Quoted 5 lines
The fundamental premise is that as far as the host is concerned, PSTATE.A is always set. So why can't we give the illusion of this at all times?
That'd be a much cleaner approach.
I'll have EL2 copy PSTATE out to the host with A set instead, and drop this patch in v3.
Cheers,
/fuad
Quoted 7 lines
Thanks,
M.--
Without deviation from the norm, progress is not possible.
Fuad Tabba
reviewerPATCH v2 14/17 KVM: arm64: Reject host access to protected VM private state
In reply to Fuad TabbaA 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.
--- arch/arm64/kvm/arm.c | 7 +++++++ arch/arm64/kvm/guest.c | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+)
arch/arm64/kvm/arm.c +7 -0
@@ -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 (vcpu_is_protected (vcpu) &&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
arch/arm64/kvm/guest.c +19 -0
@@ -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 (vcpu_is_protected (vcpu) &&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 (vcpu_is_protected (vcpu) &&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 (vcpu_is_protected (vcpu) && 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 (vcpu_is_protected (vcpu)) +return -EPERM ; +if (dbg->control & ~KVM_GUESTDBG_VALID_MASK )return -EINVAL ;
Signature · 1 line
2.39.5
Marc Zyngier
maintainerRe: [PATCH v2 14/17] KVM: arm64: Reject host access to protected VM private state
Quoted 57 lines · 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_REGandKVM_SET_ONE_REGreturn-EPERMonce 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_INITreturns-EPERMonce the vCPU has run: it would reset the host copy alone and rewritemp_state, which EL2 reads only at hyp vCPU creation, so a vCPU the guest powered off would come back RUNNABLE.
-KVM_SET_VCPU_EVENTSrejects external-abort injection with-EPERM; SError injection is forwarded and stays permitted.
-KVM_SET_GUEST_DEBUGreturns-EPERM: a protected guest's debug state is hypervisor-owned.Signed-off-by Fuad Tabba <[email protected]>--- arch/arm64/kvm/arm.c | 7 +++++++ arch/arm64/kvm/guest.c | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+)arch/arm64/kvm/arm.c +7
-0@@ -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 (vcpu_is_protected (vcpu) &&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 arch/arm64/kvm/guest.c +4
-0@@ -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 (vcpu_is_protected (vcpu) &&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 ;
arch/arm64/kvm/guest.c int kvm_arm_get_reg(struct kvm_vcpu *vcpu, const struct kvm_one_reg *reg) · line 728
Quoted 7 lines
arch/arm64/kvm/guest.c +4
-0@@ -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 (vcpu_is_protected (vcpu) &&vcpu_has_run_once (vcpu)) +return -EPERM ; +
Why can't these be moved to the ioctl callback? Something like:
arch/arm64/kvm/arm.c +17 -0
@@ -1856,6 +1856,19 @@ static int kvm_arm_vcpu_set_events(struct kvm_vcpu *vcpu,return __kvm_arm_vcpu_set_events (vcpu, events); } +static long pkvm_filter_vcpu_ioctl (struct kvm_vcpu *vcpu,unsigned int ioctl) +{ +switch (ioctl) { +case KVM_ARM_VCPU_INIT : +case KVM_SET_ONE_REG : +case KVM_GET_ONE_REG : +if (vcpu_is_protected (vcpu) &&vcpu_has_run_once (vcpu)) +return -EPERM ; + } + +return 0 ; +} +long kvm_arch_vcpu_ioctl (struct file *filp,unsigned int ioctl,unsigned long arg) {
@@ -1864,6 +1877,10 @@ long kvm_arch_vcpu_ioctl(struct file *filp,struct kvm_device_attr attr;long r; + r =pvkm_filter_vcpu_ioctl (vcpu, ioctl); +if (r) +return r; +switch (ioctl) {case KVM_ARM_VCPU_INIT : {struct kvm_vcpu_init init;
Quoted 4 lines
arch/arm64/kvm/guest.c +0
-0/* We currently use nothing arch-specific in upper 32 bits */ if ((reg->id & ~KVM_REG_SIZE_MASK ) >>32 !=KVM_REG_ARM64 >>32 )return -EINVAL ;
arch/arm64/kvm/guest.c int __kvm_arm_vcpu_set_events(struct kvm_vcpu *vcpu, · line 803
Quoted 10 lines
arch/arm64/kvm/guest.c +7
-0@@ -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 (vcpu_is_protected (vcpu) && ext_dabt_pending) +return -EPERM ; +
Where is KVM_CAP_ARM_INJECT_EXT_DABT rejected?
Thanks,
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Fuad Tabba
reviewerRe: [PATCH v2 14/17] KVM: arm64: Reject host access to protected VM private state
Hi Marc,
On Fri, 11 Sept 2026 at 13:58, Marc Zyngier <[email protected]> wrote:
[...]
Quoted 2 lines
Why can't these be moved to the ioctl callback? Something like:
[...]
Quoted 1 line
+static long
pkvm_filter_vcpu_ioctl(struct kvm_vcpu *vcpu, unsigned int ioctl)
[...]
Much nicer, I'll move the three has-run checks into that filter for v3. I think we need the other two where they are, though: the ext_dabt_pending check reads the payload, and KVM_SET_GUEST_DEBUG has its own case in kvm_vcpu_ioctl(), so it never gets as far as kvm_arch_vcpu_ioctl().
Quoted 1 line
Where is
KVM_CAP_ARM_INJECT_EXT_DABTrejected?
In kvm_pkvm_ext_allowed(): on a protected VM it returns false for any capability it doesn't list, and patch 16 doesn't add this one, so KVM_CHECK_EXTENSION on the VM fd returns 0 for it (and for KVM_CAP_SET_GUEST_DEBUG). I'd keep the ioctl check too, for the VMM that doesn't ask, since userspace has been known to act silly. I'll say so in the commit message and in pkvm.rst (maybe not the silly part).
Thanks,
/fuad
Fuad Tabba
reviewerPATCH v2 15/17 KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off
In reply to Fuad TabbaA 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.
--- arch/arm64/include/asm/kvm_host.h | 2 ++ arch/arm64/kvm/arm.c | 19 +++++++++++++++++++ arch/arm64/kvm/pkvm.c | 21 +++++++++++++++++---- arch/arm64/kvm/psci.c | 3 +++ 4 files changed, 41 insertions(+), 4 deletions(-)
arch/arm64/include/asm/kvm_host.h +2 -0
@@ -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;
arch/arm64/kvm/arm.c +19 -0
@@ -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 (vcpu_is_protected (vcpu)) + vcpu->arch.pkvm_powered_off = true;__kvm_arm_vcpu_power_off (vcpu);spin_unlock (&vcpu->arch.mp_state_lock); }
@@ -801,6 +804,12 @@ 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 vcpu_is_protected (vcpu) && vcpu->arch.pkvm_powered_off; +} +int kvm_arch_vcpu_ioctl_get_mpstate (struct kvm_vcpu *vcpu,struct kvm_mp_state *mp_state) {
@@ -818,12 +827,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 :
arch/arm64/kvm/pkvm.c +17 -4
@@ -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 (vcpu_is_protected (vcpu)) { +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 ; }/*
arch/arm64/kvm/psci.c +3 -0
@@ -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);
Signature · 1 line
2.39.5
[email protected]
AI reviewRe: [PATCH v2 15/17] KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Race condition between EL2 and Host PSCI handling leads to state desynchronization and guest failure (INTERNAL_FAILURE) --
commit 80fb3e3ea2b949c35be53322315a43ff86b0f3ea
Author: Fuad Tabba <[email protected]>
KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off
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.
[ ... ]
arch/arm64/kvm/psci.c static unsigned long kvm_psci_vcpu_on(struct kvm_vcpu *source_vcpu) · line 96
Quoted 8 lines
arch/arm64/kvm/psci.c +0
-0@@ -94,6 +94,9 @@ static unsigned long kvm_psci_vcpu_on(struct kvm_vcpu *source_vcpu)*/ smp_wmb ();
[Severity: High]
This is a pre-existing issue, but can a concurrent CPU_OFF and CPU_ON cause state desynchronization between EL2 and the host?
In kvm_psci_vcpu_on(), there is an early return path before this hunk:
spin_lock(&vcpu->arch.mp_state_lock);
if (!kvm_arm_vcpu_stopped(vcpu)) {
if (kvm_psci_version(source_vcpu) != KVM_ARM_PSCI_0_1)
ret = PSCI_RET_ALREADY_ON;
else
ret = PSCI_RET_INVALID_PARAMS;
goto out_unlock;
}If vCPU 0 executes CPU_OFF, EL2 transitions it to OFF and exits to the host. Concurrently, if vCPU 1 executes CPU_ON targeting vCPU 0, EL2 transitions it to ON_PENDING and exits to the host.
If the host thread for vCPU 1 acquires mp_state_lock before the host thread for vCPU 0, the host sees mp_state as RUNNABLE, rejects the CPU_ON, and returns ALREADY_ON. EL2 then turns this into an INTERNAL_FAILURE, which leads to the permanent loss of a vCPU.
Taking this early return path also skips the newly introduced pkvm_powered_off = false update, compromising the accuracy of the tracking.
Could this early return condition in kvm_psci_vcpu_on() be updated to properly account for pending EL2 state transitions?
Quoted 6 lines
arch/arm64/kvm/psci.c +3
-0+/* 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);
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=15
Fuad Tabba
reviewerRe: [PATCH v2 15/17] KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off
Quoted 5 lines · On Mon, 7 Sept 2026 at 08:29, <[email protected]> wrote:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Race condition between EL2 and Host PSCI handling leads to state desynchronization and guest failure (INTERNAL_FAILURE)
Good one, but not preexisting and not high...
Quoted 50 lines
--
commit
80fb3e3ea2b949c35be53322315a43ff86b0f3ea
Author: Fuad Tabba <[email protected]>KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off
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 nextCPU_ONof that vCPU then fails:mp_stateis no longer STOPPED, so the host returnsALREADY_ON, which EL2 turns intoINTERNAL_FAILURE. SUSPENDED has the same effect.[ ... ]
arch/arm64/kvm/psci.c static unsigned long kvm_psci_vcpu_on(struct kvm_vcpu *source_vcpu) · line 96
Quoted 8 lines
arch/arm64/kvm/psci.c +0
-0@@ -94,6 +94,9 @@ static unsigned long kvm_psci_vcpu_on(struct kvm_vcpu *source_vcpu)*/ smp_wmb ();[Severity: High]
This is a pre-existing issue, but can a concurrentCPU_OFFandCPU_ONcause state desynchronization between EL2 and the host?In
kvm_psci_vcpu_on(), there is an early return path before this hunk:spin_lock(&vcpu->arch.mp_state_lock); if (!kvm_arm_vcpu_stopped(vcpu)) { if (kvm_psci_version(source_vcpu) != KVM_ARM_PSCI_0_1) ret = PSCI_RET_ALREADY_ON; else ret = PSCI_RET_INVALID_PARAMS; goto out_unlock; }If vCPU 0 executes
CPU_OFF, EL2 transitions it to OFF and exits to the host. Concurrently, if vCPU 1 executesCPU_ONtargeting vCPU 0, EL2 transitions it toON_PENDINGand exits to the host.If the host thread for vCPU 1 acquires
mp_state_lockbefore the host thread for vCPU 0, the host seesmp_stateas RUNNABLE, rejects theCPU_ON, and returnsALREADY_ON. EL2 then turns this into anINTERNAL_FAILURE, which leads to the permanent loss of a vCPU.
Quoted 5 lines
Taking this early return path also skips the newly introduced
pkvm_powered_off= false update, compromising the accuracy of the tracking.Could this early return condition in
kvm_psci_vcpu_on()be updated to properly account for pending EL2 state transitions?
On the base a protected guest's PSCI is handled at the host, where AFFINITY_INFO and CPU_ON read the same mp_state. At EL2, AFFINITY_INFO already reports the target OFF, so a guest doing CPU hotplug can issue its CPU_ON before the target's host thread has handled the CPU_OFF exit.
The result is one failed CPU_ON, not a lost vCPU or a stalepkvm_powered_off: the rollback returns EL2 to OFF, the target's thread then sets STOPPED and the flag, and the next CPU_ON succeeds.
v3 will report AFFINITY_INFO from the host, as the base does, so a guest that polls it before CPU_ON, as Linux does, can't hit the window.
/fuad
Quoted 10 lines
arch/arm64/kvm/psci.c static unsigned long kvm_psci_vcpu_on(struct kvm_vcpu *source_vcpu) · line 102
Quoted 7 lines
arch/arm64/kvm/psci.c +3
-0+/* 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);--
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=15
Fuad Tabba
reviewerPATCH v2 16/17 KVM: arm64: Advertise the capabilities that protected VMs support
In reply to Fuad Tabbakvm_pkvm_ext_allowed() denies every capability it doesn't 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 under the restrictions the preceding patches add, the vGIC and the I/O bus being host-managed.
--- arch/arm64/include/asm/kvm_pkvm.h | 9 +++++++++ 1 file changed, 9 insertions(+)
arch/arm64/include/asm/kvm_pkvm.h +9 -0
@@ -41,6 +41,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;
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v2 17/17 KVM: arm64: Document the protected VM userspace API
In reply to Fuad TabbaEL2 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.
--- 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(-)
Documentation/virt/kvm/api.rst +20 -2
@@ -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.
Documentation/virt/kvm/arm/fw-pseudo-registers.rst +2 -0
@@ -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:
Documentation/virt/kvm/arm/pkvm.rst +136 -5
175 lines not on this page · raw message
Documentation/virt/kvm/devices/vcpu.rst +3 -1
@@ -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 ===========================
Signature · 1 line
2.39.5