[Lldb-commits] [lldb] [lldb][Windows] Identify the DebugBreakProcess break-in thread (PR #223985)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Wed Sep 16 05:16:06 PDT 2026
https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/223985
>From 12141fbd36eaf319fe5ed0e418d305c22752df07 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Wed, 16 Sep 2026 12:11:13 +0100
Subject: [PATCH] [lldb][Windows] Identify the DebugBreakProcess break-in
thread
---
.../Process/Windows/Common/DebuggerThread.cpp | 3 +-
.../Process/Windows/Common/IDebugDelegate.h | 3 +-
.../Windows/Common/LocalDebugDelegate.cpp | 5 +-
.../Windows/Common/LocalDebugDelegate.h | 3 +-
.../Windows/Common/NativeProcessWindows.cpp | 22 +++----
.../Windows/Common/NativeProcessWindows.h | 11 ++--
.../Windows/Common/ProcessDebugger.cpp | 50 ++++++++++++++-
.../Process/Windows/Common/ProcessDebugger.h | 13 +++-
.../Process/Windows/Common/ProcessWindows.cpp | 28 +++++----
.../Process/Windows/Common/ProcessWindows.h | 4 +-
lldb/test/API/windows/interrupt/Makefile | 3 +
.../windows/interrupt/TestWindowsInterrupt.py | 63 +++++++++++++++++++
lldb/test/API/windows/interrupt/main.c | 11 ++++
13 files changed, 178 insertions(+), 41 deletions(-)
create mode 100644 lldb/test/API/windows/interrupt/Makefile
create mode 100644 lldb/test/API/windows/interrupt/TestWindowsInterrupt.py
create mode 100644 lldb/test/API/windows/interrupt/main.c
diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
index 260ac0fe0faf6..4ba7fdd074a24 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
@@ -449,7 +449,8 @@ DebuggerThread::HandleCreateThreadEvent(const CREATE_THREAD_DEBUG_INFO &info,
m_process.GetProcessId());
HostThread thread(info.hThread);
thread.GetNativeThread().SetOwnsHandle(false);
- m_debug_delegate->OnCreateThread(thread);
+ m_debug_delegate->OnCreateThread(
+ thread, reinterpret_cast<lldb::addr_t>(info.lpStartAddress));
return DBG_CONTINUE;
}
diff --git a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
index cecd58a15f9c2..0a53cf61f0c8f 100644
--- a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
+++ b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
@@ -30,7 +30,8 @@ class IDebugDelegate {
virtual void OnDebuggerConnected(lldb::addr_t image_base) = 0;
virtual ExceptionResult OnDebugException(bool first_chance,
const ExceptionRecord &record) = 0;
- virtual void OnCreateThread(const HostThread &thread) = 0;
+ virtual void OnCreateThread(const HostThread &thread,
+ lldb::addr_t start_address) = 0;
virtual void OnExitThread(lldb::tid_t thread_id, uint32_t exit_code) = 0;
virtual DllEventAction OnLoadDll(const ModuleSpec &module_spec,
lldb::addr_t module_addr,
diff --git a/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp
index bbdc44e008331..20c023f0d6c47 100644
--- a/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp
@@ -34,9 +34,10 @@ LocalDebugDelegate::OnDebugException(bool first_chance,
return ExceptionResult::MaskException;
}
-void LocalDebugDelegate::OnCreateThread(const HostThread &thread) {
+void LocalDebugDelegate::OnCreateThread(const HostThread &thread,
+ lldb::addr_t start_address) {
if (ProcessWindowsSP process = GetProcessPointer())
- process->OnCreateThread(thread);
+ process->OnCreateThread(thread, start_address);
}
void LocalDebugDelegate::OnExitThread(lldb::tid_t thread_id,
diff --git a/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h
index f623d4e7d5436..d1c46f16258f1 100644
--- a/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h
+++ b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h
@@ -46,7 +46,8 @@ class LocalDebugDelegate : public IDebugDelegate {
void OnDebuggerConnected(lldb::addr_t image_base) override;
ExceptionResult OnDebugException(bool first_chance,
const ExceptionRecord &record) override;
- void OnCreateThread(const HostThread &thread) override;
+ void OnCreateThread(const HostThread &thread,
+ lldb::addr_t start_address) override;
void OnExitThread(lldb::tid_t thread_id, uint32_t exit_code) override;
DllEventAction OnLoadDll(const lldb_private::ModuleSpec &module_spec,
lldb::addr_t module_addr,
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index 0fa3eae2c7ed6..4808ebbe670da 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -180,13 +180,8 @@ NativeProcessWindows::GetThreadByID(lldb::tid_t thread_id) {
Status NativeProcessWindows::Halt() {
bool caused_stop = false;
StateType state = GetState();
- if (state != eStateStopped) {
- m_pending_halt = true;
- Status err = HaltProcess(caused_stop);
- if (err.Fail() || !caused_stop)
- m_pending_halt = false;
- return err;
- }
+ if (state != eStateStopped)
+ return HaltProcess(caused_stop);
return Status();
}
@@ -599,13 +594,13 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
}
// Our own DebugBreakProcess() injection, used to implement
- // Halt()/Interrupt().
- if (m_pending_halt) {
+ // Halt()/Interrupt(). The int3 runs on a thread the OS created for us, which
+ // is what tells it apart from an int3 the inferior itself executed.
+ if (IsBreakInThread(thread_id)) {
LLDB_LOG(log,
"DebugBreakProcess injection treated as Halt SIGSTOP for tid "
"{0:x}",
thread_id);
- m_pending_halt = false;
ThreadStopInfo signal_info;
signal_info.reason = StopReason::eStopReasonSignal;
signal_info.signo = 19; // SIGSTOP on POSIX
@@ -696,7 +691,10 @@ NativeProcessWindows::OnDebugException(bool first_chance,
return result;
}
-void NativeProcessWindows::OnCreateThread(const HostThread &new_thread) {
+void NativeProcessWindows::OnCreateThread(const HostThread &new_thread,
+ lldb::addr_t start_address) {
+ ProcessDebugger::OnCreateThread(new_thread, start_address);
+
llvm::sys::ScopedLock lock(m_mutex);
auto thread = std::make_unique<NativeThreadWindows>(*this, new_thread);
@@ -724,6 +722,8 @@ void NativeProcessWindows::OnCreateThread(const HostThread &new_thread) {
void NativeProcessWindows::OnExitThread(lldb::tid_t thread_id,
uint32_t exit_code) {
+ ProcessDebugger::OnExitThread(thread_id, exit_code);
+
std::lock_guard<std::recursive_mutex> guard(m_threads_mutex);
llvm::erase_if(m_threads, [thread_id](const auto &t) {
return t->GetID() == thread_id;
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index 114b4a200e18c..06e6787c8a748 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -131,7 +131,8 @@ class NativeProcessWindows : public NativeProcessProtocol,
void OnDebuggerConnected(lldb::addr_t image_base) override;
ExceptionResult OnDebugException(bool first_chance,
const ExceptionRecord &record) override;
- void OnCreateThread(const HostThread &thread) override;
+ void OnCreateThread(const HostThread &thread,
+ lldb::addr_t start_address) override;
void OnExitThread(lldb::tid_t thread_id, uint32_t exit_code) override;
DllEventAction OnLoadDll(const ModuleSpec &module_spec,
lldb::addr_t module_addr,
@@ -186,9 +187,6 @@ class NativeProcessWindows : public NativeProcessProtocol,
bool m_expecting_loader_int3 = false;
- /// Set when Halt() / Interrupt() schedules a DebugBreakProcess injection.
- bool m_pending_halt = false;
-
bool m_client_supports_libraries_read = false;
/// PseudoConsole for the lldb-server stdio-forwarding path.
@@ -229,8 +227,9 @@ class NativeDebugDelegate : public IDebugDelegate {
return m_process.OnDebugException(first_chance, record);
}
- void OnCreateThread(const HostThread &thread) override {
- m_process.OnCreateThread(thread);
+ void OnCreateThread(const HostThread &thread,
+ lldb::addr_t start_address) override {
+ m_process.OnCreateThread(thread, start_address);
}
void OnExitThread(lldb::tid_t thread_id, uint32_t exit_code) override {
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
index 51d47751ed05d..2c53a89db5cf0 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
@@ -14,6 +14,7 @@
#include "lldb/Host/FileSystem.h"
#include "lldb/Host/HostNativeProcessBase.h"
+#include "lldb/Host/HostNativeThread.h"
#include "lldb/Host/HostProcess.h"
#include "lldb/Host/HostThread.h"
#include "lldb/Host/ProcessLaunchInfo.h"
@@ -33,6 +34,25 @@
using namespace lldb;
using namespace lldb_private;
+/// Entry point of the thread DebugBreakProcess() injects into a target, i.e.
+/// ntdll!DbgUiRemoteBreakin. ntdll is mapped at the same address in every
+/// process of a given architecture for the lifetime of a boot, so the address
+/// resolved here is also the address the inferior reports in its
+/// CREATE_THREAD_DEBUG_EVENT. Returns LLDB_INVALID_ADDRESS if the export
+/// cannot be resolved.
+static lldb::addr_t GetBreakInThreadStartAddress() {
+ static const lldb::addr_t g_address = []() -> lldb::addr_t {
+ HMODULE ntdll = ::GetModuleHandleW(L"ntdll.dll");
+ if (!ntdll)
+ return LLDB_INVALID_ADDRESS;
+ FARPROC break_in = ::GetProcAddress(ntdll, "DbgUiRemoteBreakin");
+ if (!break_in)
+ return LLDB_INVALID_ADDRESS;
+ return reinterpret_cast<lldb::addr_t>(reinterpret_cast<void *>(break_in));
+ }();
+ return g_address;
+}
+
static void NormalizeWindowsPathSeparators(std::string &s) {
for (char &c : s)
if (c == '/')
@@ -597,12 +617,36 @@ ProcessDebugger::OnDebugException(bool first_chance,
return result;
}
-void ProcessDebugger::OnCreateThread(const HostThread &thread) {
- // Do nothing by default
+void ProcessDebugger::OnCreateThread(const HostThread &thread,
+ lldb::addr_t start_address) {
+ llvm::sys::ScopedLock lock(m_mutex);
+ if (!m_session_data)
+ return;
+
+ const lldb::addr_t break_in_start = GetBreakInThreadStartAddress();
+ if (break_in_start == LLDB_INVALID_ADDRESS || start_address != break_in_start)
+ return;
+
+ lldb::tid_t thread_id = thread.GetNativeThread().GetThreadId();
+ LLDB_LOG(GetLog(WindowsLog::Thread),
+ "thread {0} starts at ntdll!DbgUiRemoteBreakin, tracking it as a "
+ "break-in thread",
+ thread_id);
+ m_session_data->m_break_in_threads.insert(thread_id);
}
void ProcessDebugger::OnExitThread(lldb::tid_t thread_id, uint32_t exit_code) {
- // Do nothing by default
+ llvm::sys::ScopedLock lock(m_mutex);
+ // Windows reuses thread IDs, so this has to be dropped as soon as the thread
+ // is gone or a later thread inheriting the ID would be mistaken for it.
+ if (m_session_data)
+ m_session_data->m_break_in_threads.erase(thread_id);
+}
+
+bool ProcessDebugger::IsBreakInThread(lldb::tid_t thread_id) {
+ llvm::sys::ScopedLock lock(m_mutex);
+ return m_session_data &&
+ m_session_data->m_break_in_threads.count(thread_id) > 0;
}
DllEventAction ProcessDebugger::OnLoadDll(const ModuleSpec &module_spec,
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
index 99b59b1918a4f..60e8ae5f05014 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
@@ -47,6 +47,9 @@ class ProcessWindowsData {
bool m_stop_at_entry;
std::map<lldb::tid_t, lldb::ThreadSP> m_new_threads;
std::set<lldb::tid_t> m_exited_threads;
+ /// TIDs of the threads DebugBreakProcess() injects into the inferior. They
+ /// start at ntdll!DbgUiRemoteBreakin and exist only to run one int3.
+ std::set<lldb::tid_t> m_break_in_threads;
};
class ProcessDebugger {
@@ -58,7 +61,8 @@ class ProcessDebugger {
virtual void OnDebuggerConnected(lldb::addr_t image_base);
virtual ExceptionResult OnDebugException(bool first_chance,
const ExceptionRecord &record);
- virtual void OnCreateThread(const HostThread &thread);
+ virtual void OnCreateThread(const HostThread &thread,
+ lldb::addr_t start_address);
virtual void OnExitThread(lldb::tid_t thread_id, uint32_t exit_code);
virtual DllEventAction OnLoadDll(const ModuleSpec &module_spec,
lldb::addr_t module_addr,
@@ -73,6 +77,13 @@ class ProcessDebugger {
bool IsSystemModuleAddress(lldb::addr_t addr);
+ /// Whether `thread_id` is a break-in thread: one the OS injected into the
+ /// inferior on our behalf when HaltProcess() called DebugBreakProcess().
+ /// Such a thread runs ntdll!DbgUiRemoteBreakin, whose only job is to execute
+ /// an int3, so the EXCEPTION_BREAKPOINT it raises is the debugger's own
+ /// interrupt rather than anything the inferior did.
+ bool IsBreakInThread(lldb::tid_t thread_id);
+
protected:
Status DetachProcess();
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
index d72856bdc8d2c..16c08d7e503ec 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
@@ -301,12 +301,8 @@ Status ProcessWindows::DoDestroy() {
Status ProcessWindows::DoHalt(bool &caused_stop) {
StateType state = GetPrivateState();
- if (state != eStateStopped) {
- m_pending_halt = true;
- Status error = HaltProcess(caused_stop);
- if (error.Fail() || !caused_stop)
- m_pending_halt = false;
- }
+ if (state != eStateStopped)
+ return HaltProcess(caused_stop);
caused_stop = false;
return Status();
}
@@ -722,12 +718,13 @@ ProcessWindows::OnDebugException(bool first_chance,
switch (record.GetExceptionValue()) {
case EXCEPTION_BREAKPOINT: {
const lldb::addr_t bp_addr = record.GetExceptionAddress();
- if (m_pending_halt) {
- m_pending_halt = false;
- } else if (m_expecting_loader_int3 && first_chance &&
- m_session_data->m_initial_stop_received &&
- !GetBreakpointSiteList().FindByAddress(bp_addr) &&
- IsSystemModuleAddress(bp_addr)) {
+ // A break-in thread's int3 is the one we asked DebugBreakProcess() for, so
+ // it must reach the generic breakpoint handling below even though it lands
+ // in ntdll.
+ if (!IsBreakInThread(record.GetThreadID()) && m_expecting_loader_int3 &&
+ first_chance && m_session_data->m_initial_stop_received &&
+ !GetBreakpointSiteList().FindByAddress(bp_addr) &&
+ IsSystemModuleAddress(bp_addr)) {
m_expecting_loader_int3 = false;
LLDB_LOG(log,
"Skipping expected loader breakpoint at address {0:x} in a "
@@ -781,7 +778,10 @@ ProcessWindows::OnDebugException(bool first_chance,
return result;
}
-void ProcessWindows::OnCreateThread(const HostThread &new_thread) {
+void ProcessWindows::OnCreateThread(const HostThread &new_thread,
+ lldb::addr_t start_address) {
+ ProcessDebugger::OnCreateThread(new_thread, start_address);
+
llvm::sys::ScopedLock lock(m_mutex);
ThreadSP thread = std::make_shared<TargetThreadWindows>(*this, new_thread);
@@ -802,6 +802,8 @@ void ProcessWindows::OnCreateThread(const HostThread &new_thread) {
}
void ProcessWindows::OnExitThread(lldb::tid_t thread_id, uint32_t exit_code) {
+ ProcessDebugger::OnExitThread(thread_id, exit_code);
+
llvm::sys::ScopedLock lock(m_mutex);
// On a forced termination, we may get exit thread events after the session
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
index 73fd92f11cdff..38e16ddc69e9b 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
@@ -86,7 +86,8 @@ class ProcessWindows : public Process, public ProcessDebugger {
void OnDebuggerConnected(lldb::addr_t image_base) override;
ExceptionResult OnDebugException(bool first_chance,
const ExceptionRecord &record) override;
- void OnCreateThread(const HostThread &thread) override;
+ void OnCreateThread(const HostThread &thread,
+ lldb::addr_t start_address) override;
void OnExitThread(lldb::tid_t thread_id, uint32_t exit_code) override;
DllEventAction OnLoadDll(const ModuleSpec &module_spec,
lldb::addr_t module_addr,
@@ -133,7 +134,6 @@ class ProcessWindows : public Process, public ProcessDebugger {
std::map<lldb::break_id_t, WatchpointInfo> m_watchpoints;
std::vector<lldb::break_id_t> m_watchpoint_ids;
std::shared_ptr<PTY> m_pty;
- bool m_pending_halt = false;
bool m_expecting_loader_int3 = false;
};
} // namespace lldb_private
diff --git a/lldb/test/API/windows/interrupt/Makefile b/lldb/test/API/windows/interrupt/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/windows/interrupt/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/windows/interrupt/TestWindowsInterrupt.py b/lldb/test/API/windows/interrupt/TestWindowsInterrupt.py
new file mode 100644
index 0000000000000..e1da6d3f8df44
--- /dev/null
+++ b/lldb/test/API/windows/interrupt/TestWindowsInterrupt.py
@@ -0,0 +1,63 @@
+"""
+Test that interrupting a running process on Windows is reported as an
+interrupt rather than as an exception.
+
+Windows has no SIGSTOP, so a halt is implemented with DebugBreakProcess(),
+which injects a thread into the inferior that runs ntdll!DbgUiRemoteBreakin
+and executes an int3. lldb tells that int3 apart from one the inferior itself
+executed by the injected thread's entry point; if that stops working, the
+interrupt surfaces as a bare 0x80000003 exception instead.
+
+Only lldb-server maps the halt to a signal stop (NativeProcessWindows). The
+in-process ProcessWindows plugin has no such mapping: any int3 without a
+matching breakpoint site reaches RefreshStateAfterStop's default case and
+becomes eStopReasonException, so this test does not apply there.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+ at requireWindows
+ at skipIfWindowsAndNoLLDBServer
+class WindowsInterruptTestCase(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ def test_interrupt_is_not_reported_as_an_exception(self):
+ self.build()
+ (target, process, _, _) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.c")
+ )
+
+ self.setAsync(True)
+ listener = self.dbg.GetListener()
+
+ # Interrupt more than once: the injected thread has to be recognized
+ # every time, not just for the first halt.
+ for i in range(3):
+ process.Continue()
+ lldbutil.expect_state_changes(self, listener, process, [lldb.eStateRunning])
+
+ self.assertSuccess(process.Stop(), "interrupt #%d" % i)
+ lldbutil.expect_state_changes(self, listener, process, [lldb.eStateStopped])
+
+ for thread in process:
+ self.assertNotEqual(
+ thread.GetStopReason(),
+ lldb.eStopReasonException,
+ "interrupt #%d reported as an exception on thread %d: %s"
+ % (i, thread.GetThreadID(), thread.GetStopDescription(256)),
+ )
+
+ # The inferior must still run to completion once the loop is let go,
+ # i.e. the injected threads did not leave the process wedged.
+ self.setAsync(False)
+ keep_running = target.FindFirstGlobalVariable("keep_running")
+ self.assertTrue(keep_running.IsValid(), "found the loop's exit condition")
+ self.assertTrue(keep_running.SetValueFromCString("0"), "cleared keep_running")
+
+ process.Continue()
+ self.assertState(process.GetState(), lldb.eStateExited)
+ self.assertEqual(process.GetExitStatus(), 0)
diff --git a/lldb/test/API/windows/interrupt/main.c b/lldb/test/API/windows/interrupt/main.c
new file mode 100644
index 0000000000000..15d735d4297ae
--- /dev/null
+++ b/lldb/test/API/windows/interrupt/main.c
@@ -0,0 +1,11 @@
+#include <stdio.h>
+
+volatile int keep_running = 1;
+
+int main(int argc, char *argv[]) {
+ puts("running"); // break here
+ fflush(stdout);
+ while (keep_running)
+ ;
+ return 0;
+}
More information about the lldb-commits
mailing list