| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2020 - Google LLC |
| * Author: David Brazdil <dbrazdil@google.com> |
| */ |
| |
| #include <asm/kvm_asm.h> |
| #include <asm/kvm_hyp.h> |
| #include <asm/kvm_mmu.h> |
| #include <kvm/arm_hypercalls.h> |
| #include <linux/arm-smccc.h> |
| #include <linux/psci.h> |
| #include <kvm/arm_psci.h> |
| #include <uapi/linux/psci.h> |
| |
| #include <nvhe/memory.h> |
| #include <nvhe/spinlock.h> |
| |
| #define INVALID_CPU_ID UINT_MAX |
| |
| /* Config options set by the host. */ |
| u32 kvm_host_psci_version = PSCI_VERSION(0, 0); |
| u32 kvm_host_psci_function_id[PSCI_FN_MAX]; |
| u32 kvm_host_psci_cpu_suspend_feature; |
| |
| enum kvm_host_cpu_power_state { |
| KVM_HOST_CPU_POWER_OFF = 0, |
| KVM_HOST_CPU_POWER_PENDING_ON, |
| KVM_HOST_CPU_POWER_ON, |
| }; |
| |
| struct kvm_host_psci_cpu { |
| nvhe_spinlock_t lock; |
| enum kvm_host_cpu_power_state power_state; |
| |
| struct vcpu_reset_state reset_state; |
| }; |
| |
| static DEFINE_PER_CPU(struct kvm_host_psci_cpu, kvm_psci_host_cpu) = |
| (struct kvm_host_psci_cpu){ |
| .lock = NVHE_SPIN_LOCK_INIT, |
| .power_state = KVM_HOST_CPU_POWER_OFF, |
| }; |
| |
| static u64 get_psci_func_id(struct kvm_cpu_context *host_ctxt) |
| { |
| return host_ctxt->regs.regs[0]; |
| } |
| |
| static bool is_psci_0_1_call(u64 func_id) |
| { |
| unsigned int i; |
| |
| for (i = 0; i < ARRAY_SIZE(kvm_host_psci_function_id); ++i) { |
| if (func_id == kvm_host_psci_function_id[i]) |
| return true; |
| } |
| return false; |
| } |
| |
| static bool is_psci_0_2_fn32_call(u64 func_id) |
| { |
| return (func_id & ~PSCI_0_2_FN_ID_MASK) == PSCI_0_2_FN_BASE; |
| } |
| |
| static bool is_psci_0_2_fn64_call(u64 func_id) |
| { |
| return (func_id & ~PSCI_0_2_FN_ID_MASK) == PSCI_0_2_FN64_BASE; |
| } |
| |
| static unsigned long psci_call(unsigned long fn, unsigned long arg0, |
| unsigned long arg1, unsigned long arg2) |
| { |
| struct arm_smccc_res res; |
| arm_smccc_1_1_smc(fn, arg0, arg1, arg2, &res); |
| return res.a0; |
| } |
| |
| static bool psci_has_ext_power_state(void) |
| { |
| return kvm_host_psci_cpu_suspend_feature & PSCI_1_0_FEATURES_CPU_SUSPEND_PF_MASK; |
| } |
| |
| static bool psci_power_state_loses_context(u32 state) |
| { |
| const u32 mask = psci_has_ext_power_state() ? |
| PSCI_1_0_EXT_POWER_STATE_TYPE_MASK : |
| PSCI_0_2_POWER_STATE_TYPE_MASK; |
| |
| return state & mask; |
| } |
| |
| static unsigned int find_cpu_id(u64 mpidr) |
| { |
| unsigned int i; |
| |
| if (mpidr != INVALID_HWID) { |
| for (i = 0; i < nr_cpu_ids; i++) { |
| if (cpu_logical_map(i) == mpidr) |
| return i; |
| } |
| } |
| |
| return INVALID_CPU_ID; |
| } |
| |
| static phys_addr_t cpu_entry_pa(void) |
| { |
| extern char __kvm_hyp_cpu_entry[]; |
| unsigned long kern_va; |
| |
| asm volatile("ldr %0, =%1" : "=r" (kern_va) : "S" (__kvm_hyp_cpu_entry)); |
| return kern_va - kimage_voffset; |
| } |
| |
| static unsigned int psci_version(void) |
| { |
| return (unsigned int)psci_call(PSCI_0_2_FN_PSCI_VERSION, 0, 0, 0); |
| } |
| |
| static int psci_features(struct kvm_cpu_context *host_ctxt) |
| { |
| u32 psci_func_id = (u32)host_ctxt->regs.regs[1]; |
| |
| return (int)psci_call(PSCI_1_0_FN_PSCI_FEATURES, psci_func_id, 0, 0); |
| } |
| |
| static int psci_cpu_suspend(struct kvm_cpu_context *host_ctxt) |
| { |
| u64 state = host_ctxt->regs.regs[1]; |
| unsigned long pc = host_ctxt->regs.regs[1]; |
| unsigned long r0 = host_ctxt->regs.regs[2]; |
| struct kvm_host_psci_cpu *host_cpu = this_cpu_ptr(&kvm_psci_host_cpu); |
| struct kvm_nvhe_init_params *init_params = this_cpu_ptr(&kvm_init_params); |
| int ret; |
| |
| if (!psci_power_state_loses_context(state)) { |
| // Resuming from this state has the same semantics as WFI. |
| return (int)psci_call(PSCI_0_2_FN64_CPU_SUSPEND, 0, 0, 0); |
| } |
| |
| /* If successful, resuming from this state has the same semantics as CPU_ON. */ |
| nvhe_spin_lock(&host_cpu->lock); |
| host_cpu->reset_state = (struct vcpu_reset_state){ |
| .pc = pc, |
| .r0 = r0, |
| .reset = true, |
| }; |
| nvhe_spin_unlock(&host_cpu->lock); |
| |
| ret = (int)psci_call(kvm_host_psci_function_id[PSCI_FN_CPU_SUSPEND], |
| state, |
| cpu_entry_pa(), |
| __hyp_pa(init_params)); |
| |
| nvhe_spin_lock(&host_cpu->lock); |
| host_cpu->reset_state = (struct vcpu_reset_state){ |
| .reset = false, |
| }; |
| nvhe_spin_unlock(&host_cpu->lock); |
| |
| return ret; |
| } |
| |
| static int psci_cpu_off(struct kvm_cpu_context *host_ctxt) |
| { |
| u64 func_id = get_psci_func_id(host_ctxt); |
| u32 state = host_ctxt->regs.regs[1]; |
| struct kvm_host_psci_cpu *host_cpu = this_cpu_ptr(&kvm_psci_host_cpu); |
| int ret; |
| |
| nvhe_spin_lock(&host_cpu->lock); |
| host_cpu->power_state = KVM_HOST_CPU_POWER_OFF; |
| nvhe_spin_unlock(&host_cpu->lock); |
| |
| ret = psci_call(func_id, state, 0, 0); |
| |
| nvhe_spin_lock(&host_cpu->lock); |
| host_cpu->power_state = KVM_HOST_CPU_POWER_ON; |
| nvhe_spin_unlock(&host_cpu->lock); |
| return ret; |
| } |
| |
| static int psci_cpu_on(struct kvm_cpu_context *host_ctxt) |
| { |
| u64 mpidr = host_ctxt->regs.regs[1] & MPIDR_HWID_BITMASK; |
| unsigned long pc = host_ctxt->regs.regs[2]; |
| unsigned long r0 = host_ctxt->regs.regs[3]; |
| unsigned int target_cpu_id; |
| struct kvm_host_psci_cpu *target_cpu; |
| struct kvm_nvhe_init_params *target_init_params; |
| int ret; |
| |
| target_cpu_id = find_cpu_id(mpidr); |
| if (target_cpu_id == INVALID_CPU_ID) |
| return PSCI_RET_INVALID_PARAMS; |
| |
| target_cpu = per_cpu_ptr(&kvm_psci_host_cpu, target_cpu_id); |
| target_init_params = per_cpu_ptr(&kvm_init_params, target_cpu_id); |
| |
| do { |
| nvhe_spin_lock(&target_cpu->lock); |
| |
| if (target_cpu->power_state != KVM_HOST_CPU_POWER_OFF) { |
| if (kvm_host_psci_version == PSCI_VERSION(0, 1)) |
| ret = PSCI_RET_INVALID_PARAMS; |
| else if (target_cpu->power_state == KVM_HOST_CPU_POWER_ON) |
| ret = PSCI_RET_ALREADY_ON; |
| else |
| ret = PSCI_RET_ON_PENDING; |
| nvhe_spin_unlock(&target_cpu->lock); |
| return ret; |
| } |
| |
| target_cpu->reset_state = (struct vcpu_reset_state){ |
| .pc = pc, |
| .r0 = r0, |
| .reset = true, |
| }; |
| |
| ret = psci_call(kvm_host_psci_function_id[PSCI_FN_CPU_ON], |
| mpidr, |
| cpu_entry_pa(), |
| __hyp_pa(target_init_params)); |
| |
| if (ret == PSCI_RET_SUCCESS) |
| target_cpu->power_state = KVM_HOST_CPU_POWER_PENDING_ON; |
| |
| nvhe_spin_unlock(&target_cpu->lock); |
| |
| /* |
| * If recorded CPU state is OFF but EL3 reports that it's ON, |
| * we must have hit a race with CPU_OFF on the target core. |
| * Loop to try again. |
| */ |
| } while (ret == PSCI_RET_ALREADY_ON); |
| |
| return ret; |
| } |
| |
| static int psci_affinity_info(struct kvm_cpu_context *host_ctxt) |
| { |
| u64 mpidr = host_ctxt->regs.regs[1] & MPIDR_HWID_BITMASK; |
| unsigned long affinity_level = host_ctxt->regs.regs[2]; |
| unsigned int target_cpu_id; |
| struct kvm_host_psci_cpu *target_cpu; |
| |
| /* The PSCI host driver only ever queries about level zero. */ |
| if (affinity_level != 0) |
| return PSCI_RET_INVALID_PARAMS; |
| |
| target_cpu_id = find_cpu_id(mpidr); |
| |
| if (target_cpu_id == INVALID_CPU_ID) |
| return PSCI_RET_INVALID_PARAMS; |
| |
| target_cpu = per_cpu_ptr(&kvm_psci_host_cpu, target_cpu_id); |
| |
| switch (target_cpu->power_state) { |
| case KVM_HOST_CPU_POWER_OFF: |
| return PSCI_0_2_AFFINITY_LEVEL_OFF; |
| case KVM_HOST_CPU_POWER_PENDING_ON: |
| return PSCI_0_2_AFFINITY_LEVEL_ON_PENDING; |
| case KVM_HOST_CPU_POWER_ON: |
| return PSCI_0_2_AFFINITY_LEVEL_ON; |
| default: |
| hyp_panic(); |
| } |
| } |
| |
| static int psci_migrate(struct kvm_cpu_context *host_ctxt) |
| { |
| u64 mpidr = host_ctxt->regs.regs[1] & MPIDR_HWID_BITMASK; |
| |
| return psci_call(kvm_host_psci_function_id[PSCI_FN_MIGRATE], mpidr, 0, 0); |
| } |
| |
| static int psci_migrate_info_type(void) |
| { |
| return psci_call(PSCI_0_2_FN_MIGRATE_INFO_TYPE, 0, 0, 0); |
| } |
| |
| /* |
| * Note that unlike other PSCI calls, MIGRATE_INFO_UP_CPU returns a u64. |
| * Fortunately PSCI error codes are not valid MPIDR values. |
| */ |
| static u64 psci_migrate_info_up_cpu(void) |
| { |
| return psci_call(PSCI_0_2_FN64_MIGRATE_INFO_UP_CPU, 0, 0, 0); |
| } |
| |
| static int __noreturn psci_system_off(void) |
| { |
| psci_call(PSCI_0_2_FN_SYSTEM_OFF, 0, 0, 0); |
| hyp_panic(); /* unreachable */ |
| } |
| |
| static int __noreturn psci_system_reset(void) |
| { |
| psci_call(PSCI_0_2_FN_SYSTEM_RESET, 0, 0, 0); |
| hyp_panic(); /* unreachable */ |
| } |
| |
| static int psci_system_reset2(struct kvm_cpu_context *host_ctxt) |
| { |
| u32 reset_type = (u32)host_ctxt->regs.regs[1]; |
| u64 cookie = host_ctxt->regs.regs[2]; |
| |
| /* Returns either NOT_SUPPORTED or INVALID_PARAMETERS. */ |
| return psci_call(PSCI_1_1_FN64_SYSTEM_RESET2, reset_type, cookie, 0); |
| } |
| |
| static int psci_set_suspend_mode(struct kvm_cpu_context *host_ctxt) |
| { |
| bool osi_mode = (bool)host_ctxt->regs.regs[1]; |
| |
| return psci_call(PSCI_1_0_FN_SET_SUSPEND_MODE, osi_mode, 0, 0); |
| } |
| |
| static void psci_narrow_to_32bit(struct kvm_cpu_context *cpu_ctxt) |
| { |
| int i; |
| |
| /* |
| * Zero the input registers' upper 32 bits. They will be fully |
| * zeroed on exit, so we're fine changing them in place. |
| */ |
| for (i = 1; i < 4; i++) |
| cpu_ctxt->regs.regs[i] = lower_32_bits(cpu_ctxt->regs.regs[i]); |
| } |
| |
| static unsigned long psci_0_1_handler(struct kvm_cpu_context *host_ctxt) |
| { |
| // TODO: Need to narrow here? |
| if (func_id == kvm_host_psci_function_id[PSCI_FN_CPU_SUSPEND]) |
| return psci_cpu_suspend(host_ctxt); |
| else if (func_id == kvm_host_psci_function_id[PSCI_FN_CPU_OFF]) |
| return psci_cpu_off(host_ctxt); |
| else if (func_id == kvm_host_psci_function_id[PSCI_FN_CPU_ON]) |
| return psci_cpu_on(host_ctxt); |
| else if (func_id == kvm_host_psci_function_id[PSCI_FN_MIGRATE]) |
| return psci_migrate(host_ctxt); |
| else |
| return PSCI_RET_NOT_SUPPORTED; |
| } |
| |
| static unsigned long psci_0_2_handler(struct kvm_cpu_context *host_ctxt) |
| { |
| u64 func_id = get_psci_func_id(host_ctxt); |
| |
| if (is_psci_0_2_fn32_call(func_id)) |
| psci_narrow_to_32bit(host_ctxt); |
| |
| switch (func_id) { |
| case PSCI_0_2_FN_PSCI_VERSION: |
| return psci_version(); |
| case PSCI_0_2_FN64_CPU_SUSPEND: |
| return psci_cpu_suspend(host_ctxt); |
| case PSCI_0_2_FN_CPU_OFF: |
| return psci_cpu_off(host_ctxt); |
| case PSCI_0_2_FN64_CPU_ON: |
| return psci_cpu_on(host_ctxt); |
| case PSCI_0_2_FN64_AFFINITY_INFO: |
| return psci_affinity_info(host_ctxt); |
| case PSCI_0_2_FN64_MIGRATE: |
| return psci_migrate(host_ctxt); |
| case PSCI_0_2_FN_MIGRATE_INFO_TYPE: |
| return psci_migrate_info_type(); |
| case PSCI_0_2_FN64_MIGRATE_INFO_UP_CPU: |
| return psci_migrate_info_up_cpu(); |
| case PSCI_0_2_FN_SYSTEM_OFF: |
| psci_system_off(); |
| unreachable(); |
| case PSCI_0_2_FN_SYSTEM_RESET: |
| psci_system_reset(); |
| unreachable(); |
| default: |
| return PSCI_RET_NOT_SUPPORTED; |
| } |
| } |
| |
| static unsigned long psci_1_0_handler(struct kvm_cpu_context *host_ctxt) |
| { |
| int ret; |
| |
| ret = psci_0_2_handler(host_ctxt); |
| if (ret != PSCI_RET_NOT_SUPPORTED) |
| return ret; |
| |
| /* |
| * psci_0_2_handler already narrowed arguments of 32-bit calls, |
| * no need to do that again here. |
| */ |
| |
| switch (get_psci_func_id(host_ctxt)) { |
| case PSCI_1_0_FN_PSCI_FEATURES: |
| return psci_features(host_ctxt); |
| case PSCI_1_0_FN_SET_SUSPEND_MODE: |
| return psci_set_suspend_mode(host_ctxt); |
| case PSCI_1_1_FN64_SYSTEM_RESET2: |
| return psci_system_reset2(host_ctxt); |
| default: |
| return PSCI_RET_NOT_SUPPORTED; |
| } |
| } |
| |
| void __noreturn __host_enter(struct kvm_cpu_context *host_ctxt); |
| |
| void kvm_host_psci_cpu_init(struct kvm_cpu_context *host_ctxt) |
| { |
| struct kvm_host_psci_cpu *host_cpu = this_cpu_ptr(&kvm_psci_host_cpu); |
| |
| nvhe_spin_lock(&host_cpu->lock); |
| if (host_cpu->reset_state.reset) { |
| /* XXX - need a full wipe here? */ |
| host_ctxt->regs.regs[0] = host_cpu->reset_state.r0; |
| host_ctxt->regs.pc = host_cpu->reset_state.pc; |
| host_cpu->reset_state.reset = false; |
| write_sysreg_el2(host_ctxt->regs.pc, SYS_ELR); |
| } |
| host_cpu->power_state = KVM_HOST_CPU_POWER_ON; |
| nvhe_spin_unlock(&host_cpu->lock); |
| |
| __host_enter(host_ctxt); |
| } |
| |
| bool kvm_host_is_psci_call(struct kvm_cpu_context *host_ctxt) |
| { |
| u64 func_id = get_psci_func_id(host_ctxt); |
| |
| if (kvm_host_psci_version == PSCI_VERSION(0, 0)) |
| return false; |
| else if (kvm_host_psci_version == PSCI_VERSION(0, 1)) |
| return is_psci_0_1_call(func_id); |
| else |
| return is_psci_0_2_fn32_call(func_id) || is_psci_0_2_fn64_call(func_id); |
| } |
| |
| void kvm_host_psci_handler(struct kvm_cpu_context *host_ctxt) |
| { |
| unsigned long ret; |
| |
| if (kvm_host_psci_version == PSCI_VERSION(0, 1)) |
| ret = psci_0_1_handler(host_ctxt); |
| else if (kvm_host_psci_version == PSCI_VERSION(0, 2)) |
| ret = psci_0_2_handler(host_ctxt); |
| else if (PSCI_VERSION_MAJOR(kvm_host_psci_version) >= 1) |
| ret = psci_1_0_handler(host_ctxt); |
| else |
| ret = PSCI_RET_NOT_SUPPORTED; |
| |
| host_ctxt->regs.regs[0] = ret; |
| host_ctxt->regs.regs[1] = 0; |
| host_ctxt->regs.regs[2] = 0; |
| host_ctxt->regs.regs[3] = 0; |
| } |