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"]) -@skipIfWindows # https://github.com/llvm/llvm-project/issues/198763 class TestDAP_runInTerminal(lldbdap_testcase.DAPTestCaseBase): SHARED_BUILD_TESTCASE = False _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
