[llvm-branch-commits] [lldb] release/21.x: [lldb][Instrumentation] Set selected frame to outside sanitizer libraries (PR #157568)
via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Mon Sep 22 16:51:37 PDT 2025
https://github.com/llvmbot updated https://github.com/llvm/llvm-project/pull/157568
>From 54896838ca5e1f69f0f3040fe6847546db0463d4 Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Mon, 8 Sep 2025 17:18:25 +0100
Subject: [PATCH 1/4] [lldb][Target] Clear selected frame index after a
StopInfo::PerformAction (#133078)
The motivation for this patch is that
`StopInfo::GetSuggestedStackFrameIndex` would not take effect for
`InstrumentationRuntimeStopInfo` (which we plan to implement in
https://github.com/llvm/llvm-project/pull/133079). This was happening
because the instrumentation runtime plugins would run utility
expressions as part of the stop that would set the
`m_selected_frame_idx`. This means `SelectMostRelevantFrame` was never
called, and we would not be able to report the selected frame via the
`StopInfo` object.
This patch makes sure we clear the `m_selected_frame_idx` to allow
`GetSuggestedStackFrameIndex` to take effect, regardless of what the
frame recognizers choose to do.
(cherry picked from commit 39572f5e9168b1b44c2f9078494616fed8752086)
---
lldb/include/lldb/Target/StackFrameList.h | 12 ++++++++++++
lldb/include/lldb/Target/Thread.h | 5 +++++
lldb/source/Target/Process.cpp | 8 ++++++++
lldb/source/Target/StackFrameList.cpp | 2 ++
4 files changed, 27 insertions(+)
diff --git a/lldb/include/lldb/Target/StackFrameList.h b/lldb/include/lldb/Target/StackFrameList.h
index 8a66296346f2d..8d455dc831df3 100644
--- a/lldb/include/lldb/Target/StackFrameList.h
+++ b/lldb/include/lldb/Target/StackFrameList.h
@@ -46,6 +46,9 @@ class StackFrameList {
/// Mark a stack frame as the currently selected frame and return its index.
uint32_t SetSelectedFrame(lldb_private::StackFrame *frame);
+ /// Resets the selected frame index of this object.
+ void ClearSelectedFrameIndex();
+
/// Get the currently selected frame index.
/// We should only call SelectMostRelevantFrame if (a) the user hasn't already
/// selected a frame, and (b) if this really is a user facing
@@ -172,6 +175,15 @@ class StackFrameList {
/// The currently selected frame. An optional is used to record whether anyone
/// has set the selected frame on this stack yet. We only let recognizers
/// change the frame if this is the first time GetSelectedFrame is called.
+ ///
+ /// Thread-safety:
+ /// This member is not protected by a mutex.
+ /// LLDB really only should have an opinion about the selected frame index
+ /// when a process stops, before control gets handed back to the user.
+ /// After that, it's up to them to change it whenever they feel like it.
+ /// If two parts of lldb decided they wanted to be in control of the selected
+ /// frame index on stop the right way to fix it would need to be some explicit
+ /// negotiation for who gets to control this.
std::optional<uint32_t> m_selected_frame_idx;
/// The number of concrete frames fetched while filling the frame list. This
diff --git a/lldb/include/lldb/Target/Thread.h b/lldb/include/lldb/Target/Thread.h
index 6ede7fa301a82..688c056da2633 100644
--- a/lldb/include/lldb/Target/Thread.h
+++ b/lldb/include/lldb/Target/Thread.h
@@ -479,6 +479,11 @@ class Thread : public std::enable_shared_from_this<Thread>,
bool SetSelectedFrameByIndexNoisily(uint32_t frame_idx,
Stream &output_stream);
+ /// Resets the selected frame index of this object.
+ void ClearSelectedFrameIndex() {
+ return GetStackFrameList()->ClearSelectedFrameIndex();
+ }
+
void SetDefaultFileAndLineToSelectedFrame() {
GetStackFrameList()->SetDefaultFileAndLineToSelectedFrame();
}
diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp
index 2aa02fd58335e..a2fa88b569135 100644
--- a/lldb/source/Target/Process.cpp
+++ b/lldb/source/Target/Process.cpp
@@ -4257,6 +4257,14 @@ bool Process::ProcessEventData::ShouldStop(Event *event_ptr,
// appropriately. We also need to stop processing actions, since they
// aren't expecting the target to be running.
+ // Clear the selected frame which may have been set as part of utility
+ // expressions that have been run as part of this stop. If we didn't
+ // clear this, then StopInfo::GetSuggestedStackFrameIndex would not
+ // take affect when we next called SelectMostRelevantFrame.
+ // PerformAction should not be the one setting a selected frame, instead
+ // this should be done via GetSuggestedStackFrameIndex.
+ thread_sp->ClearSelectedFrameIndex();
+
// FIXME: we might have run.
if (stop_info_sp->HasTargetRunSinceMe()) {
SetRestarted(true);
diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index 16cd2548c2784..931b73b1e3633 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -936,3 +936,5 @@ size_t StackFrameList::GetStatus(Stream &strm, uint32_t first_frame,
strm.IndentLess();
return num_frames_displayed;
}
+
+void StackFrameList::ClearSelectedFrameIndex() { m_selected_frame_idx.reset(); }
>From faedeb1a242079c7fdea59d8b8487ab9d4c31bb9 Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Mon, 8 Sep 2025 18:03:31 +0100
Subject: [PATCH 2/4] [lldb][Instrumentation] Set selected frame to outside
sanitizer libraries (#133079)
When hitting a sanitizer breakpoint, LLDB currently displays the frame
in the sanitizer dylib (which we usually don't have debug-info for),
which isn't very helpful to the user. A more helpful frame to display
would be the first frame not in the sanitizer library (we have a
[similar heuristic when we trap inside
libc++](https://github.com/llvm/llvm-project/pull/108825)). This patch
does just that, by implementing the `GetSuggestedStackFrameIndex` API
Depends on https://github.com/llvm/llvm-project/pull/133078
(cherry picked from commit 879f40ab041b31fa73b9b25e4ec9e06e810bc767)
---
.../Target/InstrumentationRuntimeStopInfo.h | 3 ++
.../Target/InstrumentationRuntimeStopInfo.cpp | 42 +++++++++++++++++++
.../functionalities/asan/TestMemoryHistory.py | 8 ++++
.../functionalities/asan/TestReportData.py | 4 ++
.../ubsan/basic/TestUbsanBasic.py | 4 +-
5 files changed, 59 insertions(+), 2 deletions(-)
diff --git a/lldb/include/lldb/Target/InstrumentationRuntimeStopInfo.h b/lldb/include/lldb/Target/InstrumentationRuntimeStopInfo.h
index 5345160850914..dafa41c11327a 100644
--- a/lldb/include/lldb/Target/InstrumentationRuntimeStopInfo.h
+++ b/lldb/include/lldb/Target/InstrumentationRuntimeStopInfo.h
@@ -24,6 +24,9 @@ class InstrumentationRuntimeStopInfo : public StopInfo {
return lldb::eStopReasonInstrumentation;
}
+ std::optional<uint32_t>
+ GetSuggestedStackFrameIndex(bool inlined_stack) override;
+
const char *GetDescription() override;
bool DoShouldNotify(Event *event_ptr) override { return true; }
diff --git a/lldb/source/Target/InstrumentationRuntimeStopInfo.cpp b/lldb/source/Target/InstrumentationRuntimeStopInfo.cpp
index 7f82581cc601e..aef895def7939 100644
--- a/lldb/source/Target/InstrumentationRuntimeStopInfo.cpp
+++ b/lldb/source/Target/InstrumentationRuntimeStopInfo.cpp
@@ -8,13 +8,20 @@
#include "lldb/Target/InstrumentationRuntimeStopInfo.h"
+#include "lldb/Core/Module.h"
#include "lldb/Target/InstrumentationRuntime.h"
#include "lldb/Target/Process.h"
+#include "lldb/lldb-enumerations.h"
#include "lldb/lldb-private.h"
using namespace lldb;
using namespace lldb_private;
+static bool IsStoppedInDarwinSanitizer(Thread &thread, Module &module) {
+ return module.GetFileSpec().GetFilename().GetStringRef().starts_with(
+ "libclang_rt.");
+}
+
InstrumentationRuntimeStopInfo::InstrumentationRuntimeStopInfo(
Thread &thread, std::string description,
StructuredData::ObjectSP additional_data)
@@ -34,3 +41,38 @@ InstrumentationRuntimeStopInfo::CreateStopReasonWithInstrumentationData(
return StopInfoSP(
new InstrumentationRuntimeStopInfo(thread, description, additionalData));
}
+
+std::optional<uint32_t>
+InstrumentationRuntimeStopInfo::GetSuggestedStackFrameIndex(
+ bool inlined_stack) {
+ ThreadSP thread_sp = GetThread();
+ if (!thread_sp)
+ return std::nullopt;
+
+ // Defensive upper-bound of when we stop walking up the frames in
+ // case we somehow ended up looking at an infinite recursion.
+ constexpr size_t max_stack_depth = 128;
+
+ // Start at parent frame.
+ size_t stack_idx = 1;
+ StackFrameSP most_relevant_frame_sp =
+ thread_sp->GetStackFrameAtIndex(stack_idx);
+
+ while (most_relevant_frame_sp && stack_idx <= max_stack_depth) {
+ auto const &sc =
+ most_relevant_frame_sp->GetSymbolContext(lldb::eSymbolContextModule);
+
+ if (!sc.module_sp)
+ return std::nullopt;
+
+ // Found a frame outside of the sanitizer runtime libraries.
+ // That's the one we want to display.
+ if (!IsStoppedInDarwinSanitizer(*thread_sp, *sc.module_sp))
+ return stack_idx;
+
+ ++stack_idx;
+ most_relevant_frame_sp = thread_sp->GetStackFrameAtIndex(stack_idx);
+ }
+
+ return stack_idx;
+}
diff --git a/lldb/test/API/functionalities/asan/TestMemoryHistory.py b/lldb/test/API/functionalities/asan/TestMemoryHistory.py
index 1568140b355dc..0a2ae96a557ac 100644
--- a/lldb/test/API/functionalities/asan/TestMemoryHistory.py
+++ b/lldb/test/API/functionalities/asan/TestMemoryHistory.py
@@ -96,6 +96,10 @@ def libsanitizers_asan_tests(self):
)
self.check_traces(skip_line_numbers=True)
+ # Make sure we're not stopped in the sanitizer library but instead at the
+ # point of failure in the user-code.
+ self.assertEqual(self.frame().GetFunctionName(), "main")
+
# do the same using SB API
process = self.dbg.GetSelectedTarget().process
val = (
@@ -220,6 +224,10 @@ def compiler_rt_asan_tests(self):
self.check_traces()
+ # Make sure we're not stopped in the sanitizer library but instead at the
+ # point of failure in the user-code.
+ self.assertEqual(self.frame().GetFunctionName(), "main")
+
# make sure the 'memory history' command still works even when we're
# generating a report now
self.expect(
diff --git a/lldb/test/API/functionalities/asan/TestReportData.py b/lldb/test/API/functionalities/asan/TestReportData.py
index dd6834a01b80c..ccc1b846d1607 100644
--- a/lldb/test/API/functionalities/asan/TestReportData.py
+++ b/lldb/test/API/functionalities/asan/TestReportData.py
@@ -67,6 +67,10 @@ def asan_tests(self, libsanitizers=False):
lldb.eStopReasonInstrumentation,
)
+ # Make sure we're not stopped in the sanitizer library but instead at the
+ # point of failure in the user-code.
+ self.assertEqual(self.frame().GetFunctionName(), "main")
+
self.expect(
"bt",
"The backtrace should show the crashing line",
diff --git a/lldb/test/API/functionalities/ubsan/basic/TestUbsanBasic.py b/lldb/test/API/functionalities/ubsan/basic/TestUbsanBasic.py
index 868a2864d2b5e..f46d167d910ea 100644
--- a/lldb/test/API/functionalities/ubsan/basic/TestUbsanBasic.py
+++ b/lldb/test/API/functionalities/ubsan/basic/TestUbsanBasic.py
@@ -52,8 +52,8 @@ def ubsan_tests(self):
substrs=["1 match found"],
)
- # We should be stopped in __ubsan_on_report
- self.assertIn("__ubsan_on_report", frame.GetFunctionName())
+ # We should not be stopped in the sanitizer library.
+ self.assertIn("main", frame.GetFunctionName())
# The stopped thread backtrace should contain either 'align line'
found = False
>From f5ba88341e7c4f7427e33c8237c07c3c5f1e189c Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Mon, 8 Sep 2025 19:25:20 +0100
Subject: [PATCH 3/4] [lldb][test] TestTsanBasic.py: fix function name
assertion
Since https://github.com/llvm/llvm-project/pull/133079 we no longer stop
in the stanitizer library when hitting a sanitizer breakpoint. Adjust
the test accordingly.
(cherry picked from commit bdf645bb9b509b60bdb6a71d865b4f8999187977)
---
lldb/test/API/functionalities/tsan/basic/TestTsanBasic.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lldb/test/API/functionalities/tsan/basic/TestTsanBasic.py b/lldb/test/API/functionalities/tsan/basic/TestTsanBasic.py
index ca8b74e35dff6..2747b2b442195 100644
--- a/lldb/test/API/functionalities/tsan/basic/TestTsanBasic.py
+++ b/lldb/test/API/functionalities/tsan/basic/TestTsanBasic.py
@@ -63,11 +63,11 @@ def tsan_tests(self):
substrs=["1 match found"],
)
- # We should be stopped in __tsan_on_report
+ # We should not be stopped in the sanitizer library.
process = self.dbg.GetSelectedTarget().process
thread = process.GetSelectedThread()
frame = thread.GetSelectedFrame()
- self.assertIn("__tsan_on_report", frame.GetFunctionName())
+ self.assertIn("f2", frame.GetFunctionName())
# The stopped thread backtrace should contain either line1 or line2
# from main.c.
>From 2c8cb316b5d2b0121d7d364d30b86b4310fb0809 Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Mon, 8 Sep 2025 22:32:49 +0100
Subject: [PATCH 4/4] [lldb][test] Only assert function name is in user-code on
Darwin platforms
The frame recognizer for the instrumentation runtimes (added in
https://github.com/llvm/llvm-project/pull/133079) only triggers on Darwin
for `libclang_rt`. Adjust the tests accordingly.
(cherry picked from commit 5326b3b176e82191b18ffc368118b36e0103af3d)
---
.../functionalities/asan/TestMemoryHistory.py | 16 +++++++++-------
.../API/functionalities/asan/TestReportData.py | 9 +++++----
.../functionalities/tsan/basic/TestTsanBasic.py | 7 +++++--
.../ubsan/basic/TestUbsanBasic.py | 7 +++++--
4 files changed, 24 insertions(+), 15 deletions(-)
diff --git a/lldb/test/API/functionalities/asan/TestMemoryHistory.py b/lldb/test/API/functionalities/asan/TestMemoryHistory.py
index 0a2ae96a557ac..302a45c2368de 100644
--- a/lldb/test/API/functionalities/asan/TestMemoryHistory.py
+++ b/lldb/test/API/functionalities/asan/TestMemoryHistory.py
@@ -2,7 +2,6 @@
Test that ASan memory history provider returns correct stack traces
"""
-
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
@@ -10,6 +9,7 @@
from lldbsuite.test import lldbutil
from lldbsuite.test_event.build_exception import BuildError
+
class MemoryHistoryTestCase(TestBase):
@skipIfFreeBSD # llvm.org/pr21136 runtimes not yet available by default
@expectedFailureNetBSD
@@ -96,9 +96,10 @@ def libsanitizers_asan_tests(self):
)
self.check_traces(skip_line_numbers=True)
- # Make sure we're not stopped in the sanitizer library but instead at the
- # point of failure in the user-code.
- self.assertEqual(self.frame().GetFunctionName(), "main")
+ if self.platformIsDarwin():
+ # Make sure we're not stopped in the sanitizer library but instead at the
+ # point of failure in the user-code.
+ self.assertEqual(self.frame().GetFunctionName(), "main")
# do the same using SB API
process = self.dbg.GetSelectedTarget().process
@@ -224,9 +225,10 @@ def compiler_rt_asan_tests(self):
self.check_traces()
- # Make sure we're not stopped in the sanitizer library but instead at the
- # point of failure in the user-code.
- self.assertEqual(self.frame().GetFunctionName(), "main")
+ if self.platformIsDarwin():
+ # Make sure we're not stopped in the sanitizer library but instead at the
+ # point of failure in the user-code.
+ self.assertEqual(self.frame().GetFunctionName(), "main")
# make sure the 'memory history' command still works even when we're
# generating a report now
diff --git a/lldb/test/API/functionalities/asan/TestReportData.py b/lldb/test/API/functionalities/asan/TestReportData.py
index ccc1b846d1607..c832436b0f447 100644
--- a/lldb/test/API/functionalities/asan/TestReportData.py
+++ b/lldb/test/API/functionalities/asan/TestReportData.py
@@ -2,7 +2,6 @@
Test the AddressSanitizer runtime support for report breakpoint and data extraction.
"""
-
import json
import lldb
from lldbsuite.test.decorators import *
@@ -10,6 +9,7 @@
from lldbsuite.test import lldbutil
from lldbsuite.test_event.build_exception import BuildError
+
class AsanTestReportDataCase(TestBase):
@skipIfFreeBSD # llvm.org/pr21136 runtimes not yet available by default
@expectedFailureNetBSD
@@ -67,9 +67,10 @@ def asan_tests(self, libsanitizers=False):
lldb.eStopReasonInstrumentation,
)
- # Make sure we're not stopped in the sanitizer library but instead at the
- # point of failure in the user-code.
- self.assertEqual(self.frame().GetFunctionName(), "main")
+ if self.platformIsDarwin():
+ # Make sure we're not stopped in the sanitizer library but instead at the
+ # point of failure in the user-code.
+ self.assertEqual(self.frame().GetFunctionName(), "main")
self.expect(
"bt",
diff --git a/lldb/test/API/functionalities/tsan/basic/TestTsanBasic.py b/lldb/test/API/functionalities/tsan/basic/TestTsanBasic.py
index 2747b2b442195..51a28c5013071 100644
--- a/lldb/test/API/functionalities/tsan/basic/TestTsanBasic.py
+++ b/lldb/test/API/functionalities/tsan/basic/TestTsanBasic.py
@@ -63,11 +63,14 @@ def tsan_tests(self):
substrs=["1 match found"],
)
- # We should not be stopped in the sanitizer library.
process = self.dbg.GetSelectedTarget().process
thread = process.GetSelectedThread()
frame = thread.GetSelectedFrame()
- self.assertIn("f2", frame.GetFunctionName())
+ if self.platformIsDarwin():
+ # We should not be stopped in the sanitizer library.
+ self.assertIn("f2", frame.GetFunctionName())
+ else:
+ self.assertIn("__tsan_on_report", frame.GetFunctionName())
# The stopped thread backtrace should contain either line1 or line2
# from main.c.
diff --git a/lldb/test/API/functionalities/ubsan/basic/TestUbsanBasic.py b/lldb/test/API/functionalities/ubsan/basic/TestUbsanBasic.py
index f46d167d910ea..9e9ea2114196e 100644
--- a/lldb/test/API/functionalities/ubsan/basic/TestUbsanBasic.py
+++ b/lldb/test/API/functionalities/ubsan/basic/TestUbsanBasic.py
@@ -52,8 +52,11 @@ def ubsan_tests(self):
substrs=["1 match found"],
)
- # We should not be stopped in the sanitizer library.
- self.assertIn("main", frame.GetFunctionName())
+ if self.platformIsDarwin():
+ # We should not be stopped in the sanitizer library.
+ self.assertIn("main", frame.GetFunctionName())
+ else:
+ self.assertIn("__ubsan_on_report", frame.GetFunctionName())
# The stopped thread backtrace should contain either 'align line'
found = False
More information about the llvm-branch-commits
mailing list