Skip to content

[lldb] Fix variable access in old SBFrames after inferior function calls#178823

Merged
medismailben merged 1 commit intollvm:mainfrom
medismailben:queue-debugging-frame-2-var
Feb 3, 2026
Merged

[lldb] Fix variable access in old SBFrames after inferior function calls#178823
medismailben merged 1 commit intollvm:mainfrom
medismailben:queue-debugging-frame-2-var

Conversation

@medismailben
Copy link
Member

@medismailben medismailben commented Jan 30, 2026

When a user holds an SBFrame reference and then triggers an inferior function
call (via expression evaluation or GetExtendedBacktraceThread), variables in
that frame become inaccessible with "register fp is not available" errors.

This happens because inferior function calls execute through
ThreadPlanCallFunction, which calls ClearStackFrames() during cleanup to
invalidate the unwinder state. ExecutionContextRef objects in the old SBFrames
were tracking StackFrameLists via weak_ptr, which became stale when
ClearStackFrames() created new instances.

The fix uses stable StackFrameList identifiers that persist across
ClearStackFrames():

  • ID = 0: Normal unwinder frames (constant across all instances)
  • ID = sequential counter: Scripted frame provider instances

ExecutionContextRef now stores the frame list ID instead of a weak_ptr, allowing
it to resolve to the current StackFrameList with fresh unwinder state after an
inferior function call completes.

The Thread object preserves the provider chain configuration
(m_provider_chain_ids and m_next_provider_id) across ClearStackFrames() so
that recreated StackFrameLists get the same IDs. When providers need to be
recreated, GetStackFrameList() rebuilds them from the persisted configuration.

This commit also fixes a deadlock when Python scripted frame providers call
back into LLDB during frame fetching. The m_list_mutex is now released before
calling GetFrameAtIndex() on the Python scripted frame provider to prevent
same-thread deadlock. A dedicated m_unwinder_frames_sp member ensures
GetFrameListByIdentifier(0) always returns the current unwinder frames, and
proper cleanup in DestroyThread() and ClearStackFrames() to prevent modules
from lingering after a Thread (and its StackFrameLists) gets destroyed.

Added test validates that variables remain accessible after
GetExtendedBacktraceThread triggers an inferior function call to fetch
libdispatch Queue Info.

rdar://167027676

Signed-off-by: Med Ismail Bennani ismail@bennani.ma

@llvmbot
Copy link
Member

llvmbot commented Jan 30, 2026

@llvm/pr-subscribers-lldb

Author: Med Ismail Bennani (medismailben)

Changes

When a user holds an SBFrame reference and then triggers an inferior function call (via expression evaluation or GetExtendedBacktraceThread), variables in that frame become inaccessible with "register fp is not available" errors.

This happens because inferior function calls execute through ThreadPlanCallFunction, which calls ClearStackFrames() during cleanup to invalidate the unwinder state. ExecutionContextRef objects in the old SBFrames were tracking StackFrameLists via weak_ptr, which became stale when ClearStackFrames() created new instances.

The fix uses stable StackFrameList identifiers that persist across ClearStackFrames():

  • ID = 0: Normal unwinder frames (constant across all instances)
  • ID = sequential counter: Scripted frame provider instances

ExecutionContextRef now stores the frame list ID instead of a weak_ptr, allowing it to resolve to the current StackFrameList with fresh unwinder state after an inferior function call completes.

The Thread object preserves the provider chain configuration (m_provider_chain_ids and m_next_provider_id) across ClearStackFrames() so that recreated StackFrameLists get the same IDs. When providers need to be recreated, GetStackFrameList() rebuilds them from the persisted configuration.

Added test validates that variables remain accessible after GetExtendedBacktraceThread triggers an inferior function call to fetch libdispatch Queue Info.

rdar://167027676


Patch is 32.36 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/178823.diff

11 Files Affected:

  • (modified) lldb/include/lldb/Target/ExecutionContext.h (+4-8)
  • (modified) lldb/include/lldb/Target/StackFrame.h (+8-5)
  • (modified) lldb/include/lldb/Target/StackFrameList.h (+9-2)
  • (modified) lldb/include/lldb/Target/Thread.h (+23-3)
  • (modified) lldb/source/Target/ExecutionContext.cpp (+12-6)
  • (modified) lldb/source/Target/StackFrame.cpp (+3-3)
  • (modified) lldb/source/Target/StackFrameList.cpp (+10-10)
  • (modified) lldb/source/Target/Thread.cpp (+62-32)
  • (added) lldb/test/API/macosx/extended-backtrace-api/Makefile (+3)
  • (added) lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py (+185)
  • (added) lldb/test/API/macosx/extended-backtrace-api/main.m (+53)
