[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 ®_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