llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT--> @llvm/pr-subscribers-lldb Author: John Harrison (ashgti) <details> <summary>Changes</summary> In the stopped event and the exceptionInfo request I've added additional information for crash data, instrumentation data and more detailed exception data. For example, when UBSan is enabled, you now see additional information in the exception stack trace about the detected issue: <img width="1728" height="538" alt="Screenshot 2026-01-15 at 3 05 08 PM" src="https://github.com/user-attachments/assets/f7d197b1-8f31-4470-bde1-b319d00a8a6e" /> I included a new test for stopping at `lldb::eStopReasonInstrumentation` and ensuring we have additional information reported. --- Patch is 38.14 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/176273.diff 19 Files Affected: - (modified) lldb/packages/Python/lldbsuite/support/temp_file.py (+14-4) - (modified) lldb/packages/Python/lldbsuite/test/decorators.py (+1) - (modified) lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py (+30-18) - (modified) lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py (+6-2) - (modified) lldb/test/API/tools/lldb-dap/exception/Makefile (+1) - (modified) lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py (+17-1) - (modified) lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py (+2-3) - (modified) lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py (+5-5) - (added) lldb/test/API/tools/lldb-dap/exception/runtime-instruments/Makefile (+4) - (added) lldb/test/API/tools/lldb-dap/exception/runtime-instruments/TestDAP_runtime_instruments.py (+25) - (added) lldb/test/API/tools/lldb-dap/exception/runtime-instruments/categories (+1) - (added) lldb/test/API/tools/lldb-dap/exception/runtime-instruments/main.c (+5) - (modified) lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py (+1-2) - (modified) lldb/tools/lldb-dap/EventHelper.cpp (+85-38) - (modified) lldb/tools/lldb-dap/Handler/ExceptionInfoRequestHandler.cpp (+63-25) - (modified) lldb/tools/lldb-dap/JSONUtils.cpp (-157) - (modified) lldb/tools/lldb-dap/JSONUtils.h (-30) - (modified) lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp (+47) - (modified) lldb/tools/lldb-dap/Protocol/ProtocolEvents.h (+62) ``````````diff diff --git a/lldb/packages/Python/lldbsuite/support/temp_file.py b/lldb/packages/Python/lldbsuite/support/temp_file.py index a21e212d279d6..17fdd9f7d4eca 100644 --- a/lldb/packages/Python/lldbsuite/support/temp_file.py +++ b/lldb/packages/Python/lldbsuite/support/temp_file.py @@ -9,15 +9,25 @@ class OnDiskTempFile: - def __init__(self, delete=True): + def __init__(self, delete=True, immediate=False): self.path = None + if immediate: + self._set_path() - def __enter__(self): + def __del__(self): + if self.path and os.path.exists(self.path): + os.remove(self.path) + + def _set_path(self): + if self.path: + return fd, path = tempfile.mkstemp() os.close(fd) self.path = path + + def __enter__(self): + self._set_path() return self def __exit__(self, exc_type, exc_val, exc_tb): - if os.path.exists(self.path): - os.remove(self.path) + pass diff --git a/lldb/packages/Python/lldbsuite/test/decorators.py b/lldb/packages/Python/lldbsuite/test/decorators.py index a7df9fe63badc..b1516bf214a87 100644 --- a/lldb/packages/Python/lldbsuite/test/decorators.py +++ b/lldb/packages/Python/lldbsuite/test/decorators.py @@ -1114,6 +1114,7 @@ def is_compiler_with_bounds_safety(): return skipTestIfFn(is_compiler_with_bounds_safety)(func) + def skipIfAsan(func): """Skip this test if the environment is set up to run LLDB *itself* under ASAN.""" return skipTestIfFn(is_running_under_asan)(func) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py index e4a3e1c786ffe..b2cac4dfb0e5b 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py @@ -208,25 +208,40 @@ def verify_all_breakpoints_hit(self, breakpoint_ids): return self.assertTrue(False, f"breakpoints not hit, stopped_events={stopped_events}") - def verify_stop_exception_info(self, expected_description): + def verify_stop_exception_info( + self, expected_description: str, expected_text: Optional[str] = None + ) -> None: """Wait for the process we are debugging to stop, and verify the stop reason is 'exception' and that the description matches 'expected_description' """ stopped_events = self.dap_server.wait_for_stopped() + self.assertIsNotNone(stopped_events, "No stopped events detected") for stopped_event in stopped_events: - if "body" in stopped_event: - body = stopped_event["body"] - if "reason" not in body: - continue - if body["reason"] != "exception": - continue - if "description" not in body: - continue - description = body["description"] - if expected_description == description: - return True - return False + if ( + "body" not in stopped_event + or stopped_event["body"]["reason"] != "exception" + ): + continue + self.assertIn( + "description", + stopped_event["body"], + f"stopped event missing description {stopped_event}", + ) + description = stopped_event["body"]["description"] + self.assertRegex( + description, + expected_description, + f"for 'stopped' event {stopped_event!r}", + ) + if expected_text: + self.assertRegex( + stopped_event["body"]["text"], + expected_text, + f"for stopped event {stopped_event!r}", + ) + return + self.fail(f"No valid stop exception info detected in {stopped_events}") def verify_stop_on_entry(self) -> None: """Waits for the process to be stopped and then verifies at least one @@ -437,12 +452,9 @@ def continue_to_breakpoints(self, breakpoint_ids): self.do_continue() self.verify_breakpoint_hit(breakpoint_ids) - def continue_to_exception_breakpoint(self, filter_label): + def continue_to_exception_breakpoint(self, description, text=None): self.do_continue() - self.assertTrue( - self.verify_stop_exception_info(filter_label), - 'verify we got "%s"' % (filter_label), - ) + self.verify_stop_exception_info(description, text) def continue_to_exit(self, exitCode=0): self.do_continue() diff --git a/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py b/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py index 4ca733a9a59ca..684726c927bc1 100644 --- a/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py +++ b/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py @@ -35,5 +35,9 @@ def test_functionality(self): if response: self.assertTrue(response["success"]) - self.continue_to_exception_breakpoint("C++ Throw") - self.continue_to_exception_breakpoint("C++ Catch") + self.continue_to_exception_breakpoint( + r"breakpoint \d+\.\d+", text=r"C\+\+ Throw" + ) + self.continue_to_exception_breakpoint( + r"breakpoint \d+\.\d+", text=r"C\+\+ Catch" + ) diff --git a/lldb/test/API/tools/lldb-dap/exception/Makefile b/lldb/test/API/tools/lldb-dap/exception/Makefile index 10495940055b6..b27db90a40de2 100644 --- a/lldb/test/API/tools/lldb-dap/exception/Makefile +++ b/lldb/test/API/tools/lldb-dap/exception/Makefile @@ -1,3 +1,4 @@ C_SOURCES := main.c +CFLAGS_EXTRAS := -fsanitize=undefined -g include Makefile.rules diff --git a/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py b/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py index f044bcae41892..0cb48f345474e 100644 --- a/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py +++ b/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py @@ -18,8 +18,24 @@ def test_stopped_description(self): self.build_and_launch(program) self.do_continue() - self.assertTrue(self.verify_stop_exception_info("signal SIGABRT")) + self.verify_stop_exception_info("signal SIGABRT") exceptionInfo = self.get_exceptionInfo() self.assertEqual(exceptionInfo["breakMode"], "always") self.assertEqual(exceptionInfo["description"], "signal SIGABRT") self.assertEqual(exceptionInfo["exceptionId"], "signal") + + # @skipUnlessUndefinedBehaviorSanitizer + def test_ubsan(self): + """ + Test that we stop at runtime instrumentation locations. + """ + program = self.getBuildArtifact("a.out") + self.build_and_launch(program, args=["ubsan"]) + self.do_continue() + + self.verify_stop_exception_info("Out of bounds index") + exceptionInfo = self.get_exceptionInfo() + self.assertEqual(exceptionInfo["breakMode"], "always") + self.assertEqual("Out of bounds index", exceptionInfo["description"]) + self.assertEqual(exceptionInfo["exceptionId"], "runtime-instrumentation") + self.assertIn("main.c", exceptionInfo["details"]["stackTrace"]) diff --git a/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py b/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py index 6471e2b87251a..4729cbef00c11 100644 --- a/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py +++ b/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py @@ -2,7 +2,6 @@ Test exception behavior in DAP with c++ throw. """ - from lldbsuite.test.decorators import * from lldbsuite.test.lldbtest import * import lldbdap_testcase @@ -18,9 +17,9 @@ def test_stopped_description(self): program = self.getBuildArtifact("a.out") self.build_and_launch(program) self.dap_server.request_continue() - self.assertTrue(self.verify_stop_exception_info("signal SIGABRT")) + self.verify_stop_exception_info("signal SIGABRT") exceptionInfo = self.get_exceptionInfo() self.assertEqual(exceptionInfo["breakMode"], "always") - self.assertEqual(exceptionInfo["description"], "signal SIGABRT") + self.assertIn("signal SIGABRT", exceptionInfo["description"]) self.assertEqual(exceptionInfo["exceptionId"], "signal") self.assertIsNotNone(exceptionInfo["details"]) diff --git a/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py b/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py index ddedf7a6de8c6..40233af4b2bd6 100644 --- a/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py +++ b/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py @@ -16,10 +16,10 @@ def test_stopped_description(self): program = self.getBuildArtifact("a.out") self.build_and_launch(program) self.dap_server.request_continue() - self.assertTrue(self.verify_stop_exception_info("signal SIGABRT")) + self.verify_stop_exception_info("signal SIGABRT") exception_info = self.get_exceptionInfo() self.assertEqual(exception_info["breakMode"], "always") - self.assertEqual(exception_info["description"], "signal SIGABRT") + self.assertIn("signal SIGABRT", exception_info["description"]) self.assertEqual(exception_info["exceptionId"], "signal") exception_details = exception_info["details"] self.assertRegex(exception_details["message"], "SomeReason") @@ -44,7 +44,7 @@ def test_break_on_throw_and_catch(self): if response: self.assertTrue(response["success"]) - self.continue_to_exception_breakpoint("Objective-C Throw") + self.continue_to_exception_breakpoint("hit Objective-C exception") # FIXME: Catching objc exceptions do not appear to be working. # Xcode appears to set a breakpoint on '__cxa_begin_catch' for objc @@ -54,10 +54,10 @@ def test_break_on_throw_and_catch(self): self.do_continue() - self.assertTrue(self.verify_stop_exception_info("signal SIGABRT")) + self.verify_stop_exception_info("signal SIGABRT") exception_info = self.get_exceptionInfo() self.assertEqual(exception_info["breakMode"], "always") - self.assertEqual(exception_info["description"], "signal SIGABRT") + self.assertIn("signal SIGABRT", exception_info["description"]) self.assertEqual(exception_info["exceptionId"], "signal") exception_details = exception_info["details"] self.assertRegex(exception_details["message"], "SomeReason") diff --git a/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/Makefile b/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/Makefile new file mode 100644 index 0000000000000..b27db90a40de2 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/Makefile @@ -0,0 +1,4 @@ +C_SOURCES := main.c +CFLAGS_EXTRAS := -fsanitize=undefined -g + +include Makefile.rules diff --git a/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/TestDAP_runtime_instruments.py b/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/TestDAP_runtime_instruments.py new file mode 100644 index 0000000000000..caff7dd1bedcd --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/TestDAP_runtime_instruments.py @@ -0,0 +1,25 @@ +""" +Test that we stop at runtime instrumentation locations. +""" + +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +import lldbdap_testcase + + +class TestDAP_runtime_instruments(lldbdap_testcase.DAPTestCaseBase): + @skipUnlessUndefinedBehaviorSanitizer + def test_ubsan(self): + """ + Test that we stop at ubsan. + """ + program = self.getBuildArtifact("a.out") + self.build_and_launch(program) + self.do_continue() + + self.verify_stop_exception_info("Out of bounds index") + exceptionInfo = self.get_exceptionInfo() + self.assertEqual(exceptionInfo["breakMode"], "always") + self.assertEqual("Out of bounds index", exceptionInfo["description"]) + self.assertEqual(exceptionInfo["exceptionId"], "runtime-instrumentation") + self.assertIn("main.c", exceptionInfo["details"]["stackTrace"]) diff --git a/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/categories b/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/categories new file mode 100644 index 0000000000000..33fbf4be1bb2f --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/categories @@ -0,0 +1 @@ +instrumentation-runtime \ No newline at end of file diff --git a/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/main.c b/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/main.c new file mode 100644 index 0000000000000..9434b8e2d7583 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/exception/runtime-instruments/main.c @@ -0,0 +1,5 @@ +int main(int argc, char const *argv[]) { + int data[4] = {0}; + int *p = data + 5; // ubsan + return *p; +} diff --git a/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py b/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py index acd6108853787..be6dd84ec4d44 100644 --- a/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py +++ b/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py @@ -39,8 +39,7 @@ def test_correct_thread(self): "breakpoint %s." % breakpoint_ids[0] ) ) - self.assertFalse(stopped_event[0]["body"]["preserveFocusHint"]) - self.assertTrue(stopped_event[0]["body"]["threadCausedFocus"]) + self.assertNotIn("preserveFocusHint", stopped_event[0]["body"]) # All threads should be named Thread {index} threads = self.dap_server.get_threads() self.assertTrue(all(len(t["name"]) > 0 for t in threads)) diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp index 6c5a9127f131b..6764146ab8551 100644 --- a/lldb/tools/lldb-dap/EventHelper.cpp +++ b/lldb/tools/lldb-dap/EventHelper.cpp @@ -25,9 +25,12 @@ #include "lldb/API/SBListener.h" #include "lldb/API/SBPlatform.h" #include "lldb/API/SBStream.h" +#include "lldb/lldb-types.h" #include "llvm/Support/Error.h" +#include "llvm/Support/ErrorHandling.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/Threading.h" +#include "llvm/Support/raw_ostream.h" #include <mutex> #include <utility> @@ -188,54 +191,98 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) { llvm::DenseSet<lldb::tid_t> old_thread_ids; old_thread_ids.swap(dap.thread_ids); - uint32_t stop_id = on_entry ? 0 : process.GetStopID(); const uint32_t num_threads = process.GetNumThreads(); - // First make a pass through the threads to see if the focused thread - // has a stop reason. In case the focus thread doesn't have a stop - // reason, remember the first thread that has a stop reason so we can - // set it as the focus thread if below if needed. - lldb::tid_t first_tid_with_reason = LLDB_INVALID_THREAD_ID; - uint32_t num_threads_with_reason = 0; - bool focus_thread_exists = false; + lldb::tid_t stopped_thread_idx = 0; for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) { lldb::SBThread thread = process.GetThreadAtIndex(thread_idx); - const lldb::tid_t tid = thread.GetThreadID(); - const bool has_reason = ThreadHasStopReason(thread); - // If the focus thread doesn't have a stop reason, clear the thread ID - if (tid == dap.focus_tid) { - focus_thread_exists = true; - if (!has_reason) - dap.focus_tid = LLDB_INVALID_THREAD_ID; - } - if (has_reason) { - ++num_threads_with_reason; - if (first_tid_with_reason == LLDB_INVALID_THREAD_ID) - first_tid_with_reason = tid; - } + dap.thread_ids.insert(thread.GetThreadID()); + + if (stopped_thread_idx || !ThreadHasStopReason(thread)) + continue; + + // Stop at the first thread with a stop reason. + stopped_thread_idx = thread_idx; } - // We will have cleared dap.focus_tid if the focus thread doesn't have - // a stop reason, so if it was cleared, or wasn't set, or doesn't exist, - // then set the focus thread to the first thread with a stop reason. - if (!focus_thread_exists || dap.focus_tid == LLDB_INVALID_THREAD_ID) - dap.focus_tid = first_tid_with_reason; - - // If no threads stopped with a reason, then report the first one so - // we at least let the UI know we stopped. - if (num_threads_with_reason == 0) { - lldb::SBThread thread = process.GetThreadAtIndex(0); - dap.focus_tid = thread.GetThreadID(); - dap.SendJSON(CreateThreadStopped(dap, thread, stop_id)); + lldb::SBThread thread = process.GetThreadAtIndex(stopped_thread_idx); + assert(thread.IsValid() && "no valid thread found, process not stopped"); + + protocol::StoppedEventBody body; + if (on_entry) { + body.reason = protocol::eStopReasonEntry; } else { - for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) { - lldb::SBThread thread = process.GetThreadAtIndex(thread_idx); - dap.thread_ids.insert(thread.GetThreadID()); - if (ThreadHasStopReason(thread)) { - dap.SendJSON(CreateThreadStopped(dap, thread, stop_id)); + switch (thread.GetStopReason()) { + case lldb::eStopReasonTrace: + case lldb::eStopReasonPlanComplete: + body.reason = protocol::eStopReasonStep; + break; + case lldb::eStopReasonBreakpoint: { + ExceptionBreakpoint *exc_bp = dap.GetExceptionBPFromStopReason(thread); + if (exc_bp) { + body.reason = protocol::eStopReasonException; + body.text = exc_bp->GetLabel(); + } else { + InstructionBreakpoint *inst_bp = + dap.GetInstructionBPFromStopReason(thread); + body.reason = inst_bp ? protocol::eStopReasonInstructionBreakpoint + : protocol::eStopReasonBreakpoint; + + llvm::raw_string_ostream OS(body.text); + OS << "breakpoint"; + for (size_t idx = 0; idx < thread.GetStopReasonDataCount(); idx += 2) { + lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(idx); + lldb::break_id_t bp_loc_id = thread.GetStopReasonDataAtIndex(idx + 1); + body.hitBreakpointIds.push_back(bp_id); + OS << " " << bp_id << "." << bp_loc_id; + } } + } break; + case lldb::eStopReasonWatchpoint: { + body.reason = protocol::eStopReasonDataBreakpoint; + lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(0); + body.hitBreakpointIds.push_back(bp_id); + body.text = llvm::formatv("data breakpoint {0}", bp_id).str(); + } break; + case lldb::eStopReasonProcessorTrace: + body.reason = protocol::eStopReasonStep; // fallback reason + break; + case lldb::eStopReasonHistoryBoundary: + body.reason = protocol::eStopReasonStep; // fallback reason + break; + case lldb::eStopReasonSignal: + case lldb::eStopReasonException: + case lldb::eStopReasonInstrumentation: + body.reason = protocol::eStopReasonException; + break; + case lldb::eStopReasonExec: + case lldb::eStopReasonFork: + case lldb::eStopReasonVFork: + case lldb::eStopReasonVForkDone: + body.reason = protocol::eStopReasonEntry; + break; + case lldb::eStopReasonInterrupt: + body.reason = protocol::eStopReasonPause; + break; + case lldb::eStopReasonThreadExiting: + case lldb::eStopReasonInvalid: + case lldb::eStopReasonNone: + llvm_unreachable("invalid stop reason, thread is not stopped"); + break; } } + lldb::tid_t tid = thread.GetThreadID(); + lldb::SBStream description; + thread.GetStopDescription(description); + body.description = description.GetData(); + body.threadId = tid; + body.preserveFocusHint = tid == dap.focus_tid; + body.allThreadsStopped = true; + + // Update focused thread. + dap.focus_tid = tid; + + dap.Send(protocol::Event{"stopped"... [truncated] `````````` </details> https://github.com/llvm/llvm-project/pull/176273 _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
