[Lldb-commits] [lldb] 83530ce - [lldb][Windows] ignore loader breakpoints in system modules (#208233)

via lldb-commits lldb-commits at lists.llvm.org
Thu Jul 23 02:16:53 PDT 2026


Author: Charles Zablit
Date: 2026-07-23T11:16:49+02:00
New Revision: 83530ce356dd55fdc981b5325ca1f5117b4a58f8

URL: https://github.com/llvm/llvm-project/commit/83530ce356dd55fdc981b5325ca1f5117b4a58f8
DIFF: https://github.com/llvm/llvm-project/commit/83530ce356dd55fdc981b5325ca1f5117b4a58f8.diff

LOG: [lldb][Windows] ignore loader breakpoints in system modules (#208233)

Currently, when debugging a program with `lldb-dap` on Windows and using
the `integratedTerminal` option, lldb-dap immediatly stops with an
`0x80000003` Exception. This is because `ntdll` executes an `int3`
breakpoint during process initialization when a debugger is attached.

This patch makes `lldb` and `lldb-server` skip the first `int3` after
launch when it originates from a system module (the loader's debugger
notification). Only that first loader breakpoint is skipped. Any later
int3, including `__debugbreak()`, `__builtin_debugtrap()` in the
debuggee's own code, still stops the debugger.

Fixes https://github.com/llvm/llvm-project/issues/198763

Added: 
    lldb/test/API/attach/Makefile
    lldb/test/API/attach/TestWindowsAttachBreakpoint.py
    lldb/test/API/attach/main.c

Modified: 
    lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
    lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
    lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
    lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
    lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
    lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
    lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py

Removed: 
    


################################################################################
diff  --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index ada92894efc47..f87fd23f5a047 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -50,44 +50,6 @@ using namespace llvm;
 
 namespace lldb_private {
 
-namespace {
-
-void NormalizeWindowsPath(std::string &s) {
-  for (char &c : s) {
-    if (c == '/')
-      c = '\\';
-    else
-      c = std::tolower(static_cast<unsigned char>(c));
-  }
-}
-
-bool IsSystemDLL(const FileSpec &spec) {
-  if (!spec)
-    return false;
-
-  static const std::string windows_prefix = []() {
-    std::string prefix;
-    wchar_t buf[MAX_PATH];
-    UINT len = ::GetWindowsDirectoryW(buf, MAX_PATH);
-    if (len == 0 || len >= MAX_PATH)
-      return prefix;
-    llvm::convertWideToUTF8(std::wstring_view(buf, len), prefix);
-    NormalizeWindowsPath(prefix);
-    if (!prefix.empty() && prefix.back() != '\\')
-      prefix += '\\';
-    return prefix;
-  }();
-
-  if (windows_prefix.empty())
-    return false;
-
-  std::string path = spec.GetPath();
-  NormalizeWindowsPath(path);
-  return llvm::StringRef(path).starts_with(windows_prefix);
-}
-
-} // namespace
-
 NativeProcessWindows::NativeProcessWindows(ProcessLaunchInfo &launch_info,
                                            NativeDelegate &delegate,
                                            llvm::Error &E)
@@ -122,6 +84,8 @@ NativeProcessWindows::NativeProcessWindows(lldb::pid_t pid, int terminal_fd,
   if (E)
     return;
 
+  m_expecting_loader_int3 = true;
+
   SetID(GetDebuggedProcessId());
 
   ProcessInstanceInfo info;
@@ -635,9 +599,8 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
     return ExceptionResult::BreakInDebugger;
   }
 
-  // Any remaining STATUS_BREAKPOINT is a breakpoint instruction in the
-  // program's own code (e.g. `__debugbreak()` or `__builtin_debugtrap()`).
-  // Stop the debugger and let the user decide what to do.
+  // Our own DebugBreakProcess() injection, used to implement
+  // Halt()/Interrupt().
   if (m_pending_halt) {
     LLDB_LOG(log,
              "DebugBreakProcess injection treated as Halt SIGSTOP for tid "
@@ -664,6 +627,15 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
     return ExceptionResult::BreakInDebugger;
   }
 
+  if (m_expecting_loader_int3 && IsSystemModuleAddress(exception_addr)) {
+    m_expecting_loader_int3 = false;
+    LLDB_LOG(log,
+             "Skipping expected loader breakpoint at address {0:x} in a "
+             "system module.",
+             exception_addr);
+    return ExceptionResult::MaskException;
+  }
+
   std::string desc = formatv("Exception {0:x8} encountered at address {1:x8}",
                              record.GetExceptionValue(), exception_addr)
                          .str();
@@ -776,7 +748,7 @@ DllEventAction NativeProcessWindows::OnLoadDll(const ModuleSpec &module_spec,
     return DllEventAction::ContinueDebugLoop;
 
   // Can't resolve a breakpoint in a system DLL.
-  if (!resolved || IsSystemDLL(resolved))
+  if (!resolved || ProcessDebugger::IsSystemDLL(resolved.GetPath()))
     return DllEventAction::ContinueDebugLoop;
 
   NativeThreadWindows *loader_thread = GetThreadByID(thread_id);
@@ -819,7 +791,7 @@ DllEventAction NativeProcessWindows::OnUnloadDll(lldb::addr_t module_addr,
   if (!m_initial_stop_seen || !m_client_supports_libraries_read)
     return DllEventAction::ContinueDebugLoop;
 
-  if (!unloaded_spec || IsSystemDLL(unloaded_spec))
+  if (!unloaded_spec || ProcessDebugger::IsSystemDLL(unloaded_spec.GetPath()))
     return DllEventAction::ContinueDebugLoop;
 
   NativeThreadWindows *unloader_thread = GetThreadByID(thread_id);

diff  --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index fcf5d8f3c4753..17469f18fbc73 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -182,6 +182,8 @@ class NativeProcessWindows : public NativeProcessProtocol,
   /// launch / attach.
   bool m_initial_stop_seen = false;
 
+  bool m_expecting_loader_int3 = false;
+
   /// Set when Halt() / Interrupt() schedules a DebugBreakProcess injection.
   bool m_pending_halt = false;
 

diff  --git a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
index 63fc20f36b07b..6594336fde655 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
@@ -19,6 +19,7 @@
 #include "lldb/Host/ProcessLaunchInfo.h"
 #include "lldb/Target/MemoryRegionInfo.h"
 #include "lldb/Target/Process.h"
+#include "lldb/Utility/FileSpec.h"
 #include "llvm/Support/ConvertUTF.h"
 #include "llvm/Support/Error.h"
 
@@ -26,9 +27,73 @@
 #include "ExceptionRecord.h"
 #include "ProcessWindowsLog.h"
 
+#include <string>
+#include <string_view>
+
 using namespace lldb;
 using namespace lldb_private;
 
+static void NormalizeWindowsPathSeparators(std::string &s) {
+  for (char &c : s)
+    if (c == '/')
+      c = '\\';
+}
+
+bool ProcessDebugger::IsSystemDLL(llvm::StringRef path) {
+  if (path.empty())
+    return false;
+
+  static const std::string windows_prefix = []() {
+    std::string prefix;
+    wchar_t buf[MAX_PATH];
+    UINT len = ::GetWindowsDirectoryW(buf, MAX_PATH);
+    if (len == 0 || len >= MAX_PATH)
+      return prefix;
+    llvm::convertWideToUTF8(std::wstring_view(buf, len), prefix);
+    NormalizeWindowsPathSeparators(prefix);
+    if (!prefix.empty() && prefix.back() != '\\')
+      prefix += '\\';
+    return prefix;
+  }();
+
+  if (windows_prefix.empty())
+    return false;
+
+  std::string normalized = path.str();
+  NormalizeWindowsPathSeparators(normalized);
+  return llvm::StringRef(normalized).starts_with_insensitive(windows_prefix);
+}
+
+bool ProcessDebugger::IsSystemModuleAddress(lldb::addr_t addr) {
+  if (!m_session_data || !m_session_data->m_debugger)
+    return false;
+  lldb::process_t handle = m_session_data->m_debugger->GetProcess()
+                               .GetNativeProcess()
+                               .GetSystemHandle();
+  if (handle == nullptr || handle == LLDB_INVALID_PROCESS)
+    return false;
+
+  MEMORY_BASIC_INFORMATION mbi = {};
+  if (::VirtualQueryEx(handle, reinterpret_cast<LPCVOID>(addr), &mbi,
+                       sizeof(mbi)) != sizeof(mbi))
+    return false;
+  if (mbi.AllocationBase == nullptr)
+    return false;
+
+  // A truncated path still carries the leading directory, which is all
+  // IsSystemDLL() inspects. MAX_PATH is enough.
+  wchar_t module_path[MAX_PATH];
+  DWORD len = ::GetModuleFileNameExW(
+      handle, reinterpret_cast<HMODULE>(mbi.AllocationBase), module_path,
+      MAX_PATH);
+  if (len == 0)
+    return false;
+
+  std::string path_utf8;
+  llvm::convertWideToUTF8(std::wstring_view(module_path, len), path_utf8);
+  return IsSystemDLL(path_utf8);
+}
+
 static DWORD ConvertLldbToWinApiProtect(uint32_t protect) {
   // We also can process a read / write permissions here, but if the debugger
   // will make later a write into the allocated memory, it will fail. To get

diff  --git a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
index 77b7dfff14268..99b59b1918a4f 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
@@ -15,6 +15,7 @@
 #include "lldb/lldb-forward.h"
 #include "lldb/lldb-types.h"
 #include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/ErrorExtras.h"
 #include "llvm/Support/Mutex.h"
@@ -68,6 +69,10 @@ class ProcessDebugger {
                              uint16_t length_lower_word);
   virtual void OnDebuggerError(const Status &error, uint32_t type);
 
+  static bool IsSystemDLL(llvm::StringRef path);
+
+  bool IsSystemModuleAddress(lldb::addr_t addr);
+
 protected:
   Status DetachProcess();
 

diff  --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
index 7a196820cf744..2aac8dfde2c7b 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
@@ -15,6 +15,7 @@
 #include <psapi.h>
 
 #include "lldb/Breakpoint/Watchpoint.h"
+#include "lldb/Core/Address.h"
 #include "lldb/Core/IOHandler.h"
 #include "lldb/Core/Module.h"
 #include "lldb/Core/ModuleSpec.h"
@@ -231,6 +232,9 @@ ProcessWindows::DoAttachToProcessWithID(lldb::pid_t pid,
   Status error = AttachProcess(pid, attach_info, delegate);
   if (error.Success())
     SetID(GetDebuggedProcessId());
+
+  m_expecting_loader_int3 = true;
+
   return error;
 }
 
@@ -297,8 +301,12 @@ Status ProcessWindows::DoDestroy() {
 
 Status ProcessWindows::DoHalt(bool &caused_stop) {
   StateType state = GetPrivateState();
-  if (state != eStateStopped)
-    return HaltProcess(caused_stop);
+  if (state != eStateStopped) {
+    m_pending_halt = true;
+    Status error = HaltProcess(caused_stop);
+    if (error.Fail() || !caused_stop)
+      m_pending_halt = false;
+  }
   caused_stop = false;
   return Status();
 }
@@ -712,7 +720,22 @@ ProcessWindows::OnDebugException(bool first_chance,
 
   ExceptionResult result = ExceptionResult::SendToApplication;
   switch (record.GetExceptionValue()) {
-  case EXCEPTION_BREAKPOINT:
+  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)) {
+      m_expecting_loader_int3 = false;
+      LLDB_LOG(log,
+               "Skipping expected loader breakpoint at address {0:x} in a "
+               "system module.",
+               bp_addr);
+      return ExceptionResult::MaskException;
+    }
+
     // Handle breakpoints at the first chance.
     result = ExceptionResult::BreakInDebugger;
 
@@ -735,6 +758,7 @@ ProcessWindows::OnDebugException(bool first_chance,
     DrainProcessStdout();
     SetPrivateState(eStateStopped);
     break;
+  }
   case EXCEPTION_SINGLE_STEP:
     result = ExceptionResult::BreakInDebugger;
     DrainProcessStdout();

diff  --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
index 71db5e57f83b1..2d2f3ca59ac70 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
@@ -133,6 +133,8 @@ 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/attach/Makefile b/lldb/test/API/attach/Makefile
new file mode 100644
index 0000000000000..451278a0946ef
--- /dev/null
+++ b/lldb/test/API/attach/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
\ No newline at end of file

diff  --git a/lldb/test/API/attach/TestWindowsAttachBreakpoint.py b/lldb/test/API/attach/TestWindowsAttachBreakpoint.py
new file mode 100644
index 0000000000000..e40745c54bebf
--- /dev/null
+++ b/lldb/test/API/attach/TestWindowsAttachBreakpoint.py
@@ -0,0 +1,160 @@
+"""
+Test that lldb ignores the Windows loader breakpoint when attaching to a
+process, but still stops at a genuine breakpoint instruction (an int3) that
+lives in the program's own code.
+"""
+
+import ctypes
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class WindowsAttachLoaderBreakpointTestCase(TestBase):
+    def create_suspended_process(self, exe):
+        """Create ``exe`` suspended and return its (pid, hProcess, hThread).
+
+        This mirrors what the lldb-dap runInTerminal launcher does on Windows:
+        the debuggee is started with CREATE_SUSPENDED so lldb can attach while
+        the process is still being initialized by the loader. The launcher then
+        resumes the main thread once the debugger has attached.
+        """
+        from ctypes import wintypes
+
+        CREATE_SUSPENDED = 0x00000004
+
+        class STARTUPINFOW(ctypes.Structure):
+            _fields_ = [
+                ("cb", wintypes.DWORD),
+                ("lpReserved", wintypes.LPWSTR),
+                ("lpDesktop", wintypes.LPWSTR),
+                ("lpTitle", wintypes.LPWSTR),
+                ("dwX", wintypes.DWORD),
+                ("dwY", wintypes.DWORD),
+                ("dwXSize", wintypes.DWORD),
+                ("dwYSize", wintypes.DWORD),
+                ("dwXCountChars", wintypes.DWORD),
+                ("dwYCountChars", wintypes.DWORD),
+                ("dwFillAttribute", wintypes.DWORD),
+                ("dwFlags", wintypes.DWORD),
+                ("wShowWindow", wintypes.WORD),
+                ("cbReserved2", wintypes.WORD),
+                ("lpReserved2", ctypes.POINTER(ctypes.c_byte)),
+                ("hStdInput", wintypes.HANDLE),
+                ("hStdOutput", wintypes.HANDLE),
+                ("hStdError", wintypes.HANDLE),
+            ]
+
+        class PROCESS_INFORMATION(ctypes.Structure):
+            _fields_ = [
+                ("hProcess", wintypes.HANDLE),
+                ("hThread", wintypes.HANDLE),
+                ("dwProcessId", wintypes.DWORD),
+                ("dwThreadId", wintypes.DWORD),
+            ]
+
+        kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
+        kernel32.CreateProcessW.argtypes = [
+            wintypes.LPCWSTR,
+            wintypes.LPWSTR,
+            ctypes.c_void_p,
+            ctypes.c_void_p,
+            wintypes.BOOL,
+            wintypes.DWORD,
+            ctypes.c_void_p,
+            wintypes.LPCWSTR,
+            ctypes.POINTER(STARTUPINFOW),
+            ctypes.POINTER(PROCESS_INFORMATION),
+        ]
+        kernel32.CreateProcessW.restype = wintypes.BOOL
+
+        startupinfo = STARTUPINFOW()
+        startupinfo.cb = ctypes.sizeof(STARTUPINFOW)
+        process_information = PROCESS_INFORMATION()
+
+        # CreateProcessW may modify the command-line buffer, so it must be
+        # writable.
+        command_line = ctypes.create_unicode_buffer('"{}"'.format(exe))
+
+        if not kernel32.CreateProcessW(
+            exe,
+            command_line,
+            None,
+            None,
+            False,
+            CREATE_SUSPENDED,
+            None,
+            None,
+            ctypes.byref(startupinfo),
+            ctypes.byref(process_information),
+        ):
+            raise OSError(ctypes.get_last_error(), "CreateProcessW failed")
+
+        return (
+            process_information.dwProcessId,
+            process_information.hProcess,
+            process_information.hThread,
+        )
+
+    @skipUnlessWindows
+    def test_attach_ignores_loader_breakpoint(self):
+        """
+        lldb must not report the loader's int3 (raised in a system module while
+        attaching) as a user-visible stop, but must still stop at the
+        __builtin_debugtrap() in the program's own code.
+        """
+        self.build()
+        exe = self.getBuildArtifact("a.out")
+
+        pid, hProcess, hThread = self.create_suspended_process(exe)
+
+        kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
+        kernel32.ResumeThread.argtypes = [ctypes.c_void_p]
+        kernel32.ResumeThread.restype = ctypes.c_uint
+
+        def cleanup():
+            kernel32.TerminateProcess(hProcess, 0)
+            kernel32.CloseHandle(hThread)
+            kernel32.CloseHandle(hProcess)
+
+        self.addTearDownHook(cleanup)
+
+        self.dbg.SetAsync(False)
+
+        # Attach while the process is suspended and still being initialized.
+        target = self.dbg.CreateTarget(exe)
+        self.assertTrue(target, VALID_TARGET)
+
+        error = lldb.SBError()
+        process = target.AttachToProcessWithID(self.dbg.GetListener(), pid, error)
+        self.assertSuccess(error, "attach to the suspended process")
+        self.assertState(process.GetState(), lldb.eStateStopped)
+
+        self.assertNotEqual(
+            kernel32.ResumeThread(hThread), 0xFFFFFFFF, "ResumeThread failed"
+        )
+
+        # Continuing must run past the loader breakpoint (skipped because it is
+        # raised in a system module) and stop at the program's own
+        # __builtin_debugtrap().
+        process.Continue()
+        self.assertState(process.GetState(), lldb.eStateStopped)
+
+        thread = lldbutil.get_stopped_thread(process, lldb.eStopReasonException)
+        self.assertIsNotNone(
+            thread, "the process should stop at the user __builtin_debugtrap()"
+        )
+        function_name = thread.GetFrameAtIndex(0).GetFunctionName()
+        self.assertIn(
+            "main",
+            function_name,
+            "expected to stop at the program's __builtin_debugtrap(), but "
+            "stopped in '{}' (a spurious loader breakpoint in a system module "
+            "was not skipped)".format(function_name),
+        )
+
+        process.Continue()
+        self.assertState(process.GetState(), lldb.eStateExited)
+        self.assertEqual(process.GetExitStatus(), 0)

diff  --git a/lldb/test/API/attach/main.c b/lldb/test/API/attach/main.c
new file mode 100644
index 0000000000000..9ca1aa0ad8d6f
--- /dev/null
+++ b/lldb/test/API/attach/main.c
@@ -0,0 +1,4 @@
+int main(int argc, char *argv[]) {
+  __builtin_debugtrap();
+  return 0;
+}
\ No newline at end of file

diff  --git a/lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py b/lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py
index 429051c9cf9f9..4e09efadfdcdc 100644
--- a/lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py
+++ b/lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py
@@ -66,7 +66,6 @@ def read_pipe_message(pipe):
 
 
 @skipIfBuildType(["debug"])
- at skipIfWindows  # https://github.com/llvm/llvm-project/issues/198763
 class TestDAP_runInTerminal(lldbdap_testcase.DAPTestCaseBase):
     SHARED_BUILD_TESTCASE = False
 


        


More information about the lldb-commits mailing list