Add support for accessing EL1 debug registers

For now, the primary vm can access all debug registers, whereas secondary vms
cannot.  Debug event exceptions are disabled for secondary vms, so a malicious
primary cannot have active breakpoints or watchpoints for secondary vms.

This code allows us in the future to add debug support to secondary vms, and to
have fine-grained control over which registers are allowed, either by the
primary or secondary, as well as change the behavior for such accesses.

Bug: 132422368
Change-Id: I616454cc12bea6b8dfebbbdb566ac64c0a6625c2
diff --git a/Makefile b/Makefile
index 2d3a885..3473b2e 100644
--- a/Makefile
+++ b/Makefile
@@ -38,9 +38,15 @@
 NINJA ?= $(PREBUILTS)/ninja/ninja
 export PATH := $(PREBUILTS)/clang/bin:$(PATH)
 
+
 CHECKPATCH := $(PWD)/third_party/linux/scripts/checkpatch.pl \
 	--ignore BRACES,SPDX_LICENSE_TAG,VOLATILE,SPLIT_STRING,AVOID_EXTERNS,USE_SPINLOCK_T,NEW_TYPEDEFS,INITIALISED_STATIC,FILE_PATH_CHANGES,EMBEDDED_FUNCTION_NAME,SINGLE_STATEMENT_DO_WHILE_MACRO,MACRO_WITH_FLOW_CONTROL --quiet
 
+# Specifies the grep pattern for ignoring specific files in checkpatch.
+# Separate the different items in the list with a grep or (\|).
+# debug_el1.c : uses XMACROS, which checkpatch doesn't understand.
+CHECKPATCH_IGNORE := "src/arch/aarch64/hypervisor/debug_el1.c"
+
 # Select the project to build.
 PROJECT ?= reference
 
@@ -74,10 +80,10 @@
 
 .PHONY: checkpatch
 checkpatch:
-	@find src/ -name \*.c -o -name \*.h | xargs $(CHECKPATCH) -f
-	@find inc/ -name \*.c -o -name \*.h | xargs $(CHECKPATCH) -f
+	@find src/ -name \*.c -o -name \*.h | grep -v $(CHECKPATCH_IGNORE) | xargs $(CHECKPATCH) -f
+	@find inc/ -name \*.c -o -name \*.h | grep -v $(CHECKPATCH_IGNORE) | xargs $(CHECKPATCH) -f
 	# TODO: enable for test/
-	@find project/ -name \*.c -o -name \*.h | xargs $(CHECKPATCH) -f
+	@find project/ -name \*.c -o -name \*.h | grep -v $(CHECKPATCH_IGNORE) | xargs $(CHECKPATCH) -f
 
 # see .clang-tidy.
 .PHONY: tidy
diff --git a/src/arch/aarch64/cpu.c b/src/arch/aarch64/cpu.c
index 121f355..5881dd4 100644
--- a/src/arch/aarch64/cpu.c
+++ b/src/arch/aarch64/cpu.c
@@ -23,6 +23,8 @@
 #include "hf/addr.h"
 #include "hf/std.h"
 
+#include "hypervisor/debug_el1.h"
+
 void arch_irq_disable(void)
 {
 	__asm__ volatile("msr DAIFSet, #0xf");
@@ -102,6 +104,18 @@
 	r->spsr = 5 |	 /* M bits, set to EL1h. */
 		  (0xf << 6); /* DAIF bits set; disable interrupts. */
 
+	r->lazy.mdcr_el2 = get_mdcr_el2_value(vm_id);
+
+	/*
+	 * NOTE: It is important that MDSCR_EL1.MDE (bit 15) is set to 0 for
+	 * secondary VMs as long as Hafnium does not support debug register
+	 * access for secondary VMs. If adding Hafnium support for secondary VM
+	 * debug register accesses, then on context switches Hafnium needs to
+	 * save/restore EL1 debug register state that either might change, or
+	 * that needs to be protected.
+	 */
+	r->lazy.mdscr_el1 = 0x0u & ~(0x1u << 15);
+
 	gic_regs_reset(r, is_primary);
 }
 
