Implement minimal PSCI for secondary VMs to manage their vCPUs.

Bug: 132422393
Change-Id: I44643ec9eec722dfe0332b7ffefadcdd8dd98985
diff --git a/inc/hf/api.h b/inc/hf/api.h
index 259d59e..a0151c0 100644
--- a/inc/hf/api.h
+++ b/inc/hf/api.h
@@ -40,7 +40,9 @@
 
 struct vcpu *api_preempt(struct vcpu *current);
 struct vcpu *api_wait_for_interrupt(struct vcpu *current);
+struct vcpu *api_vcpu_off(struct vcpu *current);
 struct vcpu *api_abort(struct vcpu *current);
+struct vcpu *api_wake_up(struct vcpu *current, struct vcpu *target_vcpu);
 
 int64_t api_interrupt_enable(uint32_t intid, bool enable, struct vcpu *current);
 uint32_t api_interrupt_get(struct vcpu *current);
diff --git a/inc/hf/arch/timer.h b/inc/hf/arch/timer.h
index f226020..f708f7b 100644
--- a/inc/hf/arch/timer.h
+++ b/inc/hf/arch/timer.h
@@ -58,6 +58,11 @@
 bool arch_timer_enabled_current(void);
 
 /**
+ * Disable the virtual timer for the currently active vCPU.
+ */
+void arch_timer_disable_current(void);
+
+/**
  * Returns the number of ticks remaining on the virtual timer of the currently
  * active vCPU, or 0 if it has already expired. This is undefined if the timer
  * is not enabled.
diff --git a/inc/hf/cpu.h b/inc/hf/cpu.h
index 3c5db07..d0a5466 100644
--- a/inc/hf/cpu.h
+++ b/inc/hf/cpu.h
@@ -133,7 +133,8 @@
 void vcpu_init(struct vcpu *vcpu, struct vm *vm);
 void vcpu_on(struct vcpu_locked vcpu, ipaddr_t entry, uintreg_t arg);
 size_t vcpu_index(const struct vcpu *vcpu);
-void vcpu_secondary_reset_and_start(struct vcpu *vcpu, ipaddr_t entry,
+bool vcpu_is_off(struct vcpu_locked vcpu);
+bool vcpu_secondary_reset_and_start(struct vcpu *vcpu, ipaddr_t entry,
 				    uintreg_t arg);
 
 bool vcpu_handle_page_fault(const struct vcpu *current,
diff --git a/src/api.c b/src/api.c
index c1f337c..438f6da 100644
--- a/src/api.c
+++ b/src/api.c
@@ -126,6 +126,24 @@
 }
 
 /**
+ * Puts the current vCPU in off mode, and returns to the primary VM.
+ */
+struct vcpu *api_vcpu_off(struct vcpu *current)
+{
+	struct hf_vcpu_run_return ret = {
+		.code = HF_VCPU_RUN_WAIT_FOR_INTERRUPT,
+	};
+
+	/*
+	 * Disable the timer, so the scheduler doesn't get told to call back
+	 * based on it.
+	 */
+	arch_timer_disable_current();
+
+	return api_switch_to_primary(current, ret, VCPU_STATE_OFF);
+}
+
+/**
  * Returns to the primary vm to allow this cpu to be used for other tasks as the
  * vcpu does not have work to do at this moment. The current vcpu is marked as
  * ready to be scheduled again. This SPCI function always returns SPCI_SUCCESS.
@@ -148,6 +166,20 @@
 }
 
 /**
+ * Switches to the primary so that it can switch to the target, or kick it if it
+ * is already running on a different physical CPU.
+ */
+struct vcpu *api_wake_up(struct vcpu *current, struct vcpu *target_vcpu)
+{
+	struct hf_vcpu_run_return ret = {
+		.code = HF_VCPU_RUN_WAKE_UP,
+		.wake_up.vm_id = target_vcpu->vm->id,
+		.wake_up.vcpu = vcpu_index(target_vcpu),
+	};
+	return api_switch_to_primary(current, ret, VCPU_STATE_READY);
+}
+
+/**
  * Aborts the vCPU and triggers its VM to abort fully.
  */
 struct vcpu *api_abort(struct vcpu *current)
@@ -258,7 +290,6 @@
 					 uint32_t intid, struct vcpu *current,
 					 struct vcpu **next)
 {
-	struct vm *target_vm = target_vcpu->vm;
 	uint32_t intid_index = intid / INTERRUPT_REGISTER_BITS;
 	uint32_t intid_mask = 1u << (intid % INTERRUPT_REGISTER_BITS);
 	int64_t ret = 0;
@@ -297,16 +328,7 @@
 		 */
 		ret = 1;
 	} else if (current != target_vcpu && next != NULL) {
-		/*
-		 * Switch to the primary so that it can switch to the target, or
-		 * kick it if it is already running on a different physical CPU.
-		 */
-		struct hf_vcpu_run_return ret = {
-			.code = HF_VCPU_RUN_WAKE_UP,
-			.wake_up.vm_id = target_vm->id,
-			.wake_up.vcpu = target_vcpu - target_vm->vcpus,
-		};
-		*next = api_switch_to_primary(current, ret, VCPU_STATE_READY);
+		*next = api_wake_up(current, target_vcpu);
 	}
 
 out:
diff --git a/src/arch/aarch64/hftest/power_mgmt.c b/src/arch/aarch64/hftest/power_mgmt.c
index 014c408..7970970 100644
--- a/src/arch/aarch64/hftest/power_mgmt.c
+++ b/src/arch/aarch64/hftest/power_mgmt.c
@@ -16,6 +16,7 @@
 
 #include "hf/arch/vm/power_mgmt.h"
 
+#include "hf/assert.h"
 #include "hf/spinlock.h"
 
 #include "vmapi/hf/call.h"
@@ -96,6 +97,28 @@
 	}
 }
 
