[Lldb-commits] [lldb] [lldb] Fix variable access in old SBFrames after inferior function calls (PR #178823)
Med Ismail Bennani via lldb-commits
lldb-commits at lists.llvm.org
Mon Feb 2 18:08:38 PST 2026
https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/178823
>From 56558c9200f2c13712fcb4991df43c46f085e6f6 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Mon, 2 Feb 2026 18:08:21 -0800
Subject: [PATCH] [lldb] Fix variable access in old SBFrames after inferior
function calls
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 at bennani.ma>
---
lldb/include/lldb/Target/ExecutionContext.h | 19 +-
lldb/include/lldb/Target/StackFrame.h | 12 +-
lldb/include/lldb/Target/StackFrameList.h | 12 +-
lldb/include/lldb/Target/Thread.h | 31 ++-
lldb/include/lldb/lldb-defines.h | 1 +
lldb/include/lldb/lldb-types.h | 1 +
lldb/source/Target/ExecutionContext.cpp | 74 +++--
lldb/source/Target/StackFrameList.cpp | 40 ++-
lldb/source/Target/Thread.cpp | 143 +++++++---
.../TestScriptedFrameProvider.py | 263 ++++++++++++++++++
.../scripted_frame_provider/main.cpp | 30 ++
.../macosx/extended-backtrace-api/Makefile | 3 +
.../TestExtendedBacktraceAPI.py | 144 ++++++++++
.../API/macosx/extended-backtrace-api/main.m | 53 ++++
14 files changed, 743 insertions(+), 83 deletions(-)
create mode 100644 lldb/test/API/macosx/extended-backtrace-api/Makefile
create mode 100644 lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py
create mode 100644 lldb/test/API/macosx/extended-backtrace-api/main.m
diff --git a/lldb/include/lldb/Target/ExecutionContext.h b/lldb/include/lldb/Target/ExecutionContext.h
index 8637234c4fb95..47bcd729abcdd 100644
--- a/lldb/include/lldb/Target/ExecutionContext.h
+++ b/lldb/include/lldb/Target/ExecutionContext.h
@@ -13,10 +13,13 @@
#include "lldb/Host/ProcessRunLock.h"
#include "lldb/Target/StackID.h"
+#include "lldb/Target/SyntheticFrameProvider.h"
#include "lldb/lldb-private.h"
namespace lldb_private {
+struct StoppedExecutionContext;
+
//===----------------------------------------------------------------------===//
/// Execution context objects refer to objects in the execution of the program
/// that is being debugged. The consist of one or more of the following
@@ -270,9 +273,12 @@ class ExecutionContextRef {
void ClearFrame() {
m_stack_id.Clear();
- m_frame_list_wp.reset();
+ m_frame_list_id.reset();
}
+ friend llvm::Expected<StoppedExecutionContext>
+ GetStoppedExecutionContext(const ExecutionContextRef *exe_ctx_ref_ptr);
+
protected:
// Member variables
lldb::TargetWP m_target_wp; ///< A weak reference to a target
@@ -283,13 +289,10 @@ 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.
+ /// A map of identifiers to scripted frame providers used in this thread.
+ mutable std::optional<
+ std::pair<ScriptedFrameProviderDescriptor, lldb::frame_list_id_t>>
+ m_frame_list_id;
};
/// \class ExecutionContext ExecutionContext.h
diff --git a/lldb/include/lldb/Target/StackFrame.h b/lldb/include/lldb/Target/StackFrame.h
index 46922448d6e59..5cba9afe2a7e8 100644
--- a/lldb/include/lldb/Target/StackFrame.h
+++ b/lldb/include/lldb/Target/StackFrame.h
@@ -542,17 +542,17 @@ 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
+ /// Returns the StackFrameList identifier that contains this frame, allowing
/// frames to resolve execution contexts without calling
/// Thread::GetStackFrameList(), which can cause circular dependencies
/// 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
+ lldb::frame_list_id_t GetContainingStackFrameListIdentifier() const {
+ return m_frame_list_id;
}
protected:
@@ -598,8 +598,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;
+ lldb::frame_list_id_t m_frame_list_id = 0;
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..715781abb83a3 100644
--- a/lldb/include/lldb/Target/StackFrameList.h
+++ b/lldb/include/lldb/Target/StackFrameList.h
@@ -24,7 +24,8 @@ 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,
+ lldb::frame_list_id_t provider_id = 0);
virtual ~StackFrameList();
@@ -104,6 +105,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.
+ lldb::frame_list_id_t GetIdentifier() const { return m_identifier; }
+
protected:
friend class Thread;
friend class ScriptedFrameProvider;
@@ -212,6 +216,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.
+ lldb::frame_list_id_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 +251,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..9cc86a37c63e5 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,7 @@
#include "lldb/Utility/UnimplementedError.h"
#include "lldb/Utility/UserID.h"
#include "lldb/lldb-private.h"
+#include "llvm/ADT/DenseMap.h"
#include "llvm/Support/MemoryBuffer.h"
#define LLDB_THREAD_MAX_STOP_EXC_DATA 8
@@ -1297,12 +1299,18 @@ class Thread : public std::enable_shared_from_this<Thread>,
lldb::StackFrameListSP GetStackFrameList();
+ /// Get a frame list by its unique identifier.
+ lldb::StackFrameListSP GetFrameListByIdentifier(lldb::frame_list_id_t id);
+
llvm::Error
LoadScriptedFrameProvider(const ScriptedFrameProviderDescriptor &descriptor);
+ llvm::Expected<ScriptedFrameProviderDescriptor>
+ GetScriptedFrameProviderDescriptorForID(lldb::frame_list_id_t id) const;
+
void ClearScriptedFrameProvider();
- const llvm::SmallVector<lldb::SyntheticFrameProviderSP, 0> &
+ const llvm::DenseMap<lldb::frame_list_id_t, lldb::SyntheticFrameProviderSP> &
GetFrameProviders() const {
return m_frame_providers;
}
@@ -1384,6 +1392,8 @@ class Thread : public std::enable_shared_from_this<Thread>,
m_state_mutex; ///< Multithreaded protection for m_state.
mutable std::recursive_mutex
m_frame_mutex; ///< Multithreaded protection for m_state.
+ lldb::StackFrameListSP
+ m_unwinder_frames_sp; ///< The unwinder frame list (ID 0).
lldb::StackFrameListSP m_curr_frames_sp; ///< The stack frames that get lazily
///populated after a thread stops.
lldb::StackFrameListSP m_prev_frames_sp; ///< The previous stack frames from
@@ -1410,8 +1420,23 @@ 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::DenseMap<lldb::frame_list_id_t, lldb::SyntheticFrameProviderSP>
+ m_frame_providers;
+
+ /// Ordered chain of provider IDs.
+ /// Persists across ClearStackFrames() to maintain stable provider IDs.
+ std::vector<std::pair<ScriptedFrameProviderDescriptor, lldb::frame_list_id_t>>
+ m_provider_chain_ids;
+
+ /// Map from frame list identifier to frame list weak pointer.
+ mutable llvm::DenseMap<lldb::frame_list_id_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.
+ lldb::frame_list_id_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/include/lldb/lldb-defines.h b/lldb/include/lldb/lldb-defines.h
index 52bf7c5cce947..8e1029387d2da 100644
--- a/lldb/include/lldb/lldb-defines.h
+++ b/lldb/include/lldb/lldb-defines.h
@@ -89,6 +89,7 @@
#define LLDB_INVALID_PROCESS_ID 0
#define LLDB_INVALID_THREAD_ID 0
#define LLDB_INVALID_FRAME_ID UINT32_MAX
+#define LLDB_UNWINDER_FRAME_LIST_ID 0
#define LLDB_INVALID_SIGNAL_NUMBER INT32_MAX
#define LLDB_INVALID_SYMBOL_ID UINT32_MAX
#define LLDB_INVALID_OFFSET UINT64_MAX // Must match max of lldb::offset_t
diff --git a/lldb/include/lldb/lldb-types.h b/lldb/include/lldb/lldb-types.h
index e309fc8833ce9..bb4c34ef8e1f5 100644
--- a/lldb/include/lldb/lldb-types.h
+++ b/lldb/include/lldb/lldb-types.h
@@ -83,6 +83,7 @@ typedef uint64_t user_id_t;
typedef uint64_t pid_t;
typedef uint64_t tid_t;
typedef uint64_t offset_t;
+typedef uint32_t frame_list_id_t;
typedef int32_t break_id_t;
typedef int32_t watch_id_t;
typedef uint32_t wp_resource_id_t;
diff --git a/lldb/source/Target/ExecutionContext.cpp b/lldb/source/Target/ExecutionContext.cpp
index b16ff26266c53..e4b2f07d8d8d1 100644
--- a/lldb/source/Target/ExecutionContext.cpp
+++ b/lldb/source/Target/ExecutionContext.cpp
@@ -160,6 +160,15 @@ lldb_private::GetStoppedExecutionContext(
auto thread_sp = exe_ctx_ref_ptr->GetThreadSP();
auto frame_sp = exe_ctx_ref_ptr->GetFrameSP();
+
+ if (!frame_sp && exe_ctx_ref_ptr->m_frame_list_id) {
+ return llvm::createStringError(
+ "attempted to create a StoppedExecutionContext but "
+ "ScriptedFrameProvider (name = %s - id = %u) is no longer available",
+ exe_ctx_ref_ptr->m_frame_list_id->first.GetName().str().c_str(),
+ exe_ctx_ref_ptr->m_frame_list_id->second);
+ }
+
return StoppedExecutionContext(target_sp, process_sp, thread_sp, frame_sp,
std::move(api_lock), std::move(stop_locker));
}
@@ -466,12 +475,25 @@ operator=(const ExecutionContext &exe_ctx) {
else
m_tid = LLDB_INVALID_THREAD_ID;
lldb::StackFrameSP frame_sp(exe_ctx.GetFrameSP());
- if (frame_sp) {
- m_stack_id = frame_sp->GetStackID();
- m_frame_list_wp = frame_sp->GetContainingStackFrameList();
+
+ if (frame_sp && thread_sp) {
+ lldb::frame_list_id_t frame_list_id =
+ frame_sp->GetContainingStackFrameListIdentifier();
+ auto frame_list_descriptor_or_err =
+ thread_sp->GetScriptedFrameProviderDescriptorForID(frame_list_id);
+ if (frame_list_descriptor_or_err) {
+ m_stack_id = frame_sp->GetStackID();
+ m_frame_list_id = {*frame_list_descriptor_or_err, frame_list_id};
+ } else {
+ LLDB_LOG_ERROR(GetLog(LLDBLog::Process),
+ frame_list_descriptor_or_err.takeError(),
+ "Failed to fetch scripted frame provider descriptor: {0}");
+ m_stack_id.Clear();
+ m_frame_list_id.reset();
+ }
} else {
m_stack_id.Clear();
- m_frame_list_wp.reset();
+ m_frame_list_id.reset();
}
return *this;
}
@@ -512,11 +534,25 @@ void ExecutionContextRef::SetThreadSP(const lldb::ThreadSP &thread_sp) {
}
void ExecutionContextRef::SetFrameSP(const lldb::StackFrameSP &frame_sp) {
- if (frame_sp) {
+ if (!frame_sp) {
+ Clear();
+ return;
+ }
+
+ lldb::ThreadSP thread_sp = frame_sp->GetThread();
+ lldb::frame_list_id_t frame_list_id =
+ frame_sp->GetContainingStackFrameListIdentifier();
+ auto frame_list_descriptor_or_err =
+ thread_sp->GetScriptedFrameProviderDescriptorForID(frame_list_id);
+
+ if (frame_list_descriptor_or_err) {
m_stack_id = frame_sp->GetStackID();
- m_frame_list_wp = frame_sp->GetContainingStackFrameList();
- SetThreadSP(frame_sp->GetThread());
+ m_frame_list_id = {*frame_list_descriptor_or_err, frame_list_id};
+ SetThreadSP(thread_sp);
} else {
+ LLDB_LOG_ERROR(GetLog(LLDBLog::Process),
+ frame_list_descriptor_or_err.takeError(),
+ "Failed to fetch scripted frame provider descriptor: {0}");
ClearFrame();
ClearThread();
m_process_wp.reset();
@@ -641,21 +677,23 @@ lldb::ThreadSP ExecutionContextRef::GetThreadSP() const {
}
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()) {
+ lldb::ThreadSP thread_sp(GetThreadSP());
+ if (!thread_sp || !m_stack_id.IsValid())
+ return lldb::StackFrameSP();
+
+ // Try the remembered frame list first to avoid circular dependencies
+ // during frame provider initialization.
+ if (m_frame_list_id) {
+ if (auto frame_list_sp =
+ thread_sp->GetFrameListByIdentifier(m_frame_list_id->second)) {
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
- // initialization.
- lldb::ThreadSP thread_sp(GetThreadSP());
- if (thread_sp)
- return thread_sp->GetFrameWithStackID(m_stack_id);
}
- return lldb::StackFrameSP();
+
+ // Fallback: ask the thread, which might re-trigger the frame provider
+ // initialization.
+ return thread_sp->GetFrameWithStackID(m_stack_id);
}
ExecutionContext
diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index 1ad269e8783cc..4f4b06f30460b 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -38,12 +38,13 @@ using namespace lldb_private;
// StackFrameList constructor
StackFrameList::StackFrameList(Thread &thread,
const lldb::StackFrameListSP &prev_frames_sp,
- bool show_inline_frames)
+ bool show_inline_frames,
+ lldb::frame_list_id_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 +60,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)) {}
@@ -70,12 +71,25 @@ bool SyntheticStackFrameList::FetchFramesUpTo(
size_t num_synthetic_frames = 0;
// Use the provider to generate frames lazily.
if (m_provider) {
+ // Get starting index under lock.
+ uint32_t start_idx = 0;
+ {
+ std::shared_lock<std::shared_mutex> guard(m_list_mutex);
+ start_idx = m_frames.size();
+ }
+
// Keep fetching until we reach end_idx or the provider returns an error.
- for (uint32_t idx = m_frames.size(); idx <= end_idx; idx++) {
+ for (uint32_t idx = start_idx; idx <= end_idx; idx++) {
if (allow_interrupt &&
m_thread.GetProcess()->GetTarget().GetDebugger().InterruptRequested())
return true;
+
+ // Call Python WITHOUT holding lock - prevents deadlock.
auto frame_or_err = m_provider->GetFrameAtIndex(idx);
+
+ // Acquire lock to modify m_frames.
+ std::unique_lock<std::shared_mutex> guard(m_list_mutex);
+
if (!frame_or_err) {
// Provider returned error - we've reached the end.
LLDB_LOG_ERROR(GetLog(LLDBLog::Thread), frame_or_err.takeError(),
@@ -89,7 +103,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 +389,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);
}
@@ -410,6 +424,10 @@ bool StackFrameList::GetFramesUpTo(uint32_t end_idx,
return false;
}
+ // Release lock before FetchFramesUpTo which may call Python.
+ // FetchFramesUpTo will acquire locks as needed.
+ guard.unlock();
+
// We're adding concrete and inlined frames now:
was_interrupted = FetchFramesUpTo(end_idx, allow_interrupt);
@@ -491,7 +509,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 +544,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 +569,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 +626,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..2c95c2d209b45 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"
@@ -57,6 +56,8 @@
#include "lldb/ValueObject/ValueObjectConstResult.h"
#include "lldb/lldb-enumerations.h"
+#include "llvm/Support/MathExtras.h"
+
#include <memory>
#include <optional>
@@ -262,7 +263,10 @@ void Thread::DestroyThread() {
std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
m_curr_frames_sp.reset();
m_prev_frames_sp.reset();
+ m_unwinder_frames_sp.reset();
m_frame_providers.clear();
+ m_provider_chain_ids.clear();
+ m_frame_lists_by_id.clear();
m_prev_framezero_pc.reset();
}
@@ -1465,16 +1469,15 @@ StackFrameListSP Thread::GetStackFrameList() {
const auto &descriptors = target.GetScriptedFrameProviderDescriptors();
// Collect all descriptors that apply to this thread.
- std::vector<const ScriptedFrameProviderDescriptor *>
- applicable_descriptors;
+ std::vector<const ScriptedFrameProviderDescriptor *> 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 +1487,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 +1498,123 @@ 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, last_id] = m_provider_chain_ids.back();
+ auto it = m_frame_providers.find(last_id);
+ 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 {
+ LLDB_LOG(GetLog(LLDBLog::Thread),
+ "Missing frame provider (id = {0}) in Thread #{1:x}}", last_id,
+ GetID());
+ }
+ }
+
+ if (!m_curr_frames_sp) {
+ // No provider - use normal unwinder frames with stable ID = 0.
+ m_unwinder_frames_sp = std::make_shared<StackFrameList>(
+ *this, m_prev_frames_sp, true, /*provider_id=*/0);
+ m_curr_frames_sp = m_unwinder_frames_sp;
} else {
- // No provider - use normal unwinder frames.
- m_curr_frames_sp =
- std::make_shared<StackFrameList>(*this, m_prev_frames_sp, true);
+ // Register this frame list by its identifier for later lookup.
+ m_frame_lists_by_id.insert(
+ {m_curr_frames_sp->GetIdentifier(), m_curr_frames_sp});
}
return m_curr_frames_sp;
}
+lldb::StackFrameListSP
+Thread::GetFrameListByIdentifier(lldb::frame_list_id_t id) {
+ std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
+
+ // ID 0 is reserved for the unwinder frame list. Always return the unwinder
+ // frame list for ID 0.
+ if (id == 0) {
+ return m_unwinder_frames_sp;
+ }
+
+ 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: each provider's OUTPUT becomes the next
- // provider's INPUT.
- StackFrameListSP new_provider_input_frames;
+ StackFrameListSP input_frames;
if (m_frame_providers.empty()) {
- // First provider gets real unwinder frames.
- new_provider_input_frames =
- std::make_shared<StackFrameList>(*this, m_prev_frames_sp, true);
+ // First provider gets real unwinder frames with stable ID = 0.
+ m_unwinder_frames_sp =
+ std::make_shared<StackFrameList>(*this, m_prev_frames_sp, true,
+ /*provider_id=*/0);
+ input_frames = m_unwinder_frames_sp;
} else {
- // Subsequent providers get the previous provider's OUTPUT.
- // We create a SyntheticStackFrameList that wraps the previous provider.
- SyntheticFrameProviderSP prev_provider = m_frame_providers.back();
- StackFrameListSP prev_provider_frames = prev_provider->GetInputFrames();
- new_provider_input_frames = std::make_shared<SyntheticStackFrameList>(
- *this, prev_provider_frames, m_prev_frames_sp, true, prev_provider);
- }
-
- auto provider_or_err = SyntheticFrameProvider::CreateInstance(
- new_provider_input_frames, descriptor);
+ // Subsequent providers wrap the previous provider.
+ auto [last_desc, last_id] = m_provider_chain_ids.back();
+ auto it = m_frame_providers.find(last_id);
+ if (it == m_frame_providers.end())
+ return llvm::createStringError("Previous frame provider not found");
+ SyntheticFrameProviderSP last_provider = it->second;
+ StackFrameListSP last_provider_frames = last_provider->GetInputFrames();
+ input_frames = std::make_shared<SyntheticStackFrameList>(
+ *this, last_provider_frames, m_prev_frames_sp, true, last_provider,
+ last_id);
+ }
+
+ auto provider_or_err =
+ SyntheticFrameProvider::CreateInstance(input_frames, descriptor);
if (!provider_or_err)
return provider_or_err.takeError();
- // Append to the chain.
- m_frame_providers.push_back(*provider_or_err);
+ if (m_next_provider_id == std::numeric_limits<lldb::frame_list_id_t>::max())
+ m_next_provider_id = 1;
+ else
+ m_next_provider_id++;
+
+ lldb::frame_list_id_t provider_id = m_next_provider_id;
+ m_frame_providers.insert({provider_id, *provider_or_err});
+
+ // Add to the provider chain.
+ m_provider_chain_ids.push_back({descriptor, provider_id});
+
return llvm::Error::success();
}
+llvm::Expected<ScriptedFrameProviderDescriptor>
+Thread::GetScriptedFrameProviderDescriptorForID(
+ lldb::frame_list_id_t id) const {
+ if (id == LLDB_UNWINDER_FRAME_LIST_ID)
+ return ScriptedFrameProviderDescriptor();
+
+ auto it = llvm::find_if(
+ m_provider_chain_ids,
+ [id](const std::pair<ScriptedFrameProviderDescriptor,
+ lldb::frame_list_id_t> &provider_id_pair) {
+ return provider_id_pair.second == id;
+ });
+
+ if (it == m_provider_chain_ids.end())
+ return llvm::createStringError(
+ "Couldn't find ScriptedFrameProviderDescriptor for id = %u.", id);
+
+ return it->first;
+}
+
void Thread::ClearScriptedFrameProvider() {
std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
m_frame_providers.clear();
+ m_provider_chain_ids.clear();
+ m_next_provider_id = 1; // Reset counter.
+ m_unwinder_frames_sp.reset();
m_curr_frames_sp.reset();
m_prev_frames_sp.reset();
}
@@ -1570,8 +1638,13 @@ void Thread::ClearStackFrames() {
if (m_curr_frames_sp && m_curr_frames_sp->WereAllFramesFetched())
m_prev_frames_sp.swap(m_curr_frames_sp);
m_curr_frames_sp.reset();
+ m_unwinder_frames_sp.reset();
+ // Clear the provider instances, but keep the chain configuration
+ // (m_provider_chain_ids and m_next_provider_id) so provider IDs
+ // remain stable across ClearStackFrames() calls.
m_frame_providers.clear();
+ m_frame_lists_by_id.clear();
m_extended_info.reset();
m_extended_info_fetched = false;
}
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
index 7dd74013b90f8..ce5217477a0e0 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
@@ -783,3 +783,266 @@ def test_get_values(self):
self.assertEqual(var.unsigned, variables.GetValueAtIndex(0).unsigned)
varp1 = frame0.GetValueForVariablePath("variable_in_main + 1")
self.assertEqual(varp1.unsigned, 124)
+
+ def test_frame_validity_after_step(self):
+ """Test that SBFrame references from ScriptedFrameProvider remain valid after stepping.
+
+ This test verifies that ExecutionContextRef properly handles frame list identifiers
+ when the underlying stack changes. After stepping, old frame references should become
+ invalid gracefully without crashing.
+ """
+ self.build()
+ target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
+ self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
+ )
+
+ # Import the test frame provider.
+ script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
+ self.runCmd("command script import " + script_path)
+
+ # Register a provider that prepends synthetic frames.
+ error = lldb.SBError()
+ provider_id = target.RegisterScriptedFrameProvider(
+ "test_frame_providers.PrependFrameProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(error.Success(), f"Failed to register provider: {error}")
+
+ # Get frame references before stepping.
+ frame0_before = thread.GetFrameAtIndex(0)
+ frame1_before = thread.GetFrameAtIndex(1)
+ frame2_before = thread.GetFrameAtIndex(2)
+
+ self.assertIsNotNone(frame0_before)
+ self.assertIsNotNone(frame1_before)
+ self.assertIsNotNone(frame2_before)
+
+ # Verify frames are valid and have expected PCs.
+ self.assertTrue(frame0_before.IsValid(), "Frame 0 should be valid before step")
+ self.assertTrue(frame1_before.IsValid(), "Frame 1 should be valid before step")
+ self.assertTrue(frame2_before.IsValid(), "Frame 2 should be valid before step")
+
+ pc0_before = frame0_before.GetPC()
+ pc1_before = frame1_before.GetPC()
+ pc2_before = frame2_before.GetPC()
+
+ self.assertEqual(pc0_before, 0x9000, "Frame 0 should have synthetic PC 0x9000")
+ self.assertEqual(pc1_before, 0xA000, "Frame 1 should have synthetic PC 0xA000")
+
+ # Step the thread, which will invalidate the old frame list.
+ thread.StepInstruction(False)
+
+ # After stepping, the frame list has changed. Old frame references should
+ # detect this and become invalid, but shouldn't crash.
+ # The key here is that GetPC() and other operations should handle the
+ # "frame provider no longer available" case gracefully.
+
+ # Try to access the old frames - they should either:
+ # 1. Return invalid/default values gracefully, or
+ # 2. Still work if the frame provider is re-applied
+
+ # Get new frames after stepping.
+ frame0_after = thread.GetFrameAtIndex(0)
+ self.assertIsNotNone(frame0_after)
+ self.assertTrue(
+ frame0_after.IsValid(), "New frame 0 should be valid after step"
+ )
+
+ # The old frame references might or might not be valid depending on whether
+ # the frame provider is still active. What's important is that accessing
+ # them doesn't crash and handles the situation gracefully.
+ # We'll just verify we can call methods on them without crashing.
+ try:
+ _ = frame0_before.GetPC()
+ _ = frame0_before.IsValid()
+ _ = frame0_before.GetFunctionName()
+ except Exception as e:
+ self.fail(f"Accessing old frame reference should not crash: {e}")
+
+ def test_breakpoint_callbacks_with_provider_lifecycle(self):
+ """Test provider registration/removal at breakpoints and SBFrame validity across lifecycle.
+
+ This test verifies:
+ 1. Registering a provider while stopped at a breakpoint
+ 2. SBFrame references from synthetic frames persist across continues
+ 3. SBFrame references can access variables in real frames while provider is active
+ 4. Removing a provider while stopped at a breakpoint
+ 5. SBFrame references from removed provider don't crash when accessed
+ """
+ self.build()
+ target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+ self.assertTrue(target.IsValid(), "Target should be valid")
+
+ # Import the test frame provider.
+ script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
+ self.runCmd("command script import " + script_path)
+
+ # Set up breakpoints at the return statements in foo, bar, and baz
+ # This ensures local variables are initialized
+ bp_foo = target.BreakpointCreateBySourceRegex(
+ "Break in foo", lldb.SBFileSpec(self.source)
+ )
+ bp_bar = target.BreakpointCreateBySourceRegex(
+ "Break in bar", lldb.SBFileSpec(self.source)
+ )
+ bp_baz = target.BreakpointCreateBySourceRegex(
+ "Break in baz", lldb.SBFileSpec(self.source)
+ )
+
+ self.assertTrue(bp_foo.IsValid(), "Breakpoint at foo should be valid")
+ self.assertTrue(bp_bar.IsValid(), "Breakpoint at bar should be valid")
+ self.assertTrue(bp_baz.IsValid(), "Breakpoint at baz should be valid")
+
+ # Launch the process
+ process = target.LaunchSimple(None, None, self.get_process_working_directory())
+ self.assertTrue(process.IsValid(), "Process should be valid")
+
+ # We should hit the foo breakpoint first
+ self.assertEqual(
+ process.GetState(), lldb.eStateStopped, "Process should be stopped at foo"
+ )
+ thread = process.GetSelectedThread()
+ self.assertIsNotNone(thread, "Should have a selected thread")
+
+ # Register the provider at foo breakpoint
+ error = lldb.SBError()
+ provider_id = target.RegisterScriptedFrameProvider(
+ "test_frame_providers.PrependFrameProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(error.Success(), f"Failed to register provider: {error}")
+ self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+ # Verify the provider is active by checking frame count
+ # PrependFrameProvider adds 2 synthetic frames
+ original_frame_count = thread.GetNumFrames()
+ self.assertGreater(
+ original_frame_count,
+ 2,
+ "Should have at least synthetic frames + real frames",
+ )
+
+ # Check the backtrace shows the synthetic frames
+ frame0 = thread.GetFrameAtIndex(0)
+ frame1 = thread.GetFrameAtIndex(1)
+ frame2 = thread.GetFrameAtIndex(2)
+
+ self.assertIsNotNone(frame0, "Frame 0 should exist")
+ self.assertIsNotNone(frame1, "Frame 1 should exist")
+ self.assertIsNotNone(frame2, "Frame 2 should exist")
+
+ # First two frames should be synthetic with expected PCs
+ pc0 = frame0.GetPC()
+ pc1 = frame1.GetPC()
+
+ self.assertEqual(pc0, 0x9000, "Frame 0 should have synthetic PC 0x9000")
+ self.assertEqual(pc1, 0xA000, "Frame 1 should have synthetic PC 0xA000")
+
+ # Frame 2 should be the real foo frame
+ self.assertIn("foo", frame2.GetFunctionName(), "Frame 2 should be in foo")
+
+ # Save references to the synthetic frames
+ saved_frames = [frame0, frame1, frame2]
+
+ # Verify we can access variables in frame2 (real frame)
+ foo_local = frame2.FindVariable("foo_local")
+ self.assertTrue(foo_local.IsValid(), "Should find foo_local variable")
+ self.assertEqual(
+ foo_local.GetValueAsUnsigned(), 20, "foo_local should be 20 (10 * 2)"
+ )
+
+ # Continue to bar breakpoint
+ process.Continue()
+ self.assertEqual(
+ process.GetState(), lldb.eStateStopped, "Process should be stopped at bar"
+ )
+
+ # Verify the provider is still active
+ current_frame_count = thread.GetNumFrames()
+ self.assertGreater(current_frame_count, 2, "Should still have synthetic frames")
+
+ # Verify the saved frames are still accessible without crashing
+ # Note: They might not be "valid" in the traditional sense since we've moved
+ # to a different execution context, but they shouldn't crash
+ saved_frame0 = saved_frames[0]
+ saved_frame1 = saved_frames[1]
+ saved_frame2 = saved_frames[2]
+
+ try:
+ _ = saved_frame0.GetPC()
+ _ = saved_frame1.GetPC()
+ # saved_frame2 was a real frame in foo, it might be on the stack still
+ # depending on the call chain
+ except Exception as e:
+ self.fail(f"Accessing saved frames should not crash: {e}")
+
+ # Get current frames at bar
+ bar_frame0 = thread.GetFrameAtIndex(0)
+ bar_frame1 = thread.GetFrameAtIndex(1)
+ bar_frame2 = thread.GetFrameAtIndex(2)
+
+ # Verify current frames have synthetic PCs
+ self.assertEqual(
+ bar_frame0.GetPC(), 0x9000, "Frame 0 at bar should have synthetic PC"
+ )
+ self.assertEqual(
+ bar_frame1.GetPC(), 0xA000, "Frame 1 at bar should have synthetic PC"
+ )
+ self.assertIn("bar", bar_frame2.GetFunctionName(), "Frame 2 should be in bar")
+
+ # Verify we can access variables in the bar frame
+ bar_local = bar_frame2.FindVariable("bar_local")
+ self.assertTrue(bar_local.IsValid(), "Should find bar_local variable")
+ self.assertEqual(
+ bar_local.GetValueAsUnsigned(), 25, "bar_local should be 25 (5 * 5)"
+ )
+
+ # Continue to baz breakpoint
+ process.Continue()
+ self.assertEqual(
+ process.GetState(), lldb.eStateStopped, "Process should be stopped at baz"
+ )
+
+ # Now manually remove the provider
+ result = target.RemoveScriptedFrameProvider(provider_id)
+ self.assertSuccess(
+ result, f"Should successfully remove provider with ID {provider_id}"
+ )
+ # Verify frames no longer have synthetic frames
+ final_frame_count = thread.GetNumFrames()
+
+ # Without the provider, we should have fewer frames (no synthetic ones)
+ self.assertLess(
+ final_frame_count,
+ original_frame_count,
+ "Frame count should decrease after provider removal",
+ )
+
+ # First frame should now be the real baz frame (no synthetic frames)
+ baz_frame0 = thread.GetFrameAtIndex(0)
+ self.assertIn(
+ "baz", baz_frame0.GetFunctionName(), "Frame 0 should now be real baz frame"
+ )
+
+ # The synthetic PC values should no longer appear
+ for i in range(final_frame_count):
+ frame = thread.GetFrameAtIndex(i)
+ pc = frame.GetPC()
+ self.assertNotEqual(
+ pc, 0x9000, f"Frame {i} should not have synthetic PC 0x9000"
+ )
+ self.assertNotEqual(
+ pc, 0xA000, f"Frame {i} should not have synthetic PC 0xA000"
+ )
+
+ # Verify the originally saved frames are now truly invalid/stale
+ # They should still not crash when accessed
+ try:
+ _ = saved_frame0.GetPC()
+ _ = saved_frame0.IsValid()
+ _ = saved_frame1.GetPC()
+ _ = saved_frame1.IsValid()
+ except Exception as e:
+ self.fail(f"Accessing invalidated frames should not crash: {e}")
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/main.cpp b/lldb/test/API/functionalities/scripted_frame_provider/main.cpp
index e1d346c29052b..b61742d91e0fe 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/main.cpp
+++ b/lldb/test/API/functionalities/scripted_frame_provider/main.cpp
@@ -10,6 +10,24 @@ std::condition_variable cv;
int ready_count = 0;
constexpr int NUM_THREADS = 2;
+int foo(int x) {
+ int foo_local = x * 2;
+ int foo_result = foo_local + 1;
+ return foo_result; // Break in foo
+}
+
+int bar(int x) {
+ int bar_local = x * x;
+ int bar_result = bar_local - 3;
+ return bar_result; // Break in bar
+}
+
+int baz(int x) {
+ int baz_local = x + 7;
+ int baz_result = baz_local / 2;
+ return baz_result; // Break in baz
+}
+
void thread_func(int thread_num) {
std::cout << "Thread " << thread_num << " started\n";
@@ -33,6 +51,10 @@ int main(int argc, char **argv) {
int variable_in_main = 123;
(void)variable_in_main;
+ // Call foo for first breakpoint
+ int result_foo = foo(10);
+ (void)result_foo;
+
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = std::thread(thread_func, i);
}
@@ -49,6 +71,14 @@ int main(int argc, char **argv) {
std::cout << "Main thread at barrier\n";
+ // Call bar for second breakpoint
+ int result_bar = bar(5);
+ (void)result_bar;
+
+ // Call baz for third breakpoint
+ int result_baz = baz(11);
+ (void)result_baz;
+
for (int i = 0; i < NUM_THREADS; i++)
threads[i].join();
diff --git a/lldb/test/API/macosx/extended-backtrace-api/Makefile b/lldb/test/API/macosx/extended-backtrace-api/Makefile
new file mode 100644
index 0000000000000..845553d5e3f2f
--- /dev/null
+++ b/lldb/test/API/macosx/extended-backtrace-api/Makefile
@@ -0,0 +1,3 @@
+OBJC_SOURCES := main.m
+
+include Makefile.rules
diff --git a/lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py b/lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py
new file mode 100644
index 0000000000000..0e9ee0755065e
--- /dev/null
+++ b/lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py
@@ -0,0 +1,144 @@
+"""Test SBThread.GetExtendedBacktraceThread API with queue debugging."""
+
+import os
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestExtendedBacktraceAPI(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ def setUp(self):
+ TestBase.setUp(self)
+ self.main_source = "main.m"
+
+ @skipUnlessDarwin
+ @add_test_categories(["objc", "pyapi"])
+ def test_extended_backtrace_thread_api(self):
+ """Test GetExtendedBacktraceThread with queue debugging."""
+ self.build()
+ exe = self.getBuildArtifact("a.out")
+
+ # Get Xcode developer directory path.
+ # Try DEVELOPER_DIR environment variable first, then fall back to xcode-select.
+ xcode_dev_path = os.environ.get("DEVELOPER_DIR")
+
+ if not xcode_dev_path:
+ import subprocess
+
+ xcode_dev_path = (
+ subprocess.check_output(["xcode-select", "-p"]).decode("utf-8").strip()
+ )
+
+ # Check for libBacktraceRecording.dylib.
+ libbtr_path = os.path.join(
+ xcode_dev_path, "usr/lib/libBacktraceRecording.dylib"
+ )
+
+ self.assertTrue(
+ os.path.isfile(libbtr_path),
+ f"libBacktraceRecording.dylib is not present at {libbtr_path}",
+ )
+
+ self.assertTrue(
+ os.path.isfile("/usr/lib/system/introspection/libdispatch.dylib"),
+ "introspection libdispatch dylib not installed.",
+ )
+
+ # Create launch info with environment variables for libBacktraceRecording.
+ launch_info = lldb.SBLaunchInfo(None)
+ launch_info.SetWorkingDirectory(self.get_process_working_directory())
+ launch_info.SetEnvironmentEntries(
+ [
+ f"DYLD_INSERT_LIBRARIES={libbtr_path}",
+ "DYLD_LIBRARY_PATH=/usr/lib/system/introspection",
+ ],
+ True,
+ )
+
+ # Launch the process and run to breakpoint.
+ target, process, thread, bp = lldbutil.run_to_name_breakpoint(
+ self, "do_work_level_5", launch_info=launch_info, bkpt_module="a.out"
+ )
+
+ self.assertTrue(target.IsValid(), VALID_TARGET)
+ self.assertTrue(process.IsValid(), PROCESS_IS_VALID)
+ self.assertTrue(thread.IsValid(), "Stopped thread is valid")
+ self.assertTrue(bp.IsValid(), VALID_BREAKPOINT)
+
+ # Call GetNumQueues to ensure queue information is loaded.
+ num_queues = process.GetNumQueues()
+
+ # Check that we can find the com.apple.main-thread queue.
+ main_thread_queue_found = False
+ for i in range(num_queues):
+ queue = process.GetQueueAtIndex(i)
+ if queue.GetName() == "com.apple.main-thread":
+ main_thread_queue_found = True
+ break
+
+ # Verify we have at least 5 frames.
+ self.assertGreaterEqual(
+ thread.GetNumFrames(),
+ 5,
+ "Thread should have at least 5 frames in backtrace",
+ )
+
+ # Get frame 2 BEFORE calling GetExtendedBacktraceThread.
+ # This mimics what Xcode does - it has the frame objects ready.
+ frame2 = thread.GetFrameAtIndex(2)
+ self.assertTrue(frame2.IsValid(), "Frame 2 is valid")
+
+ # Now test GetExtendedBacktraceThread.
+ # This is the critical part - getting the extended backtrace calls into
+ # libBacktraceRecording which does an inferior function call, and this
+ # invalidates/clears the unwinder state.
+ extended_thread = thread.GetExtendedBacktraceThread("libdispatch")
+
+ # This should be valid since we injected libBacktraceRecording.
+ self.assertTrue(
+ extended_thread.IsValid(),
+ "Extended backtrace thread for 'libdispatch' should be valid with libBacktraceRecording loaded",
+ )
+
+ # The extended thread should have frames.
+ self.assertGreater(
+ extended_thread.GetNumFrames(),
+ 0,
+ "Extended backtrace thread should have at least one frame",
+ )
+
+ # Test frame 2 on the extended backtrace thread.
+ self.assertGreater(
+ extended_thread.GetNumFrames(),
+ 2,
+ "Extended backtrace thread should have at least 3 frames to access frame 2",
+ )
+
+ extended_frame2 = extended_thread.GetFrameAtIndex(2)
+ self.assertTrue(extended_frame2.IsValid(), "Extended thread frame 2 is valid")
+
+ # NOW try to access variables from frame 2 of the ORIGINAL thread.
+ # This is the key test - after GetExtendedBacktraceThread() has executed
+ # an inferior function call, the unwinder state may be invalidated.
+ # Xcode exhibits this bug where variables show "register fp is not available"
+ # after extended backtrace retrieval.
+
+ # Set frame 2 as the selected frame so expect_var_path works.
+ thread.SetSelectedFrame(2)
+
+ variables = frame2.GetVariables(False, True, False, True)
+ self.assertGreater(
+ variables.GetSize(), 0, "Frame 2 should have at least one variable"
+ )
+
+ # Test all variables in frame 2, like Xcode does.
+ # Use expect_var_path to verify each variable is accessible without errors.
+ for i in range(variables.GetSize()):
+ var = variables.GetValueAtIndex(i)
+ var_name = var.GetName()
+
+ # This will fail if the variable contains "not available" or has errors.
+ self.expect_var_path(var_name)
diff --git a/lldb/test/API/macosx/extended-backtrace-api/main.m b/lldb/test/API/macosx/extended-backtrace-api/main.m
new file mode 100644
index 0000000000000..8f2186845a651
--- /dev/null
+++ b/lldb/test/API/macosx/extended-backtrace-api/main.m
@@ -0,0 +1,53 @@
+#include <dispatch/dispatch.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <unistd.h>
+
+void do_work_level_5(void) {
+ // Frame 0 will have these variables.
+ int frame0_var = 100;
+ const char *frame0_string = "frame_zero";
+ float frame0_float = 1.5f;
+
+ // This is where we'll set the breakpoint.
+ printf("Level 5 work executing\n"); // Break here.
+ while (1)
+ sleep(1);
+}
+
+void do_work_level_4(void) {
+ // Frame 1 will have these variables.
+ int frame1_var = 200;
+ const char *frame1_string = "frame_one";
+ long frame1_long = 9876543210L;
+
+ do_work_level_5();
+}
+
+void do_work_level_3(void) {
+ // Frame 2 will have these variables.
+ int test_variable = 42;
+ const char *test_string = "test_value";
+ double test_double = 3.14159;
+
+ do_work_level_4();
+}
+
+void do_work_level_2(void) { do_work_level_3(); }
+
+void do_work_level_1(void *context) { do_work_level_2(); }
+
+int main(int argc, const char *argv[]) {
+ // Create a serial dispatch queue.
+ dispatch_queue_t worker_queue =
+ dispatch_queue_create("com.test.worker_queue", DISPATCH_QUEUE_SERIAL);
+ dispatch_queue_t submitter_queue =
+ dispatch_queue_create("com.test.submitter_queue", DISPATCH_QUEUE_SERIAL);
+
+ // Submit work from one queue to another to create extended backtrace.
+ dispatch_async_f(submitter_queue, &worker_queue, do_work_level_1);
+
+ // Keep main thread alive.
+ dispatch_main();
+ return 0;
+}
More information about the lldb-commits
mailing list