diff --git a/src/arch/aarch64/hypervisor/BUILD.gn b/src/arch/aarch64/hypervisor/BUILD.gn
index 984adbf..36c6baa 100644
--- a/src/arch/aarch64/hypervisor/BUILD.gn
+++ b/src/arch/aarch64/hypervisor/BUILD.gn
@@ -23,6 +23,7 @@
   ]
 
   sources += [
+    "debug_el1.c",
     "handler.c",
     "offsets.c",
     "psci_handler.c",
diff --git a/src/arch/aarch64/hypervisor/debug_el1.c b/src/arch/aarch64/hypervisor/debug_el1.c
new file mode 100644
index 0000000..d278349
--- /dev/null
+++ b/src/arch/aarch64/hypervisor/debug_el1.c
@@ -0,0 +1,289 @@
+/*
+ * 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 "debug_el1.h"
+
+#include "hf/check.h"
+#include "hf/dlog.h"
+#include "hf/panic.h"
+#include "hf/types.h"
+
+#include "msr.h"
+
+/**
+ * Controls traps for Trace Filter.
+ */
+#define MDCR_EL2_TTRF (1u << 19)
+
+/**
+ * Controls traps for Debug ROM.
+ */
+#define MDCR_EL2_TDRA (1u << 11)
+
+/**
+ * Controls traps for OS-Related Register Access.
+ */
+#define MDCR_EL2_TDOSA (1u << 10)
+
+/**
+ * Controls traps for remaining Debug Registers not trapped by TDRA and TDOSA.
+ */
+#define MDCR_EL2_TDA (1u << 9)
+
+/**
+ * Controls traps for all debug exceptions (e.g., breakpoints).
+ */
+#define MDCR_EL2_TDE (1u << 8)
+
+/**
+ * Controls traps for debug events, i.e., breakpoints, watchpoints, and vector.
+ * catch exceptions.
+ */
+#define MDSCR_EL1_MDE (1u << 15)
+
+/**
+ * System register are identified by op0, op2, op1, crn, crm. The ISS encoding
+ * includes also rt and direction. Exclude them,  @see D13.2.37 (D13-2977).
+ */
+#define ISS_SYSREG_MASK                               \
+	(((1u << 22) - 1u) & /* Select the ISS bits*/ \
+	 ~(0x1fu << 5) &     /* exclude rt */         \
+	 ~1u /* exclude direction */)
+
+#define GET_ISS_SYSREG(esr) (ISS_SYSREG_MASK & (esr))
+
+/**
+ * Op0 from the ISS encoding in the ESR.
+ */
+#define ISS_OP0_MASK 0x300000
+#define ISS_OP0_SHIFT 20
+#define GET_ISS_OP0(esr) ((ISS_OP0_MASK & (esr)) >> ISS_OP0_SHIFT)
+
+/**
+ * Op1 from the ISS encoding in the ESR.
+ */
+#define ISS_OP1_MASK 0x1c000
+#define ISS_OP1_SHIFT 14
+#define GET_ISS_OP1(esr) ((ISS_OP1_MASK & (esr)) >> ISS_OP1_SHIFT)
+
+/**
+ * Direction (i.e., read (1) or write (0), is the first bit in the ISS/ESR.
+ */
+#define ISS_DIRECTION_MASK 1u
+
+/**
+ * Gets the direction of the system register access, read (1) or write (0).
+ */
+#define GET_ISS_DIRECTION(esr) (ISS_DIRECTION_MASK & (esr))
+
+/**
+ * True if the ISS encoded in the esr indicates a read of the system register.
+ */
+#define ISS_IS_READ(esr) (ISS_DIRECTION_MASK & (esr))
+
+/**
+ * Rt, which identifies the general purpose register used for the operation.
+ */
+#define ISS_RT_MASK 0x3e0
+#define ISS_RT_SHIFT 5
+#define GET_ISS_RT(esr) ((ISS_RT_MASK & (esr)) >> ISS_RT_SHIFT)
+
+/**
+ * Definitions of read-only debug registers' ISS signatures.
+ */
+#define EL1_DEBUG_REGISTERS_READ       \
+	X(MDRAR_EL1, 0x200400)         \
+	X(DBGAUTHSTATUS_EL1, 0x2c1c1c) \
+	X(OSLSR_EL1, 0x280402)
+
+/**
+ * Definitions of readable and writeable debug registers' ISS signatures.
+ */
+#define EL1_DEBUG_REGISTERS_READ_WRITE \
+	X(DBGCLAIMCLR_EL1, 0x2c1c12)   \
+	X(DBGCLAIMSET_EL1, 0x2c1c10)   \
+	X(DBGPRCR_EL1, 0x280408)       \
+	X(MDCCINT_EL1, 0x200004)       \
+	X(MDSCR_EL1, 0x240004)         \
+	X(OSDLR_EL1, 0x280406)         \
+	X(OSDTRRX_EL1, 0x240000)       \
+	X(OSDTRTX_EL1, 0x240006)       \
+	X(OSECCR_EL1, 0x24000c)        \
+	X(DBGBCR0_EL1, 0x2a0000)       \
+	X(DBGBCR1_EL1, 0x2a0002)       \
+	X(DBGBCR2_EL1, 0x2a0004)       \
+	X(DBGBCR3_EL1, 0x2a0006)       \
+	X(DBGBCR4_EL1, 0x2a0008)       \
+	X(DBGBCR5_EL1, 0x2a000a)       \
+	X(DBGBCR6_EL1, 0x2a000c)       \
+	X(DBGBCR7_EL1, 0x2a000e)       \
+	X(DBGBCR8_EL1, 0x2a0010)       \
+	X(DBGBCR9_EL1, 0x2a0012)       \
+	X(DBGBCR10_EL1, 0x2a0014)      \
+	X(DBGBCR11_EL1, 0x2a0016)      \
+	X(DBGBCR12_EL1, 0x2a0018)      \
+	X(DBGBCR13_EL1, 0x2a001a)      \
+	X(DBGBCR14_EL1, 0x2a001c)      \
+	X(DBGBCR15_EL1, 0x2a001e)      \
+	X(DBGBVR0_EL1, 0x280000)       \
+	X(DBGBVR1_EL1, 0x280002)       \
+	X(DBGBVR2_EL1, 0x280004)       \
+	X(DBGBVR3_EL1, 0x280006)       \
+	X(DBGBVR4_EL1, 0x280008)       \
+	X(DBGBVR5_EL1, 0x28000a)       \
+	X(DBGBVR6_EL1, 0x28000c)       \
+	X(DBGBVR7_EL1, 0x28000e)       \
+	X(DBGBVR8_EL1, 0x280010)       \
+	X(DBGBVR9_EL1, 0x280012)       \
+	X(DBGBVR10_EL1, 0x280014)      \
+	X(DBGBVR11_EL1, 0x280016)      \
+	X(DBGBVR12_EL1, 0x280018)      \
+	X(DBGBVR13_EL1, 0x28001a)      \
+	X(DBGBVR14_EL1, 0x28001c)      \
+	X(DBGBVR15_EL1, 0x28001e)      \
+	X(DBGWCR0_EL1, 0x2e0000)       \
+	X(DBGWCR1_EL1, 0x2e0002)       \
+	X(DBGWCR2_EL1, 0x2e0004)       \
+	X(DBGWCR3_EL1, 0x2e0006)       \
+	X(DBGWCR4_EL1, 0x2e0008)       \
+	X(DBGWCR5_EL1, 0x2e000a)       \
+	X(DBGWCR6_EL1, 0x2e000c)       \
+	X(DBGWCR7_EL1, 0x2e000e)       \
+	X(DBGWCR8_EL1, 0x2e0010)       \
+	X(DBGWCR9_EL1, 0x2e0012)       \
+	X(DBGWCR10_EL1, 0x2e0014)      \
+	X(DBGWCR11_EL1, 0x2e0016)      \
+	X(DBGWCR12_EL1, 0x2e0018)      \
+	X(DBGWCR13_EL1, 0x2e001a)      \
+	X(DBGWCR14_EL1, 0x2e001c)      \
+	X(DBGWCR15_EL1, 0x2e001e)      \
+	X(DBGWVR0_EL1, 0x2c0000)       \
+	X(DBGWVR1_EL1, 0x2c0002)       \
+	X(DBGWVR2_EL1, 0x2c0004)       \
+	X(DBGWVR3_EL1, 0x2c0006)       \
+	X(DBGWVR4_EL1, 0x2c0008)       \
+	X(DBGWVR5_EL1, 0x2c000a)       \
+	X(DBGWVR6_EL1, 0x2c000c)       \
+	X(DBGWVR7_EL1, 0x2c000e)       \
+	X(DBGWVR8_EL1, 0x2c0010)       \
+	X(DBGWVR9_EL1, 0x2c0012)       \
+	X(DBGWVR10_EL1, 0x2c0014)      \
+	X(DBGWVR11_EL1, 0x2c0016)      \
+	X(DBGWVR12_EL1, 0x2c0018)      \
+	X(DBGWVR13_EL1, 0x2c001a)      \
+	X(DBGWVR14_EL1, 0x2c001c)      \
+	X(DBGWVR15_EL1, 0x2c001e)
+
+/**
+ * Definitions of all debug registers' ISS signatures.
+ */
+#define EL1_DEBUG_REGISTERS      \
+	EL1_DEBUG_REGISTERS_READ \
+	EL1_DEBUG_REGISTERS_READ_WRITE
+
+/**
+ * Returns the value for mdcr_el2 for the particular VM.
+ * For now, the primary VM has one value and all secondary VMs share a value.
+ */
+uintreg_t get_mdcr_el2_value(spci_vm_id_t vm_id)
+{
+	if (vm_id == HF_PRIMARY_VM_ID) {
+		/*
+		 * Trap primary VM accesses to debug registers to have fine
+		 * grained control over system register accesses.
+		 * Do not trap the Primary VM's debug events (!MDCR_EL2_TDE).
+		 */
+		return MDCR_EL2_TTRF | MDCR_EL2_TDRA | MDCR_EL2_TDOSA |
+		       MDCR_EL2_TDA;
+	}
+
+	/*
+	 * Trap all secondary VM debug register accesses as well as debug
+	 * event exceptions.
+	 * Debug event exceptions should be disabled in secondary VMs, but trap
+	 * them for additional security (MDCR_EL2_TDE).
+	 */
+	return MDCR_EL2_TTRF | MDCR_EL2_TDRA | MDCR_EL2_TDOSA | MDCR_EL2_TDA |
+	       MDCR_EL2_TDE;
+}
+
+/**
+ * Returns true if the ESR register shows an access to an EL1 debug register.
+ */
+bool is_debug_el1_register_access(uintreg_t esr_el2)
+{
+	/*
+	 * Architecture Reference Manual D12.2: op0 == 0b10 is for debug and
+	 * trace system registers.  op1 = 0x1 for trace, remaining are debug.
+	 */
+	return GET_ISS_OP0(esr_el2) == 0x2 && GET_ISS_OP1(esr_el2) != 0x1;
+}
+
+/**
+ * Processes an access (msr, mrs) to an EL1 debug register.
+ * Returns true if the access was allowed and performed, false otherwise.
+ */
+bool debug_el1_process_access(struct vcpu *vcpu, spci_vm_id_t vm_id,
+			      uintreg_t esr_el2)
+{
+	/*
+	 * For now, debug registers are not supported by secondary VMs.
+	 * Disallow accesses to them.
+	 */
+	if (vm_id != HF_PRIMARY_VM_ID) {
+		return false;
+	}
+
+	uintreg_t sys_register = GET_ISS_SYSREG(esr_el2);
+	uintreg_t rt_register = GET_ISS_RT(esr_el2);
+	uintreg_t value;
+
+	CHECK(rt_register < NUM_GP_REGS);
+
+	if (ISS_IS_READ(esr_el2)) {
+		switch (sys_register) {
+#define X(reg_name, reg_sig)                \
+	case reg_sig:                       \
+		value = read_msr(reg_name); \
+		break;
+			EL1_DEBUG_REGISTERS
+#undef X
+		default:
+			value = vcpu->regs.r[rt_register];
+			dlog("Unsupported system register read 0x%x\n",
+			     sys_register);
+			break;
+		}
+		vcpu->regs.r[rt_register] = value;
+
+	} else {
+		value = vcpu->regs.r[rt_register];
+		switch (sys_register) {
+#define X(reg_name, reg_sig)                \
+	case reg_sig:                       \
+		write_msr(reg_name, value); \
+		break;
+			EL1_DEBUG_REGISTERS_READ_WRITE
+#undef X
+		default:
+			dlog("Unsupported system register write 0x%x\n",
+			     sys_register);
+			break;
+		}
+	}
+
+	return true;
+}
diff --git a/src/arch/aarch64/hypervisor/debug_el1.h b/src/arch/aarch64/hypervisor/debug_el1.h
new file mode 100644
index 0000000..28becb5
--- /dev/null
+++ b/src/arch/aarch64/hypervisor/debug_el1.h
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "hf/arch/types.h"
+
+#include "hf/cpu.h"
+
+#include "vmapi/hf/spci.h"
+
+uintreg_t get_mdcr_el2_value(spci_vm_id_t vm_id);
+
+bool is_debug_el1_register_access(uintreg_t esr_el2);
+
+bool debug_el1_process_access(struct vcpu *vcpu, spci_vm_id_t vm_id,
+			      uintreg_t esr_el2);
diff --git a/src/arch/aarch64/hypervisor/exceptions.S b/src/arch/aarch64/hypervisor/exceptions.S
index 6a8309f..1c4d642 100644
--- a/src/arch/aarch64/hypervisor/exceptions.S
+++ b/src/arch/aarch64/hypervisor/exceptions.S
@@ -55,6 +55,54 @@
 .endm
 
 /**
+ * Save all general purpose registers into register buffer of current vcpu.
+ */
+.macro save_registers_to_vcpu
+	save_volatile_to_vcpu also_save_x18
+	stp x19, x20, [x18, #VCPU_REGS + 8 * 19]
+	stp x21, x22, [x18, #VCPU_REGS + 8 * 21]
+	stp x23, x24, [x18, #VCPU_REGS + 8 * 23]
+	stp x25, x26, [x18, #VCPU_REGS + 8 * 25]
+	stp x27, x28, [x18, #VCPU_REGS + 8 * 27]
+.endm
+
+/**
+ * Restore the volatile registers from the register buffer of the current vcpu.
+ */
+.macro restore_volatile_from_vcpu vcpu_ptr:req
+	ldp x4, x5, [\vcpu_ptr, #VCPU_REGS + 8 * 4]
+	ldp x6, x7, [\vcpu_ptr, #VCPU_REGS + 8 * 6]
+	ldp x8, x9, [\vcpu_ptr, #VCPU_REGS + 8 * 8]
+	ldp x10, x11, [\vcpu_ptr, #VCPU_REGS + 8 * 10]
+	ldp x12, x13, [\vcpu_ptr, #VCPU_REGS + 8 * 12]
+	ldp x14, x15, [\vcpu_ptr, #VCPU_REGS + 8 * 14]
+	ldp x16, x17, [\vcpu_ptr, #VCPU_REGS + 8 * 16]
+	ldr x18, [\vcpu_ptr, #VCPU_REGS + 8 * 18]
+	ldp x29, x30, [\vcpu_ptr, #VCPU_REGS + 8 * 29]
+
+	/* Restore return address & mode. */
+	ldp x1, x2, [\vcpu_ptr, #VCPU_REGS + 8 * 31]
+	msr elr_el2, x1
+	msr spsr_el2, x2
+
+	/* Restore x0..x3, which we have used as scratch before. */
+	ldp x2, x3, [\vcpu_ptr, #VCPU_REGS + 8 * 2]
+	ldp x0, x1, [\vcpu_ptr, #VCPU_REGS + 8 * 0]
+.endm
+
+/**
+ * Restore all general purpose registers from register buffer of current vcpu.
+ */
+.macro restore_registers_from_vcpu vcpu_ptr:req
+	ldp x19, x20, [\vcpu_ptr, #VCPU_REGS + 8 * 19]
+	ldp x21, x22, [\vcpu_ptr, #VCPU_REGS + 8 * 21]
+	ldp x23, x24, [\vcpu_ptr, #VCPU_REGS + 8 * 23]
+	ldp x25, x26, [\vcpu_ptr, #VCPU_REGS + 8 * 25]
+	ldp x27, x28, [\vcpu_ptr, #VCPU_REGS + 8 * 27]
+	restore_volatile_from_vcpu \vcpu_ptr
+.endm
+
+/**
  * This is a generic handler for exceptions taken at a lower EL. It saves the
  * volatile registers to the current vcpu and calls the C handler, which can
  * select one of two paths: (a) restore volatile registers and return, or
@@ -91,8 +139,8 @@
 	lsr x18, x18, #26
 
 	/* Take the slow path if exception is not due to an HVC instruction. */
-	sub x18, x18, #0x16
-	cbnz x18, slow_sync_lower
+	cmp x18, #0x16
+	b.ne slow_sync_lower
 
 	/*
 	 * Save x29 and x30, which are not saved by the callee, then jump to
@@ -193,6 +241,10 @@
 
 .balign 0x40
 slow_sync_lower:
+	/* Take the system register path for EC 0x18 */
+	cmp x18, #0x18
+	b.eq handle_system_register_access_s
+
 	/* The caller must have saved x18, so we don't save it here. */
 	save_volatile_to_vcpu
 
@@ -316,6 +368,9 @@
 	mrs x5, mdcr_el2
 	stp x4, x5, [x28], #16
 
+	mrs x6, mdscr_el1
+	str x6, [x28], #16
+
 	/* Save GIC registers. */
 #if GIC_VERSION == 3 || GIC_VERSION == 4
 	/* Offset is too large, so start from a new base. */
@@ -472,6 +527,9 @@
 	msr vttbr_el2, x4
 	msr mdcr_el2, x5
 
+	ldr x6, [x28], #16
+	msr mdscr_el1, x6
+
 	/* Restore GIC registers. */
 #if GIC_VERSION == 3 || GIC_VERSION == 4
 	/* Offset is too large, so start from a new base. */
@@ -506,30 +564,35 @@
  * x0 is a pointer to the target vcpu.
  */
 vcpu_restore_volatile_and_run:
-	ldp x4, x5, [x0, #VCPU_REGS + 8 * 4]
-	ldp x6, x7, [x0, #VCPU_REGS + 8 * 6]
-	ldp x8, x9, [x0, #VCPU_REGS + 8 * 8]
-	ldp x10, x11, [x0, #VCPU_REGS + 8 * 10]
-	ldp x12, x13, [x0, #VCPU_REGS + 8 * 12]
-	ldp x14, x15, [x0, #VCPU_REGS + 8 * 14]
-	ldp x16, x17, [x0, #VCPU_REGS + 8 * 16]
-	ldr x18, [x0, #VCPU_REGS + 8 * 18]
-	ldp x29, x30, [x0, #VCPU_REGS + 8 * 29]
-
-	/* Restore return address & mode. */
-	ldp x1, x2, [x0, #VCPU_REGS + 8 * 31]
-	msr elr_el2, x1
-	msr spsr_el2, x2
-
-	/* Restore x0..x3, which we have used as scratch before. */
-	ldp x2, x3, [x0, #VCPU_REGS + 8 * 2]
-	ldp x0, x1, [x0, #VCPU_REGS + 8 * 0]
+	restore_volatile_from_vcpu x0
 	eret
 
 .balign 0x40
 /**
- * Restores volatile registers from stack and returns.
+ * Restore volatile registers from stack and return to original caller.
  */
 restore_from_stack_and_return:
 	restore_volatile_from_stack el2
 	eret
+
+.balign 0x40
+/**
+ * Handle accesses to system registers (EC=0x18) and return to original caller.
+ */
+handle_system_register_access_s:
+	/*
+	 * All registers are (conservatively) saved because the handler can
+	 * clobber non-volatile registers that are used by the msr/mrs, which
+	 * results in the wrong value being read or written.
+	 */
+	save_registers_to_vcpu
+
+	/* Read syndrome register and call C handler. */
+	mrs x0, esr_el2
+	bl handle_system_register_access
+	cbnz x0, vcpu_switch
+
+	/* vcpu is not changing. */
+	mrs x0, tpidr_el2
+	restore_registers_from_vcpu x0
+	eret
diff --git a/src/arch/aarch64/hypervisor/handler.c b/src/arch/aarch64/hypervisor/handler.c
index 4f4ec44..b0f2259 100644
--- a/src/arch/aarch64/hypervisor/handler.c
+++ b/src/arch/aarch64/hypervisor/handler.c
@@ -21,6 +21,7 @@
 #include "hf/arch/mm.h"
 
 #include "hf/api.h"
+#include "hf/check.h"
 #include "hf/cpu.h"
 #include "hf/dlog.h"
 #include "hf/panic.h"
@@ -29,6 +30,7 @@
 
 #include "vmapi/hf/call.h"
 
+#include "debug_el1.h"
 #include "msr.h"
 #include "psci.h"
 #include "psci_handler.h"
@@ -36,12 +38,25 @@
 
 #define HCR_EL2_VI (1u << 7)
 
+/**
+ * Gets the Exception Class from the ESR.
+ */
+#define GET_EC(esr) ((esr) >> 26)
+
+/**
+ * Gets the value to increment for the next PC.
+ * The ESR encodes whether the instruction is 2 bytes or 4 bytes long.
+ */
+#define GET_NEXT_PC_INC(esr) (((esr) & (1u << 25)) ? 4 : 2)
+
 struct hvc_handler_return {
 	uintreg_t user_ret;
 	struct vcpu *new;
 };
 
-/* Gets a reference to the currently executing vCPU. */
+/**
+ * Returns a reference to the currently executing vCPU.
+ */
 static struct vcpu *current(void)
 {
 	return (struct vcpu *)read_msr(tpidr_el2);
@@ -185,13 +200,13 @@
 noreturn void sync_current_exception(uintreg_t elr, uintreg_t spsr)
 {
 	uintreg_t esr = read_msr(esr_el2);
+	uintreg_t ec = GET_EC(esr);
 
 	(void)spsr;
 
-	switch (esr >> 26) {
+	switch (ec) {
 	case 0x25: /* EC = 100101, Data abort. */
-		dlog("Data abort: pc=%#x, esr=%#x, ec=%#x", elr, esr,
-		     esr >> 26);
+		dlog("Data abort: pc=%#x, esr=%#x, ec=%#x", elr, esr, ec);
 		if (!(esr & (1u << 10))) { /* Check FnV bit. */
 			dlog(", far=%#x", read_msr(far_el2));
 		} else {
@@ -204,7 +219,7 @@
 	default:
 		dlog("Unknown current sync exception pc=%#x, esr=%#x, "
 		     "ec=%#x\n",
-		     elr, esr, esr >> 26);
+		     elr, esr, ec);
 		break;
 	}
 
@@ -473,11 +488,12 @@
 	struct vcpu *vcpu = current();
 	struct vcpu_fault_info info;
 	struct vcpu *new_vcpu;
+	uintreg_t ec = GET_EC(esr);
 
-	switch (esr >> 26) {
+	switch (ec) {
 	case 0x01: /* EC = 000001, WFI or WFE. */
 		/* Skip the instruction. */
-		vcpu->regs.pc += (esr & (1u << 25)) ? 4 : 2;
+		vcpu->regs.pc += GET_NEXT_PC_INC(esr);
 		/* Check TI bit of ISS, 0 = WFI, 1 = WFE. */
 		if (esr & 1) {
 			/* WFE */
@@ -518,7 +534,7 @@
 		}
 
 		/* Skip the SMC instruction. */
-		vcpu->regs.pc = smc_pc + (esr & (1u << 25) ? 4 : 2);
+		vcpu->regs.pc = smc_pc + GET_NEXT_PC_INC(esr);
 		vcpu->regs.r[0] = ret.res0;
 		vcpu->regs.r[1] = ret.res1;
 		vcpu->regs.r[2] = ret.res2;
@@ -526,13 +542,54 @@
 		return next;
 	}
 
+	/*
+	 * EC = 011000, MSR, MRS or System instruction execution that is not
+	 * reported using EC 000000, 000001 or 000111.
+	 */
+	case 0x18:
+		/*
+		 * NOTE: This should never be reached because it goes through a
+		 * separate path handled by handle_system_register_access().
+		 */
+		panic("Handled by handle_system_register_access().");
+
 	default:
 		dlog("Unknown lower sync exception pc=%#x, esr=%#x, "
 		     "ec=%#x\n",
-		     vcpu->regs.pc, esr, esr >> 26);
+		     vcpu->regs.pc, esr, ec);
 		break;
 	}
 
 	/* The exception wasn't handled so abort the VM. */
 	return api_abort(vcpu);
 }
+
+/**
+ * Handles EC = 011000, msr, mrs instruction traps.
+ * Returns non-null ONLY if the access failed and the vcpu is changing.
+ */
+struct vcpu *handle_system_register_access(uintreg_t esr)
+{
+	struct vcpu *vcpu = current();
+	spci_vm_id_t vm_id = vcpu->vm->id;
+	uintreg_t ec = GET_EC(esr);
+
+	CHECK(ec == 0x18);
+
+	/*
+	 * Handle accesses to other registers that trap with the same EC.
+	 * Abort when encountering unhandled register accesses.
+	 */
+	if (!is_debug_el1_register_access(esr)) {
+		return api_abort(vcpu);
+	}
+
+	/* Abort if unable to fulfill the debug register access. */
+	if (!debug_el1_process_access(vcpu, vm_id, esr)) {
+		return api_abort(vcpu);
+	}
+
+	/* Instruction was fulfilled above. Skip it and run the next one. */
+	vcpu->regs.pc += GET_NEXT_PC_INC(esr);
+	return NULL;
+}
diff --git a/src/arch/aarch64/inc/hf/arch/types.h b/src/arch/aarch64/inc/hf/arch/types.h
index e4b3c3c..25d3e0c 100644
--- a/src/arch/aarch64/inc/hf/arch/types.h
+++ b/src/arch/aarch64/inc/hf/arch/types.h
@@ -25,6 +25,7 @@
 #define PAGE_BITS 12
 #define PAGE_LEVEL_BITS 9
 #define FLOAT_REG_BYTES 16
+#define NUM_GP_REGS 31
 
 /** The type of a page table entry (PTE). */
 typedef uint64_t pte_t;
@@ -68,11 +69,15 @@
 /** Type to represent the register state of a vCPU.  */
 struct arch_regs {
 	/* General purpose registers. */
-	uintreg_t r[31];
+	uintreg_t r[NUM_GP_REGS];
 	uintreg_t pc;
 	uintreg_t spsr;
 
-	/* System registers. */
+	/*
+	 * System registers.
+	 * NOTE: Ordering is important. If adding to or reordering registers
+	 * below, make sure to update src/arch/aarch64/hypervisor/exceptions.S.
+	 */
 	struct {
 		uintreg_t vmpidr_el2;
 		uintreg_t csselr_el1;
@@ -104,6 +109,7 @@
 		uintreg_t cnthctl_el2;
 		uintreg_t vttbr_el2;
 		uintreg_t mdcr_el2;
+		uintreg_t mdscr_el1;
 	} lazy;
 
 	/* Floating point registers. */
diff --git a/test/vmapi/primary_with_secondaries/BUILD.gn b/test/vmapi/primary_with_secondaries/BUILD.gn
index 60b78a0..3c00989 100644
--- a/test/vmapi/primary_with_secondaries/BUILD.gn
+++ b/test/vmapi/primary_with_secondaries/BUILD.gn
@@ -26,6 +26,7 @@
   sources = [
     "abort.c",
     "boot.c",
+    "debug_el1.c",
     "floating_point.c",
     "interrupts.c",
     "mailbox.c",
diff --git a/test/vmapi/primary_with_secondaries/debug_el1.c b/test/vmapi/primary_with_secondaries/debug_el1.c
new file mode 100644
index 0000000..991caa2
--- /dev/null
+++ b/test/vmapi/primary_with_secondaries/debug_el1.c
@@ -0,0 +1,148 @@
+/*
+ * 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 "debug_el1.h"
+
+#include "primary_with_secondary.h"
+#include "util.h"
+
+/**
+ * QEMU does not properly handle the trapping of certain system register
+ * accesses. This was fixed in a custom local build that we could use. If not
+ * using that build, limit testing to the subset QEMU handles correctly.
+ */
+#define CUSTOM_QEMU_BUILD() 0
+
+/*
+ * TODO(b/132422368): Devise a way to test exhaustively read/write behavior to
+ * all debug registers that does not involve a separate service per register.
+ * This needs proper trap support as a starting point.
+ */
+
+TEST(debug_el1, secondary_mdccint_el1)
+{
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "debug_el1_secondary_mdccint_el1", mb.send);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_ABORTED);
+}
+
+TEST(debug_el1, secondary_dbgbcr0_el1)
+{
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "debug_el1_secondary_dbgbcr0_el1", mb.send);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_ABORTED);
+}
+
+TEST(debug_el1, secondary_dbgbvr0_el1)
+{
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "debug_el1_secondary_dbgbvr0_el1", mb.send);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_ABORTED);
+}
+
+TEST(debug_el1, secondary_dbgwcr0_el1)
+{
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "debug_el1_secondary_dbgwcr0_el1", mb.send);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_ABORTED);
+}
+
+TEST(debug_el1, secondary_dbgwvr0_el1)
+{
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "debug_el1_secondary_dbgwvr0_el1", mb.send);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_ABORTED);
+}
+
+/**
+ * Attempts to access many debug registers for read, without validating their
+ * value.
+ */
+TEST(debug_el1, primary_basic)
+{
+	EXPECT_EQ(hf_vm_get_id(), HF_PRIMARY_VM_ID);
+
+	if (CUSTOM_QEMU_BUILD()) {
+		TRY_READ(DBGAUTHSTATUS_EL1);
+		TRY_READ(DBGCLAIMCLR_EL1);
+		TRY_READ(DBGCLAIMSET_EL1);
+		TRY_READ(DBGPRCR_EL1);
+		TRY_READ(OSDTRRX_EL1);
+		TRY_READ(OSDTRTX_EL1);
+		TRY_READ(OSECCR_EL1);
+
+		TRY_READ(DBGBCR2_EL1);
+		TRY_READ(DBGBCR3_EL1);
+		TRY_READ(DBGBCR4_EL1);
+		TRY_READ(DBGBCR5_EL1);
+		TRY_READ(DBGBVR2_EL1);
+		TRY_READ(DBGBVR3_EL1);
+		TRY_READ(DBGBVR4_EL1);
+		TRY_READ(DBGBVR5_EL1);
+		TRY_READ(DBGWCR2_EL1);
+		TRY_READ(DBGWCR3_EL1);
+		TRY_READ(DBGWVR2_EL1);
+		TRY_READ(DBGWVR3_EL1);
+	}
+
+	/* The following is the subset currently supported by QEMU. */
+	TRY_READ(MDCCINT_EL1);
+	TRY_READ(MDRAR_EL1);
+	TRY_READ(MDSCR_EL1);
+	TRY_READ(OSDLR_EL1);
+	TRY_READ(OSLSR_EL1);
+
+	TRY_READ(DBGBCR0_EL1);
+	TRY_READ(DBGBCR1_EL1);
+	TRY_READ(DBGBVR0_EL1);
+	TRY_READ(DBGBVR1_EL1);
+	TRY_READ(DBGWCR0_EL1);
+	TRY_READ(DBGWCR1_EL1);
+	TRY_READ(DBGWVR0_EL1);
+	TRY_READ(DBGWVR1_EL1);
+}
+
+/**
+ * Tests a few debug registers for read and write, and checks that the expected
+ * value is written/read.
+ */
+TEST(debug_el1, primary_read_write)
+{
+	EXPECT_EQ(hf_vm_get_id(), HF_PRIMARY_VM_ID);
+
+	TRY_WRITE_READ(DBGBCR0_EL1, 0x2);
+	TRY_WRITE_READ(DBGBVR0_EL1, 0xf0);
+}
diff --git a/test/vmapi/primary_with_secondaries/debug_el1.h b/test/vmapi/primary_with_secondaries/debug_el1.h
new file mode 100644
index 0000000..d5d6d77
--- /dev/null
+++ b/test/vmapi/primary_with_secondaries/debug_el1.h
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+#pragma once
+#include "vmapi/hf/call.h"
+
+#include "../msr.h"
+#include "hftest.h"
+
+#define TRY_READ(REG) dlog(#REG "=%#x\n", read_msr(REG));
+
+#define TRY_WRITE_READ(REG, VALUE)     \
+	do {                           \
+		uintreg_t x;           \
+		write_msr(REG, VALUE); \
+		x = read_msr(REG);     \
+		EXPECT_EQ(x, VALUE);   \
+	} while (0);
diff --git a/test/vmapi/primary_with_secondaries/services/BUILD.gn b/test/vmapi/primary_with_secondaries/services/BUILD.gn
index 7dc1964..29da308 100644
--- a/test/vmapi/primary_with_secondaries/services/BUILD.gn
+++ b/test/vmapi/primary_with_secondaries/services/BUILD.gn
@@ -28,6 +28,16 @@
   ]
 }
 
+# Service to try to access EL1 debug registers.
+source_set("debug_el1") {
+  testonly = true
+  public_configs = [ "//test/hftest:hftest_config" ]
+
+  sources = [
+    "debug_el1.c",
+  ]
+}
+
 # Service to listen for messages and echo them back to the sender.
 source_set("echo") {
   testonly = true
@@ -189,6 +199,7 @@
     ":abort",
     ":boot",
     ":check_state",
+    ":debug_el1",
     ":echo",
     ":echo_with_notification",
     ":floating_point",
diff --git a/test/vmapi/primary_with_secondaries/services/debug_el1.c b/test/vmapi/primary_with_secondaries/services/debug_el1.c
new file mode 100644
index 0000000..58ab389
--- /dev/null
+++ b/test/vmapi/primary_with_secondaries/services/debug_el1.c
@@ -0,0 +1,54 @@
+/*
+ * 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 "../debug_el1.h"
+
+#include "hf/dlog.h"
+
+TEST_SERVICE(debug_el1_secondary_mdccint_el1)
+{
+	EXPECT_GT(hf_vm_get_id(), HF_PRIMARY_VM_ID);
+	TRY_READ(MDCCINT_EL1);
+	FAIL("Reading debug EL1 register in secondary VM didn't trap.");
+}
+
+TEST_SERVICE(debug_el1_secondary_dbgbcr0_el1)
+{
+	EXPECT_GT(hf_vm_get_id(), HF_PRIMARY_VM_ID);
+	TRY_READ(DBGBCR0_EL1);
+	FAIL("Reading debug EL1 register in secondary VM didn't trap.");
+}
+
+TEST_SERVICE(debug_el1_secondary_dbgbvr0_el1)
+{
+	EXPECT_GT(hf_vm_get_id(), HF_PRIMARY_VM_ID);
+	TRY_READ(DBGBVR0_EL1);
+	FAIL("Reading debug EL1 register in secondary VM didn't trap.");
+}
+
+TEST_SERVICE(debug_el1_secondary_dbgwcr0_el1)
+{
+	EXPECT_GT(hf_vm_get_id(), HF_PRIMARY_VM_ID);
+	TRY_READ(DBGWCR0_EL1);
+	FAIL("Reading debug EL1 register in secondary VM didn't trap.");
+}
+
+TEST_SERVICE(debug_el1_secondary_dbgwvr0_el1)
+{
+	EXPECT_GT(hf_vm_get_id(), HF_PRIMARY_VM_ID);
+	TRY_READ(DBGWVR0_EL1);
+	FAIL("Reading debug EL1 register in secondary VM didn't trap.");
+}