[Lldb-commits] [lldb] [lldb] Stop handing out unanchored Function pointers from CallEdge (PR #224165)

Jonas Devlieghere via lldb-commits lldb-commits at lists.llvm.org
Wed Sep 16 17:50:53 PDT 2026


https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/224165

>From 473c0dfdec1bcae56beff53773e449bef316e9d5 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Wed, 16 Sep 2026 10:04:45 -0700
Subject: [PATCH] [lldb] Stop caching cross-module callees in CallEdge

DirectCallEdge cached the Function it resolved for its callee. A
CallEdge is owned by a Module in the shared module cache, so it
outlives any one Target, while the callee it resolves to depends on
that Target's image list and dies with its own module. The cached
pointer dangled once the callee's module was unloaded, crashing the
next backtrace that walked tail call frames.

Keep only the symbol name and re-resolve on every call. GetCallee
returns a SymbolContext so the caller holds the callee's module for as
long as it uses the Function.

Scoping the lookup to the caller's own module or compile unit would
also make the cache safe, but direct tail call edges cross module
boundaries by design. Clang points DW_AT_call_origin at a declaration
DIE when the callee is external. See the cross_dso tail call test.

rdar://181262163
---
 lldb/include/lldb/Symbol/Function.h        |  25 ++--
 lldb/source/Expression/DWARFExpression.cpp |   6 +-
 lldb/source/Symbol/Function.cpp            |  54 +++++----
 lldb/source/Target/StackFrameList.cpp      |  37 +++---
 lldb/unittests/Symbol/CMakeLists.txt       |   1 +
 lldb/unittests/Symbol/CallEdgeTest.cpp     | 128 +++++++++++++++++++++
 6 files changed, 193 insertions(+), 58 deletions(-)
 create mode 100644 lldb/unittests/Symbol/CallEdgeTest.cpp

diff --git a/lldb/include/lldb/Symbol/Function.h b/lldb/include/lldb/Symbol/Function.h
index 2070b327e30a9..04a8e9a89c90e 100644
--- a/lldb/include/lldb/Symbol/Function.h
+++ b/lldb/include/lldb/Symbol/Function.h
@@ -15,6 +15,7 @@
 #include "lldb/Expression/DWARFExpressionList.h"
 #include "lldb/Symbol/Block.h"
 #include "lldb/Symbol/LineEntry.h"
+#include "lldb/Symbol/SymbolContext.h"
 #include "lldb/Utility/UserID.h"
 #include "lldb/lldb-forward.h"
 #include "llvm/ADT/ArrayRef.h"
@@ -255,12 +256,17 @@ class CallEdge {
   enum class AddrType : uint8_t { Call, AfterCall };
   virtual ~CallEdge();
 
-  /// Get the callee's definition.
+  /// Get the callee's definition, resolved against \p images.
+  ///
+  /// The result is never cached. A CallEdge is owned by a Module in the shared
+  /// module cache, so it outlives any one Target, while the callee it resolves
+  /// to depends on that Target's image list and dies with its own module. The
+  /// returned symbol context anchors that module.
   ///
   /// Note that this might lazily invoke the DWARF parser. A register context
   /// from the caller's activation is needed to find indirect call targets.
-  virtual Function *GetCallee(ModuleList &images,
-                              ExecutionContext &exe_ctx) = 0;
+  virtual SymbolContext GetCallee(ModuleList &images,
+                                  ExecutionContext &exe_ctx) = 0;
 
   /// Get the load PC address of the instruction which executes after the call
   /// returns. Returns LLDB_INVALID_ADDRESS iff this is a tail call. \p caller
@@ -298,6 +304,9 @@ class CallEdge {
   static lldb::addr_t GetLoadAddress(lldb::addr_t unresolved_pc,
                                      Function &caller, Target &target);
 
+  /// Find the function containing \p addr.
+  static SymbolContext ResolveCallee(const Address &addr);
+
   /// Like \ref GetReturnPCAddress, but returns an unresolved file address.
   lldb::addr_t GetUnresolvedReturnPCAddress() const {
     return caller_address_type == AddrType::AfterCall && !is_tail_call
@@ -324,14 +333,13 @@ class DirectCallEdge : public CallEdge {
                  lldb::addr_t caller_address, bool is_tail_call,
                  CallSiteParameterArray &&parameters);
 
-  Function *GetCallee(ModuleList &images, ExecutionContext &exe_ctx) override;
+  SymbolContext GetCallee(ModuleList &images,
+                          ExecutionContext &exe_ctx) override;
 
 private:
-  Function *ResolveCallee(ModuleList &images);
+  Address ResolveCalleeAddress(ModuleList &images) const;
 
   const char *m_symbol_name;
-  std::once_flag m_resolved_flag;
-  Function *m_callee_def = nullptr;
 };
 
 /// An indirect call site. Used to represent call sites where the address of
@@ -345,7 +353,8 @@ class IndirectCallEdge : public CallEdge {
                    AddrType caller_address_type, lldb::addr_t caller_address,
                    bool is_tail_call, CallSiteParameterArray &&parameters);
 
-  Function *GetCallee(ModuleList &images, ExecutionContext &exe_ctx) override;
+  SymbolContext GetCallee(ModuleList &images,
+                          ExecutionContext &exe_ctx) override;
 
 private:
   // Used to describe an indirect call.
diff --git a/lldb/source/Expression/DWARFExpression.cpp b/lldb/source/Expression/DWARFExpression.cpp
index 338bbf24cbb27..f884724766714 100644
--- a/lldb/source/Expression/DWARFExpression.cpp
+++ b/lldb/source/Expression/DWARFExpression.cpp
@@ -797,8 +797,8 @@ static llvm::Error Evaluate_DW_OP_entry_value(EvalContext &eval_ctx,
           "no call edge for retn-pc = {0:x} in parent frame {1}", return_pc,
           parent_func->GetName());
     }
-    Function *callee_func = call_edge->GetCallee(modlist, parent_exe_ctx);
-    if (callee_func != current_func) {
+    SymbolContext callee = call_edge->GetCallee(modlist, parent_exe_ctx);
+    if (callee.function != current_func) {
       return llvm::createStringError(
           "ambiguous call sequence, can't find real parent frame");
     }
@@ -807,7 +807,7 @@ static llvm::Error Evaluate_DW_OP_entry_value(EvalContext &eval_ctx,
     // call sequence that produced the current activation.  The first edge in
     // the parent that points to the current function must be valid.
     for (auto &edge : parent_func->GetTailCallingEdges()) {
-      if (edge->GetCallee(modlist, parent_exe_ctx) == current_func) {
+      if (edge->GetCallee(modlist, parent_exe_ctx).function == current_func) {
         call_edge = edge.get();
         break;
       }
diff --git a/lldb/source/Symbol/Function.cpp b/lldb/source/Symbol/Function.cpp
index 0f3729b82fc53..c6a51ebe2b135 100644
--- a/lldb/source/Symbol/Function.cpp
+++ b/lldb/source/Symbol/Function.cpp
@@ -150,12 +150,23 @@ lldb::addr_t CallEdge::GetReturnPCAddress(Function &caller,
   return GetLoadAddress(GetUnresolvedReturnPCAddress(), caller, target);
 }
 
-Function *DirectCallEdge::ResolveCallee(ModuleList &images) {
+SymbolContext CallEdge::ResolveCallee(const Address &addr) {
+  SymbolContext sc;
+  addr.CalculateSymbolContext(&sc, eSymbolContextFunction);
+  if (!sc.function) {
+    LLDB_LOG(GetLog(LLDBLog::Step),
+             "CallEdge: Could not find complete function");
+    return SymbolContext();
+  }
+  return sc;
+}
+
+Address DirectCallEdge::ResolveCalleeAddress(ModuleList &images) const {
   if (!m_symbol_name)
-    return nullptr;
+    return Address();
 
   Log *log = GetLog(LLDBLog::Step);
-  LLDB_LOG(log, "DirectCallEdge: Lazily parsing the call graph for {0}",
+  LLDB_LOG(log, "DirectCallEdge: Parsing the call graph for {0}",
            m_symbol_name);
 
   SymbolContextList sc_list;
@@ -165,22 +176,16 @@ Function *DirectCallEdge::ResolveCallee(ModuleList &images) {
   if (num_matches == 0 || !sc_list[0].symbol) {
     LLDB_LOG(log, "DirectCallEdge: Found no symbols for {0}, cannot resolve it",
              m_symbol_name);
-    return nullptr;
+    return Address();
   }
 
   Address callee_addr = sc_list[0].symbol->GetAddress();
   if (!callee_addr.IsValid()) {
     LLDB_LOG(log, "DirectCallEdge: Invalid symbol address");
-    return nullptr;
+    return Address();
   }
 
-  Function *f = callee_addr.CalculateSymbolContextFunction();
-  if (!f) {
-    LLDB_LOG(log, "DirectCallEdge: Could not find complete function");
-    return nullptr;
-  }
-
-  return f;
+  return callee_addr;
 }
 
 DirectCallEdge::DirectCallEdge(const char *symbol_name,
@@ -191,10 +196,9 @@ DirectCallEdge::DirectCallEdge(const char *symbol_name,
                std::move(parameters)),
       m_symbol_name(symbol_name) {}
 
-Function *DirectCallEdge::GetCallee(ModuleList &images, ExecutionContext &) {
-  std::call_once(m_resolved_flag,
-                 [&] { m_callee_def = ResolveCallee(images); });
-  return m_callee_def;
+SymbolContext DirectCallEdge::GetCallee(ModuleList &images,
+                                        ExecutionContext &) {
+  return ResolveCallee(ResolveCalleeAddress(images));
 }
 
 IndirectCallEdge::IndirectCallEdge(DWARFExpressionList call_target,
@@ -206,8 +210,8 @@ IndirectCallEdge::IndirectCallEdge(DWARFExpressionList call_target,
                std::move(parameters)),
       call_target(std::move(call_target)) {}
 
-Function *IndirectCallEdge::GetCallee(ModuleList &images,
-                                      ExecutionContext &exe_ctx) {
+SymbolContext IndirectCallEdge::GetCallee(ModuleList &images,
+                                          ExecutionContext &exe_ctx) {
   Log *log = GetLog(LLDBLog::Step);
   Status error;
   llvm::Expected<Value> callee_addr_val = call_target.Evaluate(
@@ -217,14 +221,14 @@ Function *IndirectCallEdge::GetCallee(ModuleList &images,
   if (!callee_addr_val) {
     LLDB_LOG_ERROR(log, callee_addr_val.takeError(),
                    "IndirectCallEdge: Could not evaluate expression: {0}");
-    return nullptr;
+    return SymbolContext();
   }
 
   addr_t raw_addr =
       callee_addr_val->GetScalar().ULongLong(LLDB_INVALID_ADDRESS);
   if (raw_addr == LLDB_INVALID_ADDRESS) {
     LLDB_LOG(log, "IndirectCallEdge: Could not extract address from scalar");
-    return nullptr;
+    return SymbolContext();
   }
 
   if (auto *process = exe_ctx.GetProcessPtr()) {
@@ -237,16 +241,10 @@ Function *IndirectCallEdge::GetCallee(ModuleList &images,
   Address callee_addr;
   if (!exe_ctx.GetTargetPtr()->ResolveLoadAddress(raw_addr, callee_addr)) {
     LLDB_LOG(log, "IndirectCallEdge: Could not resolve callee's load address");
-    return nullptr;
-  }
-
-  Function *f = callee_addr.CalculateSymbolContextFunction();
-  if (!f) {
-    LLDB_LOG(log, "IndirectCallEdge: Could not find complete function");
-    return nullptr;
+    return SymbolContext();
   }
 
-  return f;
+  return ResolveCallee(callee_addr);
 }
 
 /// @}
diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index b11fdc6ddbf91..32989e1c83342 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -210,10 +210,9 @@ bool StackFrameList::WereAllFramesFetched() const {
 }
 
 /// A sequence of calls that comprise some portion of a backtrace. Each frame
-/// is represented as a pair of a callee (Function *) and an address within the
-/// callee.
+/// is represented as a pair of a callee and an address within the callee.
 struct CallDescriptor {
-  Function *func;
+  SymbolContext callee;
   CallEdge::AddrType address_type = CallEdge::AddrType::Call;
   addr_t address = LLDB_INVALID_ADDRESS;
 };
@@ -246,12 +245,12 @@ static void FindInterveningFrames(Function &begin, Function &end,
   }
 
   // The first callee may not be resolved, or there may be nothing to fill in.
-  Function *first_callee = first_edge->GetCallee(images, exe_ctx);
-  if (!first_callee) {
+  SymbolContext first_callee = first_edge->GetCallee(images, exe_ctx);
+  if (!first_callee.function) {
     LLDB_LOG_VERBOSE(log, "Could not resolve callee");
     return;
   }
-  if (first_callee == &end) {
+  if (first_callee.function == &end) {
     LLDB_LOG_VERBOSE(
         log, "Not searching further, first callee is {0} (retn-PC: {1:x})",
         end.GetDisplayName(), return_pc);
@@ -275,16 +274,16 @@ static void FindInterveningFrames(Function &begin, Function &end,
         ExecutionContext &context)
         : end(end), images(images), target(target), context(context) {}
 
-    void search(CallEdge &first_edge, Function &first_callee,
+    void search(CallEdge &first_edge, const SymbolContext &first_callee,
                 CallSequence &path) {
       dfs(first_edge, first_callee);
       if (!ambiguous)
         path = std::move(solution_path);
     }
 
-    void dfs(CallEdge &current_edge, Function &callee) {
+    void dfs(CallEdge &current_edge, const SymbolContext &callee) {
       // Found a path to the target function.
-      if (&callee == end) {
+      if (callee.function == end) {
         if (solution_path.empty())
           solution_path = active_path;
         else
@@ -296,22 +295,22 @@ static void FindInterveningFrames(Function &begin, Function &end,
       // there's more than one way to reach a target. This errs on the side of
       // caution: it conservatively stops searching when some solutions are
       // still possible to save time in the average case.
-      if (!visited_nodes.insert(&callee).second) {
+      if (!visited_nodes.insert(callee.function).second) {
         ambiguous = true;
         return;
       }
 
       // Search the calls made from this callee.
-      active_path.push_back(CallDescriptor{&callee});
-      for (const auto &edge : callee.GetTailCallingEdges()) {
-        Function *next_callee = edge->GetCallee(images, context);
-        if (!next_callee)
+      active_path.push_back(CallDescriptor{callee});
+      for (const auto &edge : callee.function->GetTailCallingEdges()) {
+        SymbolContext next_callee = edge->GetCallee(images, context);
+        if (!next_callee.function)
           continue;
 
         std::tie(active_path.back().address_type, active_path.back().address) =
-            edge->GetCallerAddress(callee, target);
+            edge->GetCallerAddress(*callee.function, target);
 
-        dfs(*edge, *next_callee);
+        dfs(*edge, next_callee);
         if (ambiguous)
           return;
       }
@@ -319,7 +318,7 @@ static void FindInterveningFrames(Function &begin, Function &end,
     }
   };
 
-  DFS(&end, images, target, exe_ctx).search(*first_edge, *first_callee, path);
+  DFS(&end, images, target, exe_ctx).search(*first_edge, first_callee, path);
 }
 
 /// Given that \p next_frame will be appended to the frame list, synthesize
@@ -382,8 +381,8 @@ void StackFrameList::SynthesizeTailCallFrames(StackFrame &next_frame) {
                         path, images, log);
 
   // Push synthetic tail call frames.
-  for (auto calleeInfo : llvm::reverse(path)) {
-    Function *callee = calleeInfo.func;
+  for (const auto &calleeInfo : llvm::reverse(path)) {
+    Function *callee = calleeInfo.callee.function;
     uint32_t frame_idx = m_frames.size();
     uint32_t concrete_frame_idx = next_frame.GetConcreteFrameIndex();
     addr_t cfa = LLDB_INVALID_ADDRESS;
diff --git a/lldb/unittests/Symbol/CMakeLists.txt b/lldb/unittests/Symbol/CMakeLists.txt
index 0fdbc9d72445e..ecdce544801b3 100644
--- a/lldb/unittests/Symbol/CMakeLists.txt
+++ b/lldb/unittests/Symbol/CMakeLists.txt
@@ -1,4 +1,5 @@
 add_lldb_unittest(SymbolTests
+  CallEdgeTest.cpp
   JSONSymbolTest.cpp
   LineTableTest.cpp
   LocateSymbolFileTest.cpp
diff --git a/lldb/unittests/Symbol/CallEdgeTest.cpp b/lldb/unittests/Symbol/CallEdgeTest.cpp
new file mode 100644
index 0000000000000..3a7b188e8b0ac
--- /dev/null
+++ b/lldb/unittests/Symbol/CallEdgeTest.cpp
@@ -0,0 +1,128 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Plugins/ObjectFile/Mach-O/ObjectFileMachO.h"
+#include "Plugins/SymbolFile/DWARF/SymbolFileDWARF.h"
+#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
+#include "TestingSupport/SubsystemRAII.h"
+#include "TestingSupport/TestUtilities.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Core/ModuleList.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Symbol/Function.h"
+#include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+#include <deque>
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::plugin::dwarf;
+
+namespace {
+class CallEdgeTest : public testing::Test {
+  SubsystemRAII<FileSystem, HostInfo, ObjectFileMachO, SymbolFileDWARF,
+                TypeSystemClang>
+      subsystems;
+
+public:
+  void SetUp() override { m_module_sp = MakeModule(); }
+
+protected:
+  /// Build a module of its own from the shared test yaml. Each call yields a
+  /// distinct Module, as two Targets would have.
+  ModuleSP MakeModule() {
+    auto file = TestFile::fromYamlFile("inlined-functions.yaml");
+    if (!file) {
+      ADD_FAILURE() << llvm::toString(file.takeError());
+      return nullptr;
+    }
+    m_files.push_back(std::move(*file));
+    return std::make_shared<Module>(m_files.back().moduleSpec());
+  }
+
+  std::deque<TestFile> m_files;
+  ModuleSP m_module_sp;
+};
+} // namespace
+
+TEST_F(CallEdgeTest, DirectCallEdgeCalleeDiesWithItsModule) {
+  ModuleList images;
+  images.Append(m_module_sp);
+
+  DirectCallEdge edge("_Z4sum3iii", CallEdge::AddrType::AfterCall,
+                      /*caller_address=*/0, /*is_tail_call=*/false, {});
+  ExecutionContext exe_ctx;
+  SymbolContext callee = edge.GetCallee(images, exe_ctx);
+  ASSERT_TRUE(callee.function);
+  EXPECT_EQ(callee.function->GetName(), ConstString("sum3(int, int, int)"));
+
+  // Callee identity must remain stable while the module is loaded.
+  EXPECT_EQ(edge.GetCallee(images, exe_ctx).function, callee.function);
+
+  callee = SymbolContext();
+  images.Clear();
+  m_module_sp.reset();
+
+  EXPECT_FALSE(edge.GetCallee(images, exe_ctx).function);
+}
+
+TEST_F(CallEdgeTest, CalleeKeepsItsModuleAlive) {
+  ModuleList images;
+  images.Append(m_module_sp);
+
+  DirectCallEdge edge("_Z4sum3iii", CallEdge::AddrType::AfterCall,
+                      /*caller_address=*/0, /*is_tail_call=*/false, {});
+  ExecutionContext exe_ctx;
+  SymbolContext callee = edge.GetCallee(images, exe_ctx);
+  ASSERT_TRUE(callee.function);
+
+  ModuleWP module_wp = m_module_sp;
+  images.Clear();
+  m_module_sp.reset();
+  ASSERT_FALSE(module_wp.expired());
+
+  EXPECT_EQ(callee.function->GetName(), ConstString("sum3(int, int, int)"));
+  EXPECT_TRUE(callee.function->GetCallEdges().empty());
+}
+
+TEST_F(CallEdgeTest, UnresolvedSymbolYieldsNoCallee) {
+  ModuleList images;
+  images.Append(m_module_sp);
+
+  DirectCallEdge edge("_Z7missingv", CallEdge::AddrType::AfterCall,
+                      /*caller_address=*/0, /*is_tail_call=*/false, {});
+  ExecutionContext exe_ctx;
+  EXPECT_FALSE(edge.GetCallee(images, exe_ctx).function);
+  EXPECT_FALSE(edge.GetCallee(images, exe_ctx).function);
+}
+
+TEST_F(CallEdgeTest, CalleeFollowsTheImageList) {
+  ModuleSP other_module_sp = MakeModule();
+  ASSERT_TRUE(other_module_sp);
+
+  ModuleList images;
+  images.Append(m_module_sp);
+  ModuleList other_images;
+  other_images.Append(other_module_sp);
+
+  DirectCallEdge edge("_Z4sum3iii", CallEdge::AddrType::AfterCall,
+                      /*caller_address=*/0, /*is_tail_call=*/false, {});
+  ExecutionContext exe_ctx;
+  SymbolContext callee = edge.GetCallee(images, exe_ctx);
+  SymbolContext other_callee = edge.GetCallee(other_images, exe_ctx);
+  ASSERT_TRUE(callee.function);
+  ASSERT_TRUE(other_callee.function);
+
+  EXPECT_EQ(callee.module_sp, m_module_sp);
+  EXPECT_EQ(other_callee.module_sp, other_module_sp);
+  EXPECT_NE(callee.function, other_callee.function);
+}



More information about the lldb-commits mailing list