[Lldb-commits] [lldb] [lldb][Windows] Ignore stale traps delivered after a resume (PR #228440)

Charles Zablit via lldb-commits lldb-commits at lists.llvm.org
Fri Oct 2 06:45:01 PDT 2026


https://github.com/charles-zablit created https://github.com/llvm/llvm-project/pull/228440

A thread can hit a trap at the same time as the thread whose exception caused the stop. Windows only delivers one debug event at a time, so the second thread's event is not sent until the process is resumed. By then the trap it reports can be stale:

1. A [`STATUS_SINGLE_STEP`](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/specific-exceptions) for a thread that the current resume did not step. `NativeProcessWindows::HandleSingleStepException` reports it as a `eStopReasonTrace` stop. `NativeThreadWindows` now records whether its last `DoResume()` stepped it, and masks the trap which arrives too late.

2. A [`STATUS_BREAKPOINT`](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/specific-exceptions) for a breakpoint the client removed during the stop. The original byte is back in memory, so `HandleBreakpointException` does not find a breakpoint site and reports an exception stop, with the program counter one byte past the instruction. `RewindTrapOfRemovedBreakpoint()` detects this and moves the PC back onto the original instruction and masks the exception.

This is 1/2 patches. The second one (https://github.com/llvm/llvm-project/pull/227353) makes lldb-server collect every queued exception into one stop, which hits these late traps much more often, and re-enables the multi-threaded tests that depend on both.

>From 220086acb6c1cbe9bd30b2aac589c8f64139b0c4 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Fri, 2 Oct 2026 13:48:42 +0100
Subject: [PATCH] [lldb][Windows] Ignore stale traps delivered after a resume

---
 .../Windows/Common/NativeProcessWindows.cpp   | 42 ++++++++++++++++++-
 .../Windows/Common/NativeProcessWindows.h     |  7 ++++
 .../Windows/Common/NativeThreadWindows.cpp    |  1 +
 .../Windows/Common/NativeThreadWindows.h      |  4 ++
 4 files changed, 53 insertions(+), 1 deletion(-)

diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index 90c9102bcea58f..159fcf7b8b0f5c 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -580,6 +580,34 @@ void NativeProcessWindows::OnDebuggerConnected(lldb::addr_t image_base) {
       *this, m_session_data->m_debugger->GetMainThread()));
 }
 
+bool NativeProcessWindows::RewindTrapOfRemovedBreakpoint(
+    const ExceptionRecord &record) {
+  if (!m_initial_stop_seen)
+    return false;
+  NativeThreadWindows *thread = GetThreadByID(record.GetThreadID());
+  if (!thread)
+    return false;
+  const lldb::addr_t trap_addr = record.GetExceptionAddress();
+  llvm::ArrayRef<uint8_t> trap_opcode =
+      cantFail(GetSoftwareBreakpointTrapOpcode(0));
+  NativeRegisterContextWindows &reg_ctx = thread->GetRegisterContext();
+  if (reg_ctx.GetPC() != trap_addr + trap_opcode.size())
+    return false;
+  llvm::SmallVector<uint8_t, 4> bytes(trap_opcode.size(), 0);
+  size_t bytes_read = 0;
+  if (ProcessDebugger::ReadMemory(trap_addr, bytes.data(), bytes.size(),
+                                  bytes_read)
+          .Fail() ||
+      bytes_read != bytes.size() ||
+      llvm::ArrayRef<uint8_t>(bytes) == trap_opcode)
+    return false;
+  LLDB_LOG(GetLog(WindowsLog::Exception),
+           "Trap at {0:x} on thread {1:x} is from a removed breakpoint, "
+           "rewinding the thread onto the original instruction.",
+           trap_addr, thread->GetID());
+  return reg_ctx.SetPC(trap_addr).Success();
+}
+
 ExceptionResult
 NativeProcessWindows::HandleSingleStepException(const ExceptionRecord &record) {
   uint32_t wp_id = LLDB_INVALID_INDEX32;
@@ -603,8 +631,17 @@ NativeProcessWindows::HandleSingleStepException(const ExceptionRecord &record) {
     }
   }
 #endif
-  if (wp_id == LLDB_INVALID_INDEX32)
+  if (wp_id == LLDB_INVALID_INDEX32) {
+    NativeThreadWindows *thread = GetThreadByID(record.GetThreadID());
+    if (thread && !thread->IsSingleStepping()) {
+      LLDB_LOG(GetLog(WindowsLog::Exception),
+               "ignoring a late single-step trap on thread {0:x}, which this "
+               "resume did not step",
+               record.GetThreadID());
+      return ExceptionResult::MaskException;
+    }
     StopThread(record.GetThreadID(), StopReason::eStopReasonTrace);
+  }
 
   SetState(eStateStopped, true);
   return ExceptionResult::MaskException;
@@ -656,6 +693,9 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
         return ExceptionResult::MaskException;
       }
     }
+
+    if (RewindTrapOfRemovedBreakpoint(record))
+      return ExceptionResult::MaskException;
   }
 
   if (!m_initial_stop_seen) {
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index 114b4a200e18c3..e79596fecc9f80 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -167,6 +167,13 @@ class NativeProcessWindows : public NativeProcessProtocol,
   NativeProcessWindows(lldb::pid_t pid, int terminal_fd,
                        NativeDelegate &delegate, llvm::Error &E);
 
+  /// A breakpoint trap whose debug event was only delivered after the client
+  /// removed that breakpoint: the thread executed the trap before the stop
+  /// that removed it, but its event was held back until it ran again. Moves
+  /// the PC back onto the original instruction. Returns false if \p record is
+  /// not such a trap.
+  bool RewindTrapOfRemovedBreakpoint(const ExceptionRecord &record);
+
   ExceptionResult HandleSingleStepException(const ExceptionRecord &record);
   ExceptionResult HandleBreakpointException(const ExceptionRecord &record);
   ExceptionResult HandleGenericException(bool first_chance,
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeThreadWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeThreadWindows.cpp
index 0842968f25d603..0c6761e1027c03 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeThreadWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeThreadWindows.cpp
@@ -95,6 +95,7 @@ Status NativeThreadWindows::DoResume(lldb::StateType resume_state) {
 
     } while (previous_suspend_count > 1);
     m_state = eStateRunning;
+    m_single_stepping = resume_state == eStateStepping;
   }
 
   return Status();
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeThreadWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeThreadWindows.h
index 392a5b9a2a7d4a..fca4095ac88887 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeThreadWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeThreadWindows.h
@@ -28,6 +28,9 @@ class NativeThreadWindows : public NativeThreadProtocol {
   Status DoStop();
   Status DoResume(lldb::StateType resume_state);
 
+  /// Whether the last DoResume() single-stepped this thread.
+  bool IsSingleStepping() const { return m_single_stepping; }
+
   std::string GetName() override;
 
   lldb::StateType GetState() override { return m_state; }
@@ -55,6 +58,7 @@ class NativeThreadWindows : public NativeThreadProtocol {
 protected:
   lldb::StateType m_state = lldb::StateType::eStateInvalid;
   std::string m_name;
+  bool m_single_stepping = false;
   std::unique_ptr<NativeRegisterContextWindows> m_reg_context_up;
   // Cache address and index of the watchpoints and hardware breakpoints since
   // the register context does not.



More information about the lldb-commits mailing list