[PATCH v1 11/11] KVM: arm64: Implement lazy vCPU state sync for non-protected guests
Fuad Tabba
tabba at google.com
Mon Jun 15 09:44:55 PDT 2026
On Mon, 15 Jun 2026 at 17:25, Vincent Donnefort <vdonnefort at google.com> wrote:
>
> On Fri, Jun 12, 2026 at 07:59:25AM +0100, tabba at google.com wrote:
> > pKVM copies a non-protected guest's register context between the host
> > and the hypervisor on every world switch, even when the host never
> > inspects it. Defer the copy: on entry, flush the host context into the
> > hyp vCPU only when the host marked it dirty (PKVM_HOST_STATE_DIRTY); on
> > exit, leave it in the hyp vCPU and copy it back only when the host needs
> > it, via a __pkvm_vcpu_sync_state hypercall on trap handling or at vcpu
> > put. A protected guest's context is copied as before, since lazy sync
> > only helps where the host is trusted to see the guest's registers.
> >
> > The PC is the exception: it is copied back on every exit so the
> > kvm_exit tracepoint reports the guest's real exit PC rather than the
> > value left by the previous sync.
> >
> > Signed-off-by: Fuad Tabba <tabba at google.com>
> > ---
> > arch/arm64/include/asm/kvm_asm.h | 1 +
> > arch/arm64/include/asm/kvm_host.h | 2 +
> > arch/arm64/kvm/arm.c | 7 +++
> > arch/arm64/kvm/handle_exit.c | 22 ++++++++
> > arch/arm64/kvm/hyp/nvhe/hyp-main.c | 88 ++++++++++++++++++++++++++++--
> > 5 files changed, 115 insertions(+), 5 deletions(-)
> >
> > diff --git a/arch/arm64/include/asm/kvm_asm.h b/arch/arm64/include/asm/kvm_asm.h
> > index 043495f7fc78..6e1135b3ded4 100644
> > --- a/arch/arm64/include/asm/kvm_asm.h
> > +++ b/arch/arm64/include/asm/kvm_asm.h
> > @@ -113,6 +113,7 @@ enum __kvm_host_smccc_func {
> > __KVM_HOST_SMCCC_FUNC___pkvm_finalize_teardown_vm,
> > __KVM_HOST_SMCCC_FUNC___pkvm_vcpu_load,
> > __KVM_HOST_SMCCC_FUNC___pkvm_vcpu_put,
> > + __KVM_HOST_SMCCC_FUNC___pkvm_vcpu_sync_state,
> > __KVM_HOST_SMCCC_FUNC___pkvm_tlb_flush_vmid,
> >
> > MARKER(__KVM_HOST_SMCCC_FUNC_MAX)
> > diff --git a/arch/arm64/include/asm/kvm_host.h b/arch/arm64/include/asm/kvm_host.h
> > index a49042bfa801..1ef660774adc 100644
> > --- a/arch/arm64/include/asm/kvm_host.h
> > +++ b/arch/arm64/include/asm/kvm_host.h
> > @@ -1113,6 +1113,8 @@ struct kvm_vcpu_arch {
> > /* SError pending for nested guest */
> > #define NESTED_SERROR_PENDING __vcpu_single_flag(sflags, BIT(8))
> >
> > +/* pKVM host vcpu state is dirty, needs resync (nVHE-only) */
>
> nit: with hVHE, I guess we can just drop that nVHE-only?
Ack.
>
> > +#define PKVM_HOST_STATE_DIRTY __vcpu_single_flag(iflags, BIT(4))
> >
> > /* Pointer to the vcpu's SVE FFR for sve_{save,load}_state() */
> > #define vcpu_sve_pffr(vcpu) (kern_hyp_va((vcpu)->arch.sve_state) + \
> > diff --git a/arch/arm64/kvm/arm.c b/arch/arm64/kvm/arm.c
> > index c9f36932c980..a5c54e37778b 100644
> > --- a/arch/arm64/kvm/arm.c
> > +++ b/arch/arm64/kvm/arm.c
> > @@ -734,6 +734,10 @@ void kvm_arch_vcpu_put(struct kvm_vcpu *vcpu)
> > if (is_protected_kvm_enabled()) {
> > kvm_call_hyp(__vgic_v3_save_aprs, &vcpu->arch.vgic_cpu.vgic_v3);
> > kvm_call_hyp_nvhe(__pkvm_vcpu_put);
> > +
> > + /* __pkvm_vcpu_put implies a sync of the state */
> > + if (!kvm_vm_is_protected(vcpu->kvm))
> > + vcpu_set_flag(vcpu, PKVM_HOST_STATE_DIRTY);
> > }
> >
> > kvm_vcpu_put_debug(vcpu);
> > @@ -961,6 +965,9 @@ int kvm_arch_vcpu_run_pid_change(struct kvm_vcpu *vcpu)
> > return ret;
> >
> > if (is_protected_kvm_enabled()) {
> > + /* Start with the vcpu in a dirty state */
> > + if (!kvm_vm_is_protected(vcpu->kvm))
> > + vcpu_set_flag(vcpu, PKVM_HOST_STATE_DIRTY);
> > ret = pkvm_create_hyp_vm(kvm);
> > if (ret)
> > return ret;
> > diff --git a/arch/arm64/kvm/handle_exit.c b/arch/arm64/kvm/handle_exit.c
> > index 54aedf93c78b..dccc3786548b 100644
> > --- a/arch/arm64/kvm/handle_exit.c
> > +++ b/arch/arm64/kvm/handle_exit.c
> > @@ -422,6 +422,21 @@ static int handle_trap_exceptions(struct kvm_vcpu *vcpu)
> > {
> > int handled;
> >
> > + /*
> > + * If we run a non-protected VM when protection is enabled
> > + * system-wide, resync the state from the hypervisor and mark
> > + * it as dirty on the host side if it wasn't dirty already
> > + * (which could happen if preemption has taken place).
> > + */
> > + if (is_protected_kvm_enabled() && !kvm_vm_is_protected(vcpu->kvm)) {
> > + preempt_disable();
>
> nit: since we are introducing guard() with that series, this one could be
> guard(preempt)().
Nice one :) Done.
>
> > + if (!(vcpu_get_flag(vcpu, PKVM_HOST_STATE_DIRTY))) {
> > + kvm_call_hyp_nvhe(__pkvm_vcpu_sync_state);
> > + vcpu_set_flag(vcpu, PKVM_HOST_STATE_DIRTY);
> > + }
> > + preempt_enable();
> > + }
> > +
> > /*
> > * See ARM ARM B1.14.1: "Hyp traps on instructions
> > * that fail their condition code check"
> > @@ -489,6 +504,13 @@ int handle_exit(struct kvm_vcpu *vcpu, int exception_index)
> > /* For exit types that need handling before we can be preempted */
> > void handle_exit_early(struct kvm_vcpu *vcpu, int exception_index)
> > {
> > + /*
> > + * We just exited, so the state is clean from a hypervisor
> > + * perspective.
> > + */
> > + if (is_protected_kvm_enabled())
> > + vcpu_clear_flag(vcpu, PKVM_HOST_STATE_DIRTY);
> > +
> > if (ARM_SERROR_PENDING(exception_index)) {
> > if (this_cpu_has_cap(ARM64_HAS_RAS_EXTN)) {
> > u64 disr = kvm_vcpu_get_disr(vcpu);
> > diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > index 23e644c24a03..02383b372258 100644
> > --- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > +++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
> > @@ -139,6 +139,49 @@ 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 __copy_vcpu_state(const struct kvm_vcpu *from_vcpu,
> > + struct kvm_vcpu *to_vcpu)
> > +{
> > + int i;
> > +
> > + to_vcpu->arch.ctxt.regs = from_vcpu->arch.ctxt.regs;
> > + to_vcpu->arch.ctxt.spsr_abt = from_vcpu->arch.ctxt.spsr_abt;
> > + to_vcpu->arch.ctxt.spsr_und = from_vcpu->arch.ctxt.spsr_und;
> > + to_vcpu->arch.ctxt.spsr_irq = from_vcpu->arch.ctxt.spsr_irq;
> > + to_vcpu->arch.ctxt.spsr_fiq = from_vcpu->arch.ctxt.spsr_fiq;
> > + to_vcpu->arch.ctxt.fp_regs = from_vcpu->arch.ctxt.fp_regs;
> > +
> > + /*
> > + * Copy the sysregs, but don't mess with the timer state which
> > + * is directly handled by EL1 and is expected to be preserved.
> > + * enum vcpu_sysreg is sparse: VNCR-mapped registers take values
> > + * derived from their VNCR page offset, so the timer registers do
> > + * not form a contiguous numeric range and must be skipped by name.
> > + */
> > + for (i = 1; i < NR_SYS_REGS; i++) {
> > + switch (i) {
> > + case CNTVOFF_EL2:
> > + case CNTV_CVAL_EL0:
> > + case CNTV_CTL_EL0:
> > + case CNTP_CVAL_EL0:
> > + case CNTP_CTL_EL0:
> > + continue;
> > + }
> > + to_vcpu->arch.ctxt.sys_regs[i] = from_vcpu->arch.ctxt.sys_regs[i];
> > + }
> > +}
> > +
> > +static void __sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> > +{
> > + __copy_vcpu_state(&hyp_vcpu->vcpu, hyp_vcpu->host_vcpu);
> > +}
> > +
> > +static void __flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> > +{
> > + __copy_vcpu_state(hyp_vcpu->host_vcpu, &hyp_vcpu->vcpu);
> > +}
>
> nit: Could that be flush/sync_hyp_vcpu_state? as everything this is called
> "state" and we already have flush_debug_state() below ?
Good point, renamed to flush_hyp_vcpu_state()/sync_hyp_vcpu_state().
>
> > +
> > static void flush_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu)
> > {
> > struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
> > @@ -168,7 +211,17 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> > fpsimd_sve_flush();
> > flush_debug_state(hyp_vcpu);
> >
> > - hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
> > + /*
> > + * If we deal with a non-protected guest and the state is potentially
> > + * dirty (from a host perspective), copy the state back into the hyp
> > + * vcpu.
> > + */
> > + if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
> > + if (vcpu_get_flag(host_vcpu, PKVM_HOST_STATE_DIRTY))
> > + __flush_hyp_vcpu(hyp_vcpu);
> > + } else {
> > + hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
> > + }
> >
> > hyp_vcpu->vcpu.arch.mdcr_el2 = host_vcpu->arch.mdcr_el2;
> > hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWI | HCR_TWE);
> > @@ -191,9 +244,11 @@ static void sync_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
> > fpsimd_sve_sync(&hyp_vcpu->vcpu);
> > sync_debug_state(hyp_vcpu);
> >
> > - host_vcpu->arch.ctxt = hyp_vcpu->vcpu.arch.ctxt;
> > -
> > - host_vcpu->arch.hcr_el2 = hyp_vcpu->vcpu.arch.hcr_el2;
> > + if (pkvm_hyp_vcpu_is_protected(hyp_vcpu))
> > + host_vcpu->arch.ctxt = hyp_vcpu->vcpu.arch.ctxt;
> > + else
> > + /* Keep the PC current for the kvm_exit tracepoint (lazy ctxt sync). */
> > + host_vcpu->arch.ctxt.regs.pc = hyp_vcpu->vcpu.arch.ctxt.regs.pc;
> >
> > host_vcpu->arch.fault = hyp_vcpu->vcpu.arch.fault;
> >
> > @@ -227,8 +282,30 @@ static void handle___pkvm_vcpu_put(struct kvm_cpu_context *host_ctxt)
> > {
> > struct pkvm_hyp_vcpu *hyp_vcpu = pkvm_get_loaded_hyp_vcpu();
> >
> > - if (hyp_vcpu)
> > + if (hyp_vcpu) {
> > + struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
> > +
> > + if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu) &&
> > + !vcpu_get_flag(host_vcpu, PKVM_HOST_STATE_DIRTY)) {
> > + __sync_hyp_vcpu(hyp_vcpu);
> > + }
> > +
> > pkvm_put_hyp_vcpu(hyp_vcpu);
> > + }
> > +}
> > +
> > +static void handle___pkvm_vcpu_sync_state(struct kvm_cpu_context *host_ctxt)
> > +{
> > + struct pkvm_hyp_vcpu *hyp_vcpu;
> > +
> > + if (!is_protected_kvm_enabled())
> > + return;
>
> Since "KVM: arm64: Remove is_protected_kvm_enabled() checks from hypercalls" we
> got rid of those is_protected_kvm_enabled() for pKVM-only HVCs. (also, it is
> declared in the pKVM-only section of the HVCs)
Dropped.
Thanks a lot for the reviews!
/fuad
>
> > +
> > + hyp_vcpu = pkvm_get_loaded_hyp_vcpu();
> > + if (!hyp_vcpu || pkvm_hyp_vcpu_is_protected(hyp_vcpu))
> > + return;
> > +
> > + __sync_hyp_vcpu(hyp_vcpu);
> > }
> >
> > static struct kvm_vcpu *__get_host_hyp_vcpus(struct kvm_vcpu *arg,
> > @@ -859,6 +936,7 @@ static const hcall_t host_hcall[] = {
> > HANDLE_FUNC(__pkvm_finalize_teardown_vm),
> > HANDLE_FUNC(__pkvm_vcpu_load),
> > HANDLE_FUNC(__pkvm_vcpu_put),
> > + HANDLE_FUNC(__pkvm_vcpu_sync_state),
> > HANDLE_FUNC(__pkvm_tlb_flush_vmid),
> > };
> >
> > --
> > 2.54.0.1136.gdb2ca164c4-goog
> >
More information about the linux-arm-kernel
mailing list