+static_assert(POWER_STATUS_ON == PSCI_RETURN_ON,
+	      "power_status enum values must match PSCI return values.");
+static_assert(POWER_STATUS_OFF == PSCI_RETURN_OFF,
+	      "power_status enum values must match PSCI return values.");
+static_assert(POWER_STATUS_ON_PENDING == PSCI_RETURN_ON_PENDING,
+	      "power_status enum values must match PSCI return values.");
+
+/**
+ * Returns the power status of the given CPU.
+ */
+enum power_status cpu_status(uint64_t cpu_id)
+{
+	uint32_t lowest_affinity_level = 0;
+
+	/*
+	 * This works because the power_status enum values happen to be the same
+	 * as the PSCI_RETURN_* values. The static_asserts above validate that
+	 * this is the case.
+	 */
+	return smc(PSCI_AFFINITY_INFO, cpu_id, lowest_affinity_level, 0);
+}
+
 /**
  * Shuts down the system or exits emulation.
  */
diff --git a/src/arch/aarch64/hypervisor/BUILD.gn b/src/arch/aarch64/hypervisor/BUILD.gn
index c98b888..984adbf 100644
--- a/src/arch/aarch64/hypervisor/BUILD.gn
+++ b/src/arch/aarch64/hypervisor/BUILD.gn
@@ -25,6 +25,7 @@
   sources += [
     "handler.c",
     "offsets.c",
+    "psci_handler.c",
   ]
 
   deps = [
diff --git a/src/arch/aarch64/hypervisor/handler.c b/src/arch/aarch64/hypervisor/handler.c
index 7de6ad3..7943401 100644
--- a/src/arch/aarch64/hypervisor/handler.c
+++ b/src/arch/aarch64/hypervisor/handler.c
@@ -30,6 +30,7 @@
 
 #include "msr.h"
 #include "psci.h"
+#include "psci_handler.h"
 #include "smc.h"
 
 #define HCR_EL2_VI (1u << 7)
@@ -39,32 +40,6 @@
 	struct vcpu *new;
 };
 
-void cpu_entry(struct cpu *c);
-
-static uint32_t el3_psci_version = 0;
-
-/* Performs arch specific boot time initialisation. */
-void arch_one_time_init(void)
-{
-	el3_psci_version = smc(PSCI_VERSION, 0, 0, 0);
-
-	/* Check there's nothing unexpected about PSCI. */
-	switch (el3_psci_version) {
-	case PSCI_VERSION_0_2:
-	case PSCI_VERSION_1_0:
-	case PSCI_VERSION_1_1:
-		/* Supported EL3 PSCI version. */
-		dlog("Found PSCI version: 0x%x\n", el3_psci_version);
-		break;
-
-	default:
-		/* Unsupported EL3 PSCI version. Log a warning but continue. */
-		dlog("Warning: unknown PSCI version: 0x%x\n", el3_psci_version);
-		el3_psci_version = 0;
-		break;
-	}
-}
-
 /* Gets a reference to the currently executing vCPU. */
 static struct vcpu *current(void)
 {
@@ -229,176 +204,6 @@
 }
 
 /**
- * Handles PSCI requests received via HVC or SMC instructions from the primary
- * VM only.
- *
- * A minimal PSCI 1.1 interface is offered which can make use of previous
- * version of PSCI in EL3 by acting as an adapter.
- *
- * Returns true if the request was a PSCI one, false otherwise.
- */
-static bool psci_handler(uint32_t func, uintreg_t arg0, uintreg_t arg1,
-			 uintreg_t arg2, int32_t *ret)
-{
-	struct cpu *c;
-
-	/*
-	 * If there's a problem with the EL3 PSCI, block standard secure service
-	 * calls by marking them as unknown. Other calls will be allowed to pass
-	 * through.
-	 *
-	 * This blocks more calls than just PSCI so it may need to be made more
-	 * lenient in future.
-	 */
-	if (el3_psci_version == 0) {
-		*ret = SMCCC_RETURN_UNKNOWN;
-		return (func & SMCCC_SERVICE_CALL_MASK) ==
-		       SMCCC_STANDARD_SECURE_SERVICE_CALL;
-	}
-
-	switch (func & ~SMCCC_CONVENTION_MASK) {
-	case PSCI_VERSION:
-		*ret = PSCI_VERSION_1_1;
-		break;
-
-	case PSCI_FEATURES:
-		switch (arg0 & ~SMCCC_CONVENTION_MASK) {
-		case PSCI_CPU_SUSPEND:
-			if (el3_psci_version == PSCI_VERSION_0_2) {
-				/*
-				 * PSCI 0.2 doesn't support PSCI_FEATURES so
-				 * report PSCI 0.2 compatible features.
-				 */
-				*ret = 0;
-			} else {
-				/* PSCI 1.x only defines two feature bits. */
-				*ret = smc(func, arg0, 0, 0) & 0x3;
-			}
-			break;
-
-		case PSCI_VERSION:
-		case PSCI_FEATURES:
-		case PSCI_SYSTEM_OFF:
-		case PSCI_SYSTEM_RESET:
-		case PSCI_AFFINITY_INFO:
-		case PSCI_CPU_OFF:
-		case PSCI_CPU_ON:
-			/* These are supported without special features. */
-			*ret = 0;
-			break;
-
-		default:
-			/* Everything else is unsupported. */
-			*ret = PSCI_RETURN_NOT_SUPPORTED;
-			break;
-		}
-		break;
-
-	case PSCI_SYSTEM_OFF:
-		smc(PSCI_SYSTEM_OFF, 0, 0, 0);
-		panic("System off failed");
-		break;
-
-	case PSCI_SYSTEM_RESET:
-		smc(PSCI_SYSTEM_RESET, 0, 0, 0);
-		panic("System reset failed");
-		break;
-
-	case PSCI_AFFINITY_INFO:
-		c = cpu_find(arg0);
-		if (!c) {
-			*ret = PSCI_RETURN_INVALID_PARAMETERS;
-			break;
-		}
-
-		if (arg1 != 0) {
-			*ret = PSCI_RETURN_NOT_SUPPORTED;
-			break;
-		}
-
-		sl_lock(&c->lock);
-		if (c->is_on) {
-			*ret = 0; /* ON */
-		} else {
-			*ret = 1; /* OFF */
-		}
-		sl_unlock(&c->lock);
-		break;
-
-	case PSCI_CPU_SUSPEND: {
-		/*
-		 * Update vcpu state to wake from the provided entry point but
-		 * if suspend returns, for example because it failed or was a
-		 * standby power state, the SMC will return and the updated
-		 * vcpu registers will be ignored.
-		 */
-		struct vcpu *vcpu = current();
-
-		arch_regs_set_pc_arg(&vcpu->regs, ipa_init(arg1), arg2);
-		*ret = smc(PSCI_CPU_SUSPEND | SMCCC_64_BIT, arg0,
-			   (uintreg_t)&cpu_entry, (uintreg_t)vcpu->cpu);
-		break;
-	}
-
-	case PSCI_CPU_OFF:
-		cpu_off(current()->cpu);
-		smc(PSCI_CPU_OFF, 0, 0, 0);
-		panic("CPU off failed");
-		break;
-
-	case PSCI_CPU_ON:
-		c = cpu_find(arg0);
-		if (!c) {
-			*ret = PSCI_RETURN_INVALID_PARAMETERS;
-			break;
-		}
-
-		if (cpu_on(c, ipa_init(arg1), arg2)) {
-			*ret = PSCI_RETURN_ALREADY_ON;
-			break;
-		}
-
-		/*
-		 * There's a race when turning a CPU on when it's in the
-		 * process of turning off. We need to loop here while it is
-		 * reported that the CPU is on (because it's about to turn
-		 * itself off).
-		 */
-		do {
-			*ret = smc(PSCI_CPU_ON | SMCCC_64_BIT, arg0,
-				   (uintreg_t)&cpu_entry, (uintreg_t)c);
-		} while (*ret == PSCI_RETURN_ALREADY_ON);
-
-		if (*ret != PSCI_RETURN_SUCCESS) {
-			cpu_off(c);
-		}
-		break;
-
-	case PSCI_MIGRATE:
-	case PSCI_MIGRATE_INFO_TYPE:
-	case PSCI_MIGRATE_INFO_UP_CPU:
-	case PSCI_CPU_FREEZE:
-	case PSCI_CPU_DEFAULT_SUSPEND:
-	case PSCI_NODE_HW_STATE:
-	case PSCI_SYSTEM_SUSPEND:
-	case PSCI_SET_SYSPEND_MODE:
-	case PSCI_STAT_RESIDENCY:
-	case PSCI_STAT_COUNT:
-	case PSCI_SYSTEM_RESET2:
-	case PSCI_MEM_PROTECT:
-	case PSCI_MEM_PROTECT_CHECK_RANGE:
-		/* Block all other known PSCI calls. */
-		*ret = PSCI_RETURN_NOT_SUPPORTED;
-		break;
-
-	default:
-		return false;
-	}
-
-	return true;
-}
-
-/**
  * Sets or clears the VI bit in the HCR_EL2 register saved in the given
  * arch_regs.
  */