diff --git a/lldb/include/lldb/Target/ExecutionContext.h b/lldb/include/lldb/Target/ExecutionContext.h
index 8637234c4fb95..e3dac2613275d 100644
--- a/lldb/include/lldb/Target/ExecutionContext.h
+++ b/lldb/include/lldb/Target/ExecutionContext.h
@@ -270,7 +270,7 @@ class ExecutionContextRef {
 
   void ClearFrame() {
     m_stack_id.Clear();
-    m_frame_list_wp.reset();
+    m_frame_list_id.reset();
   }
 
 protected:
@@ -283,13 +283,9 @@ class ExecutionContextRef {
                                               /// backing object changes
   StackID m_stack_id; ///< The stack ID that this object refers to in case the
                       ///< backing object changes
-  mutable lldb::StackFrameListWP
-      m_frame_list_wp; ///< Weak reference to the
-                       ///< frame list that contains
-                       ///< this frame. If we can create a valid
-                       ///< StackFrameListSP from it, we must use it to resolve
-                       ///< the StackID, otherwise, we should ask the Thread's
-                       ///< StackFrameList.
+  mutable std::optional<uint64_t>
+      m_frame_list_id; ///< Identifier of the frame
+                       ///< list containing the frame.
 };
 
 /// \class ExecutionContext ExecutionContext.h
diff --git a/lldb/include/lldb/Target/StackFrame.h b/lldb/include/lldb/Target/StackFrame.h
index 46922448d6e59..339d3f6315ff1 100644
--- a/lldb/include/lldb/Target/StackFrame.h
+++ b/lldb/include/lldb/Target/StackFrame.h
@@ -542,7 +542,7 @@ class StackFrame : public ExecutionContextScope,
 
   virtual lldb::RecognizedStackFrameSP GetRecognizedFrame();
 
-  /// Get the StackFrameList that contains this frame.
+  /// Get the identifier of the StackFrameList that contains this frame.
   ///
   /// Returns the StackFrameList that contains this frame, allowing
   /// frames to resolve execution contexts without calling
@@ -550,9 +550,12 @@ class StackFrame : public ExecutionContextScope,
   /// during frame provider initialization.
   ///
   /// \return
-  ///   The StackFrameList that contains this frame, or nullptr if not set.
-  virtual lldb::StackFrameListSP GetContainingStackFrameList() const {
-    return m_frame_list_wp.lock();
+  ///   The identifier of the containing StackFrameList, or std::nullopt if
+  ///   the frame list is not available.
+  std::optional<uint64_t> GetContainingStackFrameListIdentifier() const {
+    if (m_frame_list_id != 0)
+      return m_frame_list_id;
+    return std::nullopt;
   }
 
 protected:
@@ -598,8 +601,8 @@ class StackFrame : public ExecutionContextScope,
   /// be the first address of its function). True for actual frame zero as
   /// well as any other frame with the same trait.
   bool m_behaves_like_zeroth_frame;
+  uint64_t m_frame_list_id;
   lldb::VariableListSP m_variable_list_sp;
-  lldb::StackFrameListWP m_frame_list_wp;
   /// Value objects for each variable in m_variable_list_sp.
   ValueObjectList m_variable_list_value_objects;
   std::optional<lldb::RecognizedStackFrameSP> m_recognized_frame_sp;
diff --git a/lldb/include/lldb/Target/StackFrameList.h b/lldb/include/lldb/Target/StackFrameList.h
index c096fe3ff61a0..eecb2cdd1a90c 100644
--- a/lldb/include/lldb/Target/StackFrameList.h
+++ b/lldb/include/lldb/Target/StackFrameList.h
@@ -24,7 +24,7 @@ class StackFrameList : public std::enable_shared_from_this<StackFrameList> {
 public:
   // Constructors and Destructors
   StackFrameList(Thread &thread, const lldb::StackFrameListSP &prev_frames_sp,
-                 bool show_inline_frames);
+                 bool show_inline_frames, uint64_t provider_id = 0);
 
   virtual ~StackFrameList();
 
@@ -104,6 +104,9 @@ class StackFrameList : public std::enable_shared_from_this<StackFrameList> {
   /// Get the thread associated with this frame list.
   Thread &GetThread() const { return m_thread; }
 
+  /// Get the unique identifier for this frame list.
+  uint64_t GetIdentifier() const { return m_identifier; }
+
 protected:
   friend class Thread;
   friend class ScriptedFrameProvider;
@@ -212,6 +215,9 @@ class StackFrameList : public std::enable_shared_from_this<StackFrameList> {
   /// Whether or not to show synthetic (inline) frames. Immutable.
   const bool m_show_inlined_frames;
 
+  /// Unique identifier for this frame list instance.
+  uint64_t m_identifier = 0;
+
   /// Returns true if fetching frames was interrupted, false otherwise.
   virtual bool FetchFramesUpTo(uint32_t end_idx,
                                InterruptionControl allow_interrupt);
@@ -244,7 +250,8 @@ class SyntheticStackFrameList : public StackFrameList {
   SyntheticStackFrameList(Thread &thread, lldb::StackFrameListSP input_frames,
                           const lldb::StackFrameListSP &prev_frames_sp,
                           bool show_inline_frames,
-                          lldb::SyntheticFrameProviderSP provider_sp);
+                          lldb::SyntheticFrameProviderSP provider_sp,
+                          uint64_t provider_id);
 
 protected:
   /// Override FetchFramesUpTo to lazily return frames from the provider
diff --git a/lldb/include/lldb/Target/Thread.h b/lldb/include/lldb/Target/Thread.h
index bc1bec57bee5f..09e7972502f5a 100644
--- a/lldb/include/lldb/Target/Thread.h
+++ b/lldb/include/lldb/Target/Thread.h
@@ -19,6 +19,7 @@
 #include "lldb/Target/ExecutionContextScope.h"
 #include "lldb/Target/RegisterCheckpoint.h"
 #include "lldb/Target/StackFrameList.h"
+#include "lldb/Target/SyntheticFrameProvider.h"
 #include "lldb/Utility/Broadcaster.h"
 #include "lldb/Utility/CompletionRequest.h"
 #include "lldb/Utility/Event.h"
@@ -26,6 +27,8 @@
 #include "lldb/Utility/UnimplementedError.h"
 #include "lldb/Utility/UserID.h"
 #include "lldb/lldb-private.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/SmallVector.h"
 #include "llvm/Support/MemoryBuffer.h"
 
 #define LLDB_THREAD_MAX_STOP_EXC_DATA 8
@@ -1297,12 +1300,15 @@ class Thread : public std::enable_shared_from_this<Thread>,
 
   lldb::StackFrameListSP GetStackFrameList();
 
+  /// Get a frame list by its unique identifier.
+  lldb::StackFrameListSP GetFrameListByIdentifier(uint64_t id);
+
   llvm::Error
   LoadScriptedFrameProvider(const ScriptedFrameProviderDescriptor &descriptor);
 
   void ClearScriptedFrameProvider();
 
-  const llvm::SmallVector<lldb::SyntheticFrameProviderSP, 0> &
+  const llvm::SmallDenseMap<uint64_t, lldb::SyntheticFrameProviderSP, 4> &
   GetFrameProviders() const {
     return m_frame_providers;
   }
@@ -1410,8 +1416,22 @@ class Thread : public std::enable_shared_from_this<Thread>,
   /// The Thread backed by this thread, if any.
   lldb::ThreadWP m_backed_thread;
 
-  /// The Scripted Frame Providers for this thread.
-  llvm::SmallVector<lldb::SyntheticFrameProviderSP, 0> m_frame_providers;
+  /// Map from frame list ID to its frame provider.
+  /// Cleared in ClearStackFrames(), repopulated in GetStackFrameList().
+  llvm::SmallDenseMap<uint64_t, lldb::SyntheticFrameProviderSP, 4>
+      m_frame_providers;
+
+  /// Ordered chain of provider IDs.
+  /// Persists across ClearStackFrames() to maintain stable provider IDs.
+  llvm::SmallVector<std::pair<ScriptedFrameProviderDescriptor, uint64_t>, 0>
+      m_provider_chain_ids;
+
+  /// Map from frame list identifier to weak pointer to frame list.
+  mutable llvm::DenseMap<uint64_t, lldb::StackFrameListWP> m_frame_lists_by_id;
+
+  /// Counter for assigning unique provider IDs. Starts at 1 since 0 is
+  /// reserved for normal unwinder frames. Persists across ClearStackFrames.
+  uint64_t m_next_provider_id = 1;
 
 private:
   bool m_extended_info_fetched; // Have we tried to retrieve the m_extended_info
diff --git a/lldb/source/Target/ExecutionContext.cpp b/lldb/source/Target/ExecutionContext.cpp
index b16ff26266c53..435c627bcdb91 100644
--- a/lldb/source/Target/ExecutionContext.cpp
+++ b/lldb/source/Target/ExecutionContext.cpp
@@ -468,10 +468,10 @@ operator=(const ExecutionContext &exe_ctx) {
   lldb::StackFrameSP frame_sp(exe_ctx.GetFrameSP());
   if (frame_sp) {
     m_stack_id = frame_sp->GetStackID();
-    m_frame_list_wp = frame_sp->GetContainingStackFrameList();
+    m_frame_list_id = frame_sp->GetContainingStackFrameListIdentifier();
   } else {
     m_stack_id.Clear();
-    m_frame_list_wp.reset();
+    m_frame_list_id.reset();
   }
   return *this;
 }
@@ -514,7 +514,7 @@ void ExecutionContextRef::SetThreadSP(const lldb::ThreadSP &thread_sp) {
 void ExecutionContextRef::SetFrameSP(const lldb::StackFrameSP &frame_sp) {
   if (frame_sp) {
     m_stack_id = frame_sp->GetStackID();
-    m_frame_list_wp = frame_sp->GetContainingStackFrameList();
+    m_frame_list_id = frame_sp->GetContainingStackFrameListIdentifier();
     SetThreadSP(frame_sp->GetThread());
   } else {
     ClearFrame();
@@ -644,9 +644,15 @@ lldb::StackFrameSP ExecutionContextRef::GetFrameSP() const {
   if (m_stack_id.IsValid()) {
     // Try the remembered frame list first to avoid circular dependencies
     // during frame provider initialization.
-    if (auto frame_list_sp = m_frame_list_wp.lock()) {
-      if (auto frame_sp = frame_list_sp->GetFrameWithStackID(m_stack_id))
-        return frame_sp;
+    if (m_frame_list_id) {
+      lldb::ThreadSP thread_sp(GetThreadSP());
+      if (thread_sp) {
+        if (auto frame_list_sp =
+                thread_sp->GetFrameListByIdentifier(*m_frame_list_id)) {
+          if (auto frame_sp = frame_list_sp->GetFrameWithStackID(m_stack_id))
+            return frame_sp;
+        }
+      }
     }
 
     // Fallback: ask the thread, which might re-trigger the frame provider
diff --git a/lldb/source/Target/StackFrame.cpp b/lldb/source/Target/StackFrame.cpp
index 340607e14abed..ab5489dd77564 100644
--- a/lldb/source/Target/StackFrame.cpp
+++ b/lldb/source/Target/StackFrame.cpp
@@ -69,7 +69,7 @@ StackFrame::StackFrame(const ThreadSP &thread_sp, user_id_t frame_idx,
       m_frame_base_error(), m_cfa_is_valid(cfa_is_valid),
       m_stack_frame_kind(kind), m_artificial(artificial),
       m_behaves_like_zeroth_frame(behaves_like_zeroth_frame),
-      m_variable_list_sp(), m_variable_list_value_objects(),
+      m_frame_list_id(0), m_variable_list_sp(), m_variable_list_value_objects(),
       m_recognized_frame_sp(), m_disassembly(), m_mutex() {
   // If we don't have a CFA value, use the frame index for our StackID so that
   // recursive functions properly aren't confused with one another on a history
@@ -97,7 +97,7 @@ StackFrame::StackFrame(const ThreadSP &thread_sp, user_id_t frame_idx,
       m_frame_base_error(), m_cfa_is_valid(true),
       m_stack_frame_kind(StackFrame::Kind::Regular), m_artificial(false),
       m_behaves_like_zeroth_frame(behaves_like_zeroth_frame),
-      m_variable_list_sp(), m_variable_list_value_objects(),
+      m_frame_list_id(0), m_variable_list_sp(), m_variable_list_value_objects(),
       m_recognized_frame_sp(), m_disassembly(), m_mutex() {
   if (sc_ptr != nullptr) {
     m_sc = *sc_ptr;
@@ -125,7 +125,7 @@ StackFrame::StackFrame(const ThreadSP &thread_sp, user_id_t frame_idx,
       m_frame_base_error(), m_cfa_is_valid(true),
       m_stack_frame_kind(StackFrame::Kind::Regular), m_artificial(false),
       m_behaves_like_zeroth_frame(behaves_like_zeroth_frame),
-      m_variable_list_sp(), m_variable_list_value_objects(),
+      m_frame_list_id(0), m_variable_list_sp(), m_variable_list_value_objects(),
       m_recognized_frame_sp(), m_disassembly(), m_mutex() {
   if (sc_ptr != nullptr) {
     m_sc = *sc_ptr;
diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index 1ad269e8783cc..65568becd745b 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -38,12 +38,12 @@ using namespace lldb_private;
 // StackFrameList constructor
 StackFrameList::StackFrameList(Thread &thread,
                                const lldb::StackFrameListSP &prev_frames_sp,
-                               bool show_inline_frames)
+                               bool show_inline_frames, uint64_t provider_id)
     : m_thread(thread), m_prev_frames_sp(prev_frames_sp), m_frames(),
       m_selected_frame_idx(), m_concrete_frames_fetched(0),
       m_current_inlined_depth(UINT32_MAX),
       m_current_inlined_pc(LLDB_INVALID_ADDRESS),
-      m_show_inlined_frames(show_inline_frames) {
+      m_show_inlined_frames(show_inline_frames), m_identifier(provider_id) {
   if (prev_frames_sp) {
     m_current_inlined_depth = prev_frames_sp->m_current_inlined_depth;
     m_current_inlined_pc = prev_frames_sp->m_current_inlined_pc;
@@ -59,8 +59,8 @@ StackFrameList::~StackFrameList() {
 SyntheticStackFrameList::SyntheticStackFrameList(
     Thread &thread, lldb::StackFrameListSP input_frames,
     const lldb::StackFrameListSP &prev_frames_sp, bool show_inline_frames,
-    lldb::SyntheticFrameProviderSP provider_sp)
-    : StackFrameList(thread, prev_frames_sp, show_inline_frames),
+    lldb::SyntheticFrameProviderSP provider_sp, uint64_t provider_id)
+    : StackFrameList(thread, prev_frames_sp, show_inline_frames, provider_id),
       m_input_frames(std::move(input_frames)),
       m_provider(std::move(provider_sp)) {}
 
@@ -89,7 +89,7 @@ bool SyntheticStackFrameList::FetchFramesUpTo(
                                       GetThread().GetProcess().get());
       // Set the frame list weak pointer so ExecutionContextRef can resolve
       // the frame without calling Thread::GetStackFrameList().
-      frame_sp->m_frame_list_wp = shared_from_this();
+      frame_sp->m_frame_list_id = GetIdentifier();
       m_frames.push_back(frame_sp);
     }
 
@@ -375,7 +375,7 @@ void StackFrameList::SynthesizeTailCallFrames(StackFrame &next_frame) {
         m_thread.shared_from_this(), frame_idx, concrete_frame_idx, cfa,
         cfa_is_valid, pc, StackFrame::Kind::Regular, artificial,
         behaves_like_zeroth_frame, &sc);
-    synth_frame->m_frame_list_wp = shared_from_this();
+    synth_frame->m_frame_list_id = GetIdentifier();
     m_frames.push_back(synth_frame);
     LLDB_LOG(log, "Pushed frame {0} at {1:x}", callee->GetDisplayName(), pc);
   }
@@ -491,7 +491,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx,
           unwind_frame_sp = std::make_shared<StackFrame>(
               m_thread.shared_from_this(), m_frames.size(), idx, reg_ctx_sp,
               cfa, pc, behaves_like_zeroth_frame, nullptr);
-          unwind_frame_sp->m_frame_list_wp = shared_from_this();
+          unwind_frame_sp->m_frame_list_id = GetIdentifier();
           m_frames.push_back(unwind_frame_sp);
         }
       } else {
@@ -526,7 +526,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx,
       // although its concrete index will stay the same.
       SynthesizeTailCallFrames(*unwind_frame_sp.get());
 
-      unwind_frame_sp->m_frame_list_wp = shared_from_this();
+      unwind_frame_sp->m_frame_list_id = GetIdentifier();
       m_frames.push_back(unwind_frame_sp);
     }
 
@@ -551,7 +551,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx,
             unwind_frame_sp->GetRegisterContextSP(), cfa, next_frame_address,
             behaves_like_zeroth_frame, &next_frame_sc));
 
-        frame_sp->m_frame_list_wp = shared_from_this();
+        frame_sp->m_frame_list_id = GetIdentifier();
         m_frames.push_back(frame_sp);
         unwind_sc = next_frame_sc;
         curr_frame_address = next_frame_address;
@@ -608,7 +608,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx,
       prev_frame->UpdatePreviousFrameFromCurrentFrame(*curr_frame);
       // Now copy the fixed up previous frame into the current frames so the
       // pointer doesn't change.
-      prev_frame_sp->m_frame_list_wp = shared_from_this();
+      prev_frame_sp->m_frame_list_id = GetIdentifier();
       m_frames[curr_frame_idx] = prev_frame_sp;
 
 #if defined(DEBUG_STACK_FRAMES)
diff --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp
index 70d8650662348..88138fd1b09a8 100644
--- a/lldb/source/Target/Thread.cpp
+++ b/lldb/source/Target/Thread.cpp
@@ -29,7 +29,6 @@
 #include "lldb/Target/ScriptedThreadPlan.h"
 #include "lldb/Target/StackFrameRecognizer.h"
 #include "lldb/Target/StopInfo.h"
-#include "lldb/Target/SyntheticFrameProvider.h"
 #include "lldb/Target/SystemRuntime.h"
 #include "lldb/Target/Target.h"
 #include "lldb/Target/ThreadPlan.h"
@@ -263,6 +262,7 @@ void Thread::DestroyThread() {
   m_curr_frames_sp.reset();
   m_prev_frames_sp.reset();
   m_frame_providers.clear();
+  m_provider_chain_ids.clear();
   m_prev_framezero_pc.reset();
 }
 
@@ -1465,16 +1465,16 @@ StackFrameListSP Thread::GetStackFrameList() {
       const auto &descriptors = target.GetScriptedFrameProviderDescriptors();
 
       // Collect all descriptors that apply to this thread.
-      std::vector<const ScriptedFrameProviderDescriptor *>
-          applicable_descriptors;
+      llvm::SmallVector<const ScriptedFrameProviderDescriptor *, 4>
+          thread_descriptors;
       for (const auto &entry : descriptors) {
         const ScriptedFrameProviderDescriptor &descriptor = entry.second;
         if (descriptor.IsValid() && descriptor.AppliesToThread(*this))
-          applicable_descriptors.push_back(&descriptor);
+          thread_descriptors.push_back(&descriptor);
       }
 
       // Sort by priority (lower number = higher priority).
-      llvm::sort(applicable_descriptors,
+      llvm::sort(thread_descriptors,
                  [](const ScriptedFrameProviderDescriptor *a,
                     const ScriptedFrameProviderDescriptor *b) {
                    // nullopt (no priority) sorts last (UINT32_MAX).
@@ -1484,7 +1484,7 @@ StackFrameListSP Thread::GetStackFrameList() {
                  });
 
       // Load ALL matching providers in priority order.
-      for (const auto *descriptor : applicable_descriptors) {
+      for (const auto *descriptor : thread_descriptors) {
         if (llvm::Error error = LoadScriptedFrameProvider(*descriptor)) {
           LLDB_LOG_ERROR(GetLog(LLDBLog::Thread), std::move(error),
                          "Failed to load scripted frame provider: {0}");
@@ -1495,58 +1495,85 @@ StackFrameListSP Thread::GetStackFrameList() {
   }
 
   // Create the frame list based on whether we have providers.
-  if (!m_frame_providers.empty()) {
+  if (!m_provider_chain_ids.empty()) {
     // We have providers - use the last one in the chain.
     // The last provider has already been chained with all previous providers.
-    StackFrameListSP input_frames = m_frame_providers.back()->GetInputFrames();
-    m_curr_frames_sp = std::make_shared<SyntheticStackFrameList>(
-        *this, input_frames, m_prev_frames_sp, true, m_frame_providers.back());
+    auto last_desc_id_pair = m_provider_chain_ids.back();
+    uint64_t last_id = last_desc_id_pair.second;
+    auto it = m_frame_providers.find(last_desc_id_pair.second);
+    if (it != m_frame_providers.end()) {
+      SyntheticFrameProviderSP last_provider = it->second;
+      StackFrameListSP input_frames = last_provider->GetInputFrames();
+      m_curr_frames_sp = std::make_shared<SyntheticStackFrameList>(
+          *this, input_frames, m_prev_frames_sp, true, last_provider, last_id);
+    }
   } else {
-    // No provider - use normal unwinder frames.
-    m_curr_frames_sp =
-        std::make_shared<StackFrameList>(*this, m_prev_frames_sp, true);
+    // No provider - use normal unwinder frames with stable ID = 0.
+    m_curr_frames_sp = std::make_shared<StackFrameList>(
+        *this, m_prev_frames_sp, true, /*provider_id=*/0);
+  }
+
+  // Register this frame list by its identifier for later lookup.
+  if (m_curr_frames_sp) {
+    m_frame_lists_by_id[m_curr_frames_sp->GetIdentifier()] = m_curr_frames_sp;
   }
 
   return m_curr_frames_sp;
 }
 
+lldb::StackFrameListSP Thread::GetFrameListByIdentifier(uint64_t id) {
+  std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
+
+  auto it = m_frame_lists_by_id.find(id);
+  if (it != m_frame_lists_by_id.end()) {
+    return it->second.lock();
+  }
+  return nullptr;
+}
+
 llvm::Error Thread::LoadScriptedFrameProvider(
     const ScriptedFrameProviderDescriptor &descriptor) {
   std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
 
-  // Create input frames for this provider:
-  // - If no providers exist yet, use real unwinder frames.
-  // - If providers exist, wrap the previous provider in a
-  // SyntheticStackFrameList.
-  //   This creates the chain:...
[truncated]

@github-actions
Copy link

github-actions bot commented Jan 30, 2026

🐧 Linux x64 Test Results

  • 33293 tests passed
  • 506 tests skipped

✅ The build succeeded and all tests passed.

@medismailben medismailben force-pushed the queue-debugging-frame-2-var branch from 2abd4b8 to 0de79d9 Compare January 30, 2026 23:58
@medismailben
Copy link
Member Author

Addressed everyone's comment but I'm still looking at the test failure (TestFrameProviderCircularDependency.py). It looks like in certain cases, calling SBThread.GetNumFrames() while having a frame provider registered is causing a deadlock. But this only happens when a frame provider is used.

@medismailben medismailben force-pushed the queue-debugging-frame-2-var branch 3 times, most recently from bf73415 to 0b9bb7c Compare February 2, 2026 02:02
@medismailben medismailben added this to the LLVM 22.x Release milestone Feb 2, 2026
@github-project-automation github-project-automation bot moved this to Needs Triage in LLVM Release Status Feb 2, 2026
@medismailben medismailben force-pushed the queue-debugging-frame-2-var branch 2 times, most recently from 8a45593 to 559a8bd Compare February 3, 2026 01:44
@github-actions
Copy link

github-actions bot commented Feb 3, 2026

✅ With the latest revision this PR passed the Python code formatter.

@medismailben medismailben force-pushed the queue-debugging-frame-2-var branch from 559a8bd to 2ed6e54 Compare February 3, 2026 01:48
Copy link
Collaborator

@jimingham jimingham left a comment

Choose a reason for hiding this comment

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

The code looks right. But there's a lot of code that handles changing providers as you go, but no tests of that behavior. We should make sure that if you get an SBFrame from a provided stack frame list, step, delete the provider, and then ask about the SBFrame, we get a reasonable error and not a crash.

@medismailben medismailben force-pushed the queue-debugging-frame-2-var branch 3 times, most recently from 56558c9 to 0bcc247 Compare February 3, 2026 02:18
@medismailben medismailben force-pushed the queue-debugging-frame-2-var branch from 0bcc247 to 12cd831 Compare February 3, 2026 02:33
Copy link
Collaborator

@jimingham jimingham left a comment

Choose a reason for hiding this comment

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

LGTM

@medismailben medismailben force-pushed the queue-debugging-frame-2-var branch from 12cd831 to 784a5f0 Compare February 3, 2026 03:00
When a user holds an SBFrame reference and then triggers an inferior function
call (via expression evaluation or GetExtendedBacktraceThread), variables in
that frame become inaccessible with "register fp is not available" errors.

This happens because inferior function calls execute through
ThreadPlanCallFunction, which calls ClearStackFrames() during cleanup to
invalidate the unwinder state. ExecutionContextRef objects in the old SBFrames
were tracking StackFrameLists via weak_ptr, which became stale when
ClearStackFrames() created new instances.

The fix uses stable StackFrameList identifiers that persist across
ClearStackFrames():
- ID = 0: Normal unwinder frames (constant across all instances)
- ID = sequential counter: Scripted frame provider instances

ExecutionContextRef now stores the frame list ID instead of a weak_ptr, allowing
it to resolve to the current StackFrameList with fresh unwinder state after an
inferior function call completes.

The Thread object preserves the provider chain configuration
(m_provider_chain_ids and m_next_provider_id) across ClearStackFrames() so
that recreated StackFrameLists get the same IDs. When providers need to be
recreated, GetStackFrameList() rebuilds them from the persisted configuration.

This commit also fixes a deadlock when Python scripted frame providers call
back into LLDB during frame fetching. The m_list_mutex is now released before
calling GetFrameAtIndex() on the Python scripted frame provider to prevent
same-thread deadlock. A dedicated m_unwinder_frames_sp member ensures
GetFrameListByIdentifier(0) always returns the current unwinder frames, and
proper cleanup in DestroyThread() and ClearStackFrames() to prevent modules
from lingering after a Thread (and its StackFrameLists) gets destroyed.

Added test validates that variables remain accessible after
GetExtendedBacktraceThread triggers an inferior function call to fetch
libdispatch Queue Info.

rdar://167027676

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
@medismailben medismailben force-pushed the queue-debugging-frame-2-var branch from 784a5f0 to f935c10 Compare February 3, 2026 03:01
@medismailben medismailben enabled auto-merge (squash) February 3, 2026 03:11
@medismailben medismailben merged commit c373d76 into llvm:main Feb 3, 2026
8 of 9 checks passed
@github-project-automation github-project-automation bot moved this from Needs Triage to Done in LLVM Release Status Feb 3, 2026
medismailben added a commit to medismailben/llvm-project that referenced this pull request Feb 3, 2026
…lls (llvm#178823)

When a user holds an SBFrame reference and then triggers an inferior
function
call (via expression evaluation or GetExtendedBacktraceThread),
variables in
that frame become inaccessible with "register fp is not available"
errors.

This happens because inferior function calls execute through
ThreadPlanCallFunction, which calls ClearStackFrames() during cleanup to
invalidate the unwinder state. ExecutionContextRef objects in the old
SBFrames
were tracking StackFrameLists via weak_ptr, which became stale when
ClearStackFrames() created new instances.

The fix uses stable StackFrameList identifiers that persist across
ClearStackFrames():
- ID = 0: Normal unwinder frames (constant across all instances)
- ID = sequential counter: Scripted frame provider instances

ExecutionContextRef now stores the frame list ID instead of a weak_ptr,
allowing
it to resolve to the current StackFrameList with fresh unwinder state
after an
inferior function call completes.

The Thread object preserves the provider chain configuration
(m_provider_chain_ids and m_next_provider_id) across ClearStackFrames()
so
that recreated StackFrameLists get the same IDs. When providers need to
be
recreated, GetStackFrameList() rebuilds them from the persisted
configuration.

This commit also fixes a deadlock when Python scripted frame providers
call
back into LLDB during frame fetching. The m_list_mutex is now released
before
calling GetFrameAtIndex() on the Python scripted frame provider to
prevent
same-thread deadlock. A dedicated m_unwinder_frames_sp member ensures
GetFrameListByIdentifier(0) always returns the current unwinder frames,
and
proper cleanup in DestroyThread() and ClearStackFrames() to prevent
modules
from lingering after a Thread (and its StackFrameLists) gets destroyed.

Added test validates that variables remain accessible after
GetExtendedBacktraceThread triggers an inferior function call to fetch
libdispatch Queue Info.

rdar://167027676

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
(cherry picked from commit c373d76)
@medismailben
Copy link
Member Author

/cherry-pick c373d76

@llvmbot
Copy link
Member

llvmbot commented Feb 3, 2026

Failed to cherry-pick: c373d76

https://github.com/llvm/llvm-project/actions/runs/21639754775

Please manually backport the fix and push it to your github fork. Once this is done, please create a pull request

medismailben added a commit to medismailben/llvm-project that referenced this pull request Feb 3, 2026
…lls (llvm#178823)

When a user holds an SBFrame reference and then triggers an inferior
function
call (via expression evaluation or GetExtendedBacktraceThread),
variables in
that frame become inaccessible with "register fp is not available"
errors.

This happens because inferior function calls execute through
ThreadPlanCallFunction, which calls ClearStackFrames() during cleanup to
invalidate the unwinder state. ExecutionContextRef objects in the old
SBFrames
were tracking StackFrameLists via weak_ptr, which became stale when
ClearStackFrames() created new instances.

The fix uses stable StackFrameList identifiers that persist across
ClearStackFrames():
- ID = 0: Normal unwinder frames (constant across all instances)
- ID = sequential counter: Scripted frame provider instances

ExecutionContextRef now stores the frame list ID instead of a weak_ptr,
allowing
it to resolve to the current StackFrameList with fresh unwinder state
after an
inferior function call completes.

The Thread object preserves the provider chain configuration
(m_provider_chain_ids and m_next_provider_id) across ClearStackFrames()
so
that recreated StackFrameLists get the same IDs. When providers need to
be
recreated, GetStackFrameList() rebuilds them from the persisted
configuration.

This commit also fixes a deadlock when Python scripted frame providers
call
back into LLDB during frame fetching. The m_list_mutex is now released
before
calling GetFrameAtIndex() on the Python scripted frame provider to
prevent
same-thread deadlock. A dedicated m_unwinder_frames_sp member ensures
GetFrameListByIdentifier(0) always returns the current unwinder frames,
and
proper cleanup in DestroyThread() and ClearStackFrames() to prevent
modules
from lingering after a Thread (and its StackFrameLists) gets destroyed.

Added test validates that variables remain accessible after
GetExtendedBacktraceThread triggers an inferior function call to fetch
libdispatch Queue Info.

rdar://167027676

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
(cherry picked from commit c373d76)
moar55 pushed a commit to moar55/llvm-project that referenced this pull request Feb 3, 2026
…lls (llvm#178823)

When a user holds an SBFrame reference and then triggers an inferior
function
call (via expression evaluation or GetExtendedBacktraceThread),
variables in
that frame become inaccessible with "register fp is not available"
errors.

This happens because inferior function calls execute through
ThreadPlanCallFunction, which calls ClearStackFrames() during cleanup to
invalidate the unwinder state. ExecutionContextRef objects in the old
SBFrames
were tracking StackFrameLists via weak_ptr, which became stale when
ClearStackFrames() created new instances.

The fix uses stable StackFrameList identifiers that persist across
ClearStackFrames():
- ID = 0: Normal unwinder frames (constant across all instances)
- ID = sequential counter: Scripted frame provider instances

ExecutionContextRef now stores the frame list ID instead of a weak_ptr,
allowing
it to resolve to the current StackFrameList with fresh unwinder state
after an
inferior function call completes.

The Thread object preserves the provider chain configuration
(m_provider_chain_ids and m_next_provider_id) across ClearStackFrames()
so
that recreated StackFrameLists get the same IDs. When providers need to
be
recreated, GetStackFrameList() rebuilds them from the persisted
configuration.

This commit also fixes a deadlock when Python scripted frame providers
call
back into LLDB during frame fetching. The m_list_mutex is now released
before
calling GetFrameAtIndex() on the Python scripted frame provider to
prevent
same-thread deadlock. A dedicated m_unwinder_frames_sp member ensures
GetFrameListByIdentifier(0) always returns the current unwinder frames,
and
proper cleanup in DestroyThread() and ClearStackFrames() to prevent
modules
from lingering after a Thread (and its StackFrameLists) gets destroyed.

Added test validates that variables remain accessible after
GetExtendedBacktraceThread triggers an inferior function call to fetch
libdispatch Queue Info.

rdar://167027676

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
c-rhodes pushed a commit to llvmbot/llvm-project that referenced this pull request Feb 9, 2026
…lls (llvm#178823)

When a user holds an SBFrame reference and then triggers an inferior
function
call (via expression evaluation or GetExtendedBacktraceThread),
variables in
that frame become inaccessible with "register fp is not available"
errors.

This happens because inferior function calls execute through
ThreadPlanCallFunction, which calls ClearStackFrames() during cleanup to
invalidate the unwinder state. ExecutionContextRef objects in the old
SBFrames
were tracking StackFrameLists via weak_ptr, which became stale when
ClearStackFrames() created new instances.

The fix uses stable StackFrameList identifiers that persist across
ClearStackFrames():
- ID = 0: Normal unwinder frames (constant across all instances)
- ID = sequential counter: Scripted frame provider instances

ExecutionContextRef now stores the frame list ID instead of a weak_ptr,
allowing
it to resolve to the current StackFrameList with fresh unwinder state
after an
inferior function call completes.

The Thread object preserves the provider chain configuration
(m_provider_chain_ids and m_next_provider_id) across ClearStackFrames()
so
that recreated StackFrameLists get the same IDs. When providers need to
be
recreated, GetStackFrameList() rebuilds them from the persisted
configuration.

This commit also fixes a deadlock when Python scripted frame providers
call
back into LLDB during frame fetching. The m_list_mutex is now released
before
calling GetFrameAtIndex() on the Python scripted frame provider to
prevent
same-thread deadlock. A dedicated m_unwinder_frames_sp member ensures
GetFrameListByIdentifier(0) always returns the current unwinder frames,
and
proper cleanup in DestroyThread() and ClearStackFrames() to prevent
modules
from lingering after a Thread (and its StackFrameLists) gets destroyed.

Added test validates that variables remain accessible after
GetExtendedBacktraceThread triggers an inferior function call to fetch
libdispatch Queue Info.

rdar://167027676

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
(cherry picked from commit c373d76)
rishabhmadan19 pushed a commit to rishabhmadan19/llvm-project that referenced this pull request Feb 9, 2026
…lls (llvm#178823)

When a user holds an SBFrame reference and then triggers an inferior
function
call (via expression evaluation or GetExtendedBacktraceThread),
variables in
that frame become inaccessible with "register fp is not available"
errors.

This happens because inferior function calls execute through
ThreadPlanCallFunction, which calls ClearStackFrames() during cleanup to
invalidate the unwinder state. ExecutionContextRef objects in the old
SBFrames
were tracking StackFrameLists via weak_ptr, which became stale when
ClearStackFrames() created new instances.

The fix uses stable StackFrameList identifiers that persist across
ClearStackFrames():
- ID = 0: Normal unwinder frames (constant across all instances)
- ID = sequential counter: Scripted frame provider instances

ExecutionContextRef now stores the frame list ID instead of a weak_ptr,
allowing
it to resolve to the current StackFrameList with fresh unwinder state
after an
inferior function call completes.

The Thread object preserves the provider chain configuration
(m_provider_chain_ids and m_next_provider_id) across ClearStackFrames()
so
that recreated StackFrameLists get the same IDs. When providers need to
be
recreated, GetStackFrameList() rebuilds them from the persisted
configuration.

This commit also fixes a deadlock when Python scripted frame providers
call
back into LLDB during frame fetching. The m_list_mutex is now released
before
calling GetFrameAtIndex() on the Python scripted frame provider to
prevent
same-thread deadlock. A dedicated m_unwinder_frames_sp member ensures
GetFrameListByIdentifier(0) always returns the current unwinder frames,
and
proper cleanup in DestroyThread() and ClearStackFrames() to prevent
modules
from lingering after a Thread (and its StackFrameLists) gets destroyed.

Added test validates that variables remain accessible after
GetExtendedBacktraceThread triggers an inferior function call to fetch
libdispatch Queue Info.

rdar://167027676

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

5 participants