Skip to content

Conversation

yln
Copy link
Collaborator

@yln yln commented Aug 19, 2025

When providing allocation and deallocation traces,
the ASan compiler-rt runtime already provides call
addresses (TracePCType::Calls).

On Darwin, system sanitizers (libsanitizers)
provides return address. It also discards a few
non-user frames at the top of the stack, because
these internal libmalloc/libsanitizers stack
frames do not provide any value when diagnosing
memory errors.

Introduce and add handling for
TracePCType::ReturnsNoZerothFrame to cover this
case and enable libsanitizers traces line-level
testing.

rdar://157596927


Commit 1 is a mechanical refactoring to introduce
and adopt TracePCType enum to replace
pcs_are_call_addresses bool. It preserve the
current behavior:

pcs_are_call_addresses:
  false  ->  TracePCType::Returns (default)
  true   ->  TracePCType::Calls

Best reviewed commit by commit.

@llvmbot
Copy link
Member

llvmbot commented Aug 19, 2025

@llvm/pr-subscribers-lldb

Author: Julian Lettner (yln)

Changes

When providing allocation and deallocation traces,
the ASan compiler-rt runtime already provides call
addresses (TracePCType::Calls).

On Darwin, system sanitizers (libsanitizers)
provides return address. It also discards a few
non-user frames at the top of the stack, because
these internal libmalloc/libsanitizers stack
frames do not provide any value when diagnosing
memory errors.

Introduce and add handling for
TracePCType::ReturnsNoZerothFrame to cover this
case and enable libsanitizers traces line-level
testing.

rdar://157596927


Commit 1 is a mechanical refactoring to introduce
and adopt TracePCType enum to replace
pcs_are_call_addresses bool. It preserve the
current behavior:

pcs_are_call_addresses:
  false  ->  TracePCType::Returns (default)
  true   ->  TracePCType::Calls

Best reviewed commit by commit.


Full diff: https://github.com/llvm/llvm-project/pull/154247.diff

10 Files Affected:

  • (modified) lldb/include/lldb/lldb-private-enumerations.h (+2)
  • (modified) lldb/source/Plugins/InstrumentationRuntime/MainThreadChecker/InstrumentationRuntimeMainThreadChecker.cpp (+3-3)
  • (modified) lldb/source/Plugins/InstrumentationRuntime/UBSan/InstrumentationRuntimeUBSan.cpp (+3-3)
  • (modified) lldb/source/Plugins/MemoryHistory/asan/MemoryHistoryASan.cpp (+25-18)
  • (modified) lldb/source/Plugins/Process/Utility/HistoryThread.cpp (+2-4)
  • (modified) lldb/source/Plugins/Process/Utility/HistoryThread.h (+3-3)
  • (modified) lldb/source/Plugins/Process/Utility/HistoryUnwind.cpp (+14-7)
  • (modified) lldb/source/Plugins/Process/Utility/HistoryUnwind.h (+2-4)
  • (modified) lldb/source/Plugins/SystemRuntime/MacOSX/SystemRuntimeMacOSX.cpp (+3-3)
  • (modified) lldb/test/API/functionalities/asan/TestMemoryHistory.py (+6-8)
diff --git a/lldb/include/lldb/lldb-private-enumerations.h b/lldb/include/lldb/lldb-private-enumerations.h
index 98c1e956bf8f7..604945011e5d5 100644
--- a/lldb/include/lldb/lldb-private-enumerations.h
+++ b/lldb/include/lldb/lldb-private-enumerations.h
@@ -248,6 +248,8 @@ enum class IterationAction {
   Stop,
 };
 