@@ -433,13 +238,9 @@
 
 	ret.new = NULL;
 
-	if (current()->vm->id == HF_PRIMARY_VM_ID) {
-		int32_t psci_ret;
-
-		if (psci_handler(arg0, arg1, arg2, arg3, &psci_ret)) {
-			ret.user_ret = psci_ret;
-			return ret;
-		}
+	if (psci_handler(current(), arg0, arg1, arg2, arg3, &ret.user_ret,
+			 &ret.new)) {
+		return ret;
 	}
 
 	switch ((uint32_t)arg0) {
@@ -633,19 +434,20 @@
 
 	case 0x17: /* EC = 010111, SMC instruction. */ {
 		uintreg_t smc_pc = vcpu->regs.pc;
-		int32_t ret;
+		uintreg_t ret;
+		struct vcpu *next = NULL;
 
-		if (vcpu->vm->id != HF_PRIMARY_VM_ID ||
-		    !psci_handler(vcpu->regs.r[0], vcpu->regs.r[1],
-				  vcpu->regs.r[2], vcpu->regs.r[3], &ret)) {
+		if (!psci_handler(vcpu, vcpu->regs.r[0], vcpu->regs.r[1],
+				  vcpu->regs.r[2], vcpu->regs.r[3], &ret,
+				  &next)) {
 			dlog("Unsupported SMC call: 0x%x\n", vcpu->regs.r[0]);
-			ret = PSCI_RETURN_NOT_SUPPORTED;
+			ret = PSCI_ERROR_NOT_SUPPORTED;
 		}
 
 		/* Skip the SMC instruction. */
 		vcpu->regs.pc = smc_pc + (esr & (1u << 25) ? 4 : 2);
 		vcpu->regs.r[0] = ret;
-		return NULL;
+		return next;
 	}
 
 	default:
diff --git a/src/arch/aarch64/hypervisor/psci_handler.c b/src/arch/aarch64/hypervisor/psci_handler.c
new file mode 100644
index 0000000..76e4798
--- /dev/null
+++ b/src/arch/aarch64/hypervisor/psci_handler.c
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2019 The Hafnium Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "psci_handler.h"
+
+#include <stdint.h>
+
+#include "hf/arch/types.h"
+
+#include "hf/api.h"
+#include "hf/cpu.h"
+#include "hf/dlog.h"
+#include "hf/panic.h"
+#include "hf/vm.h"
+
+#include "psci.h"
+#include "smc.h"
+
+static uint32_t el3_psci_version;
+
+void cpu_entry(struct cpu *c);
+
+/* Performs arch specific boot time initialisation. */
+void arch_one_time_init(void)
+{
+	el3_psci_version = smc(PSCI_VERSION, 0, 0, 0);
+
+	/* Check there's nothing unexpected about PSCI. */
+	switch (el3_psci_version) {
+	case PSCI_VERSION_0_2:
+	case PSCI_VERSION_1_0:
+	case PSCI_VERSION_1_1:
+		/* Supported EL3 PSCI version. */
+		dlog("Found PSCI version: 0x%x\n", el3_psci_version);
+		break;
+
+	default:
+		/* Unsupported EL3 PSCI version. Log a warning but continue. */
+		dlog("Warning: unknown PSCI version: 0x%x\n", el3_psci_version);
+		el3_psci_version = 0;
+		break;
+	}
+}
+
+/**
+ * Handles PSCI requests received via HVC or SMC instructions from the primary
+ * VM.
+ *
+ * A minimal PSCI 1.1 interface is offered which can make use of the
+ * implementation of PSCI in EL3 by acting as an adapter.
+ *
+ * Returns true if the request was a PSCI one, false otherwise.
+ */
+bool psci_primary_vm_handler(struct vcpu *vcpu, uint32_t func, uintreg_t arg0,
+			     uintreg_t arg1, uintreg_t arg2, uintreg_t *ret)
+{
+	struct cpu *c;
+
+	/*
+	 * If there's a problem with the EL3 PSCI, block standard secure service
+	 * calls by marking them as unknown. Other calls will be allowed to pass
+	 * through.
+	 *
+	 * This blocks more calls than just PSCI so it may need to be made more
+	 * lenient in future.
+	 */
+	if (el3_psci_version == 0) {
+		*ret = SMCCC_ERROR_UNKNOWN;
+		return (func & SMCCC_SERVICE_CALL_MASK) ==
+		       SMCCC_STANDARD_SECURE_SERVICE_CALL;
+	}
+
+	switch (func & ~SMCCC_CONVENTION_MASK) {
+	case PSCI_VERSION:
+		*ret = PSCI_VERSION_1_1;
+		break;
+
+	case PSCI_FEATURES:
+		switch (arg0 & ~SMCCC_CONVENTION_MASK) {
+		case PSCI_CPU_SUSPEND:
+			if (el3_psci_version == PSCI_VERSION_0_2) {
+				/*
+				 * PSCI 0.2 doesn't support PSCI_FEATURES so
+				 * report PSCI 0.2 compatible features.
+				 */
+				*ret = 0;
+			} else {
+				/* PSCI 1.x only defines two feature bits. */
+				*ret = smc(func, arg0, 0, 0) & 0x3;
+			}
+			break;
+
+		case PSCI_VERSION:
+		case PSCI_FEATURES:
+		case PSCI_SYSTEM_OFF:
+		case PSCI_SYSTEM_RESET:
+		case PSCI_AFFINITY_INFO:
+		case PSCI_CPU_OFF:
+		case PSCI_CPU_ON:
+			/* These are supported without special features. */
+			*ret = 0;
+			break;
+
+		default:
+			/* Everything else is unsupported. */
+			*ret = PSCI_ERROR_NOT_SUPPORTED;
+			break;
+		}
+		break;
+
+	case PSCI_SYSTEM_OFF:
+		smc(PSCI_SYSTEM_OFF, 0, 0, 0);
+		panic("System off failed");
+		break;
+
+	case PSCI_SYSTEM_RESET:
+		smc(PSCI_SYSTEM_RESET, 0, 0, 0);
+		panic("System reset failed");
+		break;
+
+	case PSCI_AFFINITY_INFO:
+		c = cpu_find(arg0);
+		if (!c) {
+			*ret = PSCI_ERROR_INVALID_PARAMETERS;
+			break;
+		}
+
+		if (arg1 != 0) {
+			*ret = PSCI_ERROR_NOT_SUPPORTED;
+			break;
+		}
+
+		sl_lock(&c->lock);
+		if (c->is_on) {
+			*ret = PSCI_RETURN_ON;
+		} else {
+			*ret = PSCI_RETURN_OFF;
+		}
+		sl_unlock(&c->lock);
+		break;
+
+	case PSCI_CPU_SUSPEND: {
+		/*
+		 * Update vcpu state to wake from the provided entry point but
+		 * if suspend returns, for example because it failed or was a
+		 * standby power state, the SMC will return and the updated
+		 * vcpu registers will be ignored.
+		 */
+		arch_regs_set_pc_arg(&vcpu->regs, ipa_init(arg1), arg2);
+		*ret = smc(PSCI_CPU_SUSPEND | SMCCC_64_BIT, arg0,
+			   (uintreg_t)&cpu_entry, (uintreg_t)vcpu->cpu);
+		break;
+	}
+
+	case PSCI_CPU_OFF:
+		cpu_off(vcpu->cpu);
+		smc(PSCI_CPU_OFF, 0, 0, 0);
+		panic("CPU off failed");
+		break;
+
+	case PSCI_CPU_ON:
+		c = cpu_find(arg0);
+		if (!c) {
+			*ret = PSCI_ERROR_INVALID_PARAMETERS;
+			break;
+		}
+
+		if (cpu_on(c, ipa_init(arg1), arg2)) {
+			*ret = PSCI_ERROR_ALREADY_ON;
+			break;
+		}
+
+		/*
+		 * There's a race when turning a CPU on when it's in the
+		 * process of turning off. We need to loop here while it is
+		 * reported that the CPU is on (because it's about to turn
+		 * itself off).
+		 */
+		do {
+			*ret = smc(PSCI_CPU_ON | SMCCC_64_BIT, arg0,
+				   (uintreg_t)&cpu_entry, (uintreg_t)c);
+		} while (*ret == PSCI_ERROR_ALREADY_ON);
+
+		if (*ret != PSCI_RETURN_SUCCESS) {
+			cpu_off(c);
+		}
+		break;
+
+	case PSCI_MIGRATE:
+	case PSCI_MIGRATE_INFO_TYPE:
+	case PSCI_MIGRATE_INFO_UP_CPU:
+	case PSCI_CPU_FREEZE:
+	case PSCI_CPU_DEFAULT_SUSPEND:
+	case PSCI_NODE_HW_STATE:
+	case PSCI_SYSTEM_SUSPEND:
+	case PSCI_SET_SYSPEND_MODE:
+	case PSCI_STAT_RESIDENCY:
+	case PSCI_STAT_COUNT:
+	case PSCI_SYSTEM_RESET2:
+	case PSCI_MEM_PROTECT:
+	case PSCI_MEM_PROTECT_CHECK_RANGE:
+		/* Block all other known PSCI calls. */
+		*ret = PSCI_ERROR_NOT_SUPPORTED;
+		break;
+
+	default:
+		return false;
+	}
+
+	return true;
+}
+
+/**
+ * Convert a PSCI CPU / affinity ID for a secondary VM to the corresponding vCPU
+ * index.
+ */
+uint32_t vcpu_id_to_index(uint64_t vcpu_id)
+{
+	/* For now we use indices as IDs for the purposes of PSCI. */
+	return vcpu_id;
+}
+
+/**
+ * Handles PSCI requests received via HVC or SMC instructions from a secondary
+ * VM.
+ *
+ * A minimal PSCI 1.1 interface is offered which can start and stop vCPUs in
+ * collaboration with the scheduler in the primary VM.
+ *
+ * Returns true if the request was a PSCI one, false otherwise.
+ */
+bool psci_secondary_vm_handler(struct vcpu *vcpu, uint32_t func, uintreg_t arg0,
+			       uintreg_t arg1, uintreg_t arg2, uintreg_t *ret,
+			       struct vcpu **next)
+{
+	switch (func & ~SMCCC_CONVENTION_MASK) {
+	case PSCI_VERSION:
+		*ret = PSCI_VERSION_1_1;
+		break;
+
+	case PSCI_FEATURES:
+		switch (arg0 & ~SMCCC_CONVENTION_MASK) {
+		case PSCI_CPU_SUSPEND:
+			/*
+			 * Does not offer OS-initiated mode but does use
+			 * extended StateID Format.
+			 */
+			*ret = 0x2;
+			break;
+
+		case PSCI_VERSION:
+		case PSCI_FEATURES:
+		case PSCI_AFFINITY_INFO:
+		case PSCI_CPU_OFF:
+		case PSCI_CPU_ON:
+			/* These are supported without special features. */
+			*ret = 0;
+			break;
+
+		default:
+			/* Everything else is unsupported. */
+			*ret = PSCI_ERROR_NOT_SUPPORTED;
+			break;
+		}
+		break;
+
+	case PSCI_AFFINITY_INFO: {
+		uint64_t target_affinity = arg0;
+		uint32_t lowest_affinity_level = arg1;
+		struct vm *vm = vcpu->vm;
+		struct vcpu_locked target_vcpu;
+		uint32_t target_vcpu_index = vcpu_id_to_index(target_affinity);
+
+		if (lowest_affinity_level != 0) {
+			/* Affinity levels greater than 0 not supported. */
+			*ret = PSCI_ERROR_INVALID_PARAMETERS;
+			break;
+		}
+
+		if (target_vcpu_index >= vm->vcpu_count) {
+			*ret = PSCI_ERROR_INVALID_PARAMETERS;
+			break;
+		}
+
+		target_vcpu = vcpu_lock(vm_get_vcpu(vm, target_vcpu_index));
+		*ret = vcpu_is_off(target_vcpu) ? PSCI_RETURN_OFF
+						: PSCI_RETURN_ON;
+		vcpu_unlock(&target_vcpu);
+		break;
+	}
+
+	case PSCI_CPU_SUSPEND: {
+		/*
+		 * Downgrade suspend request to WFI and return SUCCESS, as
+		 * allowed by the specification.
+		 */
+		*next = api_wait_for_interrupt(vcpu);
+		*ret = PSCI_RETURN_SUCCESS;
+		break;
+	}
+
+	case PSCI_CPU_OFF:
+		/*
+		 * Should never return to the caller, but in case it somehow
+		 * does.
+		 */
+		*ret = PSCI_ERROR_DENIED;
+		/* Tell the scheduler not to run the vCPU again. */
+		*next = api_vcpu_off(vcpu);
+		break;
+
+	case PSCI_CPU_ON: {
+		/* Parameter names as per PSCI specification. */
+		uint64_t target_cpu = arg0;
+		ipaddr_t entry_point_address = ipa_init(arg1);
+		uint64_t context_id = arg2;
+		uint32_t target_vcpu_index = vcpu_id_to_index(target_cpu);
+		struct vm *vm = vcpu->vm;
+		struct vcpu *target_vcpu;
+
+		if (target_vcpu_index >= vm->vcpu_count) {
+			*ret = PSCI_ERROR_INVALID_PARAMETERS;
+			break;
+		}
+
+		target_vcpu = vm_get_vcpu(vm, target_vcpu_index);
+
+		if (vcpu_secondary_reset_and_start(
+			    target_vcpu, entry_point_address, context_id)) {
+			/*
+			 * Tell the scheduler that it can start running the new
+			 * vCPU now.
+			 */
+			*next = api_wake_up(vcpu, target_vcpu);
+			*ret = PSCI_RETURN_SUCCESS;
+		} else {
+			*ret = PSCI_ERROR_ALREADY_ON;
+		}
+
+		break;
+	}
+
+	case PSCI_SYSTEM_OFF:
+	case PSCI_SYSTEM_RESET:
+	case PSCI_MIGRATE:
+	case PSCI_MIGRATE_INFO_TYPE:
+	case PSCI_MIGRATE_INFO_UP_CPU:
+	case PSCI_CPU_FREEZE:
+	case PSCI_CPU_DEFAULT_SUSPEND:
+	case PSCI_NODE_HW_STATE:
+	case PSCI_SYSTEM_SUSPEND:
+	case PSCI_SET_SYSPEND_MODE:
+	case PSCI_STAT_RESIDENCY:
+	case PSCI_STAT_COUNT:
+	case PSCI_SYSTEM_RESET2:
+	case PSCI_MEM_PROTECT:
+	case PSCI_MEM_PROTECT_CHECK_RANGE:
+		/* Block all other known PSCI calls. */
+		*ret = PSCI_ERROR_NOT_SUPPORTED;
+		break;
+
+	default:
+		return false;
+	}
+
+	return true;
+}
+
+/**
+ * Handles PSCI requests received via HVC or SMC instructions from a VM.
+ * Requests from primary and secondary VMs are dealt with differently.
+ *
+ * Returns true if the request was a PSCI one, false otherwise.
+ */
+bool psci_handler(struct vcpu *vcpu, uint32_t func, uintreg_t arg0,
+		  uintreg_t arg1, uintreg_t arg2, uintreg_t *ret,
+		  struct vcpu **next)
+{
+	if (vcpu->vm->id == HF_PRIMARY_VM_ID) {
+		return psci_primary_vm_handler(vcpu, func, arg0, arg1, arg2,
+					       ret);
+	}
+	return psci_secondary_vm_handler(vcpu, func, arg0, arg1, arg2, ret,
+					 next);
+}
diff --git a/src/arch/aarch64/hypervisor/psci_handler.h b/src/arch/aarch64/hypervisor/psci_handler.h
new file mode 100644
index 0000000..479c1cd
--- /dev/null
+++ b/src/arch/aarch64/hypervisor/psci_handler.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018 The Hafnium Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include "hf/arch/types.h"
+
+#include "hf/cpu.h"
+
+bool psci_handler(struct vcpu *vcpu, uint32_t func, uintreg_t arg0,
+		  uintreg_t arg1, uintreg_t arg2, uintreg_t *ret,
+		  struct vcpu **next);
diff --git a/src/arch/aarch64/inc/hf/arch/vm/power_mgmt.h b/src/arch/aarch64/inc/hf/arch/vm/power_mgmt.h
index dfd4198..956091f 100644
--- a/src/arch/aarch64/inc/hf/arch/vm/power_mgmt.h
+++ b/src/arch/aarch64/inc/hf/arch/vm/power_mgmt.h
@@ -21,9 +21,16 @@
 #include <stdint.h>
 #include <stdnoreturn.h>
 
+enum power_status {
+	POWER_STATUS_ON,
+	POWER_STATUS_OFF,
+	POWER_STATUS_ON_PENDING,
+};
+
 noreturn void arch_power_off(void);
 
 bool cpu_start(uintptr_t id, void *stack, size_t stack_size,
 	       void (*entry)(uintptr_t arg), uintptr_t arg);
 
 noreturn void cpu_stop(void);
+enum power_status cpu_status(uint64_t cpu_id);
diff --git a/src/arch/aarch64/psci.h b/src/arch/aarch64/psci.h
index 041ac44..eef3fc0 100644
--- a/src/arch/aarch64/psci.h
+++ b/src/arch/aarch64/psci.h
@@ -39,7 +39,7 @@
  * TODO: Trusted OS call: 0x32000000 - 0x3f000000
  */
 
-#define SMCCC_RETURN_UNKNOWN  (-1)
+#define SMCCC_ERROR_UNKNOWN  (-1)
 
 /* The following are PSCI version codes. */
 #define PSCI_VERSION_0_2 0x00000002
@@ -70,15 +70,18 @@
 #define PSCI_MEM_PROTECT_CHECK_RANGE 0x84000014
 
 /* The following are return codes for PSCI. */
+#define PSCI_RETURN_ON_PENDING         2
+#define PSCI_RETURN_OFF                1
+#define PSCI_RETURN_ON                 0
 #define PSCI_RETURN_SUCCESS            0
-#define PSCI_RETURN_NOT_SUPPORTED      SMCCC_RETURN_UNKNOWN
-#define PSCI_RETURN_INVALID_PARAMETERS (-2)
-#define PSCI_RETURN_DENIED             (-3)
-#define PSCI_RETURN_ALREADY_ON         (-4)
-#define PSCI_RETURN_ON_PENDING         (-5)
-#define PSCI_RETURN_INTERNAL_FAILURE   (-6)
-#define PSCI_NOT_PRESENT               (-7)
-#define PSCI_DISABLE                   (-8)
-#define PSCI_INVALID_ADDRESS           (-9)
+#define PSCI_ERROR_NOT_SUPPORTED       SMCCC_ERROR_UNKNOWN
+#define PSCI_ERROR_INVALID_PARAMETERS  (-2)
+#define PSCI_ERROR_DENIED              (-3)
+#define PSCI_ERROR_ALREADY_ON          (-4)
+#define PSCI_ERROR_ON_PENDING          (-5)
+#define PSCI_ERROR_INTERNAL_FAILURE    (-6)
+#define PSCI_ERROR_NOT_PRESENT         (-7)
+#define PSCI_ERROR_DISABLE             (-8)
+#define PSCI_ERROR_INVALID_ADDRESS     (-9)
 
 /* clang-format on */
diff --git a/src/arch/aarch64/timer.c b/src/arch/aarch64/timer.c
index 12f4132..90b9ca8 100644
--- a/src/arch/aarch64/timer.c
+++ b/src/arch/aarch64/timer.c
@@ -129,6 +129,14 @@
 }
 
 /**
+ * Disables the virtual timer for the currently active vCPU.
+ */
+void arch_timer_disable_current(void)
+{
+	write_msr(cntv_ctl_el0, 0x0);
+}
+
+/**
  * Returns the number of ticks remaining on the virtual timer of the currently
  * active vCPU, or 0 if it has already expired. This is undefined if the timer
  * is not enabled.
diff --git a/src/arch/fake/timer.c b/src/arch/fake/timer.c
index ba1294a..1c0d449 100644
--- a/src/arch/fake/timer.c
+++ b/src/arch/fake/timer.c
@@ -54,6 +54,11 @@
 	return false;
 }
 
+void arch_timer_disable_current(void)
+{
+	/* TODO */
+}
+
 uint64_t arch_timer_remaining_ns_current(void)
 {
 	/* TODO */
diff --git a/src/cpu.c b/src/cpu.c
index e35b1ec..affba4a 100644
--- a/src/cpu.c
+++ b/src/cpu.c
@@ -209,27 +209,62 @@
 }
 
 /**
- * Starts a vCPU of a secondary VM.
+ * Check whether the given vcpu_state is an off state, for the purpose of
+ * turning vCPUs on and off. Note that aborted still counts as on in this
+ * context.
  */
-void vcpu_secondary_reset_and_start(struct vcpu *vcpu, ipaddr_t entry,
+bool vcpu_is_off(struct vcpu_locked vcpu)
+{
+	switch (vcpu.vcpu->state) {
+	case VCPU_STATE_OFF:
+		return true;
+	case VCPU_STATE_READY:
+	case VCPU_STATE_RUNNING:
+	case VCPU_STATE_BLOCKED_MAILBOX:
+	case VCPU_STATE_BLOCKED_INTERRUPT:
+	case VCPU_STATE_ABORTED:
+		/*
+		 * Aborted still counts as ON for the purposes of PSCI,
+		 * because according to the PSCI specification (section
+		 * 5.7.1) a core is only considered to be off if it has
+		 * been turned off with a CPU_OFF call or hasn't yet
+		 * been turned on with a CPU_ON call.
+		 */
+		return false;
+	}
+}
+
+/**
+ * Starts a vCPU of a secondary VM.
+ *
+ * Returns true if the secondary was reset and started, or false if it was
+ * already on and so nothing was done.
+ */
+bool vcpu_secondary_reset_and_start(struct vcpu *vcpu, ipaddr_t entry,
 				    uintreg_t arg)
 {
 	struct vcpu_locked vcpu_locked;
 	struct vm *vm = vcpu->vm;
+	bool vcpu_was_off;
 
 	assert(vm->id != HF_PRIMARY_VM_ID);
 
 	vcpu_locked = vcpu_lock(vcpu);
-	/*
-	 * Set vCPU registers to a clean state ready for boot. As this is a
-	 * secondary which can migrate between pCPUs, the ID of the vCPU is
-	 * defined as the index and does not match the ID of the pCPU it is
-	 * running on.
-	 */
-	arch_regs_reset(&vcpu->regs, false, vm->id, vcpu_index(vcpu),
-			vm->ptable.root);
-	vcpu_on(vcpu_locked, entry, arg);
+	vcpu_was_off = vcpu_is_off(vcpu_locked);
+	if (vcpu_was_off) {
+		/*
+		 * Set vCPU registers to a clean state ready for boot. As this
+		 * is a secondary which can migrate between pCPUs, the ID of the
+		 * vCPU is defined as the index and does not match the ID of the
+		 * pCPU it is running on.
+		 */
+		arch_regs_reset(&vcpu->regs, false, vm->id, vcpu_index(vcpu),
+				vm->ptable.root);
+		vcpu_on(vcpu_locked, entry, arg);
+	}
 	vcpu_unlock(&vcpu_locked);
+
+	return vcpu_was_off;
 }
 
 /**
diff --git a/test/vmapi/primary_with_secondaries/BUILD.gn b/test/vmapi/primary_with_secondaries/BUILD.gn
index 15b190c..d24570f 100644
--- a/test/vmapi/primary_with_secondaries/BUILD.gn
+++ b/test/vmapi/primary_with_secondaries/BUILD.gn
@@ -32,6 +32,7 @@
     "memory_sharing.c",
     "no_services.c",
     "run_race.c",
+    "smp.c",
     "spci.c",
   ]
 
@@ -60,5 +61,11 @@
       "services1",
       "services:service_vm1",
     ],
+    [
+      "1048576",
+      "2",
+      "services2",
+      "services:service_vm2",
+    ],
   ]
 }
diff --git a/test/vmapi/primary_with_secondaries/inc/primary_with_secondary.h b/test/vmapi/primary_with_secondaries/inc/primary_with_secondary.h
index b2498f7..a8ff882 100644
--- a/test/vmapi/primary_with_secondaries/inc/primary_with_secondary.h
+++ b/test/vmapi/primary_with_secondaries/inc/primary_with_secondary.h
@@ -18,6 +18,7 @@
 
 #define SERVICE_VM0 1
 #define SERVICE_VM1 2
+#define SERVICE_VM2 3
 
 #define SELF_INTERRUPT_ID 5
 #define EXTERNAL_INTERRUPT_ID_A 7
diff --git a/test/vmapi/primary_with_secondaries/no_services.c b/test/vmapi/primary_with_secondaries/no_services.c
index 98687ec..9f190e0 100644
--- a/test/vmapi/primary_with_secondaries/no_services.c
+++ b/test/vmapi/primary_with_secondaries/no_services.c
@@ -43,11 +43,11 @@
 }
 
 /**
- * Confirm there are 2 secondary VMs as well as this primary VM.
+ * Confirm there are 3 secondary VMs as well as this primary VM.
  */
-TEST(hf_vm_get_count, four_secondary_vms)
+TEST(hf_vm_get_count, three_secondary_vms)
 {
-	EXPECT_EQ(hf_vm_get_count(), 3);
+	EXPECT_EQ(hf_vm_get_count(), 4);
 }
 
 /**
diff --git a/test/vmapi/primary_with_secondaries/services/BUILD.gn b/test/vmapi/primary_with_secondaries/services/BUILD.gn
index e8382bf..073494d 100644
--- a/test/vmapi/primary_with_secondaries/services/BUILD.gn
+++ b/test/vmapi/primary_with_secondaries/services/BUILD.gn
@@ -140,6 +140,18 @@
   ]
 }
 
+# Service to start a second vCPU and send messages from both.
+source_set("smp") {
+  testonly = true
+  public_configs = [
+    "..:config",
+    "//test/hftest:hftest_config",
+  ]
+  sources = [
+    "smp.c",
+  ]
+}
+
 # Service to check that WFI is a no-op when there are pending interrupts.
 source_set("wfi") {
   testonly = true
@@ -195,3 +207,12 @@
     "//test/hftest:hftest_secondary_vm",
   ]
 }
+
+vm_kernel("service_vm2") {
+  testonly = true
+
+  deps = [
+    ":smp",
+    "//test/hftest:hftest_secondary_vm",
+  ]
+}
diff --git a/test/vmapi/primary_with_secondaries/services/smp.c b/test/vmapi/primary_with_secondaries/services/smp.c
new file mode 100644
index 0000000..863919d
--- /dev/null
+++ b/test/vmapi/primary_with_secondaries/services/smp.c
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2019 The Hafnium Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdalign.h>
+#include <stdint.h>
+
+#include "hf/arch/cpu.h"
+#include "hf/arch/vm/power_mgmt.h"
+
+#include "hf/dlog.h"
+#include "hf/std.h"
+
+#include "vmapi/hf/call.h"
+#include "vmapi/hf/spci.h"
+
+#include "../psci.h"
+#include "hftest.h"
+#include "primary_with_secondary.h"
+
+#define ARG_VALUE 42
+
+/*
+ * Secondary VM that starts a second vCPU and then sends messages from both.
+ */
+
+alignas(4096) static char stack[4096];
+
+/** Send a message back to the primary. */
+void send_message(const char *message, uint32_t size)
+{
+	memcpy_s(SERVICE_SEND_BUFFER()->payload, SPCI_MSG_PAYLOAD_MAX, message,
+		 size);
+	spci_message_init(SERVICE_SEND_BUFFER(), size, HF_PRIMARY_VM_ID,
+			  hf_vm_get_id());
+
+	ASSERT_EQ(spci_msg_send(0), SPCI_SUCCESS);
+}
+
+/**
+ * Entry point of the second vCPU.
+ */
+static void vm_cpu_entry(uintptr_t arg)
+{
+	ASSERT_EQ(arg, ARG_VALUE);
+
+	/* Check that vCPU statuses are as expected. */
+	ASSERT_EQ(cpu_status(0), POWER_STATUS_ON);
+	ASSERT_EQ(cpu_status(1), POWER_STATUS_ON);
+
+	dlog("Secondary second vCPU started.\n");
+	send_message("vCPU 1", sizeof("vCPU 1"));
+	dlog("Secondary second vCPU finishing\n");
+}
+
+TEST_SERVICE(smp)
+{
+	/* Check that vCPU statuses are as expected. */
+	ASSERT_EQ(cpu_status(0), POWER_STATUS_ON);
+	ASSERT_EQ(cpu_status(1), POWER_STATUS_OFF);
+
+	/* Start second vCPU. */
+	dlog("Secondary starting second vCPU.\n");
+	ASSERT_TRUE(
+		cpu_start(1, stack, sizeof(stack), vm_cpu_entry, ARG_VALUE));
+	dlog("Secondary started second vCPU.\n");
+
+	/* Check that vCPU statuses are as expected. */
+	ASSERT_EQ(cpu_status(0), POWER_STATUS_ON);
+	ASSERT_EQ(cpu_status(1), POWER_STATUS_ON);
+
+	send_message("vCPU 0", sizeof("vCPU 0"));
+}
diff --git a/test/vmapi/primary_with_secondaries/smp.c b/test/vmapi/primary_with_secondaries/smp.c
new file mode 100644
index 0000000..64e0739
--- /dev/null
+++ b/test/vmapi/primary_with_secondaries/smp.c
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2019 The Hafnium Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdint.h>
+
+#include "hf/std.h"
+
+#include "vmapi/hf/call.h"
+
+#include "hftest.h"
+#include "primary_with_secondary.h"
+#include "util.h"
+
+/**
+ * Run a service that starts a second vCPU, and check that both the first and
+ * second vCPU send messages to us.
+ */
+TEST(smp, two_vcpus)
+{
+	const char expected_response_0[] = "vCPU 0";
+	const char expected_response_1[] = "vCPU 1";
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM2, "smp", mb.send);
+
+	/* Let the first vCPU start the second vCPU. */
+	run_res = hf_vcpu_run(SERVICE_VM2, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAKE_UP);
+	EXPECT_EQ(run_res.wake_up.vm_id, SERVICE_VM2);
+	EXPECT_EQ(run_res.wake_up.vcpu, 1);
+
+	/* Run the second vCPU and wait for a message. */
+	dlog("Run second vCPU for message\n");
+	run_res = hf_vcpu_run(SERVICE_VM2, 1);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+	EXPECT_EQ(mb.recv->length, sizeof(expected_response_1));
+	EXPECT_EQ(memcmp(mb.recv->payload, expected_response_1,
+			 sizeof(expected_response_1)),
+		  0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+
+	/* Run the first vCPU and wait for a different message. */
+	dlog("Run first vCPU for message\n");
+	run_res = hf_vcpu_run(SERVICE_VM2, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+	EXPECT_EQ(mb.recv->length, sizeof(expected_response_0));
+	EXPECT_EQ(memcmp(mb.recv->payload, expected_response_0,
+			 sizeof(expected_response_0)),
+		  0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+
+	/* Run the second vCPU again, and expect it to turn itself off. */
+	dlog("Run second vCPU for poweroff.\n");
+	run_res = hf_vcpu_run(SERVICE_VM2, 1);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+	EXPECT_EQ(run_res.sleep.ns, HF_SLEEP_INDEFINITE);
+}