KVM: arm64: Confine protected VM vCPU state to EL2
Subsystems (4)
Fuad Tabba
reviewerv3 cover KVM: arm64: Confine protected VM vCPU state to EL2
Hi folks,
Changes since v2 [1]:
- Reworked the host PC adjustment patch: a get/put pair inside __kvm_adjust_pc(), and exception.c keeps its signatures. (Marc)
- The pin that patch carried in v2 was fixing a hyp panic in the base; it is patches 2 and 3 now, going out separately.
- A CPU_ON that reaches the host ahead of the target's CPU_OFF returns ALREADY_ON, and the host's CPU_ON of a protected vCPU is gated on EL2's power-off record, not on STOPPED. (sashiko)
- Dropped the patch pending a protected guest's SError with HCR_EL2.VSE: EL2 copies the host's view of PSTATE out with A set instead. (Marc)
- Moved the has-run -EPERM checks into one pkvm_filter_vcpu_ioctl() at the top of kvm_arch_vcpu_ioctl(). (Marc)
- Take MDCR_EL2.HPMN from the PE's own ID_AA64DFR0_EL1 and PMCR_EL0, not from a system-wide cpucap.
- Disable steal time through kvm_arm_pvtime_supported(), so KVM_ARM_VCPU_PVTIME_CTRL returns -ENXIO, as the CCA series [2] does. (Suzuki)
- PSCI corrections: keep advertising KVM_CAP_ARM_PSCI; leave AFFINITY_INFO to the host; publish OFF with a release in pvm_psci_vcpu_off().
- Fixed cache maintenance on unbacked memory: the host's instruction skip is applied to it, not only to an emulated MMIO access.
- Tidied up some code and wording: the reset table, the HVC handler, the WFx entry handler and pkvm.rst (Joey), and the ESR read in the marshalling patch's sysreg handler.
- Collected Joey's Reviewed-by on the reset framework patch. His Acked-by on the PC adjustment patch isn't carried, that code having changed. (thanks!)
- Rebased onto v7.3-rc3.
Following the vCPU state-sync series [3], 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 is left to the host, and CPU_ON and CPU_OFF are decided at EL2 with the host only scheduling or parking the target. A protected guest's TRNG calls, which the host handled until now, return NOT_SUPPORTED until TRNG for protected guests follows. EL2 implements PSCI 1.1, so a protected guest also loses the functions the host supports above that version, SYSTEM_OFF2 included. 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 [4].
Patches 1 to 3 go out separately: the HCR_EL2.VSE fix [5], posted, and the host vCPU pin fix [6], where Sashiko found a pre-existing unvalidated read of the host vCPU's VM on the same path. Patches 2 and 3 are v2 of [6], still to be posted. None of the three is part of this series. They're carried so the series applies as is and Sashiko can run on it.
The KVM_ARM_PREFERRED_TARGET documentation fix [7] went out just ahead of v1. Nothing here needs it to apply, but patch 18 documents vCPU feature availability as something the capabilities report, while api.rst 4.83 still points userspace at a bitmap that has always been empty.
The series is structured as follows:
01: The HCR_EL2.VSE fix, posted separately. 02-03: The host vCPU VM read and pin fixes, to be posted separately. 04: Steal time disabled for protected VMs. 05-06: Per-exception-class entry handlers; EL2 owns a protected
vCPU's trap configuration.
07-09: Timer state, system register reset and HVC handling at EL2. 10-11: PSCI at EL2, and the KVM_ARM_VCPU_INIT and PSCI version
restrictions.
12-14: Host PC adjustments blocked; an UNDEF at EL2 for exit
classes the host doesn't emulate; per-class state marshalling.
15-16: Host access to private state, and host power-on of a vCPU
EL2 holds powered off, rejected.
17: Capability allowlist.
18: Documentation.
Still to come: selftests, TRNG, self-hosted debug and SVE for protected guests, and much more, as separate series.
Based on v7.3-rc3 (fd73f4a665989).
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]/
[6] https://lore.kernel.org/all/[email protected]/
[7] https://lore.kernel.org/all/[email protected]/
Fuad Tabba (16):
KVM: arm64: Sync HCR_EL2.VSE back to the host vCPU under pKVM
KVM: arm64: Validate the host vCPU's VM before reading it under pKVM
KVM: arm64: Pin the host vCPU before adjusting its PC under pKVM
KVM: arm64: Disable steal time 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: 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 | 161 ++++- Documentation/virt/kvm/devices/vcpu.rst | 3 +- arch/arm64/include/asm/kvm_host.h | 23 +- arch/arm64/include/asm/kvm_hyp.h | 4 + arch/arm64/include/asm/kvm_pkvm.h | 33 + arch/arm64/kvm/arm.c | 59 +- arch/arm64/kvm/guest.c | 11 + arch/arm64/kvm/hyp/exception.c | 26 +- arch/arm64/kvm/hyp/include/hyp/adjust_pc.h | 37 ++ arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 18 + arch/arm64/kvm/hyp/nvhe/hyp-main.c | 586 +++++++++++++++++- arch/arm64/kvm/hyp/nvhe/pkvm.c | 414 ++++++++++++- arch/arm64/kvm/hyp/nvhe/switch.c | 29 +- arch/arm64/kvm/hyp/nvhe/sys_regs.c | 60 ++ arch/arm64/kvm/hypercalls.c | 7 + arch/arm64/kvm/pkvm.c | 21 +- arch/arm64/kvm/psci.c | 10 +- arch/arm64/kvm/pvtime.c | 10 +- 20 files changed, 1456 insertions(+), 80 deletions(-)
base-commit: fd73f4a6659897191fa0d40695fe370925dd3780
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v3 1/18 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 v3 2/18 KVM: arm64: Validate the host vCPU's VM before reading it under pKVM
In reply to Fuad TabbaOn an MTE-capable host under pKVM, enter_exception64() reads the VM's MTE flag through vcpu->kvm, which for a host vCPU is a host-writable pointer nothing validates. The host can point it at any address in the hyp linear map and read back bit 1 of that word through PSR_TCO in the vCPU's CPSR, or panic the hypervisor with an unmapped one.
Get the VM through a get/put pair around the read: a loaded vCPU's is the hyp VM, an unloaded host vCPU's is read once and pinned, and a pointer the host never shared leaves TCO clear.
--- arch/arm64/kvm/hyp/exception.c | 5 ++- arch/arm64/kvm/hyp/include/hyp/adjust_pc.h | 18 ++++++++++ arch/arm64/kvm/hyp/nvhe/pkvm.c | 39 ++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-)
arch/arm64/kvm/hyp/exception.c +4 -1
@@ -70,6 +70,7 @@ static void enter_exception64(struct kvm_vcpu *vcpu, unsigned long target_mode, enum exception_type type) {unsigned long sctlr, vbar, old, new, mode; +struct kvm *kvm;u64 exc_offset; mode = *vcpu_cpsr (vcpu) & (PSR_MODE_MASK |PSR_MODE32_BIT );
@@ -109,8 +110,10 @@ 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))) + kvm =vcpu_get_kvm (vcpu); +if (kvm &&kvm_has_mte (kvm)) new |=PSR_TCO_BIT ; +vcpu_put_kvm (vcpu, kvm); new |= (old &PSR_DIT_BIT );
arch/arm64/kvm/hyp/include/hyp/adjust_pc.h +18 -0
@@ -13,6 +13,24 @@#include <asm/kvm_emulate.h> #include <asm/kvm_host.h> +/* +* Under pKVM a host vCPU's ->kvm is host-writable: the nVHE pair +* validates it. +*/ +#ifdef __KVM_NVHE_HYPERVISOR__ +struct kvm *vcpu_get_kvm (struct kvm_vcpu *vcpu); +void vcpu_put_kvm (struct kvm_vcpu *vcpu,struct kvm *kvm); +#else +static inline struct kvm *vcpu_get_kvm (struct kvm_vcpu *vcpu) +{ +return vcpu->kvm; +} + +static inline void vcpu_put_kvm (struct kvm_vcpu *vcpu,struct kvm *kvm) +{ +} +#endif +static inline void kvm_skip_instr (struct kvm_vcpu *vcpu) {if (vcpu_mode_is_32bit (vcpu)) {
arch/arm64/kvm/hyp/nvhe/pkvm.c +39 -0
@@ -11,6 +11,8 @@#include <asm/kvm_emulate.h> +#include <hyp/adjust_pc.h> +#include <nvhe/mem_protect.h> #include <nvhe/memory.h> #include <nvhe/pkvm.h>
@@ -304,6 +306,43 @@ struct pkvm_hyp_vcpu *pkvm_get_loaded_hyp_vcpu(void) } +static struct pkvm_hyp_vm *loaded_hyp_vm_of (struct kvm_vcpu *vcpu) +{ +struct pkvm_hyp_vcpu *hyp_vcpu =pkvm_get_loaded_hyp_vcpu (); + +if (hyp_vcpu && + (vcpu == &hyp_vcpu->vcpu || vcpu == hyp_vcpu->host_vcpu)) +return pkvm_hyp_vcpu_to_hyp_vm (hyp_vcpu); + +return NULL ; +} + +/* An unloaded host vCPU's VM is mapped at EL2 only while pinned. */ +struct kvm *vcpu_get_kvm (struct kvm_vcpu *vcpu) +{ +struct pkvm_hyp_vm *hyp_vm; +struct kvm *kvm; + +if (!is_protected_kvm_enabled ()) +return kern_hyp_va (vcpu->kvm); + + hyp_vm =loaded_hyp_vm_of (vcpu); +if (hyp_vm) +return &hyp_vm->kvm; + + kvm =kern_hyp_va (READ_ONCE (vcpu->kvm)); +if (hyp_pin_shared_mem (kvm, kvm +1 )) +return NULL ; + +return kvm; +} + +void vcpu_put_kvm (struct kvm_vcpu *vcpu,struct kvm *kvm) +{ +if (kvm &&is_protected_kvm_enabled () && !loaded_hyp_vm_of (vcpu)) +hyp_unpin_shared_mem (kvm, kvm +1 ); +} +struct pkvm_hyp_vm *get_pkvm_hyp_vm (pkvm_handle_t handle) {struct pkvm_hyp_vm *hyp_vm;
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v3 3/18 KVM: arm64: Pin the host vCPU before adjusting its PC under pKVM
In reply to Fuad TabbaUnder pKVM, a page the host shares with EL2 is mapped at EL2 only while it's pinned, and the host vCPU is pinned at its first KVM_RUN. KVM_SET_VCPU_EVENTS with ext_dabt_pending reaches __kvm_adjust_pc() at EL2 before that, and dereferencing the unmapped host vCPU panics the hypervisor. Any process holding /dev/kvm on a pKVM host can trigger it.
Pin the host vCPU around the adjustment when no hyp vCPU is loaded for it. A loaded hyp vCPU implies it's pinned. A pin fails only for memory the host never shared, and the request is then dropped like any other bad host pointer.
--- arch/arm64/kvm/hyp/nvhe/hyp-main.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-)
arch/arm64/kvm/hyp/nvhe/hyp-main.c +18 -1
@@ -546,8 +546,25 @@ static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt)static void handle___kvm_adjust_pc (struct kvm_cpu_context *host_ctxt) { DECLARE_REG(struct kvm_vcpu *, vcpu, host_ctxt, 1); +struct pkvm_hyp_vcpu *hyp_vcpu; +struct kvm_vcpu *host_vcpu; -__kvm_adjust_pc (kern_hyp_va (vcpu)); + host_vcpu =__get_host_hyp_vcpus (vcpu, &hyp_vcpu); +if (host_vcpu) { +__kvm_adjust_pc (host_vcpu); +return ; + } + +/* +* With no hyp vCPU loaded for it, the host vCPU may be unpinned, +* and so unmapped at EL2: its first run pins it. +*/ + host_vcpu =kern_hyp_va (vcpu); +if (hyp_pin_shared_mem (host_vcpu, host_vcpu +1 )) +return ; + +__kvm_adjust_pc (host_vcpu); +hyp_unpin_shared_mem (host_vcpu, host_vcpu +1 ); }static void handle___kvm_flush_vm_context (struct kvm_cpu_context *host_ctxt)
Signature · 1 line
2.39.5
[email protected]
AI reviewRe: [PATCH v3 03/18] KVM: arm64: Pin the host vCPU before adjusting its PC under pKVM
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:
- [Critical] Unmapped host kvm_s2_mmu pointer dereference in EL2 TLB flush handlers panics the hypervisor before the first KVM_RUN. --
commit 201c7baf4784bec3cde62591e1662c2fd50608dc
Author: Fuad Tabba <[email protected]>
KVM: arm64: Pin the host vCPU before adjusting its PC under pKVM
This commit fixes a hypervisor panic in pKVM caused by dereferencing an unmapped host vCPU before its first KVM_RUN. It introduces temporary pinning of the host vCPU around __kvm_adjust_pc() when no hyp vCPU is loaded.
Quoted 19 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +11
-0@@ -546,8 +546,25 @@ static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt)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; + + host_vcpu =__get_host_hyp_vcpus (vcpu, &hyp_vcpu); +if (host_vcpu) { +__kvm_adjust_pc (host_vcpu); +return ; + } + +/* +* With no hyp vCPU loaded for it, the host vCPU may be unpinned,
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 568
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +7
-1+* and so unmapped at EL2: its first run pins it. +*/ + host_vcpu =kern_hyp_va (vcpu); +if (hyp_pin_shared_mem (host_vcpu, host_vcpu +1 )) +return ; -__kvm_adjust_pc (kern_hyp_va (vcpu)); +__kvm_adjust_pc (host_vcpu); +hyp_unpin_shared_mem (host_vcpu, host_vcpu +1 ); }
[Severity: Critical]
This is a pre-existing issue, but does this same vulnerability apply to the TLB flush handlers in this file?
If a userspace process creates a VM and a memory slot, and then immediately deletes it via KVM_SET_USER_MEMORY_REGION with a zero memory size before the first KVM_RUN, this triggers kvm_arch_flush_shadow_memslot().
This unconditionally calls kvm_tlb_flush_vmid_range(), which issues an HVC call that ends up in handle___kvm_tlb_flush_vmid_range():
arch/arm64/kvm/hyp/nvhe/hyp-main.c:handle___kvm_tlb_flush_vmid_range() {
DECLARE_REG(struct kvm_s2_mmu *, mmu, host_ctxt, 1);
DECLARE_REG(phys_addr_t, start, host_ctxt, 2);
DECLARE_REG(unsigned long, pages, host_ctxt, 3);
__kvm_tlb_flush_vmid_range(kern_hyp_va(mmu), start, pages);
}Since the kvm struct (containing kvm_s2_mmu) is not pinned until pkvm_create_hyp_vm() runs during the first KVM_RUN, passing kern_hyp_va(mmu) without pinning it first might cause a data abort at EL2.
The unmapped pointer is eventually dereferenced in enter_vmid_context():
arch/arm64/kvm/hyp/nvhe/tlb.c:enter_vmid_context() {
...
__load_stage2(mmu);
...
}arch/arm64/kvm/hyp/nvhe/tlb.c:__load_stage2() {
write_sysreg(mmu->vtcr, vtcr_el2);
...
}Since this is the exact same sequence described in the commit message (a host pointer unmapped at EL2 being dereferenced before the first run), should we also pin the mmu pointer around these TLB flush operations?
Quoted 2 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0static void handle___kvm_flush_vm_context (struct kvm_cpu_context *host_ctxt)
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
Fuad Tabba
reviewerRe: [PATCH v3 03/18] KVM: arm64: Pin the host vCPU before adjusting its PC under pKVM
On Mon, 14 Sep 2026 at 14:01, <[email protected]> wrote:
[...]
Quoted 6 lines
If a userspace process creates a VM and a memory slot, and then immediately deletes it via
KVM_SET_USER_MEMORY_REGIONwith a zero memory size before the firstKVM_RUN, this triggerskvm_arch_flush_shadow_memslot().This unconditionally calls
kvm_tlb_flush_vmid_range(), which issues an HVC call that ends up inhandle___kvm_tlb_flush_vmid_range():
With pKVM enabled that path doesn't get there.
[...]
Quoted 3 lines
Since this is the exact same sequence described in the commit message (a host pointer unmapped at EL2 being dereferenced before the first run), should we also pin the mmu pointer around these TLB flush operations?
The handlers are unvalidated, but under pKVM the host owns no guest stage-2, so none of the kvm_s2_mmu hypercalls has a valid caller there. Rather than pin, I'll send a separate patch that rejects them when protected mode is enabled.
Cheers,
/fuad
Fuad Tabba
reviewerPATCH v3 4/18 KVM: arm64: Disable steal time for protected VMs
In reply to Fuad TabbapKVM doesn't support steal time for protected guests, and KVM_CAP_STEAL_TIME already reads 0 for them on the VM file descriptor, but kvm_arm_pvtime_supported() doesn't know the VM, so 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. Pass the VM in and return false for a protected one, so the attribute returns -ENXIO as it does where steal time isn't implemented.
--- arch/arm64/include/asm/kvm_host.h | 2 +- arch/arm64/kvm/arm.c | 2 +- arch/arm64/kvm/pvtime.c | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-)
arch/arm64/include/asm/kvm_host.h +1 -1
@@ -1346,7 +1346,7 @@ long kvm_hypercall_pv_features(struct kvm_vcpu *vcpu);gpa_t kvm_init_stolen_time (struct kvm_vcpu *vcpu);void kvm_update_stolen_time (struct kvm_vcpu *vcpu); -bool kvm_arm_pvtime_supported (void ); +bool kvm_arm_pvtime_supported (struct kvm *kvm);int kvm_arm_pvtime_set_attr (struct kvm_vcpu *vcpu,struct kvm_device_attr *attr); int kvm_arm_pvtime_get_attr(struct kvm_vcpu *vcpu,
arch/arm64/kvm/arm.c +1 -1
@@ -447,7 +447,7 @@ int kvm_vm_ioctl_check_extension(struct kvm *kvm, long ext) r =system_supports_mte ();break ;case KVM_CAP_STEAL_TIME : - r =kvm_arm_pvtime_supported (); + r =kvm_arm_pvtime_supported (kvm);break ;case KVM_CAP_ARM_EL1_32BIT : r =cpus_have_final_cap (ARM64_HAS_32BIT_EL1 );
arch/arm64/kvm/pvtime.c +5 -5
@@ -67,9 +67,9 @@ gpa_t kvm_init_stolen_time(struct kvm_vcpu *vcpu)return base; } -bool kvm_arm_pvtime_supported (void ) +bool kvm_arm_pvtime_supported (struct kvm *kvm) { -return !!sched_info_on (); +return !!sched_info_on () && (!kvm || !kvm_vm_is_protected (kvm)); } int kvm_arm_pvtime_set_attr(struct kvm_vcpu *vcpu,
@@ -81,7 +81,7 @@ int kvm_arm_pvtime_set_attr(struct kvm_vcpu *vcpu,int ret =0 ;int idx; -if (!kvm_arm_pvtime_supported () || +if (!kvm_arm_pvtime_supported (kvm) || attr->attr !=KVM_ARM_VCPU_PVTIME_IPA )return -ENXIO ;
@@ -110,7 +110,7 @@ int kvm_arm_pvtime_get_attr(struct kvm_vcpu *vcpu,u64 __user *user = (u64 __user *)attr->addr;u64 ipa; -if (!kvm_arm_pvtime_supported () || +if (!kvm_arm_pvtime_supported (vcpu->kvm) || attr->attr !=KVM_ARM_VCPU_PVTIME_IPA )return -ENXIO ;
@@ -126,7 +126,7 @@ int kvm_arm_pvtime_has_attr(struct kvm_vcpu *vcpu, {switch (attr->attr) {case KVM_ARM_VCPU_PVTIME_IPA : -if (kvm_arm_pvtime_supported ()) +if (kvm_arm_pvtime_supported (vcpu->kvm))return 0 ; }return -ENXIO ;
Signature · 1 line
2.39.5
Re: [PATCH v3 04/18] KVM: arm64: Disable steal time for protected VMs
Quoted 9 lines · Fuad Tabba
pKVM doesn't support steal time for protected guests, and
KVM_CAP_STEAL_TIMEalready reads 0 for them on the VM file descriptor, butkvm_arm_pvtime_supported()doesn't know the VM, so the host still accepts theKVM_ARM_VCPU_PVTIME_CTRLattribute, whose IPA would pointkvm_update_stolen_time()at the guest's private memory on every vCPU load. Pass the VM in and return false for a protected one, so the attribute returns-ENXIOas it does where steal time isn't implemented.Signed-off-by Fuad Tabba <[email protected]>
Quoted 78 lines
--- arch/arm64/include/asm/kvm_host.h | 2 +- arch/arm64/kvm/arm.c | 2 +- arch/arm64/kvm/pvtime.c | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-)arch/arm64/include/asm/kvm_host.h +1
-1@@ -1346,7 +1346,7 @@ long kvm_hypercall_pv_features(struct kvm_vcpu *vcpu);gpa_t kvm_init_stolen_time (struct kvm_vcpu *vcpu);void kvm_update_stolen_time (struct kvm_vcpu *vcpu); -bool kvm_arm_pvtime_supported (void ); +bool kvm_arm_pvtime_supported (struct kvm *kvm);int kvm_arm_pvtime_set_attr (struct kvm_vcpu *vcpu,struct kvm_device_attr *attr); int kvm_arm_pvtime_get_attr(struct kvm_vcpu *vcpu,arch/arm64/kvm/arm.c +1
-1@@ -447,7 +447,7 @@ int kvm_vm_ioctl_check_extension(struct kvm *kvm, long ext) r =system_supports_mte ();break ;case KVM_CAP_STEAL_TIME : - r =kvm_arm_pvtime_supported (); + r =kvm_arm_pvtime_supported (kvm);break ;case KVM_CAP_ARM_EL1_32BIT : r =cpus_have_final_cap (ARM64_HAS_32BIT_EL1 );arch/arm64/kvm/pvtime.c +5
-5@@ -67,9 +67,9 @@ gpa_t kvm_init_stolen_time(struct kvm_vcpu *vcpu)return base; } -bool kvm_arm_pvtime_supported (void ) +bool kvm_arm_pvtime_supported (struct kvm *kvm) { -return !!sched_info_on (); +return !!sched_info_on () && (!kvm || !kvm_vm_is_protected (kvm)); } int kvm_arm_pvtime_set_attr(struct kvm_vcpu *vcpu,@@ -81,7 +81,7 @@ int kvm_arm_pvtime_set_attr(struct kvm_vcpu *vcpu,int ret =0 ;int idx; -if (!kvm_arm_pvtime_supported () || +if (!kvm_arm_pvtime_supported (kvm) || attr->attr !=KVM_ARM_VCPU_PVTIME_IPA )return -ENXIO ;@@ -110,7 +110,7 @@ int kvm_arm_pvtime_get_attr(struct kvm_vcpu *vcpu,u64 __user *user = (u64 __user *)attr->addr;u64 ipa; -if (!kvm_arm_pvtime_supported () || +if (!kvm_arm_pvtime_supported (vcpu->kvm) || attr->attr !=KVM_ARM_VCPU_PVTIME_IPA )return -ENXIO ;@@ -126,7 +126,7 @@ int kvm_arm_pvtime_has_attr(struct kvm_vcpu *vcpu, {switch (attr->attr) {case KVM_ARM_VCPU_PVTIME_IPA : -if (kvm_arm_pvtime_supported ()) +if (kvm_arm_pvtime_supported (vcpu->kvm))return 0 ; }return -ENXIO ;Signature · 1 line
2.39.5
Signature · 1 line
Vincent
Fuad Tabba
reviewerPATCH v3 5/18 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 v3 6/18 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 at vCPU load to PMCR_EL0.N on a PE whose ID_AA64DFR0_EL1 implements PMUv3, the value the host's copy carried, read as kvm_init_host_debug_data() reads it: the PMUv3 cpucap is system-wide and off without CONFIG_HW_PERF_EVENTS. A protected guest's PMU accesses trap regardless.
--- arch/arm64/kvm/hyp/nvhe/hyp-main.c | 34 ++++++++++++++++++++++-------- arch/arm64/kvm/hyp/nvhe/pkvm.c | 2 +- 2 files changed, 26 insertions(+), 10 deletions(-)
arch/arm64/kvm/hyp/nvhe/hyp-main.c +25 -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);
@@ -326,9 +334,17 @@ static void handle___pkvm_vcpu_load(struct kvm_cpu_context *host_ctxt)return ;if (pkvm_hyp_vcpu_is_protected (hyp_vcpu)) { +u64 dfr0 =read_sysreg (id_aa64dfr0_el1); +/* 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 (pmuv3_implemented (FIELD_GET (ID_AA64DFR0_EL1_PMUVer_MASK, dfr0))) +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
@@ -104,7 +104,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 v3 06/18] 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: - [Critical] Early return in `flush_debug_state` leaves the protected guest's `debug_owner` as…
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] Early return in flush_debug_state leaves the protected guest's debug_owner as VCPU_DEBUG_FREE, completely bypassing hardware debug register context switching and exposing host physical debug registers to the guest. --
commit 3ff31a206fa3951489ae8f183dab7b2241b1018e
Author: Fuad Tabba <[email protected]>
KVM: arm64: Skip fixed-feature state flush for protected vCPUs
This commit skips the fixed-feature state flush for protected vCPUs by returning early in flush_debug_state() and sync_debug_state(), and moving the mdcr_el2 copy into the non-protected branch in flush_hyp_vcpu().
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: Critical]
Does this early return leave the protected guest's debug_owner as VCPU_DEBUG_FREE (its zero-initialized value) instead of synchronizing it? This could bypass hardware debug register context switching, potentially exposing host physical debug registers to the guest.
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: Critical]
Could this early return ignore debug state updates on exit, further contributing to the debug isolation bypass?
If KVM's world switch sees !kvm_debug_regs_in_use(), it might skip restoring the host's debug registers.
Quoted 3 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;else if (kvm_host_owns_debug_regs (&hyp_vcpu->vcpu))
[ ... ]
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu) · line 255
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +6
-0@@ -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;
[Severity: Critical]
By moving the mdcr_el2 copy into the non-protected branch, does the protected guest retain the EL2-computed mdcr_el2 where TDA is deliberately cleared?
This seems to prevent debug accesses from trapping. If a protected guest directly executes MSR DBGBVR0_EL1, <addr> without trapping to EL2, and KVM skips restoring the host's debug registers on exit due to the early returns above, the guest's malicious values could remain active when the host resumes.
If the host has breakpoints enabled (MDSCR_EL1.MDE = 1), these guest-injected breakpoints could fire in the host kernel.
Quoted 8 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-1hyp_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;
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=6
Fuad Tabba
reviewerRe: [PATCH v3 06/18] KVM: arm64: Skip fixed-feature state flush for protected vCPUs
Quoted 3 lines · On Mon, 14 Sept 2026 at 14:43, <[email protected]> wrote:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] Early return inflush_debug_stateleaves the protected guest'sdebug_ownerasVCPU_DEBUG_FREE, completely bypassing hardware debug register context switching and exposing host physical debug registers to the guest.
Same finding as on v2, same answer.
[...]
Quoted 5 lines
[Severity: Critical]
Does this early return leave the protected guest'sdebug_ownerasVCPU_DEBUG_FREE(its zero-initialized value) instead of synchronizing it? This could bypass hardware debug register context switching, potentially exposing host physical debug registers to the guest.
[...]
Quoted 3 lines
[Severity: Critical]
By moving themdcr_el2copy into the non-protected branch, does the protected guest retain the EL2-computedmdcr_el2where TDA is deliberately cleared?
The EL2-computed mdcr_el2 has TDA and TDRA set: a protected VM's DebugVer is NI, so pvm_init_traps_mdcr() sets both and the guest's debug register accesses trap. Keeping that value rather than the host's is what this patch is for.
/fuad
Fuad Tabba
reviewerPATCH v3 7/18 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 v3 8/18 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 KVM resets that EL2 loads for a protected vCPU: the ones __sysreg_restore_state_nvhe() restores, and CNTV_CTL_EL0, which flush_hyp_timer_state() loads from a copy the host's kvm_timer_vcpu_reset() doesn't write. The ptrauth keys, which entry.S loads, are not in the table: sys_regs.c resets them to UNKNOWN, and a vCPU that CPU_ON brings back keeps the keys the guest last set, one UNKNOWN value in place of another.
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.
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. It preserves CNTV_CTL_EL0, whose entry writes the zero the page already holds. So this patch has no observable effect on its own.
No functional change intended.
--- 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 | 60 ++++++++++++++++++++++++++ 3 files changed, 66 insertions(+)
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
@@ -590,6 +590,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 +60 -0
66 lines not on this page · raw message
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v3 9/18 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 implements: 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 | 68 ++++++++++++++++++++++++++++++++ 3 files changed, 76 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 +68 -0
88 lines not on this page · raw message
Signature · 1 line
2.39.5
Fuad Tabba
reviewerPATCH v3 10/18 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, 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), AFFINITY_INFO, CPU_SUSPEND and the platform power operations are forwarded to the host, and anything else returns NOT_SUPPORTED, including the TRNG calls and the functions above 1.1, SYSTEM_OFF2 among them, that the host handled for a protected guest until now. TRNG for protected guests is a follow-up. AFFINITY_INFO stays with the host, which returns OFF only once it has parked the target:
the host is what a guest polls to see a CPU_OFF complete before it issues the next CPU_ON, as Linux does on hotplug.
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. CPU_OFF publishes OFF with a release, so the target's clear of reset_state.reset is ordered before it and a CPU_ON that then wins on OFF republishes after the clear. 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, and the reset has no observable effect: flush_hyp_vcpu() copies the host's context in on every entry until that patch removes the copy, so the target enters on the host's values rather than the ones EL2 reset.
--- 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 | 283 ++++++++++++++++++++++++- 3 files changed, 311 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>
@@ -456,14 +457,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)) {/*
@@ -472,8 +471,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 +278 -5
355 lines not on this page · raw message
Signature · 1 line
2.39.5
Re: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
Quoted 226 lines · Fuad Tabba
EL2 implements PSCI 1.1 for protected VMs:
CPU_ON,CPU_OFF,PSCI_VERSIONandPSCI_FEATURESare decided at EL2 (CPU_ONandCPU_OFFstill exit to the host, which only schedules or parks the target),AFFINITY_INFO,CPU_SUSPENDand the platform power operations are forwarded to the host, and anything else returnsNOT_SUPPORTED, including the TRNG calls and the functions above 1.1,SYSTEM_OFF2among them, that the host handled for a protected guest until now. TRNG for protected guests is a follow-up.AFFINITY_INFOstays with the host, which returns OFF only once it has parked the target:
the host is what a guest polls to see aCPU_OFFcomplete before it issues the nextCPU_ON, as Linux does on hotplug.Three consequences follow:
- A protected VM has one primary vCPU, the first whose hyp vCPU is created with
mp_stateRUNNABLE. A second one, or anmp_stateother than RUNNABLE or STOPPED, fails that vCPU's firstKVM_RUNwith-EINVAL.-
CPU_ONfinds its target among the hyp vCPUs, which exist from the target's firstKVM_RUN; before that the guest getsINVALID_PARAMETERS.- A vCPU EL2 holds powered off doesn't run:
handle___kvm_vcpu_run()returnsARM_EXCEPTION_IL, reported asKVM_EXIT_FAIL_ENTRY. Its existing bail-outs return the same code instead of an-EINVALthathandle_exit()didn't recognise, for every hyp vCPU.Non-protected VMs keep
power_stateON and accept anymp_state.Each protected vCPU is OFF,
ON_PENDINGor ON.CPU_ONmoves the target toON_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.CPU_OFFpublishes OFF with a release, so the target's clear of reset_state.reset is ordered before it and aCPU_ONthat then wins on OFF republishes after the clear. Rolling aCPU_ONthe 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 staysON_PENDING, and the reset has no observable effect:flush_hyp_vcpu()copies the host's context in on every entry until that patch removes the copy, so the target enters on the host's values rather than the ones EL2 reset.Signed-off-by Fuad Tabba <[email protected]>--- 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 | 283 ++++++++++++++++++++++++- 3 files changed, 311 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> @@ -456,14 +457,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)) {/* @@ -472,8 +471,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 +40
-1@@ -5,6 +5,7 @@*/ #include <kvm/arm_hypercalls.h> +#include <kvm/arm_psci.h> #include <linux/kvm_host.h> #include <linux/mm.h> @@ -433,6 +434,40 @@ static void pkvm_init_features_from_host(struct pkvm_hyp_vm *hyp_vm, const struc allowed_features, KVM_VCPU_MAX_FEATURES); } +static int pkvm_vcpu_init_psci (struct pkvm_hyp_vcpu *hyp_vcpu,u32 mp_state) +{ +struct vcpu_reset_state *reset_state = &hyp_vcpu->vcpu.arch.reset_state; +struct pkvm_hyp_vm *hyp_vm =pkvm_hyp_vcpu_to_hyp_vm (hyp_vcpu); +struct kvm_vcpu *host_vcpu; + +if (!pkvm_hyp_vcpu_is_protected (hyp_vcpu)) { +/* The host manages a non-protected vCPU: always ON at EL2. */ + hyp_vcpu->power_state =PSCI_0_2_AFFINITY_LEVEL_ON ; +return 0 ; + } + +if (mp_state !=KVM_MP_STATE_RUNNABLE && mp_state !=KVM_MP_STATE_STOPPED ) +return -EINVAL ; + +if (mp_state ==KVM_MP_STATE_STOPPED ) { + reset_state->reset = false; + hyp_vcpu->power_state =PSCI_0_2_AFFINITY_LEVEL_OFF ; +return 0 ; + } + +hyp_assert_lock_held (&vm_table_lock); +if (hyp_vm->primary_vcpu) +return -EINVAL ; + hyp_vm->primary_vcpu = hyp_vcpu; + + host_vcpu = hyp_vcpu->host_vcpu; + reset_state->pc =READ_ONCE (host_vcpu->arch.ctxt.regs.pc); + reset_state->r0 =READ_ONCE (host_vcpu->arch.ctxt.regs.regs[0 ]); + reset_state->reset = true; + hyp_vcpu->power_state =PSCI_0_2_AFFINITY_LEVEL_ON_PENDING ; +return 0 ; +} +static void unpin_host_vcpu (struct kvm_vcpu *host_vcpu) {if (host_vcpu)@@ -447,6 +482,9 @@ static void unpin_host_sve_state(struct pkvm_hyp_vcpu *hyp_vcpu)return ; sve_state = hyp_vcpu->vcpu.arch.sve_state; +if (!sve_state) +return ; +hyp_unpin_shared_mem (sve_state, sve_state +vcpu_sve_state_size (&hyp_vcpu->vcpu)); }@@ -559,10 +597,12 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, struct kvm_vcpu *host_vcpu) {int ret =0 ; +u32 mp_state;if (hyp_pin_shared_mem (host_vcpu, host_vcpu +1 ))return -EBUSY ; + mp_state =READ_ONCE (host_vcpu->arch.mp_state.mp_state); hyp_vcpu->host_vcpu = host_vcpu; hyp_vcpu->vcpu.kvm = &hyp_vm->kvm;@@ -571,7 +611,6 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, hyp_vcpu->vcpu.arch.hw_mmu = &hyp_vm->kvm.arch.mmu; hyp_vcpu->vcpu.arch.cflags =READ_ONCE (host_vcpu->arch.cflags); - hyp_vcpu->vcpu.arch.mp_state.mp_state =KVM_MP_STATE_STOPPED ;if (!pkvm_hyp_vcpu_is_protected (hyp_vcpu)) {/*
arch/arm64/kvm/hyp/nvhe/pkvm.c static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, · line 648
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +4
-1@@ -601,9 +640,12 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu,if (pkvm_hyp_vcpu_is_protected (hyp_vcpu))kvm_reset_pvm_sys_regs (&hyp_vcpu->vcpu); + ret =pkvm_vcpu_init_psci (hyp_vcpu, mp_state); done: -if (ret) +if (ret) {unpin_host_vcpu (host_vcpu); +unpin_host_sve_state (hyp_vcpu); + }
Was it planned for a different commit of the series?
Quoted 277 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +234
-3return ret; }@@ -980,13 +1022,22 @@ int __pkvm_init_vcpu(pkvm_handle_t handle, struct kvm_vcpu *host_vcpu, ret =init_pkvm_hyp_vcpu (hyp_vcpu, hyp_vm, host_vcpu);if (ret) - goto unlock; + goto unclaim; ret =register_hyp_vcpu (hyp_vm, hyp_vcpu);if (ret) {unpin_host_vcpu (host_vcpu);unpin_host_sve_state (hyp_vcpu); + goto unclaim; } + goto unlock; +unclaim: +/* +* Under vm_table_lock, so no other claim can have landed: undo +* this one. +*/ +if (hyp_vm->primary_vcpu == hyp_vcpu) + hyp_vm->primary_vcpu =NULL ; unlock:hyp_spin_unlock (&vm_table_lock);@@ -1178,6 +1229,228 @@ static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu) ret[0 ] =SMCCC_RET_SUCCESS ; } +/* +* Reset the vCPU to its power-on state and commit ON_PENDING -> ON, on the +* target's own CPU. Returns -ECANCELED, with no side effects, if a rollback +* raced the reset. +*/ +int pkvm_reset_vcpu (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct vcpu_reset_state *reset_state = &hyp_vcpu->vcpu.arch.reset_state; +int prev; + +/* +* Pairs with smp_store_release(&reset_state->reset, true) in +* pvm_psci_vcpu_on(). The acquire must precede the cmpxchg: reversed, a +* winning cmpxchg with a false acquire would leave power_state == ON +* with the reset skipped. +*/ +if (!smp_load_acquire (&reset_state->reset)) +return -ECANCELED ; + + prev =cmpxchg_relaxed (&hyp_vcpu->power_state, +PSCI_0_2_AFFINITY_LEVEL_ON_PENDING , +PSCI_0_2_AFFINITY_LEVEL_ON ); +if (prev !=PSCI_0_2_AFFINITY_LEVEL_ON_PENDING ) { +/* The only other writer of ON_PENDING is the rollback. */ +WARN_ON (prev !=PSCI_0_2_AFFINITY_LEVEL_OFF ); +return -ECANCELED ; + } + +kvm_reset_vcpu_core (&hyp_vcpu->vcpu); +kvm_reset_pvm_sys_regs (&hyp_vcpu->vcpu); + +/* Must be done after resetting sys registers. */ +kvm_reset_vcpu_psci (&hyp_vcpu->vcpu, reset_state); + + hyp_vcpu->exit_code =0 ; +/* +* A source that raced a rollback can publish after this clear, and +* the next cycle then passes the acquire on a stale flag: the pc, +* r0 and be it reads are still the guest's own. +*/ + reset_state->reset = false; +return 0 ; +} + +struct pkvm_hyp_vcpu *pkvm_mpidr_to_hyp_vcpu (struct pkvm_hyp_vm *hyp_vm, +u64 mpidr) +{ +struct pkvm_hyp_vcpu *hyp_vcpu; +int i; + + mpidr &=MPIDR_HWID_BITMASK ; + +for (i =0 ; i < hyp_vm->kvm.created_vcpus; i++) { +/* Pairs with smp_store_release() in register_hyp_vcpu(). */ + hyp_vcpu =smp_load_acquire (&hyp_vm->vcpus[i]); + +if (!hyp_vcpu) +continue ; + +if (mpidr ==kvm_vcpu_get_mpidr_aff (&hyp_vcpu->vcpu)) +return hyp_vcpu; + } + +return NULL ; +} + +/* +* Returns true when handled at EL2, false when the host must wake the target +* vCPU. +*/ +static bool pvm_psci_vcpu_on (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct pkvm_hyp_vm *hyp_vm =pkvm_hyp_vcpu_to_hyp_vm (hyp_vcpu); +struct vcpu_reset_state *reset_state; +struct pkvm_hyp_vcpu *target; +unsigned long cpu_id, ret; +int power_state; + + cpu_id =smccc_get_arg1 (&hyp_vcpu->vcpu); +if (!kvm_psci_valid_affinity (&hyp_vcpu->vcpu, cpu_id)) { + ret =PSCI_RET_INVALID_PARAMS ; + goto error; + } + + target =pkvm_mpidr_to_hyp_vcpu (hyp_vm, cpu_id); +if (!target) { + ret =PSCI_RET_INVALID_PARAMS ; + goto error; + } + +/* +* vCPUs race to power on the same target. Relaxed: reset_state +* is published by the release on reset_state.reset below. +*/ + power_state =cmpxchg_relaxed (&target->power_state, +PSCI_0_2_AFFINITY_LEVEL_OFF , +PSCI_0_2_AFFINITY_LEVEL_ON_PENDING ); +switch (power_state) { +case PSCI_0_2_AFFINITY_LEVEL_ON_PENDING : + ret =PSCI_RET_ON_PENDING ; + goto error; +case PSCI_0_2_AFFINITY_LEVEL_ON : + ret =PSCI_RET_ALREADY_ON ; + goto error; +case PSCI_0_2_AFFINITY_LEVEL_OFF : +break ; +default : + ret =PSCI_RET_INTERNAL_FAILURE ; + goto error; + } + + reset_state = &target->vcpu.arch.reset_state; + reset_state->pc =smccc_get_arg2 (&hyp_vcpu->vcpu); + reset_state->r0 =smccc_get_arg3 (&hyp_vcpu->vcpu); + reset_state->be =kvm_vcpu_is_be (&hyp_vcpu->vcpu); +/* +* Publish reset_state.{pc, r0, be} to the target vCPU. Pairs with +* smp_load_acquire(&reset_state->reset) in pkvm_reset_vcpu(). +*/ +smp_store_release (&reset_state->reset, true); + +/* The host requests KVM_REQ_VCPU_RESET and wakes the target. */ +return false; + +error: +smccc_set_retval (&hyp_vcpu->vcpu, ret,0 ,0 ,0 ); +return true; +} + +/* +* Returns true when handled at EL2, false when the host must stop scheduling +* the vCPU. +*/ +static bool pvm_psci_vcpu_off (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +/* No other writer runs while this vCPU is ON and executing. */ +WARN_ON (READ_ONCE (hyp_vcpu->power_state) !=PSCI_0_2_AFFINITY_LEVEL_ON ); + +/* +* Orders pkvm_reset_vcpu()'s clear of reset_state.reset before OFF, so +* a CPU_ON that wins on OFF republishes after it. Pairs with the +* cmpxchg in pvm_psci_vcpu_on(). +*/ +smp_store_release (&hyp_vcpu->power_state,PSCI_0_2_AFFINITY_LEVEL_OFF ); + +/* Return to the host so that it can finish powering off the vcpu. */ +return false; +} + +static bool pvm_psci_version (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +/* Nothing to be handled by the host. Go back to the guest. */ +smccc_set_retval (&hyp_vcpu->vcpu,KVM_ARM_PSCI_1_1 ,0 ,0 ,0 ); +return true; +} + +static bool pvm_psci_features (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct kvm_vcpu *vcpu = &hyp_vcpu->vcpu; +u32 feature =smccc_get_arg1 (vcpu); +unsigned long val; + +switch (feature) { +case PSCI_0_2_FN_PSCI_VERSION : +case PSCI_0_2_FN_CPU_SUSPEND : +case PSCI_0_2_FN64_CPU_SUSPEND : +case PSCI_0_2_FN_CPU_OFF : +case PSCI_0_2_FN_CPU_ON : +case PSCI_0_2_FN64_CPU_ON : +case PSCI_0_2_FN_AFFINITY_INFO : +case PSCI_0_2_FN64_AFFINITY_INFO : +case PSCI_0_2_FN_SYSTEM_OFF : +case PSCI_0_2_FN_SYSTEM_RESET : +case PSCI_1_0_FN_PSCI_FEATURES : +case PSCI_1_1_FN_SYSTEM_RESET2 : +case PSCI_1_1_FN64_SYSTEM_RESET2 : +case ARM_SMCCC_VERSION_FUNC_ID : + val =PSCI_RET_SUCCESS ; +break ; +default : + val =PSCI_RET_NOT_SUPPORTED ; +break ; + } + +/* Nothing to be handled by the host. Go back to the guest. */ +smccc_set_retval (vcpu, val,0 ,0 ,0 ); +return true; +} + +static bool pkvm_handle_psci (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct kvm_vcpu *vcpu = &hyp_vcpu->vcpu; +u32 psci_fn =smccc_get_function (vcpu); + +switch (psci_fn) { +case PSCI_0_2_FN_CPU_ON : +kvm_psci_narrow_to_32bit (vcpu); + fallthrough; +case PSCI_0_2_FN64_CPU_ON : +return pvm_psci_vcpu_on (hyp_vcpu); +case PSCI_0_2_FN_CPU_OFF : +return pvm_psci_vcpu_off (hyp_vcpu); +case PSCI_0_2_FN_PSCI_VERSION : +return pvm_psci_version (hyp_vcpu); +case PSCI_1_0_FN_PSCI_FEATURES : +return pvm_psci_features (hyp_vcpu); +case PSCI_0_2_FN_AFFINITY_INFO : +case PSCI_0_2_FN64_AFFINITY_INFO : +case PSCI_0_2_FN_SYSTEM_RESET : +case PSCI_0_2_FN_CPU_SUSPEND : +case PSCI_0_2_FN64_CPU_SUSPEND : +case PSCI_0_2_FN_SYSTEM_OFF : +case PSCI_1_1_FN_SYSTEM_RESET2 : +case PSCI_1_1_FN64_SYSTEM_RESET2 : +return false;/* Handled by the host. */ +default : +/* Unknown PSCI calls are answered here, not forwarded. */ +smccc_set_retval (vcpu,PSCI_RET_NOT_SUPPORTED ,0 ,0 ,0 ); +return true; + } +} +/* * Handler for protected VM HVC calls. * @@ -1187,6 +1460,7 @@ static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu)*/ bool kvm_handle_pvm_hvc64 (struct kvm_vcpu *vcpu,u64 *exit_code) { + struct pkvm_hyp_vcpu *hyp_vcpu = container_of(vcpu, struct pkvm_hyp_vcpu, vcpu);u64 val[4 ] = {SMCCC_RET_INVALID_PARAMETER };bool handled = true;u32 feature;@@ -1285,8 +1559,7 @@ bool kvm_handle_pvm_hvc64(struct kvm_vcpu *vcpu, u64 *exit_code)pkvm_memunshare_call (val, vcpu);break ;default : -/* Punt everything else back to the host, for now. */ - handled = false; +return pkvm_handle_psci (hyp_vcpu); }if (handled)Signature · 1 line
2.39.5
Signature · 1 line
Vincent
Re: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
Quoted 228 lines · Vincent Donnefort
Quoted 227 lines · Fuad Tabba
EL2 implements PSCI 1.1 for protected VMs:
CPU_ON,CPU_OFF,PSCI_VERSIONandPSCI_FEATURESare decided at EL2 (CPU_ONandCPU_OFFstill exit to the host, which only schedules or parks the target),AFFINITY_INFO,CPU_SUSPENDand the platform power operations are forwarded to the host, and anything else returnsNOT_SUPPORTED, including the TRNG calls and the functions above 1.1,SYSTEM_OFF2among them, that the host handled for a protected guest until now. TRNG for protected guests is a follow-up.AFFINITY_INFOstays with the host, which returns OFF only once it has parked the target:
the host is what a guest polls to see aCPU_OFFcomplete before it issues the nextCPU_ON, as Linux does on hotplug.Three consequences follow:
- A protected VM has one primary vCPU, the first whose hyp vCPU is created with
mp_stateRUNNABLE. A second one, or anmp_stateother than RUNNABLE or STOPPED, fails that vCPU's firstKVM_RUNwith-EINVAL.-
CPU_ONfinds its target among the hyp vCPUs, which exist from the target's firstKVM_RUN; before that the guest getsINVALID_PARAMETERS.- A vCPU EL2 holds powered off doesn't run:
handle___kvm_vcpu_run()returnsARM_EXCEPTION_IL, reported asKVM_EXIT_FAIL_ENTRY. Its existing bail-outs return the same code instead of an-EINVALthathandle_exit()didn't recognise, for every hyp vCPU.Non-protected VMs keep
power_stateON and accept anymp_state.Each protected vCPU is OFF,
ON_PENDINGor ON.CPU_ONmoves the target toON_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.CPU_OFFpublishes OFF with a release, so the target's clear of reset_state.reset is ordered before it and aCPU_ONthat then wins on OFF republishes after the clear. Rolling aCPU_ONthe 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 staysON_PENDING, and the reset has no observable effect:flush_hyp_vcpu()copies the host's context in on every entry until that patch removes the copy, so the target enters on the host's values rather than the ones EL2 reset.Signed-off-by Fuad Tabba <[email protected]>--- 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 | 283 ++++++++++++++++++++++++- 3 files changed, 311 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> @@ -456,14 +457,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)) {/* @@ -472,8 +471,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 +40
-1@@ -5,6 +5,7 @@*/ #include <kvm/arm_hypercalls.h> +#include <kvm/arm_psci.h> #include <linux/kvm_host.h> #include <linux/mm.h> @@ -433,6 +434,40 @@ static void pkvm_init_features_from_host(struct pkvm_hyp_vm *hyp_vm, const struc allowed_features, KVM_VCPU_MAX_FEATURES); } +static int pkvm_vcpu_init_psci (struct pkvm_hyp_vcpu *hyp_vcpu,u32 mp_state) +{ +struct vcpu_reset_state *reset_state = &hyp_vcpu->vcpu.arch.reset_state; +struct pkvm_hyp_vm *hyp_vm =pkvm_hyp_vcpu_to_hyp_vm (hyp_vcpu); +struct kvm_vcpu *host_vcpu; + +if (!pkvm_hyp_vcpu_is_protected (hyp_vcpu)) { +/* The host manages a non-protected vCPU: always ON at EL2. */ + hyp_vcpu->power_state =PSCI_0_2_AFFINITY_LEVEL_ON ; +return 0 ; + } + +if (mp_state !=KVM_MP_STATE_RUNNABLE && mp_state !=KVM_MP_STATE_STOPPED ) +return -EINVAL ; + +if (mp_state ==KVM_MP_STATE_STOPPED ) { + reset_state->reset = false; + hyp_vcpu->power_state =PSCI_0_2_AFFINITY_LEVEL_OFF ; +return 0 ; + } + +hyp_assert_lock_held (&vm_table_lock); +if (hyp_vm->primary_vcpu) +return -EINVAL ; + hyp_vm->primary_vcpu = hyp_vcpu; + + host_vcpu = hyp_vcpu->host_vcpu; + reset_state->pc =READ_ONCE (host_vcpu->arch.ctxt.regs.pc); + reset_state->r0 =READ_ONCE (host_vcpu->arch.ctxt.regs.regs[0 ]); + reset_state->reset = true; + hyp_vcpu->power_state =PSCI_0_2_AFFINITY_LEVEL_ON_PENDING ; +return 0 ; +} +static void unpin_host_vcpu (struct kvm_vcpu *host_vcpu) {if (host_vcpu)@@ -447,6 +482,9 @@ static void unpin_host_sve_state(struct pkvm_hyp_vcpu *hyp_vcpu)return ; sve_state = hyp_vcpu->vcpu.arch.sve_state; +if (!sve_state) +return ; +hyp_unpin_shared_mem (sve_state, sve_state +vcpu_sve_state_size (&hyp_vcpu->vcpu)); }@@ -559,10 +597,12 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, struct kvm_vcpu *host_vcpu) {int ret =0 ; +u32 mp_state;if (hyp_pin_shared_mem (host_vcpu, host_vcpu +1 ))return -EBUSY ; + mp_state =READ_ONCE (host_vcpu->arch.mp_state.mp_state); hyp_vcpu->host_vcpu = host_vcpu; hyp_vcpu->vcpu.kvm = &hyp_vm->kvm;@@ -571,7 +611,6 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, hyp_vcpu->vcpu.arch.hw_mmu = &hyp_vm->kvm.arch.mmu; hyp_vcpu->vcpu.arch.cflags =READ_ONCE (host_vcpu->arch.cflags); - hyp_vcpu->vcpu.arch.mp_state.mp_state =KVM_MP_STATE_STOPPED ;if (!pkvm_hyp_vcpu_is_protected (hyp_vcpu)) {/* @@ -601,9 +640,12 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu,
Quoted 11 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, · line 648
Quoted 9 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +4
-1if (pkvm_hyp_vcpu_is_protected (hyp_vcpu))kvm_reset_pvm_sys_regs (&hyp_vcpu->vcpu); + ret =pkvm_vcpu_init_psci (hyp_vcpu, mp_state); done: -if (ret) +if (ret) {unpin_host_vcpu (host_vcpu); +unpin_host_sve_state (hyp_vcpu); + }Was it planned for a different commit of the series?
Ha no it wasn't, you're calling unpin_host_sve_state() unconditionally.
Quoted 4 lines
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +0
-0return ret; }
[...]
Re: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
In reply to Fuad TabbaQuoted 50 lines · Fuad Tabba
EL2 implements PSCI 1.1 for protected VMs:
CPU_ON,CPU_OFF,PSCI_VERSIONandPSCI_FEATURESare decided at EL2 (CPU_ONandCPU_OFFstill exit to the host, which only schedules or parks the target),AFFINITY_INFO,CPU_SUSPENDand the platform power operations are forwarded to the host, and anything else returnsNOT_SUPPORTED, including the TRNG calls and the functions above 1.1,SYSTEM_OFF2among them, that the host handled for a protected guest until now. TRNG for protected guests is a follow-up.AFFINITY_INFOstays with the host, which returns OFF only once it has parked the target:
the host is what a guest polls to see aCPU_OFFcomplete before it issues the nextCPU_ON, as Linux does on hotplug.Three consequences follow:
- A protected VM has one primary vCPU, the first whose hyp vCPU is created with
mp_stateRUNNABLE. A second one, or anmp_stateother than RUNNABLE or STOPPED, fails that vCPU's firstKVM_RUNwith-EINVAL.-
CPU_ONfinds its target among the hyp vCPUs, which exist from the target's firstKVM_RUN; before that the guest getsINVALID_PARAMETERS.- A vCPU EL2 holds powered off doesn't run:
handle___kvm_vcpu_run()returnsARM_EXCEPTION_IL, reported asKVM_EXIT_FAIL_ENTRY. Its existing bail-outs return the same code instead of an-EINVALthathandle_exit()didn't recognise, for every hyp vCPU.Non-protected VMs keep
power_stateON and accept anymp_state.Each protected vCPU is OFF,
ON_PENDINGor ON.CPU_ONmoves the target toON_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.CPU_OFFpublishes OFF with a release, so the target's clear of reset_state.reset is ordered before it and aCPU_ONthat then wins on OFF republishes after the clear. Rolling aCPU_ONthe 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 staysON_PENDING, and the reset has no observable effect:flush_hyp_vcpu()copies the host's context in on every entry until that patch removes the copy, so the target enters on the host's values rather than the ones EL2 reset.Signed-off-by Fuad Tabba <[email protected]>--- 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 | 283 ++++++++++++++++++++++++- 3 files changed, 311 insertions(+), 11 deletions(-)
[...]
Quoted 69 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +69
-0+ +/* +* Returns true when handled at EL2, false when the host must wake the target +* vCPU. +*/ +static bool pvm_psci_vcpu_on (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct pkvm_hyp_vm *hyp_vm =pkvm_hyp_vcpu_to_hyp_vm (hyp_vcpu); +struct vcpu_reset_state *reset_state; +struct pkvm_hyp_vcpu *target; +unsigned long cpu_id, ret; +int power_state; + + cpu_id =smccc_get_arg1 (&hyp_vcpu->vcpu); +if (!kvm_psci_valid_affinity (&hyp_vcpu->vcpu, cpu_id)) { + ret =PSCI_RET_INVALID_PARAMS ; + goto error; + } + + target =pkvm_mpidr_to_hyp_vcpu (hyp_vm, cpu_id); +if (!target) { + ret =PSCI_RET_INVALID_PARAMS ; + goto error; + } + +/* +* vCPUs race to power on the same target. Relaxed: reset_state +* is published by the release on reset_state.reset below. +*/ + power_state =cmpxchg_relaxed (&target->power_state, +PSCI_0_2_AFFINITY_LEVEL_OFF , +PSCI_0_2_AFFINITY_LEVEL_ON_PENDING ); +switch (power_state) { +case PSCI_0_2_AFFINITY_LEVEL_ON_PENDING : + ret =PSCI_RET_ON_PENDING ; + goto error; +case PSCI_0_2_AFFINITY_LEVEL_ON : + ret =PSCI_RET_ALREADY_ON ; + goto error; +case PSCI_0_2_AFFINITY_LEVEL_OFF : +break ; +default : + ret =PSCI_RET_INTERNAL_FAILURE ; + goto error; + } + + reset_state = &target->vcpu.arch.reset_state; + reset_state->pc =smccc_get_arg2 (&hyp_vcpu->vcpu); + reset_state->r0 =smccc_get_arg3 (&hyp_vcpu->vcpu); + reset_state->be =kvm_vcpu_is_be (&hyp_vcpu->vcpu); +/* +* Publish reset_state.{pc, r0, be} to the target vCPU. Pairs with +* smp_load_acquire(&reset_state->reset) in pkvm_reset_vcpu(). +*/ +smp_store_release (&reset_state->reset, true); + +/* The host requests KVM_REQ_VCPU_RESET and wakes the target. */ +return false; + +error: +smccc_set_retval (&hyp_vcpu->vcpu, ret,0 ,0 ,0 ); +return true; +} + +/* +* Returns true when handled at EL2, false when the host must stop scheduling +* the vCPU. +*/ +static bool pvm_psci_vcpu_off (struct pkvm_hyp_vcpu *hyp_vcpu)
arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu) · line 1375
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +10
-0+{ +/* No other writer runs while this vCPU is ON and executing. */ +WARN_ON (READ_ONCE (hyp_vcpu->power_state) !=PSCI_0_2_AFFINITY_LEVEL_ON ); + +/* +* Orders pkvm_reset_vcpu()'s clear of reset_state.reset before OFF, so +* a CPU_ON that wins on OFF republishes after it. Pairs with the +* cmpxchg in pvm_psci_vcpu_on(). +*/ +smp_store_release (&hyp_vcpu->power_state,PSCI_0_2_AFFINITY_LEVEL_OFF );
Is there an issue either with the comment or with pvm_psci_vcpu_on()? the cmpxchg is relaxed. I would have expected cmpxchg_acquire().
Quoted 5 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +5
-0+ +/* Return to the host so that it can finish powering off the vcpu. */ +return false; +} +
[...]
Signature · 1 line
Vincent
Fuad Tabba
reviewerRe: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
Hi Vincent,
Quoted 10 lines
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +10
-0+/* +* Returns true when handled at EL2, false when the host must stop scheduling +* the vCPU. +*/ +static bool pvm_psci_vcpu_off (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +/* No other writer runs while this vCPU is ON and executing. */ +WARN_ON (READ_ONCE (hyp_vcpu->power_state) !=PSCI_0_2_AFFINITY_LEVEL_ON ); + +/*
Quoted 8 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu) · line 1375
Quoted 5 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +5
-0+* Orders pkvm_reset_vcpu()'s clear of reset_state.reset before OFF, so +* a CPU_ON that wins on OFF republishes after it. Pairs with the +* cmpxchg in pvm_psci_vcpu_on(). +*/ +smp_store_release (&hyp_vcpu->power_state,PSCI_0_2_AFFINITY_LEVEL_OFF );Is there an issue either with the comment or with
pvm_psci_vcpu_on()? the cmpxchg is relaxed. I would have expectedcmpxchg_acquire().
It's the comment. The ordering it describes holds with the relaxed cmpxchg. The release of OFF orders the clear before OFF. The winner's smp_store_release(&reset_state->reset, true) orders its cmpxchg before that store. So the clear can't land after the republish. The comment should name that store-release as the pair, not the cmpxchg.
You're right about the acquire though, for the plain stores to reset_state.{pc, r0, be} after the cmpxchg. I'll make it cmpxchg_acquire() in v4 and reword both comments.
Cheers,
/fuad
Will Deacon
maintainerRe: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
Quoted 21 lines · Fuad Tabba
Hi Vincent,
Quoted 10 lines
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +10
-0+/* +* Returns true when handled at EL2, false when the host must stop scheduling +* the vCPU. +*/ +static bool pvm_psci_vcpu_off (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +/* No other writer runs while this vCPU is ON and executing. */ +WARN_ON (READ_ONCE (hyp_vcpu->power_state) !=PSCI_0_2_AFFINITY_LEVEL_ON ); + +/* Quoted 8 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu) · line 1375
Quoted 5 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +5
-0+* Orders pkvm_reset_vcpu()'s clear of reset_state.reset before OFF, so +* a CPU_ON that wins on OFF republishes after it. Pairs with the +* cmpxchg in pvm_psci_vcpu_on(). +*/ +smp_store_release (&hyp_vcpu->power_state,PSCI_0_2_AFFINITY_LEVEL_OFF );Is there an issue either with the comment or with
pvm_psci_vcpu_on()? the cmpxchg is relaxed. I would have expectedcmpxchg_acquire().
Quoted 5 lines
It's the comment. The ordering it describes holds with the relaxed cmpxchg. The release of OFF orders the clear before OFF. The winner's
smp_store_release(&reset_state->reset, true)orders its cmpxchg before that store. So the clear can't land after the republish. The comment should name that store-release as the pair, not the cmpxchg.
Sorry, but I'm really confused by this and it appears to be different to what we've got in Android as well. Why do we need release semantics for the store to 'hyp_vcpu->power_state' in pvm_psci_vcpu_off()? What is it that we are publishing here? The comment talks about pkvm_reset_vcpu(), but how is that relevant to the vCPU _off_ path? You say the comment is wrong, but what _should_ it say?
I'm a bit baffled!
Will
Fuad Tabba
reviewerRe: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
Hi Will,
On Thu, 24 Sep 2026 09:30:06 +0100, Will Deacon <[email protected]> wrote:
[...]
Quoted 8 lines
Sorry, but I'm really confused by this and it appears to be different to what we've got in Android as well. Why do we need release semantics for the store to '
hyp_vcpu->power_state' inpvm_psci_vcpu_off()? What is it that we are publishing here? The comment talks aboutpkvm_reset_vcpu(), but how is that relevant to the vCPU _off_ path? You say the comment is wrong, but what _should_ it say?I'm a bit baffled!
It's not publishing data, it's handing reset_state back. The CPU_ON winner writes reset_state.{pc, r0, be}, then reset_state.reset with a release. The target reads them and clears reset in pkvm_reset_vcpu() on its next run, and its CPU_OFF hands reset_state on to the next CPU_ON. The release on OFF orders those reads and that clear before OFF, and an acquire on the winner's cmpxchg orders its writes after
it: release on the way out, acquire on the way in, like a lock. Without the release, the target's clear of reset can become visible after the next winner's reset = true, and the target's next pkvm_reset_vcpu() then reads a clear flag and returns -ECANCELED, leaving the vCPU stuck at ON_PENDING.
The comment described the flag ordering, which holds through the winner's release on reset even with a relaxed cmpxchg, and named the cmpxchg as its pair when the cmpxchg wasn't an acquire. The acquire is for the winner's plain writes of pc/r0/be: nothing else orders them after the target's reads. v4 has cmpxchg_acquire() and the two comments name what's ordered and each other:
/*
* Orders pkvm_reset_vcpu()'s accesses to reset_state before OFF. Pairs
* with the acquire cmpxchg in pvm_psci_vcpu_on().
*/
smp_store_release(&hyp_vcpu->power_state, PSCI_0_2_AFFINITY_LEVEL_OFF);and in pvm_psci_vcpu_on():
/*
* vCPUs race to power on the same target. The acquire pairs with the
* release of OFF in pvm_psci_vcpu_off(): the target's accesses to
* reset_state in pkvm_reset_vcpu() precede the writes below.
*/
power_state = cmpxchg_acquire(&target->power_state,
PSCI_0_2_AFFINITY_LEVEL_OFF,
PSCI_0_2_AFFINITY_LEVEL_ON_PENDING);Android has neither end (a WRITE_ONCE() of OFF and a relaxed cmpxchg), so it has the lost flag above and the data race on pc/r0/be. Both stay within the guest's own target vCPU, so I haven't treated it as urgent. I'm working on a fix for Android separately.
Cheers,
/fuad
Will Deacon
maintainerRe: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
Quoted 9 lines · Fuad Tabba
On Thu, 24 Sep 2026 09:30:06 +0100, Will Deacon <[email protected]> wrote:
[...]Quoted 7 lines
Sorry, but I'm really confused by this and it appears to be different to what we've got in Android as well. Why do we need release semantics for the store to '
hyp_vcpu->power_state' inpvm_psci_vcpu_off()? What is it that we are publishing here? The comment talks aboutpkvm_reset_vcpu(), but how is that relevant to the vCPU _off_ path? You say the comment is wrong, but what _should_ it say?
Quoted 3 lines
Quoted 1 line
I'm a bit baffled!
It's not publishing data, it's handing
reset_stateback.
What's the difference? The usual pattern for acquire/release is:
<write data>
<store-release flag>
on one CPU and then on another:
<load-acquire flag> // If this reads from the release above...
<read data> // ... then this is guaranteed to read the written data
That's a message-passing shape and you would normally say that the first CPU (the producer) is publishing the data to the other CPU (the consumer).
Is this what is happening with the 'reset_state' (data) and the 'power_state' (flag)? If not, then what is the shape?
Quoted 3 lines
The
CPU_ONwinner writes reset_state.{pc, r0, be}, then reset_state.reset with a release. The target reads them and clears reset inpkvm_reset_vcpu()on its next run
By 'next run' you mean, at EL2 on the entry path into the guest following a successful CPU_ON operation?
Quoted 4 lines
and its
CPU_OFFhandsreset_stateon to the nextCPU_ON. The release on OFF orders those reads and that clear before OFF, and an acquire on the winner's cmpxchg orders its writes after
it: release on the way out, acquire on the way in, like a lock.
That doesn't make sense to me, sorry. You're saying that the release store in the EL2 CPU_OFF hypercall handler is ordering stores that were made during the initial CPU_ON handling on that vCPU? Since then, we've been in and out of the guest. We really shouldn't need extra barriers to create order there.
Quoted 4 lines
Without the release, the target's clear of reset can become visible after the next winner's reset = true, and the target's next
pkvm_reset_vcpu()then reads a clear flag and returns-ECANCELED, leaving the vCPU stuck atON_PENDING.
This needs a litmus test because I can't see it myself. As above, CPU_OFF does not clear the reset state, so it's bizarre to put the release there.
Quoted 15 lines
The comment described the flag ordering, which holds through the winner's release on reset even with a relaxed cmpxchg, and named the cmpxchg as its pair when the cmpxchg wasn't an acquire. The acquire is for the winner's plain writes of pc/r0/be: nothing else orders them after the target's reads. v4 has
cmpxchg_acquire()and the two comments name what's ordered and each other:/* * Orders pkvm_reset_vcpu()'s accesses to reset_state before OFF. Pairs * with the acquire cmpxchg in pvm_psci_vcpu_on(). */ smp_store_release(&hyp_vcpu->power_state, PSCI_0_2_AFFINITY_LEVEL_OFF);and in
pvm_psci_vcpu_on():
Quoted 8 lines
/*
* vCPUs race to power on the same target. The acquire pairs with the
* release of OFF in pvm_psci_vcpu_off(): the target's accesses to
* reset_state in pkvm_reset_vcpu() precede the writes below.
*/
power_state = cmpxchg_acquire(&target->power_state,
PSCI_0_2_AFFINITY_LEVEL_OFF,
PSCI_0_2_AFFINITY_LEVEL_ON_PENDING);This confused me more :( Why are you talking about accesses preceding an acquire? An acquire only orders later accesses.
I really think we need some litmus tests to understand the general ordering problems we have here before adding the memory barriers.
Will
Fuad Tabba
reviewerRe: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
Hi Will,
On Thu, 24 Sep 2026 13:14:00 +0100, Will Deacon <[email protected]> wrote:
[...]
Quoted 9 lines
Quoted 1 line
It's not publishing data, it's handing
reset_stateback.What's the difference? The usual pattern for acquire/release is:
<write data>
<store-release flag>on one CPU and then on another:
Quoted 8 lines
<load-acquire flag> // If this reads from the release above...
<read data> // ... then this is guaranteed to read the written dataThat's a message-passing shape and you would normally say that the first CPU (the producer) is publishing the data to the other CPU (the consumer).
Is this what is happening with the '
reset_state' (data) and the 'power_state' (flag)? If not, then what is the shape?
I don't think so, it's the other direction. That pattern is the pair on reset_state.reset, and it isn't in question. The pair on power_state, as I see it, runs the other way: the target's last accesses to reset_state are its reads of the payload and its clear of the flag in pkvm_reset_vcpu(), and the next CPU_ON's accesses are stores. Release on OFF, acquire on the cmpxchg: unlock then lock, with power_state as the lock word. The winner takes it with the cmpxchg, hands it to the target through reset, and the target gives it up at CPU_OFF. Nothing is published. What I was after is the winner's stores being ordered after the target's reads and its clear.
Quoted 6 lines
Quoted 3 lines
The
CPU_ONwinner writes reset_state.{pc, r0, be}, then reset_state.reset with a release. The target reads them and clears reset inpkvm_reset_vcpu()on its next runBy 'next run' you mean, at EL2 on the entry path into the guest following a successful
CPU_ONoperation?
Yes. The target's first __kvm_vcpu_run hypercall after the host's kvm_psci_vcpu_on() has woken it. handle___kvm_vcpu_run() reads power_state as ON_PENDING and calls pkvm_reset_vcpu() before entering the guest.
Quoted 10 lines
Quoted 4 lines
and its
CPU_OFFhandsreset_stateon to the nextCPU_ON. The release on OFF orders those reads and that clear before OFF, and an acquire on the winner's cmpxchg orders its writes after
it: release on the way out, acquire on the way in, like a lock.That doesn't make sense to me, sorry. You're saying that the release store in the EL2
CPU_OFFhypercall handler is ordering stores that were made during the initialCPU_ONhandling on that vCPU? Since then, we've been in and out of the guest. We really shouldn't need extra barriers to create order there.
The target's own accesses at its reset, its reads of the payload and its clear of the flag. You're right that the hardware orders them:__kvm_vcpu_run() runs dsb(nsh) before it restores the guest's state, and since R24234 the NSH is only a TLBI/IC scope, so that DSB orders the target's accesses before OFF for every PE. My reasoning for the release and the acquire was that the protocol shouldn't rest on a barrier that is there for the translation regime, that nothing documents as ordering EL2 state for other CPUs, and that the LKMM, having no dsb(), can't see: under the model the target's accesses and the next winner's are unordered without them. Would you rather rely on the world switch and say so at that dsb(nsh)?
Quoted 8 lines
Quoted 4 lines
Without the release, the target's clear of reset can become visible after the next winner's reset = true, and the target's next
pkvm_reset_vcpu()then reads a clear flag and returns-ECANCELED, leaving the vCPU stuck atON_PENDING.This needs a litmus test because I can't see it myself. As above,
CPU_OFFdoes not clear the reset state, so it's bizarre to put the release there.
I'd actually done that earlier.
The flag, under the LKMM (herd7 7.58 with mainline's tools/memory-model):
C CPU_OFF-flag
(*
* P0, the target: its reset clears reset_state.reset, its
* CPU_OFF stores power_state = OFF. P1, the next CPU_ON:
* cmpxchg(OFF -> ON_PENDING), then a release of reset.
* OFF=0 ON=1 ON_PENDING=2.
*)
{
power_state = 1;
reset = 1;
}
P0(int *power_state, int *reset)
{
*reset = 0;
WRITE_ONCE(*power_state, 0);
}
P1(int *power_state, int *reset)
{
int r1;
r1 = cmpxchg_relaxed(power_state, 0, 2);
if (r1 == 0)
smp_store_release(reset, 1);
}
exists (1:r1=0 /\ reset=0)As written: Sometimes, and a data race. With smp_store_release() for the OFF store: Never. CPU_OFF doesn't touch reset_state, but its OFF store is what a CPU_ON takes with the cmpxchg, so it seemed to me the store the target's accesses should be ordered before.
Quoted 11 lines
Quoted 10 lines
and in
pvm_psci_vcpu_on():/* * vCPUs race to power on the same target. The acquire pairs with the * release of OFF in pvm_psci_vcpu_off(): the target's accesses to * reset_state in pkvm_reset_vcpu() precede the writes below. */ power_state = cmpxchg_acquire(&target->power_state, PSCI_0_2_AFFINITY_LEVEL_OFF, PSCI_0_2_AFFINITY_LEVEL_ON_PENDING);
Quoted 2 lines
This confused me more :( Why are you talking about accesses preceding an acquire? An acquire only orders later accesses.
Agreed, it reads as if the acquire ordered earlier accesses. For v4:
/*
* vCPUs race to power on the same target. The acquire orders the
* writes below after the cmpxchg's read of OFF, and with the release
* of OFF in pvm_psci_vcpu_off() that puts them after the target's
* reads and clear of reset_state in pkvm_reset_vcpu(). */
Quoted 2 lines
I really think we need some litmus tests to understand the general ordering problems we have here before adding the memory barriers.
The other direction, the payload:
C CPU_ON-payload
(*
* P0, the target: its reset reads reset_state.pc, its CPU_OFF
* stores OFF with a release. P1, the next CPU_ON:
* cmpxchg(OFF -> ON_PENDING), then a plain store to pc.
* OFF=0 ON=1 ON_PENDING=2.
*)
{
power_state = 1;
pc = 0;
}
P0(int *power_state, int *pc)
{
int r0;
r0 = *pc;
smp_store_release(power_state, 0);
}
P1(int *power_state, int *pc)
{
int r1;
r1 = cmpxchg_relaxed(power_state, 0, 2);
if (r1 == 0)
*pc = 1;
}
exists (1:r1=0 /\ 0:r0=1)As written: Sometimes, and a data race. With cmpxchg_acquire(): Never. Those two are what's behind the release (v3) and the acquire (v4). The rollback's ON_PENDING -> OFF cmpxchg in handle_pvm_entry_hvc64() has the same pattern on the source side, so v4 makes it a release too.
My head is spinning now :)
Cheers,
/fuad
Will Deacon
maintainerRe: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
Hi Fuad,
Quoted 20 lines · Fuad Tabba
On Thu, 24 Sep 2026 13:14:00 +0100, Will Deacon <[email protected]> wrote:
[...]Quoted 9 lines
Quoted 1 line
It's not publishing data, it's handing
reset_stateback.What's the difference? The usual pattern for acquire/release is:
<write data>
<store-release flag>on one CPU and then on another:
Quoted 8 lines
<load-acquire flag> // If this reads from the release above...
<read data> // ... then this is guaranteed to read the written dataThat's a message-passing shape and you would normally say that the first CPU (the producer) is publishing the data to the other CPU (the consumer).
Is this what is happening with the '
reset_state' (data) and the 'power_state' (flag)? If not, then what is the shape?
Quoted 10 lines
I don't think so, it's the other direction. That pattern is the pair on reset_state.reset, and it isn't in question. The pair on
power_state, as I see it, runs the other way: the target's last accesses toreset_stateare its reads of the payload and its clear of the flag inpkvm_reset_vcpu(), and the next CPU_ON's accesses are stores. Release on OFF, acquire on the cmpxchg: unlock then lock, withpower_stateas the lock word. The winner takes it with the cmpxchg, hands it to the target through reset, and the target gives it up atCPU_OFF. Nothing is published. What I was after is the winner's stores being ordered after the target's reads and its clear.
For the benefit of everybody else, we had a fire alarm in the office yesterday evening so Fuad and I sat outside a bar with a beer and a piece of paper and went through this together...
There are two confusing aspects to the current code:
1. The OFF->ON_PENDING transition happens on the vCPU requesting CPU_ON whereas the ON_PENDING->ON transition happens on the incoming (target) vCPU, with reset_state->reset used to synchronise between the two.
2. reset_state->reset is cleared to false by the target vCPU on the guest entry path, rather than on the CPU_OFF path.
If we address (2), then I think the sequence becomes a little easier to reason about. The OFF->ON_PENDING transition on the requestor looks like:
// PSCI CPU_ON cmpxchg_relaxed(power_state): OFF -> ON_PENDING <ctrl> write_reset_state(); smp_store_release(reset_state->reset, true);
and then the whole ON_PENDING -> ON -> run_guest() -> OFF sequence on the target looks like:
// Target vCPU comes online smp_load_acquire(reset_state->reset) == true; cmpxchg_relaxed(power_state): ON_PENDING -> ON; read_reset_state(); <run guest> // PSCI CPU_OFF WRITE_ONCE(reset_state->reset, false); smp_store_release(power_state, OFF);
which I think makes sense. WDYT? If you agree, I wonder if we can include a comment similar to the above in the code?
Will
Fuad Tabba
reviewerRe: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
Hi Will,
On Thu, 01 Oct 2026 13:58:01 +0100, Will Deacon <[email protected]> wrote:
[...]
Quoted 3 lines
For the benefit of everybody else, we had a fire alarm in the office yesterday evening so Fuad and I sat outside a bar with a beer and a piece of paper and went through this together...
Memory ordering is hard work...
[...]
Quoted 3 lines
If we address (2), then I think the sequence becomes a little easier to reason about. The
OFF->ON_PENDINGtransition on the requestor looks like:
[...]
Quoted 2 lines
which I think makes sense. WDYT? If you agree, I wonder if we can include a comment similar to the above in the code?
Agreed, and thanks for going through it with me (and for the beer!). v4 already clears reset on the CPU_OFF path rather than on guest entry, and I'll add your sequence as a comment above pkvm_reset_vcpu().
Cheers,
/fuad
Will Deacon
maintainerRe: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
In reply to Fuad TabbaQuoted 20 lines · Fuad Tabba
arch/arm64/kvm/hyp/nvhe/pkvm.c +15
-0+/* +* Reset the vCPU to its power-on state and commit ON_PENDING -> ON, on the +* target's own CPU. Returns -ECANCELED, with no side effects, if a rollback +* raced the reset. +*/ +int pkvm_reset_vcpu (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct vcpu_reset_state *reset_state = &hyp_vcpu->vcpu.arch.reset_state; +int prev; + +/* +* Pairs with smp_store_release(&reset_state->reset, true) in +* pvm_psci_vcpu_on(). The acquire must precede the cmpxchg: reversed, a +* winning cmpxchg with a false acquire would leave power_state == ON +* with the reset skipped.
arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu) · line 1256
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 );
If you go with the idea we're discussing later in the thread, then I think you need to drop this WARN_ON() and the comment that goes with it.
Will
Fuad Tabba
reviewerRe: [PATCH v3 10/18] KVM: arm64: Handle PSCI calls for protected VMs at EL2
On Thu, 01 Oct 2026 13:59:27 +0100, Will Deacon <[email protected]> wrote:
[...]
Quoted 6 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_memunshare_call(u64 *ret, struct kvm_vcpu *vcpu) · line 1256
Quoted 3 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +3
-0+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 );If you go with the idea we're discussing later in the thread, then I think you need to drop this
WARN_ON()and the comment that goes with it.
Will do, both go in v4.
/fuad
Fuad Tabba
reviewerPATCH v3 11/18 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.
--- arch/arm64/include/asm/kvm_pkvm.h | 24 ++++++++++++++++++++++++ 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(+), 15 deletions(-)
arch/arm64/include/asm/kvm_pkvm.h +24 -0
@@ -53,6 +53,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
@@ -412,23 +412,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 v3 12/18 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.
__kvm_adjust_pc() adjusts the vCPU a get/put pair returns, the one it was given outside pKVM. Both host callers hold the vCPU mutex, so a hyp vCPU loaded for the vCPU is loaded on the calling CPU. The request for a loaded protected vCPU is dropped. For a loaded non-protected vCPU, PKVM_HOST_STATE_DIRTY selects the copy, since adjusting the hyp vCPU while the host copy is authoritative loses the update at the next flush. 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(). With no hyp vCPU loaded, as under KVM_SET_VCPU_EVENTS, the host copy is adjusted as before.
Until the marshalling patch clears PC_UPDATE_REQ on the host copy at exit, a KVM_RUN that returns to userspace with INCREMENT_PC set leaves it on the host copy of a loaded protected vCPU, and a KVM_SET_VCPU_EVENTS before the next run then trips that WARN_ON.
--- arch/arm64/kvm/hyp/exception.c | 21 +++++++++----- arch/arm64/kvm/hyp/include/hyp/adjust_pc.h | 19 +++++++++++++ arch/arm64/kvm/hyp/nvhe/hyp-main.c | 32 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 7 deletions(-)
arch/arm64/kvm/hyp/exception.c +14 -7
@@ -353,12 +353,19 @@ static void kvm_inject_exception(struct kvm_vcpu *vcpu)*/ void __kvm_adjust_pc (struct kvm_vcpu *vcpu) { -if (vcpu_get_flag (vcpu,PENDING_EXCEPTION )) { -kvm_inject_exception (vcpu); -vcpu_clear_flag (vcpu,PENDING_EXCEPTION ); -vcpu_clear_flag (vcpu,EXCEPT_MASK ); - }else if (vcpu_get_flag (vcpu,INCREMENT_PC )) { -kvm_skip_instr (vcpu); -vcpu_clear_flag (vcpu,INCREMENT_PC ); +struct kvm_vcpu *target =pkvm_adjust_pc_get (vcpu); + +if (!target) +return ; + +if (vcpu_get_flag (target,PENDING_EXCEPTION )) { +kvm_inject_exception (target); +vcpu_clear_flag (target,PENDING_EXCEPTION ); +vcpu_clear_flag (target,EXCEPT_MASK ); + }else if (vcpu_get_flag (target,INCREMENT_PC )) { +kvm_skip_instr (target); +vcpu_clear_flag (target,INCREMENT_PC ); } + +pkvm_adjust_pc_put (vcpu, target); }
arch/arm64/kvm/hyp/include/hyp/adjust_pc.h +19 -0
@@ -68,4 +68,23 @@ static inline void kvm_skip_host_instr(void) write_sysreg_el2(read_sysreg_el2(SYS_ELR) + 4, SYS_ELR); } +/* +* Under pKVM, the vCPU __kvm_adjust_pc() adjusts for @vcpu (NULL drops the +* request), and the copy of the consumed PC_UPDATE_REQ back to @vcpu. +*/ +#ifdef __KVM_NVHE_HYPERVISOR__ +struct kvm_vcpu *pkvm_adjust_pc_get (struct kvm_vcpu *vcpu); +void pkvm_adjust_pc_put (struct kvm_vcpu *vcpu,struct kvm_vcpu *target); +#else +static inline struct kvm_vcpu *pkvm_adjust_pc_get (struct kvm_vcpu *vcpu) +{ +return vcpu; +} + +static inline void pkvm_adjust_pc_put (struct kvm_vcpu *vcpu, +struct kvm_vcpu *target) +{ +} +#endif +#endif
arch/arm64/kvm/hyp/nvhe/hyp-main.c +32 -0
@@ -642,6 +642,38 @@ static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt)cpu_reg (host_ctxt,1 ) = ret; } +/* +* PKVM_HOST_STATE_DIRTY names the authoritative copy, the host's when set. +* A loaded protected vCPU takes the request at its next entry instead. +*/ +struct kvm_vcpu *pkvm_adjust_pc_get (struct kvm_vcpu *vcpu) +{ +struct pkvm_hyp_vcpu *hyp_vcpu; + +if (!is_protected_kvm_enabled ()) +return vcpu; + + hyp_vcpu =pkvm_get_loaded_hyp_vcpu (); +if (!hyp_vcpu || hyp_vcpu->host_vcpu != vcpu) +return vcpu; + +if (pkvm_hyp_vcpu_is_protected (hyp_vcpu)) +return NULL ; + +if (vcpu_get_flag (vcpu,PKVM_HOST_STATE_DIRTY )) +return vcpu; + +vcpu_copy_flag (&hyp_vcpu->vcpu, vcpu,PC_UPDATE_REQ ); +return &hyp_vcpu->vcpu; +} + +/* Reflect the consumed request back, otherwise it stays pending. */ +void pkvm_adjust_pc_put (struct kvm_vcpu *vcpu,struct kvm_vcpu *target) +{ +if (target != vcpu) +vcpu_copy_flag (vcpu, target,PC_UPDATE_REQ ); +} +static void handle___kvm_adjust_pc (struct kvm_cpu_context *host_ctxt) { DECLARE_REG(struct kvm_vcpu *, vcpu, host_ctxt, 1);
Signature · 1 line
2.39.5
Marc Zyngier
maintainerRe: [PATCH v3 12/18] KVM: arm64: Prevent host PC adjustments for protected vCPUs
Quoted 37 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.
__kvm_adjust_pc()adjusts the vCPU a get/put pair returns, the one it was given outside pKVM. Both host callers hold the vCPU mutex, so a hyp vCPU loaded for the vCPU is loaded on the calling CPU. The request for a loaded protected vCPU is dropped. For a loaded non-protected vCPU,PKVM_HOST_STATE_DIRTYselects the copy, since adjusting the hyp vCPU while the host copy is authoritative loses the update at the next flush. 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(). With no hyp vCPU loaded, as underKVM_SET_VCPU_EVENTS, the host copy is adjusted as before.Until the marshalling patch clears
PC_UPDATE_REQon the host copy at exit, aKVM_RUNthat returns to userspace withINCREMENT_PCset leaves it on the host copy of a loaded protected vCPU, and aKVM_SET_VCPU_EVENTSbefore the next run then trips thatWARN_ON.Suggested-by Marc Zyngier <[email protected]>Signed-off-by Fuad Tabba <[email protected]>--- arch/arm64/kvm/hyp/exception.c | 21 +++++++++----- arch/arm64/kvm/hyp/include/hyp/adjust_pc.h | 19 +++++++++++++ arch/arm64/kvm/hyp/nvhe/hyp-main.c | 32 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 7 deletions(-)arch/arm64/kvm/hyp/exception.c +0
-0@@ -353,12 +353,19 @@ static void kvm_inject_exception(struct kvm_vcpu *vcpu)*/
arch/arm64/kvm/hyp/exception.c static void kvm_inject_exception(struct kvm_vcpu *vcpu) · line 356
Quoted 10 lines
arch/arm64/kvm/hyp/exception.c +1
-7void __kvm_adjust_pc (struct kvm_vcpu *vcpu) { -if (vcpu_get_flag (vcpu,PENDING_EXCEPTION )) { -kvm_inject_exception (vcpu); -vcpu_clear_flag (vcpu,PENDING_EXCEPTION ); -vcpu_clear_flag (vcpu,EXCEPT_MASK ); - }else if (vcpu_get_flag (vcpu,INCREMENT_PC )) { -kvm_skip_instr (vcpu); -vcpu_clear_flag (vcpu,INCREMENT_PC ); +struct kvm_vcpu *target =pkvm_adjust_pc_get (vcpu);
nit: it is really odd to see this 'pkvm' prefix in generic code. The point of it is not only to abstract the pvkm complexity away, but also to have a wrapper that may be of use in other situations.
Don't respin the series just for this though, I may end-up changing this when applying it.
arch/arm64/kvm/hyp/exception.c static void kvm_inject_exception(struct kvm_vcpu *vcpu) · line 360
Quoted 3 lines
arch/arm64/kvm/hyp/exception.c +3
-0+if (!target) +return ; +
I find this one scary, see below.
[...]
Quoted 11 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +3
-0@@ -642,6 +642,38 @@ static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt)cpu_reg (host_ctxt,1 ) = ret; } +/* +* PKVM_HOST_STATE_DIRTY names the authoritative copy, the host's when set. +* A loaded protected vCPU takes the request at its next entry instead.
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 657
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +10
-0+*/ +struct kvm_vcpu *pkvm_adjust_pc_get (struct kvm_vcpu *vcpu) +{ +struct pkvm_hyp_vcpu *hyp_vcpu; + +if (!is_protected_kvm_enabled ()) +return vcpu; + + hyp_vcpu =pkvm_get_loaded_hyp_vcpu (); +if (!hyp_vcpu || hyp_vcpu->host_vcpu != vcpu)
Under which circumstances do we get hyp_vcpu->host_vcpu != vcpu?
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 661
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +4
-0+return vcpu; + +if (pkvm_hyp_vcpu_is_protected (hyp_vcpu)) +return NULL ;
Is it always the case that a protected vcpu cannot see its PC adjusted at all? How is PC updated after an exit for MMIO? I feel there is an interaction with the above, but I'm not 100% certain...
Quoted 8 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +8
-0+ +if (vcpu_get_flag (vcpu,PKVM_HOST_STATE_DIRTY )) +return vcpu; + +vcpu_copy_flag (&hyp_vcpu->vcpu, vcpu,PC_UPDATE_REQ ); +return &hyp_vcpu->vcpu; +} +
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 679
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +7
-0+/* Reflect the consumed request back, otherwise it stays pending. */ +void pkvm_adjust_pc_put (struct kvm_vcpu *vcpu,struct kvm_vcpu *target) +{ +if (target != vcpu) +vcpu_copy_flag (vcpu, target,PC_UPDATE_REQ ); +} +static void handle___kvm_adjust_pc (struct kvm_cpu_context *host_ctxt) { DECLARE_REG(struct kvm_vcpu *, vcpu, host_ctxt, 1);
Thanks,
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Fuad Tabba
reviewerRe: [PATCH v3 12/18] KVM: arm64: Prevent host PC adjustments for protected vCPUs
Hi Marc,
On Mon, 14 Sept 2026 at 14:42, Marc Zyngier <[email protected]> wrote:
[...]
Quoted 6 lines
nit: it is really odd to see this 'pkvm' prefix in generic code. The point of it is not only to abstract the pvkm complexity away, but also to have a wrapper that may be of use in other situations.
Don't respin the series just for this though, I may end-up changing this when applying it.
Agree, if I respin I'll rename it.
[...]
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 657
Quoted 2 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +2
-0+ hyp_vcpu =pkvm_get_loaded_hyp_vcpu (); +if (!hyp_vcpu || hyp_vcpu->host_vcpu != vcpu)Under which circumstances do we get
hyp_vcpu->host_vcpu!= vcpu?
When EL2 calls __kvm_adjust_pc() on the hyp vCPU itself:__kvm_vcpu_run() at guest entry and inject_sync64() in nvhe/sys_regs.c both pass &hyp_vcpu->vcpu. There the vCPU passed is the one to adjust, so it's returned as is.
Quoted 6 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c static void handle___pkvm_host_mkyoung_guest(struct kvm_cpu_context *host_ctxt) · line 661
Quoted 2 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +2
-0+if (pkvm_hyp_vcpu_is_protected (hyp_vcpu)) +return NULL ;Is it always the case that a protected vcpu cannot see its PC adjusted at all? How is PC updated after an exit for MMIO? I feel there is an interaction with the above, but I'm not 100% certain...
The host can't adjust it directly, EL2 applies the host's request at the next entry. For MMIO, handle_pvm_exit_dabt() sets mmio_needed on the hyp vCPU from EL2's own syndrome, the host completes the access and kvm_handle_mmio_return() sets INCREMENT_PC on the host copy, and handle_pvm_entry_dabt() increments the hyp vCPU's PC at the next entry if EL2 had an MMIO pending, through the &hyp_vcpu->vcpu case above.
The NULL return clears nothing, so the request stays on the host copy for that entry. It's reached from the epilogue of kvm_arch_vcpu_ioctl_run(), e.g. an MMIO completed on an immediate_exit run, and consuming it on the host copy there would clear INCREMENT_PC before EL2 sees it, so the guest would re-execute the access.
"Drop" in the commit message and the adjust_pc.h comment reads as losing the request. I'll reword if I respin.
Cheers,
/fuad
Marc Zyngier
maintainerRe: [PATCH v3 12/18] KVM: arm64: Prevent host PC adjustments for protected vCPUs
Quoted 10 lines · Fuad Tabba
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 657
Quoted 3 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +2
-0+ hyp_vcpu =pkvm_get_loaded_hyp_vcpu (); +if (!hyp_vcpu || hyp_vcpu->host_vcpu != vcpu)Under which circumstances do we get
hyp_vcpu->host_vcpu!= vcpu?When EL2 calls
__kvm_adjust_pc()on the hyp vCPU itself:__kvm_vcpu_run()at guest entry andinject_sync64()innvhe/sys_regs.cboth pass&hyp_vcpu->vcpu. There the vCPU passed is the one to adjust, so it's returned as is.
If this is the sole case, why not check for '&hyp_cpu->vcpu == vcpu' instead of this odd inequality?
Thanks,
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Fuad Tabba
reviewerRe: [PATCH v3 12/18] KVM: arm64: Prevent host PC adjustments for protected vCPUs
Quoted 9 lines · Marc Zyngier
Quoted 6 lines · Fuad Tabba
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 657
Quoted 3 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +2
-0+ hyp_vcpu =pkvm_get_loaded_hyp_vcpu (); +if (!hyp_vcpu || hyp_vcpu->host_vcpu != vcpu)Under which circumstances do we get
hyp_vcpu->host_vcpu!= vcpu?
Quoted 7 lines
Quoted 4 lines
When EL2 calls
__kvm_adjust_pc()on the hyp vCPU itself:__kvm_vcpu_run()at guest entry andinject_sync64()innvhe/sys_regs.cboth pass&hyp_vcpu->vcpu. There the vCPU passed is the one to adjust, so it's returned as is.If this is the sole case, why not check for '
&hyp_cpu->vcpu== vcpu' instead of this odd inequality?
That's the only case. The inequality is the same test as in __get_host_hyp_vcpus(), but &hyp_vcpu->vcpu == vcpu is clearer here. I'll change it if there's a v4, or feel free to fold it in if you're applying v3 as is.
Cheers,
/fuad
Quoted 7 lines
Thanks,
M.--
Without deviation from the norm, progress is not possible.
Fuad Tabba
reviewerPATCH v3 13/18 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 v3 14/18 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: the PC update or exception it requested (PC_UPDATE_REQ), the value of a read it emulated, and a forwarded PSCI call's return value. 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, the deferred SError syndrome (DISR_EL1), and 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. A data abort's PC update is taken for a completed MMIO access, and for a cache maintenance operation the host skips on unbacked memory, as it does for any guest.
The host's copy of a protected vCPU's PSTATE is a view the guest never runs from: its reset value has PSTATE.A set, and EL2 sets PSTATE.A in the mode it copies out regardless of the guest's, so, unless the VMM wrote the host copy's PSTATE or SCTLR2_EL1 before the first run, serror_is_masked() is true and kvm_inject_serror_esr() pends a host-injected SError through HCR_EL2.VSE, masked by the guest's own PSTATE.A, instead of emulating the entry on that copy.
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. A CPU_ON the host failed is rolled back to OFF and returned as INTERNAL_FAILURE, or as ALREADY_ON when that's what the host returned: PSCI defines it as the retry signal for a CPU_ON that reaches the implementation before the target's CPU_OFF has been processed (DEN0022 section 6.6), which a guest that doesn't poll AFFINITY_INFO first can do. A target that already ran returns SUCCESS. The rollback leaves the published reset state in place: clearing it races a fresh CPU_ON's publication and wedges the target at ON_PENDING, and the entry point left behind is one the guest supplied. The rollback's cmpxchg carries no generation, so one that lands after a later CPU_ON has republished ON_PENDING cancels that cycle too. The target is then OFF at EL2 while the host has it runnable, and its CPU_ONs return ALREADY_ON until the VMM stops it again.
--- arch/arm64/kvm/hyp/nvhe/hyp-main.c | 430 +++++++++++++++++++++++++++-- 1 file changed, 407 insertions(+), 23 deletions(-)
arch/arm64/kvm/hyp/nvhe/hyp-main.c +407 -23
@@ -4,6 +4,8 @@* Author: Andrew Scull <[email protected]> */ +#include <kvm/arm_hypercalls.h> +#include <hyp/adjust_pc.h> #include <hyp/switch.h>
@@ -34,13 +36,366 @@ 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 bool pvm_sys64_is_write (u64 esr) { -vcpu_copy_flag (&hyp_vcpu->vcpu, hyp_vcpu->host_vcpu,PC_UPDATE_REQ ); +return (esr & ESR_ELx_SYS64_ISS_DIR_MASK) == ESR_ELx_SYS64_ISS_DIR_WRITE; } -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_wfx (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu; + +/* Exceptions have priority; the host injects none on WFx. */ +if (vcpu_get_flag (host_vcpu,PENDING_EXCEPTION )) +return ; + +if (vcpu_get_flag (host_vcpu,INCREMENT_PC )) { +vcpu_clear_flag (&hyp_vcpu->vcpu,PC_UPDATE_REQ ); +kvm_incr_pc (&hyp_vcpu->vcpu); + } +} + +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 && + !pvm_sys64_is_write (hyp_vcpu->vcpu.arch.fault.esr_el2)) { +/* 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, or on a CMO the host skipped */ + pc_update =vcpu_get_flag (host_vcpu,INCREMENT_PC ) && + (hyp_vcpu->vcpu.mmio_needed || +kvm_vcpu_dabt_is_cm (&hyp_vcpu->vcpu)); +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 && hyp_vcpu->vcpu.mmio_needed && + !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. +*/ + 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: a clear races a +* fresh CPU_ON's publish. The stale pc/r0/be are +* the guest's own. ALREADY_ON is PSCI's retry +* signal for a CPU_ON that raced the CPU_OFF. +*/ +if (ret !=PSCI_RET_ALREADY_ON ) + 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; + } +} + +/* +* The host's view of PSTATE: the mode, with SErrors masked so that the host +* pends an SError through HCR_EL2.VSE rather than emulating the entry. +*/ +static u64 pvm_host_pstate (u64 pstate) +{ +return (pstate &PSR_MODE_MASK ) |PSR_A_BIT ; +} + +static void handle_pvm_exit_wfx (struct pkvm_hyp_vcpu *hyp_vcpu) +{ + hyp_vcpu->host_vcpu->arch.ctxt.regs.pstate = +pvm_host_pstate (hyp_vcpu->vcpu.arch.ctxt.regs.pstate); +} + +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 = +pvm_host_pstate (hyp_vcpu->vcpu.arch.ctxt.regs.pstate); + +/* r0 as transfer register between the guest and the host. */ +if (pvm_sys64_is_write (esr_el2)) { +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. On unbacked memory the host injects an SEA +* for one with ISV clear (LDP/STP, atomics), which EL2 does not +* decode, and skips a cache maintenance operation, as for any guest. +*/ + 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 = +pvm_host_pstate (hyp_vcpu->vcpu.arch.ctxt.regs.pstate); + 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). EL2 resets the +* target from its own copy of the entry point and context id. +*/ +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_0_2_FN_AFFINITY_INFO : +case PSCI_0_2_FN64_AFFINITY_INFO : +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 +637,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 +661,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 +682,26 @@ 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);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,9 +710,28 @@ 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 ;
@@ -359,6 +740,9 @@ static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, u32 exit_reason)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
Marc Zyngier
maintainerRe: [PATCH v3 14/18] KVM: arm64: Add per-EC entry/exit state marshalling for protected guests
Quoted 113 lines · Fuad Tabba
Move a protected guest's state between the hyp vCPU and the host per exception class instead of copying the whole context. Add entry_hyp_pvm_handlers[] and exit_hyp_pvm_handlers[] for WFx, SYS64, IABT, DABT and HVC64, and route protected guests through them: on exit each handler copies out only what its class needs, and on re-entry only what the host may have changed: the PC update or exception it requested (
PC_UPDATE_REQ), the value of a read it emulated, and a forwarded PSCI call's return value. 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, the deferred SError syndrome (
DISR_EL1), and 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. A data abort's PC update is taken for a completed MMIO access, and for a cache maintenance operation the host skips on unbacked memory, as it does for any guest.The host's copy of a protected vCPU's PSTATE is a view the guest never runs from: its reset value has PSTATE.A set, and EL2 sets PSTATE.A in the mode it copies out regardless of the guest's, so, unless the VMM wrote the host copy's PSTATE or
SCTLR2_EL1before the first run,serror_is_masked()is true andkvm_inject_serror_esr()pends a host-injected SError through HCR_EL2.VSE, masked by the guest's own PSTATE.A, instead of emulating the entry on that copy.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. A
CPU_ONthe host failed is rolled back to OFF and returned asINTERNAL_FAILURE, or asALREADY_ONwhen that's what the host returned: PSCI defines it as the retry signal for aCPU_ONthat reaches the implementation before the target'sCPU_OFFhas been processed (DEN0022 section 6.6), which a guest that doesn't pollAFFINITY_INFOfirst can do. A target that already ran returns SUCCESS. The rollback leaves the published reset state in place: clearing it races a fresh CPU_ON's publication and wedges the target atON_PENDING, and the entry point left behind is one the guest supplied. The rollback's cmpxchg carries no generation, so one that lands after a laterCPU_ONhas republishedON_PENDINGcancels that cycle too. The target is then OFF at EL2 while the host has it runnable, and itsCPU_ONsreturnALREADY_ONuntil the VMM stops it again.Signed-off-by Fuad Tabba <[email protected]>--- arch/arm64/kvm/hyp/nvhe/hyp-main.c | 430 +++++++++++++++++++++++++++-- 1 file changed, 407 insertions(+), 23 deletions(-)arch/arm64/kvm/hyp/nvhe/hyp-main.c +35
-4@@ -4,6 +4,8 @@* Author: Andrew Scull <[email protected]> */ +#include <kvm/arm_hypercalls.h> +#include <hyp/adjust_pc.h> #include <hyp/switch.h> @@ -34,13 +36,366 @@ 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 bool pvm_sys64_is_write (u64 esr) { -vcpu_copy_flag (&hyp_vcpu->vcpu, hyp_vcpu->host_vcpu,PC_UPDATE_REQ ); +return (esr & ESR_ELx_SYS64_ISS_DIR_MASK) == ESR_ELx_SYS64_ISS_DIR_WRITE; } -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_wfx (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu; + +/* Exceptions have priority; the host injects none on WFx. */ +if (vcpu_get_flag (host_vcpu,PENDING_EXCEPTION )) +return ; + +if (vcpu_get_flag (host_vcpu,INCREMENT_PC )) { +vcpu_clear_flag (&hyp_vcpu->vcpu,PC_UPDATE_REQ ); +kvm_incr_pc (&hyp_vcpu->vcpu); + } +} + +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 );
arch/arm64/kvm/hyp/nvhe/hyp-main.c void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt); · line 84
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +10
-0+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 && + !pvm_sys64_is_write (hyp_vcpu->vcpu.arch.fault.esr_el2)) { +/* r0 as transfer register between the guest and the host. */ +u64 rt_val =READ_ONCE (host_vcpu->arch.ctxt.regs.regs[0 ]);
Why isn't this
rt_val = READ_ONCE(vcpu_gp_regs(host_vcpu)->regs[0]);
and similarly everywhere else?
[...]
Quoted 5 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +5
-0+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; +
arch/arm64/kvm/hyp/nvhe/hyp-main.c void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt); · line 299
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +10
-0+/* The mode is required for the host to emulate some sysregs */ + host_vcpu->arch.ctxt.regs.pstate = +pvm_host_pstate (hyp_vcpu->vcpu.arch.ctxt.regs.pstate); + +/* r0 as transfer register between the guest and the host. */ +if (pvm_sys64_is_write (esr_el2)) { +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;
and this should be
vcpu_set_reg(host_vcpu, 0, rt_val);
assuming you don't need a WRITE_ONCE() to match the READ_ONCE() in the other direction.
arch/arm64/kvm/hyp/nvhe/hyp-main.c void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt); · line 306
Quoted 7 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +7
-0+ } +} + +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;
Please keep assignments on a single line (everywhere).
Thanks,
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Fuad Tabba
reviewerRe: [PATCH v3 14/18] KVM: arm64: Add per-EC entry/exit state marshalling for protected guests
Hi Marc,
On Wed, 16 Sept 2026 at 17:27, Marc Zyngier <[email protected]> wrote:
[...]
Quoted 1 line
Quoted 1 line
arch/arm64/kvm/hyp/nvhe/hyp-main.c +0
-0
[...]
Quoted 1 line
Quoted 1 line
+static void
handle_pvm_entry_sys64(struct pkvm_hyp_vcpu *hyp_vcpu)
[...]
Quoted 11 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt); · line 84
Quoted 5 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +5
-0+/* If the host emulated a read access, update the register */ +if (pc_update && + !pvm_sys64_is_write (hyp_vcpu->vcpu.arch.fault.esr_el2)) { +/* r0 as transfer register between the guest and the host. */ +u64 rt_val =READ_ONCE (host_vcpu->arch.ctxt.regs.regs[0 ]);Why isn't this
rt_val = READ_ONCE(vcpu_gp_regs(host_vcpu)->regs[0]);and similarly everywhere else?
Old code momentum. I'll use the accessors throughout in v4.
[...]
Quoted 18 lines
Quoted 5 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +5
-0+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; +arch/arm64/kvm/hyp/nvhe/hyp-main.c void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt); · line 299
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +10
-0+/* The mode is required for the host to emulate some sysregs */ + host_vcpu->arch.ctxt.regs.pstate = +pvm_host_pstate (hyp_vcpu->vcpu.arch.ctxt.regs.pstate); + +/* r0 as transfer register between the guest and the host. */ +if (pvm_sys64_is_write (esr_el2)) { +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;and this should be
Quoted 4 lines
vcpu_set_reg(host_vcpu, 0, rt_val);assuming you don't need a
WRITE_ONCE()to match theREAD_ONCE()in the other direction.
No WRITE_ONCE() needed, EL2 is the only writer on the way out. I'll switch to vcpu_set_reg().
[...]
Quoted 6 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c void __kvm_hyp_host_forward_smc(struct kvm_cpu_context *host_ctxt); · line 306
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +4
-0+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;Please keep assignments on a single line (everywhere).
Will do.
Cheers,
/fuad
Fuad Tabba
reviewerPATCH v3 15/18 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 isn't 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.
The KVM_{GET,SET}_ONE_REG and KVM_ARM_VCPU_INIT checks are one filter on the ioctl number in kvm_arch_vcpu_ioctl(), the only caller of the three functions they were in. Its -EPERM now precedes the cases' -EFAULT and the ONE_REG case's pending-reset handling, which the next KVM_RUN performs. The external-abort check reads the payload, and KVM_SET_GUEST_DEBUG has its own case in kvm_vcpu_ioctl(), so it never reaches kvm_arch_vcpu_ioctl(): those two stay in their handlers.
KVM_CHECK_EXTENSION returns 0 for KVM_CAP_ARM_INJECT_EXT_DABT and KVM_CAP_SET_GUEST_DEBUG on a protected VM: kvm_pkvm_ext_allowed() returns false on every capability it doesn't list, and the patch that advertises the capabilities protected VMs support leaves these two out. The two ioctl checks stay for a VMM that doesn't query KVM_CHECK_EXTENSION.
--- arch/arm64/kvm/arm.c | 21 +++++++++++++++++++++ arch/arm64/kvm/guest.c | 11 +++++++++++ 2 files changed, 32 insertions(+)
arch/arm64/kvm/arm.c +21 -0
@@ -1865,6 +1865,23 @@ static int kvm_arm_vcpu_set_events(struct kvm_vcpu *vcpu,return __kvm_arm_vcpu_set_events (vcpu, events); } +/* +* Once a protected vCPU has run, the host copy is not the guest's state, +* and EL2 has read mp_state, which it does only at hyp vCPU creation. +*/ +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) {
@@ -1873,6 +1890,10 @@ long kvm_arch_vcpu_ioctl(struct file *filp,struct kvm_device_attr attr;long r; + r =pkvm_filter_vcpu_ioctl (vcpu, ioctl); +if (r) +return r; +switch (ioctl) {case KVM_ARM_VCPU_INIT : {struct kvm_vcpu_init init;
arch/arm64/kvm/guest.c +11 -0
@@ -786,6 +786,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 +890,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 v3 15/18] KVM: arm64: Reject host access to protected VM private state
Quoted 53 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 isn't 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.The KVM_{GET,SET}_ONE_REG and
KVM_ARM_VCPU_INITchecks are one filter on the ioctl number inkvm_arch_vcpu_ioctl(), the only caller of the three functions they were in. Its-EPERMnow precedes the cases'-EFAULTand theONE_REGcase's pending-reset handling, which the nextKVM_RUNperforms. The external-abort check reads the payload, andKVM_SET_GUEST_DEBUGhas its own case inkvm_vcpu_ioctl(), so it never reacheskvm_arch_vcpu_ioctl(): those two stay in their handlers.
KVM_CHECK_EXTENSIONreturns 0 forKVM_CAP_ARM_INJECT_EXT_DABTandKVM_CAP_SET_GUEST_DEBUGon a protected VM:kvm_pkvm_ext_allowed()returns false on every capability it doesn't list, and the patch that advertises the capabilities protected VMs support leaves these two out. The two ioctl checks stay for a VMM that doesn't queryKVM_CHECK_EXTENSION.Signed-off-by Fuad Tabba <[email protected]>--- arch/arm64/kvm/arm.c | 21 +++++++++++++++++++++ arch/arm64/kvm/guest.c | 11 +++++++++++ 2 files changed, 32 insertions(+)arch/arm64/kvm/arm.c +7
-0@@ -1865,6 +1865,23 @@ static int kvm_arm_vcpu_set_events(struct kvm_vcpu *vcpu,return __kvm_arm_vcpu_set_events (vcpu, events); } +/* +* Once a protected vCPU has run, the host copy is not the guest's state, +* and EL2 has read mp_state, which it does only at hyp vCPU creation. +*/ +static long pkvm_filter_vcpu_ioctl (struct kvm_vcpu *vcpu,unsigned int ioctl) +{ +switch (ioctl) {
arch/arm64/kvm/arm.c static int kvm_arm_vcpu_set_events(struct kvm_vcpu *vcpu, · line 1884
Quoted 10 lines
arch/arm64/kvm/arm.c +10
-0+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 ; +} +
I'm not keen on returning -EPERM for the ONE_REG stuff. For a start, X0 *is* valid on MMIO, and when you want to support LD64B and co, you'll need to show the actual data there.
I'd rather return what is in the host vcpu structure, as normal.
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Fuad Tabba
reviewerRe: [PATCH v3 15/18] KVM: arm64: Reject host access to protected VM private state
Hi Marc,
On Wed, 16 Sept 2026 at 17:30, Marc Zyngier <[email protected]> wrote:
[...]
Quoted 1 line
Quoted 1 line
arch/arm64/kvm/arm.c +0
-0
[...]
Quoted 22 lines
Quoted 7 lines
arch/arm64/kvm/arm.c +7
-0+/* +* Once a protected vCPU has run, the host copy is not the guest's state, +* and EL2 has read mp_state, which it does only at hyp vCPU creation. +*/ +static long pkvm_filter_vcpu_ioctl (struct kvm_vcpu *vcpu,unsigned int ioctl) +{ +switch (ioctl) {arch/arm64/kvm/arm.c static int kvm_arm_vcpu_set_events(struct kvm_vcpu *vcpu, · line 1884
Quoted 10 lines
arch/arm64/kvm/arm.c +10
-0+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 ; +} +I'm not keen on returning
-EPERMfor theONE_REGstuff. For a start, X0 *is* valid on MMIO, and when you want to support LD64B and co, you'll need to show the actual data there.
Quoted 1 line
I'd rather return what is in the host vcpu structure, as normal.
I think we should keep the error, for the reason s390 returns -EINVAL from ONE_REG on a protected vCPU and x86 does from the register ioctls for protected VM types: once the vCPU has run, the copy is the VMM's boot state plus what the exit handlers copy out, and the VMM can't distinguish them. x0 on MMIO is one of those fields, but the VMM reads it from kvm_run->mmio. For LD64B, EL2 would copy the operands out the same way and the error could be relaxed to those registers. Loosening later breaks nobody. The comment and the message do overstate it by calling the copy "not the guest's state", and I'll reword both in v4. More on the kvmtool thread [1].
Cheers,
/fuad
[1] https://lore.kernel.org/all/CA+EHjTwCs8B4du2=rAdoTOpOfh64p5cbW902e5g0u6NZT=r=gA@mail.gmail.com/
Marc Zyngier
maintainerRe: [PATCH v3 15/18] KVM: arm64: Reject host access to protected VM private state
Quoted 29 lines · Fuad Tabba
Hi Marc,
On Wed, 16 Sept 2026 at 17:30, Marc Zyngier <[email protected]> wrote:
[...]Quoted 1 line
Quoted 1 line
arch/arm64/kvm/arm.c +0
-0[...]
Quoted 22 lines
Quoted 7 lines
arch/arm64/kvm/arm.c +7
-0+/* +* Once a protected vCPU has run, the host copy is not the guest's state, +* and EL2 has read mp_state, which it does only at hyp vCPU creation. +*/ +static long pkvm_filter_vcpu_ioctl (struct kvm_vcpu *vcpu,unsigned int ioctl) +{ +switch (ioctl) {arch/arm64/kvm/arm.c static int kvm_arm_vcpu_set_events(struct kvm_vcpu *vcpu, · line 1884
Quoted 10 lines
arch/arm64/kvm/arm.c +10
-0+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 ; +} +I'm not keen on returning
-EPERMfor theONE_REGstuff. For a start, X0 *is* valid on MMIO, and when you want to support LD64B and co, you'll need to show the actual data there.
Quoted 5 lines
Quoted 1 line
I'd rather return what is in the host vcpu structure, as normal.
I think we should keep the error, for the reason s390 returns
-EINVALfromONE_REGon a protected vCPU and x86 does from the register ioctls for protected VM types:
News flash, this is not x86, nor s390. I don't feel constrained by other architecture, and we deviate *everywhere* already.
Quoted 8 lines
once the vCPU has run, the copy is the VMM's
boot state plus what the exit handlers copy out, and the VMM can't distinguish them. x0 on MMIO is one of those fields, but the VMM reads it fromkvm_run->mmio. For LD64B, EL2 would copy the operands out the same way and the error could be relaxed to those registers. Loosening later breaks nobody. The comment and the message do overstate it by calling the copy "not the guest's state", and I'll reword both in v4. More on the kvmtool thread [1].
A protected-aware VMM already knows it cannot obtain the registers.
I don't want to have to revisit the userspace interface once you have to relax it, because I know for sure that you will have to.
As far as LD64B is concerned, there is no place to copy anything in the run structure, and the relaxation would require to cover all the GPRs, ESR, and FAR. At this stage, returning whatever is there is the correct thing to do IMO.
Thanks,
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Fuad Tabba
reviewerRe: [PATCH v3 15/18] KVM: arm64: Reject host access to protected VM private state
Hi Marc,
On Thu, 17 Sept 2026 at 09:06, Marc Zyngier <[email protected]> wrote:
[...]
Quoted 2 lines
News flash, this is not x86, nor s390. I don't feel constrained by other architecture, and we deviate *everywhere* already.
Not x86 nor s390: and I'm grateful for that every day :P
[...]
Quoted 4 lines
A protected-aware VMM already knows it cannot obtain the registers.
I don't want to have to revisit the userspace interface once you have to relax it, because I know for sure that you will have to.
What the error buys is that a read outside what EL2 copies out fails rather than returning the copy, which is what a register dump would otherwise print. But then how would the VMM know, once EL2 starts copying more out on an exit it already gets, that those registers are now the guest's? A cap is all I can think of, but I may well be missing something. With the error, the relaxation is a window on that exit for the registers EL2 copies out, and a read that succeeds there returns something EL2 copied out.
Quoted 4 lines
As far as LD64B is concerned, there is no place to copy anything in the run structure, and the relaxation would require to cover all the GPRs, ESR, and FAR. At this stage, returning whatever is there is the correct thing to do IMO.
For a protected VM the operands would go through fixed registers, the way x0 carries the MMIO data today, so the window on that exit is those registers rather than all the GPRs.
Hoping this one doesn't return -EPERM :)
Cheers,
/fuad
Will Deacon
maintainerRe: [PATCH v3 15/18] KVM: arm64: Reject host access to protected VM private state
In reply to Marc ZyngierHi Marc, Fuad,
Quoted 28 lines · Marc Zyngier
Quoted 26 lines · Fuad Tabba
On Wed, 16 Sept 2026 at 17:30, Marc Zyngier <[email protected]> wrote:
[...]Quoted 1 line
Quoted 1 line
arch/arm64/kvm/arm.c +0
-0[...]
Quoted 22 lines
Quoted 7 lines
arch/arm64/kvm/arm.c +7
-0+/* +* Once a protected vCPU has run, the host copy is not the guest's state, +* and EL2 has read mp_state, which it does only at hyp vCPU creation. +*/ +static long pkvm_filter_vcpu_ioctl (struct kvm_vcpu *vcpu,unsigned int ioctl) +{ +switch (ioctl) {arch/arm64/kvm/arm.c static int kvm_arm_vcpu_set_events(struct kvm_vcpu *vcpu, · line 1884
Quoted 10 lines
arch/arm64/kvm/arm.c +10
-0+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 ; +} +I'm not keen on returning
-EPERMfor theONE_REGstuff. For a start, X0 *is* valid on MMIO, and when you want to support LD64B and co, you'll need to show the actual data there.
Quoted 1 line
Quoted 1 line
Quoted 1 line
I'd rather return what is in the host vcpu structure, as normal.
I just wanted to chuck my thoughts in here, as some of this is my fault and it would be really helpful to discuss it on the list. As you probably know, on Android, pKVM has the semantics you describe above: the ONE_REG calls succeed, but the register state isn't propagated to the guest once it's started running. I think we largely did it this way because we were solving a million problems at once during the initial development and having to pipe-clean VMMs wasn't particularly appealing at the time. It's also worth adding that, to my knowledge, we've never had any issues in Android because of this decision.
So, based on that, it really sounds like it's the best option. If it ain't broke, don't fix it!
*However*, I can't think of another upstream interface that behaves like that and, when we came to document it, it was quite difficult to explain it in a way that could be extended in the future. If we say that ONE_REG doesn't propagate to the vCPU after first run and returns whatever was last written, then userspace could (even accidentally) rely on that. If we wanted to extend it, I think we'd not only need a method to advertise the new behaviour (which I think you probably want even if ONE_REG returned an error before) but also an opt-in for userspace to say that it's ok with the new behaviour. In some ways, it feels a bit like the "unchecked flags" problem for syscall arguments.
Quoted 6 lines
Quoted 3 lines
I think we should keep the error, for the reason s390 returns
-EINVALfromONE_REGon a protected vCPU and x86 does from the register ioctls for protected VM types:News flash, this is not x86, nor s390. I don't feel constrained by other architecture, and we deviate *everywhere* already.
Right, this wasn't the rationale.
Quoted 10 lines
Quoted 8 lines
once the vCPU has run, the copy is the VMM's
boot state plus what the exit handlers copy out, and the VMM can't distinguish them. x0 on MMIO is one of those fields, but the VMM reads it fromkvm_run->mmio. For LD64B, EL2 would copy the operands out the same way and the error could be relaxed to those registers. Loosening later breaks nobody. The comment and the message do overstate it by calling the copy "not the guest's state", and I'll reword both in v4. More on the kvmtool thread [1].A protected-aware VMM already knows it cannot obtain the registers.
I think you can use that argument both ways: if the VMM knows it cannot obtain the registers, then it's fine to return an error if the VMM does something wrong.
In our recent investigation, it turned out that both kvmtool and crosvm were perfectly happy with ONE_REG returning an error in normal operation but we needed a handful of fixes for kvmtool [1] to handle things like the 'debug' command.
If it's helpful, we can ask the crosvm developers here if they have opinions for/against the two behaviours?
Quoted 2 lines
I don't want to have to revisit the userspace interface once you have to relax it, because I know for sure that you will have to.
I think we'll have to do that either way, no? The VMM needs a way to know that ONE_REG works and the situations in which it works. If the old behaviour wasn't to return an error, it's also going to need a way to enable the new behaviour.
Quoted 4 lines
As far as LD64B is concerned, there is no place to copy anything in the run structure, and the relaxation would require to cover all the GPRs, ESR, and FAR. At this stage, returning whatever is there is the correct thing to do IMO.
The nice thing about that is that we can presumably tie it in with LD64B support for protected VMs.
Will
Will Deacon
maintainerRe: [PATCH v3 15/18] KVM: arm64: Reject host access to protected VM private state
Quoted 4 lines · Will Deacon
In our recent investigation, it turned out that both kvmtool and crosvm were perfectly happy with
ONE_REGreturning an error in normal operation but we needed a handful of fixes for kvmtool [1] to handle things like the 'debug' command.
... and here's the link I forgot:
[1] https://lore.kernel.org/all/[email protected]/
Will
Marc Zyngier
maintainerRe: [PATCH v3 15/18] KVM: arm64: Reject host access to protected VM private state
In reply to Will DeaconHi Will,
Sorry for the delay replying, I'm catching up on email between travels...
Quoted 2 lines · Will Deacon
Hi Marc, Fuad,
[...]
Quoted 19 lines
Quoted 5 lines
Quoted 5 lines
Quoted 5 lines
I'm not keen on returning
-EPERMfor theONE_REGstuff. For a start, X0 *is* valid on MMIO, and when you want to support LD64B and co, you'll need to show the actual data there.I'd rather return what is in the host vcpu structure, as normal.
I just wanted to chuck my thoughts in here, as some of this is my fault and it would be really helpful to discuss it on the list. As you probably know, on Android, pKVM has the semantics you describe above: the
ONE_REGcalls succeed, but the register state isn't propagated to the guest once it's started running. I think we largely did it this way because we were solving a million problems at once during the initial development and having to pipe-clean VMMs wasn't particularly appealing at the time. It's also worth adding that, to my knowledge, we've never had any issues in Android because of this decision.So, based on that, it really sounds like it's the best option. If it ain't broke, don't fix it!
Quoted 10 lines
*However*, I can't think of another upstream interface that behaves like that and, when we came to document it, it was quite difficult to explain it in a way that could be extended in the future. If we say that
ONE_REGdoesn't propagate to the vCPU after first run and returns whatever was last written, then userspace could (even accidentally) rely on that. If we wanted to extend it, I think we'd not only need a method to advertise the new behaviour (which I think you probably want even ifONE_REGreturned an error before) but also an opt-in for userspace to say that it's ok with the new behaviour. In some ways, it feels a bit like the "unchecked flags" problem for syscall arguments.
My take on this is that by asking for a protected VM, userspace has already acknowledged that it may not be able to access the guest state at all, and that trying to access it could result in stale data being obtained.
For example, we don't have any particular "buy-in" for the anonymous memory that the guest has mapped and that userspace then tries to access. We don't slap it on the wrist, but instead give it a page full of zeroes.
[...]
Quoted 9 lines
Quoted 9 lines
Quoted 8 lines
once the vCPU has run, the copy is the VMM's
boot state plus what the exit handlers copy out, and the VMM can't distinguish them. x0 on MMIO is one of those fields, but the VMM reads it fromkvm_run->mmio. For LD64B, EL2 would copy the operands out the same way and the error could be relaxed to those registers. Loosening later breaks nobody. The comment and the message do overstate it by calling the copy "not the guest's state", and I'll reword both in v4. More on the kvmtool thread [1].
Quoted 5 lines
Quoted 1 line
A protected-aware VMM already knows it cannot obtain the registers.
I think you can use that argument both ways: if the VMM knows it cannot obtain the registers, then it's fine to return an error if the VMM does something wrong.
But this is introducing unnecessary friction for userspace to support pKVM/CCA/whatever hypervisors.
Quoted 8 lines
In our recent investigation, it turned out that both kvmtool and crosvm were perfectly happy with
ONE_REGreturning an error in normal operation but we needed a handful of fixes for kvmtool [1] to handle things like the 'debug' command.If it's helpful, we can ask the crosvm developers here if they have opinions for/against the two behaviours?
I guess that'd be useful to know what they'd prefer, and also to look into what QEMU does in this case -- I'm not exactly a good judge when it comes to UAPI definition... ;-)
Quoted 8 lines
Quoted 3 lines
I don't want to have to revisit the userspace interface once you have to relax it, because I know for sure that you will have to.
I think we'll have to do that either way, no? The VMM needs a way to know that
ONE_REGworks and the situations in which it works. If the old behaviour wasn't to return an error, it's also going to need a way to enable the new behaviour.
As above: my take is that this is being covered by creating a protected VM. But let's find out from the VMM folks what behaviour they prefer. After all, they have to deal with this...
Thanks,
M.
Signature · 1 line
Jazz isn't dead. It just smells funny.
Fuad Tabba
reviewerRe: [PATCH v3 15/18] KVM: arm64: Reject host access to protected VM private state
Hi Marc, Will,
On Sun, 27 Sep 2026 09:20:52 +0100, Marc Zyngier <[email protected]> wrote:
[...]
Quoted 8 lines
Quoted 8 lines
and it would be really helpful to discuss it on the list. As you probably know, on Android, pKVM has the semantics you describe above: the
ONE_REGcalls succeed, but the register state isn't propagated to the guest once it's started running. I think we largely did it this way because we were solving a million problems at once during the initial development and having to pipe-clean VMMs wasn't particularly appealing at the time. It's also worth adding that, to my knowledge, we've never had any issues in Android because of this decision.
That's Android 15 (6.6). Android 16 and 17 reject ONE_REG once a protected vCPU is finalized, except x0-x3 during one forwarded hypercall so the VMM can complete it [1]. crosvm has shipped against that on both.
[...]
Quoted 6 lines
Quoted 2 lines
If it's helpful, we can ask the crosvm developers here if they have opinions for/against the two behaviours?
I guess that'd be useful to know what they'd prefer, and also to look into what QEMU does in this case -- I'm not exactly a good judge when it comes to UAPI definition... ;-)
QEMU stops syncing registers once it has marked the guest state protected [2] (bar the virtual counter save/restore on stop/cont), and the QEMU CCA series sets that flag when the VM starts [3].
On the kernel side, the CCA series rejects a SET_ONE_REG for a realm outside the GPRs, PC and two configuration registers with -EINVAL, and lets GET return what its message calls "(stale)" values [4].
Given that, are you ok with v4 keeping the error? :D
Cheers,
/fuad
[1] https://android.googlesource.com/kernel/common/+/cfc696b1fb110
[2] https://gitlab.com/qemu-project/qemu/-/commit/5c3131c392f84c660033d511ec39872d8beb4b1e
[3] https://lore.kernel.org/all/[email protected]/
[4] https://lore.kernel.org/all/[email protected]/
Fuad Tabba
reviewerRe: [PATCH v3 15/18] KVM: arm64: Reject host access to protected VM private state
In reply to Marc ZyngierHi Marc, Will,
On Sun, 27 Sept 2026 at 09:17, Marc Zyngier <[email protected]> wrote:
[...]
Quoted 14 lines
Quoted 14 lines
Quoted 10 lines
Quoted 8 lines
once the vCPU has run, the copy is the VMM's
boot state plus what the exit handlers copy out, and the VMM can't distinguish them. x0 on MMIO is one of those fields, but the VMM reads it fromkvm_run->mmio. For LD64B, EL2 would copy the operands out the same way and the error could be relaxed to those registers. Loosening later breaks nobody. The comment and the message do overstate it by calling the copy "not the guest's state", and I'll reword both in v4. More on the kvmtool thread [1].A protected-aware VMM already knows it cannot obtain the registers.
I think you can use that argument both ways: if the VMM knows it cannot obtain the registers, then it's fine to return an error if the VMM does something wrong.
[...]
Quoted 6 lines
Quoted 2 lines
If it's helpful, we can ask the crosvm developers here if they have opinions for/against the two behaviours?
I guess that'd be useful to know what they'd prefer, and also to look into what QEMU does in this case -- I'm not exactly a good judge when it comes to UAPI definition... ;-)
I asked Frederick Mayle (CC'ed, AVF/pKVM support in crosvm) about what they think in crosvm. Their preference is an error rather than a silent failure: an explicit failure is easier to debug, and the earlier the error surfaces, the easier it is to correlate with the misbehaviour. Frederick also raised the MMIO case as a concrete risk of the silent option: a VMM could reasonably think it can complete an MMIO access with SET_ONE_REG and never find out that it was a no-op.
Cheers,
/fuad
Fuad Tabba
reviewerPATCH v3 16/18 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, and the guest's retry fails the same way until the VMM stops the vCPU again. 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.
The guest's CPU_ON is gated on the same record rather than on STOPPED. A VMM that sets STOPPED between the target's CPU_OFF exit and the host's power-off of it would otherwise let the CPU_ON through, and the power-off would then record the target powered off while EL2 holds it ON_PENDING, with no way back before VM teardown. Gated on the record, that CPU_ON returns ALREADY_ON and the retry succeeds once the power-off has run.
--- 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 | 10 +++++++++- 4 files changed, 47 insertions(+), 5 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 +9 -1
@@ -63,7 +63,12 @@ static unsigned long kvm_psci_vcpu_on(struct kvm_vcpu *source_vcpu)return PSCI_RET_INVALID_PARAMS ;spin_lock (&vcpu->arch.mp_state_lock); -if (!kvm_arm_vcpu_stopped (vcpu)) { +/* +* A protected vCPU is off once the host has acted on EL2's CPU_OFF. +* STOPPED before that is the VMM's pause. +*/ +if (!kvm_arm_vcpu_stopped (vcpu) || + (vcpu_is_protected (vcpu) && !vcpu->arch.pkvm_powered_off)) {if (kvm_psci_version (source_vcpu) !=KVM_ARM_PSCI_0_1 ) ret =PSCI_RET_ALREADY_ON ;else
@@ -94,6 +99,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
Marc Zyngier
maintainerRe: [PATCH v3 16/18] KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off
Quoted 34 lines · Fuad Tabba
A protected vCPU's power state belongs to EL2, which changes it only on the guest's own PSCI calls.
KVM_SET_MP_STATE(RUNNABLE)on a vCPU EL2 holds powered off changes the host's copy alone, and the guest's nextCPU_ONof that vCPU then fails:mp_stateis no longer STOPPED, so the host returnsALREADY_ON, and the guest's retry fails the same way until the VMM stops the vCPU again. SUSPENDED has the same effect.Track at the host whether EL2 holds a protected vCPU powered off and return
-EPERMfor 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.The guest's
CPU_ONis gated on the same record rather than on STOPPED. A VMM that sets STOPPED between the target'sCPU_OFFexit and the host's power-off of it would otherwise let theCPU_ONthrough, and the power-off would then record the target powered off while EL2 holds itON_PENDING, with no way back before VM teardown. Gated on the record, thatCPU_ONreturnsALREADY_ONand the retry succeeds once the power-off has run.Signed-off-by Fuad Tabba <[email protected]>--- 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 | 10 +++++++++- 4 files changed, 47 insertions(+), 5 deletions(-)arch/arm64/include/asm/kvm_host.h +0
-0
arch/arm64/include/asm/kvm_host.h struct kvm_vcpu_arch { · line 926
Quoted 5 lines
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;
I'm definitely not keen on yet another random boolean to describe some new state. We already have mp_state. Why can't we use one of the unused encodings and check that, since it very much looks like a case of "let userspace change the MP_STATE unless EL2 says otherwise".
Can that be made to work?
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Fuad Tabba
reviewerRe: [PATCH v3 16/18] KVM: arm64: Reject host power-on of a vCPU that EL2 holds powered off
Hi Marc,
On Wed, 16 Sept 2026 at 17:43, Marc Zyngier <[email protected]> wrote:
[...]
Quoted 16 lines
arch/arm64/include/asm/kvm_host.h struct kvm_vcpu_arch { · line 926
Quoted 10 lines
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;I'm definitely not keen on yet another random boolean to describe some new state. We already have
mp_state. Why can't we use one of the unused encodings and check that, since it very much looks like a case of "let userspace change theMP_STATEunless EL2 says otherwise".
Quoted 1 line
Can that be made to work?
Yes. The bool kept the VMM's STOPPED and EL2's off apart without a new userspace-visible value, but a distinct mp_state value does that and lets the VMM see why RUNNABLE is rejected.
It's set where the bool is set today, on the guest's CPU_OFF and at hyp vCPU creation for a vCPU that starts STOPPED, and only the guest's CPU_ON sets it back to RUNNABLE. KVM_SET_MP_STATE returns -EPERM for RUNNABLE and SUSPENDED on it and -EINVAL for the value itself, STOPPED and kvm_prepare_system_event() leave it in place, and kvm_arm_vcpu_stopped() returns true for it.
KVM_MP_STATE_UNINITIALIZED looks like the closest fit, a vCPU the guest has yet to bring up. I'll use that in v4 unless you'd rather another.
Cheers,
/fuad
Fuad Tabba
reviewerPATCH v3 17/18 KVM: arm64: Advertise the capabilities that protected VMs support
In reply to Fuad TabbaFor a protected VM, kvm_pkvm_ext_allowed() returns false on every capability it doesn't name, so KVM_CHECK_EXTENSION returns 0 for interfaces the VM implements: it 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
@@ -42,6 +42,15 @@ static inline bool kvm_pkvm_ext_allowed(struct kvm *kvm, long ext)case KVM_CAP_ARM_VM_IPA_SIZE :case KVM_CAP_ARM_PTRAUTH_ADDRESS :case KVM_CAP_ARM_PTRAUTH_GENERIC : +case KVM_CAP_ONE_REG : +case KVM_CAP_MP_STATE : +case KVM_CAP_VCPU_EVENTS : +case KVM_CAP_VCPU_ATTRIBUTES : +case KVM_CAP_IMMEDIATE_EXIT : +case KVM_CAP_IOEVENTFD : +case KVM_CAP_IRQFD_RESAMPLE : +case KVM_CAP_ARM_IRQ_LINE_LAYOUT_2 : +case KVM_CAP_ARM_INJECT_SERROR_ESR :return true;case KVM_CAP_ARM_MTE :return false;
Signature · 1 line
2.39.5
[email protected]
AI reviewRe: [PATCH v3 17/18] KVM: arm64: Advertise the capabilities that protected VMs support
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] Software SError injection via KVM_SET_VCPU_EVENTS silently fails for protected VMs and permanently leaves the host vCPU in a degraded state. --
commit 3a73d995c015585fa4c9bb7392c4fb3d6d828c9c
Author: Fuad Tabba <[email protected]>
KVM: arm64: Advertise the capabilities that protected VMs support
This patch updates kvm_pkvm_ext_allowed() to explicitly allow a list of capabilities that are expected to work for a protected guest, including various VCPU event and IRQ line capabilities.
Quoted 8 lines
arch/arm64/include/asm/kvm_pkvm.h +0
-0@@ -42,6 +42,15 @@ static inline bool kvm_pkvm_ext_allowed(struct kvm *kvm, long ext)case KVM_CAP_ARM_VM_IPA_SIZE :case KVM_CAP_ARM_PTRAUTH_ADDRESS :case KVM_CAP_ARM_PTRAUTH_GENERIC :
arch/arm64/include/asm/kvm_pkvm.h static inline bool kvm_pkvm_ext_allowed(struct kvm *kvm, long ext) · line 54
Quoted 10 lines
arch/arm64/include/asm/kvm_pkvm.h +9
-0+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;
[Severity: High]
This is a pre-existing issue, but advertising these capabilities formally exposes a bug where software SError injection is broken for protected VMs and leaves the host vCPU in a permanently degraded state.
If userspace sets an SError pending via KVM_SET_VCPU_EVENTS, kvm_inject_serror_esr() in arch/arm64/kvm/inject_fault.c attempts injection based on a stale host pstate. It sets PENDING_EXCEPTION and writes to the host's ESR_EL1.
For a protected VM, this is mishandled at EL2. If the VM traps on an MMIO access, handle_pvm_entry_dabt() in arch/arm64/kvm/hyp/nvhe/hyp-main.c sees the PENDING_EXCEPTION flag left by the software injection and incorrectly injects a Synchronous External Abort (DABT) instead.
Furthermore, EL2 does not clear PENDING_EXCEPTION on the host vcpu. When the host vCPU returns to userspace, kvm_arch_vcpu_ioctl_run() in arch/arm64/kvm/arm.c sees the uncleared flag. Because pkvm_adjust_pc_get() returns NULL for protected VMs, __kvm_adjust_pc() returns early without clearing the flag, triggering an unnecessary hypercall loop on every return to userspace.
Does it make sense to delay advertising KVM_CAP_VCPU_EVENTS and KVM_CAP_ARM_INJECT_SERROR_ESR for protected VMs until the software SError injection path is fixed?
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=17
Fuad Tabba
reviewerRe: [PATCH v3 17/18] KVM: arm64: Advertise the capabilities that protected VMs support
Quoted 5 lines · On Mon, 14 Sept 2026 at 17:23, <[email protected]> wrote:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Software SError injection viaKVM_SET_VCPU_EVENTSsilently fails for protected VMs and permanently leaves the host vCPU in a degraded state.
[...]
Quoted 3 lines
Pre-existing issues:
- [High] Software SError injection viaKVM_SET_VCPU_EVENTSsilently fails for protected VMs and permanently leaves the host vCPU in a degraded state.
[...]
The host's copy of a protected vCPU's PSTATE has PSTATE.A set, so serror_is_masked() is true and kvm_inject_serror_esr() pends the SError through HCR_EL2.VSE, which flush_hyp_vcpu() carries into the guest. The emulated entry you describe isn't taken.
Nothing is left behind either. The NULL return from pkvm_adjust_pc_get() clears nothing, the request stays on the host copy for the next entry, where the entry handler applies it, and sync_hyp_vcpu() clears PC_UPDATE_REQ at every exit of a protected vCPU. Marc asked the same question on patch 12 [1].
/fuad
[1] https://lore.kernel.org/all/CA+EHjTy6F9tG=eRLMuykc12P-Oc2pjMoQqAz34iGR+Q5GzV2jQ@mail.gmail.com/
Quoted 57 lines
--
commit
3a73d995c015585fa4c9bb7392c4fb3d6d828c9c
Author: Fuad Tabba <[email protected]>
KVM: arm64: Advertise the capabilities that protected VMs supportThis patch updates
kvm_pkvm_ext_allowed()to explicitly allow a list of capabilities that are expected to work for a protected guest, including various VCPU event and IRQ line capabilities.Quoted 8 lines
arch/arm64/include/asm/kvm_pkvm.h +0
-0@@ -42,6 +42,15 @@ static inline bool kvm_pkvm_ext_allowed(struct kvm *kvm, long ext)case KVM_CAP_ARM_VM_IPA_SIZE :case KVM_CAP_ARM_PTRAUTH_ADDRESS :case KVM_CAP_ARM_PTRAUTH_GENERIC :arch/arm64/include/asm/kvm_pkvm.h static inline bool kvm_pkvm_ext_allowed(struct kvm *kvm, long ext) · line 54
Quoted 10 lines
arch/arm64/include/asm/kvm_pkvm.h +9
-0+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;[Severity: High]
This is a pre-existing issue, but advertising these capabilities formally exposes a bug where software SError injection is broken for protected VMs and leaves the host vCPU in a permanently degraded state.If userspace sets an SError pending via
KVM_SET_VCPU_EVENTS,kvm_inject_serror_esr()inarch/arm64/kvm/inject_fault.cattempts injection based on a stale host pstate. It setsPENDING_EXCEPTIONand writes to the host'sESR_EL1.For a protected VM, this is mishandled at EL2. If the VM traps on an MMIO access,
handle_pvm_entry_dabt()inarch/arm64/kvm/hyp/nvhe/hyp-main.csees thePENDING_EXCEPTIONflag left by the software injection and incorrectly injects a Synchronous External Abort (DABT) instead.Furthermore, EL2 does not clear
PENDING_EXCEPTIONon the host vcpu. When the host vCPU returns to userspace,kvm_arch_vcpu_ioctl_run()inarch/arm64/kvm/arm.csees the uncleared flag. Becausepkvm_adjust_pc_get()returns NULL for protected VMs,__kvm_adjust_pc()returns early without clearing the flag, triggering an unnecessary hypercall loop on every return to userspace.Does it make sense to delay advertising
KVM_CAP_VCPU_EVENTSandKVM_CAP_ARM_INJECT_SERROR_ESRfor protected VMs until the software SError injection path is fixed?--
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=17
Fuad Tabba
reviewerPATCH v3 18/18 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 isn't the guest's. Describe the resulting API in pkvm.rst, point api.rst at it from each ioctl that behaves differently, and say beside the PVTIME attribute in devices/vcpu.rst and the PSCI version register in fw-pseudo-registers.rst how protected VMs differ. The overview also states the GIC support pKVM enforces: GICv2 is not supported in protected mode (vgic-v2.c) and GICv5-based guests aren't registered under pKVM (vgic-v5.c).
--- Documentation/virt/kvm/api.rst | 22 ++- .../virt/kvm/arm/fw-pseudo-registers.rst | 2 + Documentation/virt/kvm/arm/pkvm.rst | 161 +++++++++++++++++- Documentation/virt/kvm/devices/vcpu.rst | 3 +- 4 files changed, 178 insertions(+), 10 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 +154 -7
201 lines not on this page · raw message
Documentation/virt/kvm/devices/vcpu.rst +2 -1
@@ -231,7 +231,8 @@ Returns: 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