+enum class TracePCType { Returns, ReturnsNoZerothFrame, Calls };
+
 inline std::string GetStatDescription(lldb_private::StatisticKind K) {
    switch (K) {
    case StatisticKind::ExpressionSuccessful:
diff --git a/lldb/source/Plugins/InstrumentationRuntime/MainThreadChecker/InstrumentationRuntimeMainThreadChecker.cpp b/lldb/source/Plugins/InstrumentationRuntime/MainThreadChecker/InstrumentationRuntimeMainThreadChecker.cpp
index e67e60b4a3957..3c817f9171d7b 100644
--- a/lldb/source/Plugins/InstrumentationRuntime/MainThreadChecker/InstrumentationRuntimeMainThreadChecker.cpp
+++ b/lldb/source/Plugins/InstrumentationRuntime/MainThreadChecker/InstrumentationRuntimeMainThreadChecker.cpp
@@ -266,9 +266,9 @@ InstrumentationRuntimeMainThreadChecker::GetBacktracesFromExtendedStopInfo(
 
   // We gather symbolication addresses above, so no need for HistoryThread to
   // try to infer the call addresses.
-  bool pcs_are_call_addresses = true;
-  ThreadSP new_thread_sp = std::make_shared<HistoryThread>(
-      *process_sp, tid, PCs, pcs_are_call_addresses);
+  auto pc_type = TracePCType::Calls;
+  ThreadSP new_thread_sp =
+      std::make_shared<HistoryThread>(*process_sp, tid, PCs, pc_type);
 
   // Save this in the Process' ExtendedThreadList so a strong pointer retains
   // the object
diff --git a/lldb/source/Plugins/InstrumentationRuntime/UBSan/InstrumentationRuntimeUBSan.cpp b/lldb/source/Plugins/InstrumentationRuntime/UBSan/InstrumentationRuntimeUBSan.cpp
index c2db3540a797b..57a705715f1a0 100644
--- a/lldb/source/Plugins/InstrumentationRuntime/UBSan/InstrumentationRuntimeUBSan.cpp
+++ b/lldb/source/Plugins/InstrumentationRuntime/UBSan/InstrumentationRuntimeUBSan.cpp
@@ -324,9 +324,9 @@ InstrumentationRuntimeUBSan::GetBacktracesFromExtendedStopInfo(
 
   // We gather symbolication addresses above, so no need for HistoryThread to
   // try to infer the call addresses.
-  bool pcs_are_call_addresses = true;
-  ThreadSP new_thread_sp = std::make_shared<HistoryThread>(
-      *process_sp, tid, PCs, pcs_are_call_addresses);
+  auto pc_type = TracePCType::Calls;
+  ThreadSP new_thread_sp =
+      std::make_shared<HistoryThread>(*process_sp, tid, PCs, pc_type);
   std::string stop_reason_description = GetStopReasonDescription(info);
   new_thread_sp->SetName(stop_reason_description.c_str());
 
diff --git a/lldb/source/Plugins/MemoryHistory/asan/MemoryHistoryASan.cpp b/lldb/source/Plugins/MemoryHistory/asan/MemoryHistoryASan.cpp
index afaaa57b09587..9401673676d79 100644
--- a/lldb/source/Plugins/MemoryHistory/asan/MemoryHistoryASan.cpp
+++ b/lldb/source/Plugins/MemoryHistory/asan/MemoryHistoryASan.cpp
@@ -91,11 +91,9 @@ const char *memory_history_asan_command_format =
     t;
 )";
 
-static void CreateHistoryThreadFromValueObject(ProcessSP process_sp,
-                                               ValueObjectSP return_value_sp,
-                                               const char *type,
-                                               const char *thread_name,
-                                               HistoryThreads &result) {
+static void CreateHistoryThreadFromValueObject(
+    ProcessSP process_sp, ValueObjectSP return_value_sp, TracePCType pc_type,
+    const char *type, const char *thread_name, HistoryThreads &result) {
   std::string count_path = "." + std::string(type) + "_count";
   std::string tid_path = "." + std::string(type) + "_tid";
   std::string trace_path = "." + std::string(type) + "_trace";
@@ -128,12 +126,8 @@ static void CreateHistoryThreadFromValueObject(ProcessSP process_sp,
     pcs.push_back(pc);
   }
 
-  // The ASAN runtime already massages the return addresses into call
-  // addresses, we don't want LLDB's unwinder to try to locate the previous
-  // instruction again as this might lead to us reporting a different line.
-  bool pcs_are_call_addresses = true;
   HistoryThread *history_thread =
-      new HistoryThread(*process_sp, tid, pcs, pcs_are_call_addresses);
+      new HistoryThread(*process_sp, tid, pcs, pc_type);
   ThreadSP new_thread_sp(history_thread);
   std::ostringstream thread_name_with_number;
   thread_name_with_number << thread_name << " Thread " << tid;
@@ -176,10 +170,23 @@ HistoryThreads MemoryHistoryASan::GetHistoryThreads(lldb::addr_t address) {
   options.SetAutoApplyFixIts(false);
   options.SetLanguage(eLanguageTypeObjC_plus_plus);
 
-  if (auto m = GetPreferredAsanModule(process_sp->GetTarget())) {
-    SymbolContextList sc_list;
-    sc_list.Append(SymbolContext(std::move(m)));
-    options.SetPreferredSymbolContexts(std::move(sc_list));
+  // The ASan compiler-rt runtime already massages the return addresses into
+  // call addresses, so we don't want LLDB's unwinder to try to locate the
+  // previous instruction again as this might lead to us reporting a different
+  // line.
+  auto pc_type = TracePCType::Calls;
+
+  if (process_sp->GetTarget().GetArchitecture().GetTriple().isOSDarwin()) {
+    if (auto m = GetPreferredAsanModule(process_sp->GetTarget())) {
+      SymbolContextList sc_list;
+      sc_list.Append(SymbolContext(std::move(m)));
+      options.SetPreferredSymbolContexts(std::move(sc_list));
+    } else {
+      // Darwin, but not ASan compiler-rt implies libsanitizers which collects
+      // return addresses.  It also discards a few non-user frames at the top of
+      // the stack.
+      pc_type = TracePCType::ReturnsNoZerothFrame;
+    }
   }
 
   ExpressionResults expr_result = UserExpression::Evaluate(
@@ -197,10 +204,10 @@ HistoryThreads MemoryHistoryASan::GetHistoryThreads(lldb::addr_t address) {
   if (!return_value_sp)
     return result;
 
-  CreateHistoryThreadFromValueObject(process_sp, return_value_sp, "free",
-                                     "Memory deallocated by", result);
-  CreateHistoryThreadFromValueObject(process_sp, return_value_sp, "alloc",
-                                     "Memory allocated by", result);
+  CreateHistoryThreadFromValueObject(process_sp, return_value_sp, pc_type,
+                                     "free", "Memory deallocated by", result);
+  CreateHistoryThreadFromValueObject(process_sp, return_value_sp, pc_type,
+                                     "alloc", "Memory allocated by", result);
 
   return result;
 }
diff --git a/lldb/source/Plugins/Process/Utility/HistoryThread.cpp b/lldb/source/Plugins/Process/Utility/HistoryThread.cpp
index bc06757c806a9..b5d2395f978a8 100644
--- a/lldb/source/Plugins/Process/Utility/HistoryThread.cpp
+++ b/lldb/source/Plugins/Process/Utility/HistoryThread.cpp
@@ -26,14 +26,12 @@ using namespace lldb_private;
 //  Constructor
 
 HistoryThread::HistoryThread(lldb_private::Process &process, lldb::tid_t tid,
-                             std::vector<lldb::addr_t> pcs,
-                             bool pcs_are_call_addresses)
+                             std::vector<lldb::addr_t> pcs, TracePCType pc_type)
     : Thread(process, tid, true), m_framelist_mutex(), m_framelist(),
       m_pcs(pcs), m_extended_unwind_token(LLDB_INVALID_ADDRESS), m_queue_name(),
       m_thread_name(), m_originating_unique_thread_id(tid),
       m_queue_id(LLDB_INVALID_QUEUE_ID) {
-  m_unwinder_up =
-      std::make_unique<HistoryUnwind>(*this, pcs, pcs_are_call_addresses);
+  m_unwinder_up = std::make_unique<HistoryUnwind>(*this, pcs, pc_type);
   Log *log = GetLog(LLDBLog::Object);
   LLDB_LOGF(log, "%p HistoryThread::HistoryThread", static_cast<void *>(this));
 }
diff --git a/lldb/source/Plugins/Process/Utility/HistoryThread.h b/lldb/source/Plugins/Process/Utility/HistoryThread.h
index a66e0f2d4207c..2f6c48518672a 100644
--- a/lldb/source/Plugins/Process/Utility/HistoryThread.h
+++ b/lldb/source/Plugins/Process/Utility/HistoryThread.h
@@ -27,14 +27,14 @@ namespace lldb_private {
 /// process execution
 ///
 /// This subclass of Thread is used to provide a backtrace from earlier in
-/// process execution.  It is given a backtrace list of pc addresses and it
-/// will create stack frames for them.
+/// process execution.  It is given a backtrace list of pcs (return or call
+/// addresses) and it will create stack frames for them.
 
 class HistoryThread : public lldb_private::Thread {
 public:
   HistoryThread(lldb_private::Process &process, lldb::tid_t tid,
                 std::vector<lldb::addr_t> pcs,
-                bool pcs_are_call_addresses = false);
+                TracePCType pc_type = TracePCType::Returns);
 
   ~HistoryThread() override;
 
diff --git a/lldb/source/Plugins/Process/Utility/HistoryUnwind.cpp b/lldb/source/Plugins/Process/Utility/HistoryUnwind.cpp
index 7749dc6f5d514..3754e4320eaf5 100644
--- a/lldb/source/Plugins/Process/Utility/HistoryUnwind.cpp
+++ b/lldb/source/Plugins/Process/Utility/HistoryUnwind.cpp
@@ -24,9 +24,8 @@ using namespace lldb_private;
 // Constructor
 
 HistoryUnwind::HistoryUnwind(Thread &thread, std::vector<lldb::addr_t> pcs,
-                             bool pcs_are_call_addresses)
-    : Unwind(thread), m_pcs(pcs),
-      m_pcs_are_call_addresses(pcs_are_call_addresses) {}
+                             TracePCType pc_type)
+    : Unwind(thread), m_pcs(pcs), m_pc_type(pc_type) {}
 
 // Destructor
 
@@ -52,6 +51,17 @@ HistoryUnwind::DoCreateRegisterContextForFrame(StackFrame *frame) {
   return rctx;
 }
 
+static bool BehavesLikeZerothFrame(TracePCType pc_type, uint32_t frame_idx) {
+  switch (pc_type) {
+  case TracePCType::Returns:
+    return (frame_idx == 0);
+  case TracePCType::ReturnsNoZerothFrame:
+    return false;
+  case TracePCType::Calls:
+    return true;
+  }
+}
+
 bool HistoryUnwind::DoGetFrameInfoAtIndex(uint32_t frame_idx, lldb::addr_t &cfa,
                                           lldb::addr_t &pc,
                                           bool &behaves_like_zeroth_frame) {
@@ -61,10 +71,7 @@ bool HistoryUnwind::DoGetFrameInfoAtIndex(uint32_t frame_idx, lldb::addr_t &cfa,
   if (frame_idx < m_pcs.size()) {
     cfa = frame_idx;
     pc = m_pcs[frame_idx];
-    if (m_pcs_are_call_addresses)
-      behaves_like_zeroth_frame = true;
-    else
-      behaves_like_zeroth_frame = (frame_idx == 0);
+    behaves_like_zeroth_frame = BehavesLikeZerothFrame(m_pc_type, frame_idx);
     return true;
   }
   return false;
diff --git a/lldb/source/Plugins/Process/Utility/HistoryUnwind.h b/lldb/source/Plugins/Process/Utility/HistoryUnwind.h
index cb72b5d0a1764..95d1716f39776 100644
--- a/lldb/source/Plugins/Process/Utility/HistoryUnwind.h
+++ b/lldb/source/Plugins/Process/Utility/HistoryUnwind.h
@@ -19,7 +19,7 @@ namespace lldb_private {
 class HistoryUnwind : public lldb_private::Unwind {
 public:
   HistoryUnwind(Thread &thread, std::vector<lldb::addr_t> pcs,
-                bool pcs_are_call_addresses = false);
+                TracePCType pc_type = TracePCType::Returns);
 
   ~HistoryUnwind() override;
 
@@ -36,9 +36,7 @@ class HistoryUnwind : public lldb_private::Unwind {
 
 private:
   std::vector<lldb::addr_t> m_pcs;
-  /// This boolean indicates that the PCs in the non-0 frames are call
-  /// addresses and not return addresses.
-  bool m_pcs_are_call_addresses;
+  TracePCType m_pc_type;
 };
 
 } // namespace lldb_private
diff --git a/lldb/source/Plugins/SystemRuntime/MacOSX/SystemRuntimeMacOSX.cpp b/lldb/source/Plugins/SystemRuntime/MacOSX/SystemRuntimeMacOSX.cpp
index b23f64210cc80..a20f04b78492c 100644
--- a/lldb/source/Plugins/SystemRuntime/MacOSX/SystemRuntimeMacOSX.cpp
+++ b/lldb/source/Plugins/SystemRuntime/MacOSX/SystemRuntimeMacOSX.cpp
@@ -544,9 +544,9 @@ ThreadSP SystemRuntimeMacOSX::GetExtendedBacktraceThread(ThreadSP real_thread,
     if (!thread_extended_info->ForEach(extract_frame_pc))
       return {};
 
-    originating_thread_sp =
-        std::make_shared<HistoryThread>(*m_process, real_thread->GetIndexID(),
-                                        app_specific_backtrace_pcs, true);
+    originating_thread_sp = std::make_shared<HistoryThread>(
+        *m_process, real_thread->GetIndexID(), app_specific_backtrace_pcs,
+        TracePCType::Calls);
     originating_thread_sp->SetQueueName(type.AsCString());
   }
   return originating_thread_sp;
diff --git a/lldb/test/API/functionalities/asan/TestMemoryHistory.py b/lldb/test/API/functionalities/asan/TestMemoryHistory.py
index 1568140b355dc..66f6e3e7502c1 100644
--- a/lldb/test/API/functionalities/asan/TestMemoryHistory.py
+++ b/lldb/test/API/functionalities/asan/TestMemoryHistory.py
@@ -41,18 +41,16 @@ def setUp(self):
         self.line_free = line_number("main.c", "// free line")
         self.line_breakpoint = line_number("main.c", "// break line")
 
-    # Test line numbers: rdar://126237493
-    # for libsanitizers and remove `skip_line_numbers` parameter
-    def check_traces(self, skip_line_numbers=False):
+    def check_traces(self):
         self.expect(
             "memory history 'pointer'",
             substrs=[
                 "Memory deallocated by Thread",
                 "a.out`f2",
-                "main.c" if skip_line_numbers else f"main.c:{self.line_free}",
+                f"main.c:{self.line_free}",
                 "Memory allocated by Thread",
                 "a.out`f1",
-                "main.c" if skip_line_numbers else f"main.c:{self.line_malloc}",
+                f"main.c:{self.line_malloc}",
             ],
         )
 
@@ -76,7 +74,7 @@ def libsanitizers_traces_tests(self):
         self.runCmd("env SanitizersAllocationTraces=all")
 
         self.run_to_breakpoint(target)
-        self.check_traces(skip_line_numbers=True)
+        self.check_traces()
 
     def libsanitizers_asan_tests(self):
         target = self.createTestTarget()
@@ -84,7 +82,7 @@ def libsanitizers_asan_tests(self):
         self.runCmd("env SanitizersAddress=1 MallocSanitizerZone=1")
 
         self.run_to_breakpoint(target)
-        self.check_traces(skip_line_numbers=True)
+        self.check_traces()
 
         self.runCmd("continue")
 
@@ -94,7 +92,7 @@ def libsanitizers_asan_tests(self):
             "Process should be stopped due to ASan report",
             substrs=["stopped", "stop reason = Use of deallocated memory"],
         )
-        self.check_traces(skip_line_numbers=True)
+        self.check_traces()
 
         # do the same using SB API
         process = self.dbg.GetSelectedTarget().process

Copy link
Contributor

@thetruestblue thetruestblue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both commits make sense to me.

Refactor of commit 1 looks good.

Logic + documentation make sense for commit 2.

Tests make sense.

Looks good.

@@ -248,6 +248,8 @@ enum class IterationAction {
Stop,
};

enum class TracePCType { Returns, ReturnsNoZerothFrame, Calls };

This comment was marked as resolved.

Comment on lines 179 to 189
if (process_sp->GetTarget().GetArchitecture().GetTriple().isOSDarwin()) {
if (auto m = GetPreferredAsanModule(process_sp->GetTarget())) {
SymbolContextList sc_list;
sc_list.Append(SymbolContext(std::move(m)));
options.SetPreferredSymbolContexts(std::move(sc_list));
} else {
// Darwin, but not ASan compiler-rt implies libsanitizers which collects
// return addresses. It also discards a few non-user frames at the top of
// the stack.
pc_type = TracePCType::ReturnsNoZerothFrame;
}

This comment was marked as resolved.

@yln yln force-pushed the users/yln/lldb-libsanitizers-trace-accuracy branch from cba1c0f to 6ba21cb Compare August 19, 2025 21:40
Comment on lines 256 to 260
/// - Some trace providers (e.g., ASan compiler-rt runtime) already perform this
/// mapping, so we need to prevent LLDB from doing it again.
/// - Other trace providers (e.g., libsanitizers traces) collect return
/// addresses but prune the topmost frames, so we should skip the special
/// treatment of frame 0.

This comment was marked as resolved.

Copy link
Member

@Michael137 Michael137 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

I'd wait a day or so before merging so @JDevlieghere or @jasonmolenda can chime in.

/// - Other trace providers (e.g., libsanitizers traces) collect return
/// addresses but prune the topmost frames, so we should skip the special
/// treatment of frame 0.
enum class TracePCType {

This comment was marked as resolved.

This comment was marked as resolved.

@yln yln force-pushed the users/yln/lldb-libsanitizers-trace-accuracy branch from 6ba21cb to d9157e0 Compare August 20, 2025 17:23
/// - Other trace providers (e.g., libsanitizers traces) collect return
/// addresses but prune the topmost frames, so we should skip the special
/// treatment of frame 0.
enum class TracePCType {

This comment was marked as resolved.

Comment on lines 179 to 178
if (auto m = GetPreferredAsanModule(process_sp->GetTarget())) {
SymbolContextList sc_list;
sc_list.Append(SymbolContext(std::move(m)));
options.SetPreferredSymbolContexts(std::move(sc_list));
} else if (process_sp->GetTarget()
.GetArchitecture()
.GetTriple()
.isOSDarwin()) {
// Darwin, but not ASan compiler-rt implies libsanitizers which collects
// return addresses. It also discards a few non-user frames at the top of
// the stack.
pc_type = TracePCType::ReturnsNoZerothFrame;
}

This comment was marked as resolved.

yln added 3 commits August 20, 2025 11:04
Introduce and adopt `HistoryPCType` enum to replace
`pcs_are_call_addresses` bool to make handling of
history back traces more clear and allow for
extension of behavior.

This change is a mechanical refactoring and
preservers current behavior:
```
pcs_are_call_addresses:
  false  ->  HistoryPCType::Returns (default)
  true   ->  HistoryPCType::Calls
```

rdar://157596927
When providing allocation and deallocation traces,
the ASan compiler-rt runtime already provides call
addresses (`HistoryPCType::Calls`).

On Darwin, system sanitizers (libsanitizers)
provides return address.  It also discards a few
non-user frames at the top of the stack, because
these internal libmalloc/libsanitizers stack
frames do not provide any value when diagnosing
memory errors.

Introduce and add handling for
`HistoryPCType::ReturnsNoZerothFrame` to cover
this case and enable libsanitizers traces
line-level testing.

rdar://157596927
@yln yln force-pushed the users/yln/lldb-libsanitizers-trace-accuracy branch from d9157e0 to 32ec465 Compare August 20, 2025 19:10
Copy link
Member

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@yln yln merged commit 484d040 into main Aug 20, 2025
9 checks passed
@yln yln deleted the users/yln/lldb-libsanitizers-trace-accuracy branch August 20, 2025 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants