Kernel KVM virtualization development
 help / color / mirror / Atom feed
From: Sean Christopherson <seanjc@google.com>
To: Paolo Bonzini <pbonzini@redhat.com>
Cc: kvm@vger.kernel.org, Sean Christopherson <seanjc@google.com>
Subject: [kvm-unit-tests PATCH 3/4] x86/debug: Verify that KVM correctly emulates DR6 active-low and fixed-1 semantics
Date: Fri, 25 Sep 2026 16:00:02 -0700	[thread overview]
Message-ID: <20260925230003.2362261-4-seanjc@google.com> (raw)
In-Reply-To: <20260925230003.2362261-1-seanjc@google.com>

Run the main debug testcases with a control DR6 value (the value written to
DR6 prior to doing a #DB test) with both active-low bits set and active-low
bits clear.  Setting only one or the other means the test will miss bugs,
e.g. will fail to detect cases where hardware/KVM incorrectlys sets/clears
an active-low bit.

Note, DR6.RTM and DR6.BLD have different semantics.  DR6.RTM is modified by
all #DBs, i.e. is explicitly set/cleared based on whether or not a #DB
occurred in an RTM region.  DR6.BLD on the other hand is never supposed to
be set by hardware; it's cleared on Bus Lock #DBs, and otherwise isn't
modified.  Or at least, it's not supposed to be modified.  AMD CPUs appear
to have a ucode bug where DR6.BLD is forced to '1' on *all* writes to DR6
if Bus Lock Detect isn't fully enabled, including DR6 loads via VMRUN.  To
fudge around the quirk, simply enable BUS_LOCK_DETECT in DEBUGCTL so that
the test can write DR6.BLD at will.  As a bonus, enabling BUS_LOCK_DETECT
will help catch spurious Bus Lock #DBs.

Don't bother running the test that clobbers the #DB IDT entry directly
with both configurations; just run it once at the end to avoid having to
restore the IDT.

Signed-off-by: Sean Christopherson <seanjc@google.com>
---
 x86/debug.c | 109 +++++++++++++++++++++++++++++++++++++++-------------
 1 file changed, 83 insertions(+), 26 deletions(-)

diff --git a/x86/debug.c b/x86/debug.c
index c677af99..5427797f 100644
--- a/x86/debug.c
+++ b/x86/debug.c
@@ -16,6 +16,9 @@
 #include "desc.h"
 #include "usermode.h"
 
+static unsigned long dr6_control_value;
+static unsigned long dr6_base_value;
+
 static volatile unsigned long bp_addr;
 static volatile unsigned long db_addr[10], dr6[10];
 static volatile unsigned int n;
@@ -49,17 +52,17 @@ static void handle_db(struct ex_regs *regs)
 
 static inline bool is_single_step_db(unsigned long dr6_val)
 {
-	return dr6_val == (DR6_ACTIVE_LOW | DR6_BS);
+	return dr6_val == (dr6_base_value | DR6_BS);
 }
 
 static inline bool is_general_detect_db(unsigned long dr6_val)
 {
-	return dr6_val == (DR6_ACTIVE_LOW | DR6_BD);
+	return dr6_val == (dr6_base_value | DR6_BD);
 }
 
 static inline bool is_icebp_db(unsigned long dr6_val)
 {
-	return dr6_val == DR6_ACTIVE_LOW;
+	return dr6_val == dr6_base_value;
 }
 
 extern unsigned char handle_db_save_rip;
@@ -100,7 +103,7 @@ static void __run_single_step_db_test(db_test_fn test, db_report_fn report_fn)
 	bool ign;
 
 	n = 0;
-	write_dr6(DR6_ACTIVE_LOW);
+	write_dr6(dr6_control_value);
 
 	start = test();
 	report_fn(start, "");
@@ -114,7 +117,7 @@ static void __run_single_step_db_test(db_test_fn test, db_report_fn report_fn)
 		return;
 
 	n = 0;
-	write_dr6(DR6_ACTIVE_LOW);
+	write_dr6(dr6_control_value);
 
 	/*
 	 * Run the test in usermode.  Use the expected start RIP from the first
@@ -336,7 +339,7 @@ static void report_singlestep_with_movss_blocking_and_dr7_gd(unsigned long start
 
 static noinline unsigned long singlestep_with_movss_blocking_and_dr7_gd(void)
 {
-	unsigned long scratch = DR6_ACTIVE_LOW;
+	unsigned long scratch = dr6_control_value;
 
 	write_dr7(DR7_GD);
 
@@ -452,17 +455,34 @@ static void bus_lock_test(void)
 	got_ac = false;
 }
 
-int main(int ac, char **av)
+static void run_tests(unsigned long __dr6_control_value)
 {
+	u64 debugctl = rdmsr(MSR_IA32_DEBUGCTLMSR);
 	unsigned long cr4;
 
-	handle_exception(DB_VECTOR, handle_db);
-	handle_exception(BP_VECTOR, handle_bp);
-	handle_exception(UD_VECTOR, handle_ud);
-	handle_exception(AC_VECTOR, handle_ac);
+	dr6_control_value = __dr6_control_value;
+
+	/*
+	 * DR6.RTM is modified on all #DBs, and is '0' if and only if the #DB
+	 * occurred in an RTM region.  This test doesn't do RTM, and so DR6.RTM
+	 * should always be set, even if it's '0' in the control value.
+	 */
+	dr6_base_value = dr6_control_value | DR6_FIXED_1 | DR6_RTM;
+
+	/* DR6.BLD is fixed-1 if Bus Lock Detect is supported. */
+	if (!this_cpu_has(X86_FEATURE_BUS_LOCK_DETECT))
+		dr6_base_value |= DR6_BUS_LOCK;
 
 	bus_lock_test();
 
+	/*
+	 * Enable Bus Lock Detect to workaround an AMD ucode bug where DR6.BLD
+	 * is clobbered to '1', i.e. is "reset" (it's an active-low bit), on
+	 * *any* DR6 load, including asynchronous loads via #VMEXIT => VMRUN.
+	 */
+	if (this_cpu_has(X86_FEATURE_BUS_LOCK_DETECT))
+		wrmsr(MSR_IA32_DEBUGCTLMSR, debugctl | DEBUGCTLMSR_BUS_LOCK_DETECT);
+
 	/*
 	 * DR4 is an alias for DR6 (and DR5 aliases DR7) if CR4.DE is NOT set,
 	 * and is reserved if CR4.DE=1 (Debug Extensions enabled).
@@ -471,15 +491,16 @@ int main(int ac, char **av)
 	cr4 = read_cr4();
 	write_cr4(cr4 & ~X86_CR4_DE);
 	write_dr4(0);
-	write_dr6(DR6_ACTIVE_LOW | DR6_BS | DR6_TRAP1);
-	report(read_dr4() == (DR6_ACTIVE_LOW | DR6_BS | DR6_TRAP1) && !got_ud,
-	       "DR4==DR6 with CR4.DE == 0");
+	write_dr6(dr6_control_value | DR6_BS | DR6_TRAP1);
+	report(read_dr4() == read_dr6() && !got_ud,
+	       "DR4 (0x%lx) == DR6 (0x%lx) with CR4.DE == 0",
+	       read_dr4(), read_dr6());
 
 	cr4 = read_cr4();
 	write_cr4(cr4 | X86_CR4_DE);
 	read_dr4();
 	report(got_ud, "DR4 read got #UD with CR4.DE == 1");
-	write_dr6(DR6_ACTIVE_LOW);
+	write_dr6(dr6_control_value);
 
 	extern unsigned char sw_bp;
 	asm volatile("int3; sw_bp:");
@@ -500,21 +521,21 @@ int main(int ac, char **av)
 	asm volatile("hw_bp1: nop");
 	report(n == 1 &&
 	       db_addr[0] == ((unsigned long)&hw_bp1) &&
-	       dr6[0] == (DR6_ACTIVE_LOW | DR6_TRAP2),
+	       dr6[0] == (dr6_base_value | DR6_TRAP2),
 	       "Wanted #DB on 0x%lx w/ DR6 = 0x%lx, got %u #DBs, addr[0] = 0x%lx, DR6 = 0x%lx",
-	       ((unsigned long)&hw_bp1), DR6_ACTIVE_LOW | DR6_TRAP2,
+	       ((unsigned long)&hw_bp1), dr6_base_value | DR6_TRAP2,
 	       n, db_addr[0], dr6[0]);
 
 	n = 0;
 	extern unsigned char hw_bp2;
 	write_dr2(&hw_bp2);
-	write_dr6(DR6_ACTIVE_LOW | DR6_BS | DR6_TRAP1);
+	write_dr6(dr6_control_value | DR6_BS | DR6_TRAP1);
 	asm volatile("hw_bp2: nop");
 	report(n == 1 &&
 	       db_addr[0] == ((unsigned long)&hw_bp2) &&
-	       dr6[0] == (DR6_ACTIVE_LOW | DR6_BS | DR6_TRAP2),
+	       dr6[0] == (dr6_base_value | DR6_BS | DR6_TRAP2),
 	       "Wanted #DB on 0x%lx w/ DR6 = 0x%lx, got %u #DBs, addr[0] = 0x%lx, DR6 = 0x%lx",
-	       ((unsigned long)&hw_bp2), DR6_ACTIVE_LOW | DR6_BS | DR6_TRAP2,
+	       ((unsigned long)&hw_bp2), dr6_base_value | DR6_BS | DR6_TRAP2,
 	       n, db_addr[0], dr6[0]);
 
 	run_ss_db_test(singlestep_basic);
@@ -527,7 +548,7 @@ int main(int ac, char **av)
 
 	n = 0;
 	write_dr1((void *)&value);
-	write_dr6(DR6_ACTIVE_LOW | DR6_BS);
+	write_dr6(dr6_control_value | DR6_BS);
 	write_dr7(0x00d0040a); // 4-byte write
 
 	extern unsigned char hw_wp1;
@@ -537,11 +558,11 @@ int main(int ac, char **av)
 		: "=m" (value) : : "rax");
 	report(n == 1 &&
 	       db_addr[0] == ((unsigned long)&hw_wp1) &&
-	       dr6[0] == (DR6_ACTIVE_LOW | DR6_BS | DR6_TRAP1),
+	       dr6[0] == (dr6_base_value | DR6_BS | DR6_TRAP1),
 	       "hw watchpoint (test that dr6.BS is not cleared)");
 
 	n = 0;
-	write_dr6(DR6_ACTIVE_LOW);
+	write_dr6(dr6_control_value);
 
 	extern unsigned char hw_wp2;
 	asm volatile(
@@ -550,18 +571,36 @@ int main(int ac, char **av)
 		: "=m" (value) : : "rax");
 	report(n == 1 &&
 	       db_addr[0] == ((unsigned long)&hw_wp2) &&
-	       dr6[0] == (DR6_ACTIVE_LOW | DR6_TRAP1),
+	       dr6[0] == (dr6_base_value | DR6_TRAP1),
 	       "hw watchpoint (test that dr6.BS is not set)");
 
 	n = 0;
-	write_dr6(DR6_ACTIVE_LOW);
+	write_dr6(dr6_control_value);
 	extern unsigned char sw_icebp;
 	asm volatile(".byte 0xf1; sw_icebp:");
 	report(n == 1 &&
-	       db_addr[0] == (unsigned long)&sw_icebp && dr6[0] == DR6_ACTIVE_LOW,
+	       db_addr[0] == (unsigned long)&sw_icebp && dr6[0] == dr6_base_value,
 	       "icebp");
 
+	write_dr7(DR7_FIXED_1);
+	write_dr0(0);
+	write_dr1(0);
+	write_dr2(0);
+	write_dr3(0);
+	write_dr6(DR6_ACTIVE_LOW);
+	write_cr4(cr4);
+	wrmsr(MSR_IA32_DEBUGCTLMSR, debugctl);
+
+	n = 0;
+	value = 0;
+	got_ud = false;
+	got_ac = false;
+}
+
+static void test_watchpoints_precise(void)
+{
 	write_dr7(0x400);
+	write_dr1((void *)&value);
 	value = KERNEL_DS;
 	write_dr7(0x00f0040a); // 4-byte read or write
 
@@ -606,5 +645,23 @@ int main(int ac, char **av)
 	extern unsigned char sw_bp2;
 	report(n == 3 && bp_addr == (unsigned long)&sw_bp2,
 	       "MOV SS + watchpoint + INT3");
+}
+
+int main(int ac, char **av)
+{
+	handle_exception(DB_VECTOR, handle_db);
+	handle_exception(BP_VECTOR, handle_bp);
+	handle_exception(UD_VECTOR, handle_ud);
+	handle_exception(AC_VECTOR, handle_ac);
+
+	run_tests(DR6_ACTIVE_LOW);
+	run_tests(0);
+
+	/*
+	 * Run the "precise" test only once as it is destructive (clobbers the
+	 * #DB IDT entry).
+	 */
+	test_watchpoints_precise();
+
 	return report_summary();
 }
-- 
2.56.0.rc1.315.gc6ed9934b7-goog


  parent reply	other threads:[~2026-09-25 23:00 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-25 22:59 [kvm-unit-tests PATCH 0/4] x86/debug: Add Bus Lock Detect coverage Sean Christopherson
2026-09-25 23:00 ` [kvm-unit-tests PATCH 1/4] x86/debug: Set all active-low DR6 bits when resetting DR6 Sean Christopherson
2026-09-25 23:00 ` [kvm-unit-tests PATCH 2/4] x86/debug: Run the "debug" test with the host CPU model Sean Christopherson
2026-09-25 23:00 ` Sean Christopherson [this message]
2026-09-25 23:00 ` [kvm-unit-tests PATCH 4/4] x86/debug: Add actual test coverage for Bus Lock Detect #DBs Sean Christopherson
2026-10-02 21:07 ` [kvm-unit-tests PATCH 0/4] x86/debug: Add Bus Lock Detect coverage Sean Christopherson

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260925230003.2362261-4-seanjc@google.com \
    --to=seanjc@google.com \
    --cc=kvm@vger.kernel.org \
    --cc=pbonzini@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox