diff --git a/x86/unittests.cfg b/x86/unittests.cfg
index 2d04806..e455d96 100644
--- a/x86/unittests.cfg
+++ b/x86/unittests.cfg
@@ -566,6 +566,12 @@ extra_params = -cpu host,+vmx -m 2560 -append vmx_nm_test
 arch = x86_64
 groups = vmx
 
+[vmx_db_test]
+file = vmx.flat
+extra_params = -cpu host,+vmx -m 2560 -append vmx_db_test
+arch = x86_64
+groups = vmx
+
 [vmx_eoi_bitmap_ioapic_scan]
 file = vmx.flat
 smp = 2
diff --git a/x86/vmx_tests.c b/x86/vmx_tests.c
index 1574970..524b82d 100644
--- a/x86/vmx_tests.c
+++ b/x86/vmx_tests.c
@@ -3,6 +3,9 @@
  *
  * Author : Arthur Chunqi Li <yzt356@gmail.com>
  */
+
+#include <asm/debugreg.h>
+
 #include "vmx.h"
 #include "msr.h"
 #include "processor.h"
@@ -4684,6 +4687,159 @@ static void vmx_nm_test(void)
 	enter_guest();
 }
 
+static void vmx_db_test_guest(void)
+{
+	/*
+	 * For a hardware generated single-step #DB.
+	 */
+	asm volatile("vmcall;"
+		     "nop;"
+		     ".Lpost_nop:");
+	/*
+	 * For an L0 synthesized single-step #DB. (L0 intercepts WBINVD and
+	 * emulates it in software.)
+	 */
+	asm volatile("vmcall;"
+		     "wbinvd;"
+		     ".Lpost_wbinvd:");
+	/*
+	 * For a hardware generated single-step #DB in a transactional region.
+	 */
+	asm volatile("vmcall;"
+		     ".Lxbegin: xbegin .Lpost_xbegin;"
+		     ".Lpost_xbegin:");
+}
+
+/*
+ * Clear the pending debug exceptions and RFLAGS.TF and re-enter
+ * L2. No #DB is delivered and L2 continues to the next point of
+ * interest.
+ */
+static void dismiss_db(void)
+{
+	vmcs_write(GUEST_PENDING_DEBUG, 0);
+	vmcs_write(GUEST_RFLAGS, X86_EFLAGS_FIXED);
+	enter_guest();
+}
+
+/*
+ * Check a variety of VMCS fields relevant to an intercepted #DB exception.
+ * Then throw away the #DB exception and resume L2.
+ */
+static void check_db_exit(bool xfail, void *expected_rip,
+			  u64 expected_exit_qual, u64 expected_dr6)
+{
+	u32 reason = vmcs_read(EXI_REASON);
+	u32 intr_info = vmcs_read(EXI_INTR_INFO);
+	u64 exit_qual = vmcs_read(EXI_QUALIFICATION);
+	u64 guest_rip = vmcs_read(GUEST_RIP);
+	u64 guest_pending_dbg = vmcs_read(GUEST_PENDING_DEBUG);
+	u64 dr6 = read_dr6();
+	const u32 expected_intr_info = INTR_INFO_VALID_MASK |
+		INTR_TYPE_HARD_EXCEPTION | DB_VECTOR;
+
+	report("Expected #DB VM-exit",
+	       reason == VMX_EXC_NMI && intr_info == expected_intr_info);
+	report("Expected RIP %p (actual %lx)", (u64)expected_rip == guest_rip,
+	       expected_rip, guest_rip);
+	report("Expected pending debug exceptions 0 (actual %lx)",
+	       0 == guest_pending_dbg, guest_pending_dbg);
+	report_xfail("Expected exit qualification %lx (actual %lx)", xfail,
+		     expected_exit_qual == exit_qual,
+		     expected_exit_qual, exit_qual);
+	report_xfail("Expected DR6 %lx (actual %lx)", xfail,
+		     expected_dr6 == dr6, expected_dr6, dr6);
+	dismiss_db();
+}
+
+/*
+ * Assuming the guest has just exited on a VMCALL instruction, skip
+ * over the vmcall, set the guest's RFLAGS.TF in the VMCS, and resume
+ * L2.
+ */
+static void single_step_guest(u64 starting_dr6)
+{
+	skip_exit_vmcall();
+	write_dr6(starting_dr6);
+	vmcs_write(GUEST_RFLAGS, X86_EFLAGS_FIXED | X86_EFLAGS_TF);
+	enter_guest();
+}
+
+/*
+ * If L1 intercepts #DB, verify that a single-step trap clears pending
+ * debug exceptions, populates the exit qualification field properly,
+ * and that DR6 is not prematurely clobbered.
+ */
+static void vmx_db_test(void)
+{
+	/*
+	 * We are going to set a few arbitrary bits in DR6 to verify that
+	 * (a) DR6 is not modified by an intercepted #DB, and
+	 * (b) stale bits in DR6 don't leak into the exit qualification
+	 *     field for a subsequent #DB exception.
+	 */
+	const u64 starting_dr6 = DR6_RESERVED | DR_SWITCH | 0xa;
+	extern char post_nop asm(".Lpost_nop");
+	extern char post_wbinvd asm(".Lpost_wbinvd");
+	extern char xbegin asm(".Lxbegin");
+	extern char post_xbegin asm(".Lpost_xbegin");
+
+	/*
+	 * L1 wants to intercept #DB exceptions encountered in L2.
+	 */
+	vmcs_write(EXC_BITMAP, BIT(DB_VECTOR));
+
+	/*
+	 * Start L2 and run it up to the first point of interest.
+	 */
+	test_set_guest(vmx_db_test_guest);
+	enter_guest();
+
+	/*
+	 * Hardware-delivered #DB trap for single-step sets the standard.
+	 */
+	single_step_guest(starting_dr6);
+	printf("\nHardware delivered single-step\n");
+	check_db_exit(false, &post_nop, DR_STEP, starting_dr6);
+
+	/*
+	 * L0 synthesized #DB trap for single-step is buggy, because
+	 * kvm (a) clobbers DR6 too early, and (b) tries its best to
+	 * reconstitute the exit qualification from the prematurely
+	 * modified DR6, but fails miserably.
+	 */
+	single_step_guest(starting_dr6);
+	printf("\nSoftware synthesized single-step\n");
+	check_db_exit(true, &post_wbinvd, DR_STEP, starting_dr6);
+
+	/*
+	 * Optional RTM test for hardware that supports RTM, to
+	 * demonstrate that the current volume 3 of the SDM
+	 * (325384-067US), table 27-1 is incorrect. Bit 16 of the exit
+	 * qualification for debug exceptions is not reserved. It is
+	 * set to 1 if a debug exception (#DB) or a breakpoint
+	 * exception (#BP) occurs inside an RTM region while advanced
+	 * debugging of RTM transactional regions is enabled.
+	 */
+	if (cpuid(7).b & BIT(11)) {
+		vmcs_write(ENT_CONTROLS,
+			   vmcs_read(ENT_CONTROLS) | ENT_LOAD_DBGCTLS);
+		/*
+		 * Set DR7.RTM[bit 11] and IA32_DEBUGCTL.RTM[bit 15]
+		 * in the guest to enable advanced debugging of RTM
+		 * transactional regions.
+		 */
+		vmcs_write(GUEST_DR7, BIT(11));
+		vmcs_write(GUEST_DEBUGCTL, BIT(15));
+		single_step_guest(starting_dr6);
+		printf("\nHardware delivered single-step in transactional region\n");
+		check_db_exit(false, &xbegin, BIT(16), starting_dr6);
+	} else {
+		vmcs_write(GUEST_RIP, (u64)&post_xbegin);
+		enter_guest();
+	}
+}
+
 static bool cpu_has_apicv(void)
 {
 	return ((ctrl_cpu_rev[1].clr & CPU_APIC_REG_VIRT) &&
@@ -5245,6 +5401,7 @@ struct vmx_test vmx_tests[] = {
 	/* Regression tests */
 	TEST(vmx_cr_load_test),
 	TEST(vmx_nm_test),
+	TEST(vmx_db_test),
 	/* EPT access tests. */
 	TEST(ept_access_test_not_present),
 	TEST(ept_access_test_read_only),
