| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2020 - Google Inc |
| * Author: David Brazdil <dbrazdil@google.com> |
| */ |
| |
| #include <linux/compiler.h> |
| |
| #include <asm/kvm_asm.h> |
| #include <asm/kvm_emulate.h> |
| #include <asm/kvm_hyp.h> |
| #include <asm/kvm_mmu.h> |
| #include <asm/kvm_host.h> |
| #include <asm/kvm_psci.h> |
| |
| #include <kvm/arm_hypercalls.h> |
| #include <kvm/arm_psci.h> |
| |
| #include <uapi/linux/psci.h> |
| |
| #include <nvhe/spinlock.h> |
| |
| #include "../debug-pl011.h" |
| |
| static DEFINE_PER_CPU(nvhe_spinlock_t, kvm_psci_cpu_lock)= NVHE_SPIN_LOCK_INIT; |
| DECLARE_PER_CPU(struct kvm_hyp_init_params, kvm_cpu_params); |
| |
| #define kvm_next_host_vcpu(idx) \ |
| ({ \ |
| do { \ |
| idx++; \ |
| } while (idx < nr_cpu_ids && \ |
| cpu_logical_map(idx) == INVALID_HWID); \ |
| idx; \ |
| }) |
| |
| #define kvm_for_each_host_vcpu(idx) \ |
| for (idx = 0; idx < nr_cpu_ids; kvm_next_host_vcpu(idx)) |
| |
| static int find_cpu_id(u64 mpidr) |
| { |
| int cpu; |
| |
| kvm_for_each_host_vcpu(cpu) { |
| if (cpu_logical_map(cpu) == mpidr) |
| return cpu; |
| } |
| return -1; |
| } |
| |
| static unsigned long kvm_cpu_start_pa(void) |
| { |
| extern char __kvm_cpu_start[]; |
| unsigned long sym_kern_va; |
| |
| asm volatile("ldr %0, =%1" : "=r" (sym_kern_va) : "S" (__kvm_cpu_start)); |
| return sym_kern_va - kimage_voffset; |
| } |
| |
| void kvm_host_psci_init_cpu(struct kvm_vcpu *vcpu) |
| { |
| nvhe_spinlock_t *cpu_lock = this_cpu_ptr(&kvm_psci_cpu_lock); |
| |
| nvhe_spin_lock(cpu_lock); |
| vcpu->arch.power_off = false; |
| if (vcpu->arch.reset_state.reset) { |
| vcpu->arch.reset_state.reset = false; |
| |
| *vcpu_pc(vcpu) = vcpu->arch.reset_state.pc; |
| vcpu_set_reg(vcpu, 0, vcpu->arch.reset_state.r0); |
| |
| /* XXX - add more from KVM vcpu reset (search for arch.reset_state) */ |
| |
| write_sysreg_el2(vcpu_gp_regs(vcpu)->pstate, SYS_SPSR); |
| write_sysreg_el2(*vcpu_pc(vcpu), SYS_ELR); |
| } |
| nvhe_spin_unlock(cpu_lock); |
| } |
| |
| static int kvm_host_psci_cpu_on(unsigned long mpidr, unsigned long pc, |
| unsigned long r0) |
| { |
| int ret; |
| struct arm_smccc_res res; |
| int cpu_id = find_cpu_id(mpidr); |
| nvhe_spinlock_t *cpu_lock; |
| struct kvm_vcpu *vcpu; |
| |
| if (cpu_id < 0) |
| return PSCI_RET_INVALID_PARAMS; |
| |
| cpu_lock = per_cpu_ptr(&kvm_psci_cpu_lock, cpu_id); |
| vcpu = per_cpu_ptr(&kvm_host_vcpu, cpu_id); |
| |
| nvhe_spin_lock(cpu_lock); |
| |
| /* XXX - check that this CPU had KVM initialized on it */ |
| // ret = PSCI_RET_INTERNAL_FAILURE; |
| // goto out; |
| // } |
| |
| if (!vcpu->arch.power_off) { |
| ret = PSCI_RET_ALREADY_ON; |
| goto out; |
| } |
| |
| vcpu->arch.reset_state.reset = true; |
| vcpu->arch.reset_state.pc = pc; |
| vcpu->arch.reset_state.r0 = r0; |
| |
| /* |
| * There is a race with CPU_OFF. Repeat if the CPU is in the process |
| * of being turned off. |
| */ |
| do { |
| arm_smccc_1_1_smc(PSCI_0_2_FN64_CPU_ON, mpidr, |
| kvm_cpu_start_pa(), |
| per_cpu_ptr(&kvm_cpu_params, cpu_id)->this_phys_addr, |
| &res); |
| ret = res.a0; |
| } while (ret == PSCI_RET_ALREADY_ON); |
| |
| vcpu->arch.power_off = false; |
| |
| out: |
| nvhe_spin_unlock(cpu_lock); |
| return ret; |
| } |
| |
| static int kvm_host_psci_cpu_off(void) |
| { |
| nvhe_spinlock_t *cpu_lock = this_cpu_ptr(&kvm_psci_cpu_lock); |
| struct kvm_vcpu *vcpu = this_cpu_ptr(&kvm_host_vcpu); |
| |
| nvhe_spin_lock(cpu_lock); |
| vcpu->arch.power_off = true; |
| nvhe_spin_unlock(cpu_lock); |
| |
| hyp_puts("CPU_OFF"); |
| arm_smccc_1_1_smc(PSCI_0_2_FN_CPU_OFF, NULL); |
| |
| /* XXX - do we want to panic instead? */ |
| nvhe_spin_lock(cpu_lock); |
| vcpu->arch.power_off = false; |
| nvhe_spin_unlock(cpu_lock); |
| return PSCI_RET_DENIED; |
| } |
| |
| static int kvm_host_psci_cpu_suspend(unsigned long power_state, |
| unsigned long pc, unsigned long r0) |
| { |
| struct arm_smccc_res res; |
| nvhe_spinlock_t *cpu_lock; |
| struct kvm_vcpu *vcpu; |
| |
| hyp_puts("CPU_SUSPEND"); |
| |
| cpu_lock = this_cpu_ptr(&kvm_psci_cpu_lock); |
| vcpu = this_cpu_ptr(&kvm_host_vcpu); |
| |
| nvhe_spin_lock(cpu_lock); |
| vcpu->arch.reset_state.reset = true; |
| vcpu->arch.reset_state.pc = pc; |
| vcpu->arch.reset_state.r0 = r0; |
| |
| /* Unlock here? There would be a race with CPU_ON... */ |
| nvhe_spin_unlock(cpu_lock); |
| |
| hyp_puts("ENTER"); |
| arm_smccc_1_1_smc(PSCI_0_2_FN64_CPU_SUSPEND, |
| power_state, kvm_cpu_start_pa(), |
| this_cpu_ptr(&kvm_cpu_params)->this_phys_addr, |
| &res); |
| hyp_puts("EXIT"); |
| return res.a0; |
| } |
| |
| static int kvm_host_psci_affinity_info(unsigned long target_affinity, |
| unsigned long lowest_affinity_level) |
| { |
| unsigned int cpu_id; |
| unsigned long target_affinity_mask; |
| bool found_matching_cpu; |
| struct kvm_vcpu *vcpu; |
| |
| target_affinity_mask = kvm_psci_affinity_mask(lowest_affinity_level); |
| if (!target_affinity_mask) |
| return PSCI_RET_INVALID_PARAMS; |
| |
| found_matching_cpu = false; |
| kvm_for_each_host_vcpu(cpu_id) { |
| if ((cpu_logical_map(cpu_id) & target_affinity_mask) == target_affinity) { |
| found_matching_cpu = true; |
| vcpu = per_cpu_ptr(&kvm_host_vcpu, cpu_id); |
| if (!vcpu->arch.power_off) |
| return PSCI_0_2_AFFINITY_LEVEL_ON; |
| } |
| } |
| |
| if (!found_matching_cpu) |
| return PSCI_RET_INVALID_PARAMS; |
| |
| return PSCI_0_2_AFFINITY_LEVEL_OFF; |
| } |
| |
| static void __noreturn kvm_host_psci_system_off(void) |
| { |
| struct arm_smccc_res res; |
| arm_smccc_1_1_smc(PSCI_0_2_FN_SYSTEM_OFF, &res); |
| /* SYSTEM_OFF should never return. */ |
| for (;;) {} |
| } |
| |
| static void __noreturn kvm_host_psci_system_reset(void) |
| { |
| struct arm_smccc_res res; |
| arm_smccc_1_1_smc(PSCI_0_2_FN_SYSTEM_RESET, &res); |
| /* SYSTEM_RESET should never return. */ |
| for (;;) {} |
| } |
| |
| static int kvm_host_psci_0_2_call(unsigned long func_id, struct kvm_vcpu *host_vcpu) |
| { |
| switch (func_id) { |
| case PSCI_0_2_FN_PSCI_VERSION: |
| return KVM_ARM_PSCI_0_2; |
| case PSCI_0_2_FN_AFFINITY_INFO: |
| kvm_psci_narrow_to_32bit(host_vcpu); |
| fallthrough; |
| case PSCI_0_2_FN64_AFFINITY_INFO: |
| return kvm_host_psci_affinity_info(smccc_get_arg1(host_vcpu), |
| smccc_get_arg2(host_vcpu)); |
| case PSCI_0_2_FN_CPU_OFF: |
| return kvm_host_psci_cpu_off(); |
| case PSCI_0_2_FN_CPU_ON: |
| kvm_psci_narrow_to_32bit(host_vcpu); |
| fallthrough; |
| case PSCI_0_2_FN64_CPU_ON: |
| return kvm_host_psci_cpu_on(smccc_get_arg1(host_vcpu), |
| smccc_get_arg2(host_vcpu), |
| smccc_get_arg3(host_vcpu)); |
| case PSCI_0_2_FN_CPU_SUSPEND: |
| kvm_psci_narrow_to_32bit(host_vcpu); |
| fallthrough; |
| case PSCI_0_2_FN64_CPU_SUSPEND: |
| return kvm_host_psci_cpu_suspend(smccc_get_arg1(host_vcpu), |
| smccc_get_arg2(host_vcpu), |
| smccc_get_arg3(host_vcpu)); |
| case PSCI_0_2_FN_SYSTEM_OFF: |
| kvm_host_psci_system_off(); |
| unreachable(); |
| case PSCI_0_2_FN_SYSTEM_RESET: |
| kvm_host_psci_system_reset(); |
| unreachable(); |
| } |
| |
| return -EINVAL; |
| } |
| |
| int kvm_host_psci_call(struct kvm_vcpu *host_vcpu) |
| { |
| unsigned long func_id = smccc_get_function(host_vcpu); |
| unsigned long func_base = func_id & ~PSCI_0_2_FN_ID_MASK; |
| int ret; |
| |
| /* Early exit if this clearly isn't a PSCI call. */ |
| if (func_base != PSCI_0_2_FN_BASE && func_base != PSCI_0_2_FN64_BASE) |
| return -EINVAL; |
| |
| hyp_putx32(func_id); |
| ret = kvm_host_psci_0_2_call(func_id, host_vcpu); |
| return ret; |
| } |