[lldb] Fix circular dependency and deadlock in scripted frame providers (#187411)
When a scripted frame provider calls back into the thread's frame machinery (e.g. via HandleCommand or EvaluateExpression), two problems arise: 1. GetStackFrameList() re-enters the SyntheticStackFrameList construction, causing infinite recursion. 2. ClearStackFrames() tries to read-lock the StackFrameList's shared_mutex that is already write-locked by GetFramesUpTo, causing a deadlock. This patch fixes those issues by tracking when a provider is actively fetching frames via a per-host-thread map (m_provider_frames_by_thread) keyed by HostThread. The map is pushed/popped in SyntheticStackFrameList::FetchFramesUpTo before calling into the provider. GetStackFrameList() checks it to route re-entrant calls: - The provider's own host thread gets the parent frame list, preventing circular dependency when get_frame_at_index calls back into GetFrameAtIndex. - The private state thread also gets the parent frame list, preventing deadlock when a provider calls EvaluateExpression (which needs the private state thread to process events). - Other host threads proceed normally and block on the frame list mutex until the provider finishes, getting the correct synthetic result. ClearStackFrames() returns early if any provider is active, since the frame state is shared and tearing it down while a provider is mid-construction is both unnecessary and unsafe. rdar://171558394 Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
This commit is contained in:
committed by
GitHub
parent
b580bed84a
commit
e1cd55879b
@@ -53,6 +53,16 @@ class ScriptedFrameProvider(metaclass=ABCMeta):
|
||||
|
||||
You can register your frame provider either via the CLI command ``target frame-provider register`` or
|
||||
via the API ``SBThread.RegisterScriptedFrameProvider``.
|
||||
|
||||
.. note::
|
||||
|
||||
Changing the process state either directly (e.g. stepping or resuming)
|
||||
or indirectly (e.g. expression evaluation) within the provider will not
|
||||
trigger reconstructing the input frame list. Expression evaluation is
|
||||
additionally restricted to run only the current thread
|
||||
(``SetStopOthers(true)``, ``SetTryAllThreads(false)``) while a provider
|
||||
is active, to avoid unwanted process state changes during frame
|
||||
construction.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "lldb/Host/HostNativeThreadForward.h"
|
||||
#include "lldb/Utility/Status.h"
|
||||
#include "lldb/lldb-types.h"
|
||||
#include "llvm/ADT/DenseMapInfo.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -51,4 +52,20 @@ private:
|
||||
};
|
||||
}
|
||||
|
||||
namespace llvm {
|
||||
template <> struct DenseMapInfo<lldb_private::HostThread> {
|
||||
static inline lldb_private::HostThread getEmptyKey() {
|
||||
return lldb_private::HostThread(
|
||||
DenseMapInfo<lldb::thread_t>::getEmptyKey());
|
||||
}
|
||||
static inline lldb_private::HostThread getTombstoneKey() {
|
||||
return lldb_private::HostThread(
|
||||
DenseMapInfo<lldb::thread_t>::getTombstoneKey());
|
||||
}
|
||||
static unsigned getHashValue(const lldb_private::HostThread &val);
|
||||
static bool isEqual(const lldb_private::HostThread &lhs,
|
||||
const lldb_private::HostThread &rhs);
|
||||
};
|
||||
} // namespace llvm
|
||||
|
||||
#endif
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "lldb/Core/UserSettingsController.h"
|
||||
#include "lldb/Host/HostThread.h"
|
||||
#include "lldb/Target/ExecutionContextScope.h"
|
||||
#include "lldb/Target/RegisterCheckpoint.h"
|
||||
#include "lldb/Target/StackFrameList.h"
|
||||
@@ -1299,6 +1300,11 @@ public:
|
||||
|
||||
lldb::StackFrameListSP GetStackFrameList();
|
||||
|
||||
/// Push/pop provider input frames for the current host thread.
|
||||
/// Used by SyntheticStackFrameList to scope re-entrant frame lookups.
|
||||
void PushProviderFrameList(lldb::StackFrameListSP frames);
|
||||
void PopProviderFrameList();
|
||||
|
||||
/// Get a frame list by its unique identifier.
|
||||
lldb::StackFrameListSP GetFrameListByIdentifier(lldb::frame_list_id_t id);
|
||||
|
||||
@@ -1315,6 +1321,9 @@ public:
|
||||
return m_frame_providers;
|
||||
}
|
||||
|
||||
/// Returns true if any host thread is currently inside a provider.
|
||||
bool IsAnyProviderActive();
|
||||
|
||||
protected:
|
||||
friend class ThreadPlan;
|
||||
friend class ThreadList;
|
||||
@@ -1396,6 +1405,21 @@ protected:
|
||||
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.
|
||||
/// Per-host-thread stack of active provider input frames. A provider
|
||||
/// always operates on its parent StackFrameList — not the synthetic list
|
||||
/// currently being constructed. While a provider is running, its parent
|
||||
/// list is pushed here so that any code the provider executes that
|
||||
/// fetches a StackFrameList (e.g. GetFrameAtIndex, EvaluateExpression)
|
||||
/// transparently sees the parent list rather than the in-construction
|
||||
/// list at the end of the provider chain.
|
||||
///
|
||||
/// Keyed by host thread so the provider's own thread and the private state
|
||||
/// thread get the parent list, while unrelated threads proceed normally.
|
||||
/// ClearStackFrames() is also guarded: frame state is shared, so it must
|
||||
/// not be torn down while any provider is mid-construction.
|
||||
std::mutex m_provider_frames_mutex;
|
||||
llvm::DenseMap<HostThread, std::vector<lldb::StackFrameListSP>>
|
||||
m_active_frame_providers_by_thread;
|
||||
lldb::StackFrameListSP m_prev_frames_sp; ///< The previous stack frames from
|
||||
///the last time this thread stopped.
|
||||
std::optional<lldb::addr_t>
|
||||
|
||||
@@ -55,3 +55,13 @@ bool HostThread::HasThread() const {
|
||||
return false;
|
||||
return m_native_thread->GetSystemHandle() != LLDB_INVALID_HOST_THREAD;
|
||||
}
|
||||
|
||||
unsigned llvm::DenseMapInfo<HostThread>::getHashValue(const HostThread &val) {
|
||||
return DenseMapInfo<thread_t>::getHashValue(
|
||||
val.GetNativeThread().GetSystemHandle());
|
||||
}
|
||||
|
||||
bool llvm::DenseMapInfo<HostThread>::isEqual(const HostThread &lhs,
|
||||
const HostThread &rhs) {
|
||||
return lhs.EqualsThread(rhs);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "lldb/Target/Unwind.h"
|
||||
#include "lldb/Utility/LLDBLog.h"
|
||||
#include "lldb/Utility/Log.h"
|
||||
#include "llvm/ADT/ScopeExit.h"
|
||||
#include "llvm/ADT/SmallPtrSet.h"
|
||||
#include "llvm/Support/ConvertUTF.h"
|
||||
|
||||
@@ -78,6 +79,16 @@ bool SyntheticStackFrameList::FetchFramesUpTo(
|
||||
m_thread.GetProcess()->GetTarget().GetDebugger().InterruptRequested())
|
||||
return true;
|
||||
|
||||
// Ensure the provider sees its parent StackFrameList, not the
|
||||
// synthetic list being constructed. In a chain A->B->C, provider C
|
||||
// must consult B's output - using its own list would be nonsensical.
|
||||
// This also applies when the provider runs commands or expressions:
|
||||
// any path that fetches a StackFrameList should transparently get the
|
||||
// parent list. As a side benefit, this avoids circular re-entrancy and
|
||||
// deadlocks on the private state thread.
|
||||
m_thread.PushProviderFrameList(m_input_frames);
|
||||
auto clear_active_frames =
|
||||
llvm::scope_exit([&]() { m_thread.PopProviderFrameList(); });
|
||||
auto frame_or_err = m_provider->GetFrameAtIndex(idx);
|
||||
|
||||
if (!frame_or_err) {
|
||||
|
||||
@@ -2896,9 +2896,19 @@ ExpressionResults Target::EvaluateExpression(
|
||||
result_valobj_sp = persistent_var_sp->GetValueObject();
|
||||
execution_results = eExpressionCompleted;
|
||||
} else {
|
||||
// If this expression is being evaluated from inside a frame provider,
|
||||
// force single-thread execution. Resuming all threads while a provider
|
||||
// is mid-construction could cause unwanted process state changes.
|
||||
EvaluateExpressionOptions effective_options = options;
|
||||
if (ThreadSP thread_sp = exe_ctx.GetThreadSP()) {
|
||||
if (thread_sp->IsAnyProviderActive()) {
|
||||
effective_options.SetStopOthers(true);
|
||||
effective_options.SetTryAllThreads(false);
|
||||
}
|
||||
}
|
||||
llvm::StringRef prefix = GetExpressionPrefixContents();
|
||||
execution_results =
|
||||
UserExpression::Evaluate(exe_ctx, options, expr, prefix,
|
||||
UserExpression::Evaluate(exe_ctx, effective_options, expr, prefix,
|
||||
result_valobj_sp, fixed_expression, ctx_obj);
|
||||
}
|
||||
|
||||
|
||||
@@ -267,6 +267,10 @@ void Thread::DestroyThread() {
|
||||
m_frame_providers.clear();
|
||||
m_provider_chain_ids.clear();
|
||||
m_frame_lists_by_id.clear();
|
||||
{
|
||||
std::lock_guard<std::mutex> pguard(m_provider_frames_mutex);
|
||||
m_active_frame_providers_by_thread.clear();
|
||||
}
|
||||
m_prev_framezero_pc.reset();
|
||||
}
|
||||
|
||||
@@ -1452,9 +1456,70 @@ void Thread::CalculateExecutionContext(ExecutionContext &exe_ctx) {
|
||||
exe_ctx.SetContext(shared_from_this());
|
||||
}
|
||||
|
||||
void Thread::PushProviderFrameList(StackFrameListSP frames) {
|
||||
std::lock_guard<std::mutex> guard(m_provider_frames_mutex);
|
||||
HostThread current(Host::GetCurrentThread());
|
||||
auto &stack = m_active_frame_providers_by_thread[current];
|
||||
LLDB_LOG(GetLog(LLDBLog::Thread),
|
||||
"Thread::PushProviderFrameList: tid = 0x{0:x}, depth = {1} -> {2}",
|
||||
GetID(), stack.size(), stack.size() + 1);
|
||||
stack.push_back(std::move(frames));
|
||||
}
|
||||
|
||||
void Thread::PopProviderFrameList() {
|
||||
std::lock_guard<std::mutex> guard(m_provider_frames_mutex);
|
||||
HostThread current(Host::GetCurrentThread());
|
||||
auto it = m_active_frame_providers_by_thread.find(current);
|
||||
size_t pre_pop_depth =
|
||||
(it != m_active_frame_providers_by_thread.end()) ? it->second.size() : 0;
|
||||
LLDB_LOG(GetLog(LLDBLog::Thread),
|
||||
"Thread::PopProviderFrameList: tid = 0x{0:x}, depth = {1} -> {2}",
|
||||
GetID(), pre_pop_depth, pre_pop_depth ? pre_pop_depth - 1 : 0);
|
||||
assert(it != m_active_frame_providers_by_thread.end() && !it->second.empty());
|
||||
if (it == m_active_frame_providers_by_thread.end() || it->second.empty())
|
||||
return;
|
||||
it->second.pop_back();
|
||||
if (it->second.empty())
|
||||
m_active_frame_providers_by_thread.erase(it);
|
||||
}
|
||||
|
||||
bool Thread::IsAnyProviderActive() {
|
||||
std::lock_guard<std::mutex> guard(m_provider_frames_mutex);
|
||||
return !m_active_frame_providers_by_thread.empty();
|
||||
}
|
||||
|
||||
StackFrameListSP Thread::GetStackFrameList() {
|
||||
std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
|
||||
|
||||
// If a provider is currently fetching frames, return the provider's input
|
||||
// frames instead of m_curr_frames_sp. m_curr_frames_sp IS the
|
||||
// SyntheticStackFrameList, and accessing it would trigger provider code on
|
||||
// THIS thread too. That is dangerous because:
|
||||
// - On the provider's own host thread: circular dependency / deadlock.
|
||||
// - On the private state thread: the provider may call EvaluateExpression
|
||||
// which needs the private state thread to process events -> deadlock.
|
||||
// - On any other thread: would run the provider concurrently.
|
||||
// Returning the input (parent) frames is always safe.
|
||||
{
|
||||
std::lock_guard<std::mutex> pguard(m_provider_frames_mutex);
|
||||
if (!m_active_frame_providers_by_thread.empty()) {
|
||||
// Check if the current host thread is inside a provider call.
|
||||
HostThread current(Host::GetCurrentThread());
|
||||
auto it = m_active_frame_providers_by_thread.find(current);
|
||||
if (it != m_active_frame_providers_by_thread.end() && !it->second.empty())
|
||||
return it->second.back();
|
||||
|
||||
// If the private state thread calls GetStackFrameList while a provider
|
||||
// is active on another thread, return parent frames too. The provider
|
||||
// may call EvaluateExpression which needs the private state thread to
|
||||
// process events — touching m_curr_frames_sp (the synthetic list) would
|
||||
// trigger the provider and deadlock.
|
||||
ProcessSP process_sp = GetProcess();
|
||||
if (process_sp && process_sp->CurrentThreadIsPrivateStateThread())
|
||||
return m_active_frame_providers_by_thread.begin()->second.back();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_curr_frames_sp)
|
||||
return m_curr_frames_sp;
|
||||
|
||||
@@ -1623,6 +1688,15 @@ std::optional<addr_t> Thread::GetPreviousFrameZeroPC() {
|
||||
void Thread::ClearStackFrames() {
|
||||
std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
|
||||
|
||||
// If any host thread is inside a frame provider call (e.g. the provider
|
||||
// called EvaluateExpression which resumes the process), don't tear down the
|
||||
// frame state. The synthetic frame list is still being constructed and the
|
||||
// thread will stop right back where it was after the expression finishes.
|
||||
// This must be a global check (not per-host-thread) because the frame state
|
||||
// is shared and clearing it would destroy in-progress provider work.
|
||||
if (IsAnyProviderActive())
|
||||
return;
|
||||
|
||||
GetUnwinder().Clear();
|
||||
m_prev_framezero_pc.reset();
|
||||
if (RegisterContextSP reg_ctx_sp = GetRegisterContext())
|
||||
|
||||
@@ -16,12 +16,8 @@ class FrameProviderCircularDependencyTestCase(TestBase):
|
||||
TestBase.setUp(self)
|
||||
self.source = "main.c"
|
||||
|
||||
@expectedFailureAll(oslist=["linux"], archs=["arm$"])
|
||||
@expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
|
||||
def test_circular_dependency_with_function_replacement(self):
|
||||
"""
|
||||
Test the circular dependency fix with a provider that replaces function names.
|
||||
"""
|
||||
def launch_and_stop_at_breakpoint(self):
|
||||
"""Build, launch and stop at the breakpoint in bar(). Returns (target, thread)."""
|
||||
self.build()
|
||||
|
||||
target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
|
||||
@@ -45,6 +41,19 @@ class FrameProviderCircularDependencyTestCase(TestBase):
|
||||
frame0 = thread.GetFrameAtIndex(0)
|
||||
self.assertIn("bar", frame0.GetFunctionName(), "Should be stopped in bar()")
|
||||
|
||||
script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
|
||||
self.runCmd("command script import " + script_path)
|
||||
|
||||
return target, thread
|
||||
|
||||
@expectedFailureAll(oslist=["linux"], archs=["arm$"])
|
||||
@expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
|
||||
def test_circular_dependency_with_function_replacement(self):
|
||||
"""
|
||||
Test the circular dependency fix with a provider that replaces function names.
|
||||
"""
|
||||
target, thread = self.launch_and_stop_at_breakpoint()
|
||||
|
||||
original_frame_count = thread.GetNumFrames()
|
||||
self.assertGreaterEqual(
|
||||
original_frame_count, 3, "Should have at least 3 frames: bar, foo, main"
|
||||
@@ -55,9 +64,6 @@ class FrameProviderCircularDependencyTestCase(TestBase):
|
||||
self.assertEqual(frame_names[1], "foo", "Frame 1 should be foo")
|
||||
self.assertEqual(frame_names[2], "main", "Frame 2 should be main")
|
||||
|
||||
script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
|
||||
self.runCmd("command script import " + script_path)
|
||||
|
||||
# Register the frame provider that accesses input_frames.
|
||||
# Before the fix, this registration would trigger the circular dependency:
|
||||
# - Thread::GetStackFrameList() creates provider
|
||||
@@ -117,3 +123,89 @@ class FrameProviderCircularDependencyTestCase(TestBase):
|
||||
self.assertNotEqual(pc, 0, f"Frame {i} should have valid PC")
|
||||
func_name = frame.GetFunctionName()
|
||||
self.assertIsNotNone(func_name, f"Frame {i} should have function name")
|
||||
|
||||
@expectedFailureAll(oslist=["linux"], archs=["arm$"])
|
||||
@expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
|
||||
def test_circular_dependency_handle_command_in_init(self):
|
||||
"""
|
||||
Test that calling HandleCommand('bt') in __init__ doesn't cause
|
||||
a circular dependency / deadlock.
|
||||
"""
|
||||
target, thread = self.launch_and_stop_at_breakpoint()
|
||||
|
||||
original_frame_count = thread.GetNumFrames()
|
||||
|
||||
# Register a provider that calls HandleCommand("bt") during __init__.
|
||||
# Before the fix, this would deadlock because HandleCommand accesses
|
||||
# the thread's stack frames, re-entering Thread::GetStackFrameList().
|
||||
error = lldb.SBError()
|
||||
provider_id = target.RegisterScriptedFrameProvider(
|
||||
"frame_provider.HandleCommandInInitProvider",
|
||||
lldb.SBStructuredData(),
|
||||
error,
|
||||
)
|
||||
|
||||
# If we reach here without hanging, the fix is working.
|
||||
self.assertTrue(
|
||||
error.Success(),
|
||||
f"Should successfully register provider: {error}",
|
||||
)
|
||||
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
||||
|
||||
# Verify the provider passes through all frames unchanged.
|
||||
new_frame_count = thread.GetNumFrames()
|
||||
self.assertEqual(
|
||||
new_frame_count,
|
||||
original_frame_count,
|
||||
"Frame count should be unchanged (pass-through provider)",
|
||||
)
|
||||
|
||||
for i in range(min(new_frame_count, 3)):
|
||||
frame = thread.GetFrameAtIndex(i)
|
||||
self.assertIsNotNone(frame, f"Frame {i} should exist")
|
||||
self.assertIsNotNone(
|
||||
frame.GetFunctionName(), f"Frame {i} should have function name"
|
||||
)
|
||||
|
||||
@expectedFailureAll(oslist=["linux"], archs=["arm$"])
|
||||
@expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
|
||||
def test_circular_dependency_evaluate_expression_in_get_frame(self):
|
||||
"""
|
||||
Test that calling EvaluateExpression in get_frame_at_index doesn't
|
||||
cause a circular dependency / deadlock.
|
||||
"""
|
||||
target, thread = self.launch_and_stop_at_breakpoint()
|
||||
|
||||
original_frame_count = thread.GetNumFrames()
|
||||
|
||||
# Register a provider that calls EvaluateExpression("baz()") during
|
||||
# get_frame_at_index. Before the fix, this would cause a circular
|
||||
# dependency because expression evaluation accesses thread frames.
|
||||
error = lldb.SBError()
|
||||
provider_id = target.RegisterScriptedFrameProvider(
|
||||
"frame_provider.EvaluateExpressionInGetFrameProvider",
|
||||
lldb.SBStructuredData(),
|
||||
error,
|
||||
)
|
||||
|
||||
# If we reach here without hanging, the fix is working.
|
||||
self.assertTrue(
|
||||
error.Success(),
|
||||
f"Should successfully register provider: {error}",
|
||||
)
|
||||
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
||||
|
||||
# Verify the provider passes through all frames unchanged.
|
||||
new_frame_count = thread.GetNumFrames()
|
||||
self.assertEqual(
|
||||
new_frame_count,
|
||||
original_frame_count,
|
||||
"Frame count should be unchanged (pass-through provider)",
|
||||
)
|
||||
|
||||
for i in range(min(new_frame_count, 3)):
|
||||
frame = thread.GetFrameAtIndex(i)
|
||||
self.assertIsNotNone(frame, f"Frame {i} should exist")
|
||||
self.assertIsNotNone(
|
||||
frame.GetFunctionName(), f"Frame {i} should have function name"
|
||||
)
|
||||
|
||||
@@ -100,3 +100,63 @@ class ScriptedFrameObjectProvider(ScriptedFrameProvider):
|
||||
return idx
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class HandleCommandInInitProvider(ScriptedFrameProvider):
|
||||
"""
|
||||
Provider that calls HandleCommand during __init__.
|
||||
|
||||
This reproduces the circular dependency by running a command that
|
||||
accesses the thread's stack frames while the provider is being
|
||||
initialized (i.e. while the SyntheticStackFrameList is being built).
|
||||
Before the fix, HandleCommand("bt") would call Thread::GetStackFrameList()
|
||||
which would try to create the SyntheticStackFrameList again -> deadlock.
|
||||
"""
|
||||
|
||||
def __init__(self, input_frames, args):
|
||||
super().__init__(input_frames, args)
|
||||
# Running "bt" during init triggers frame access on the thread,
|
||||
# which before the fix would cause a circular dependency.
|
||||
result = lldb.SBCommandReturnObject()
|
||||
self.thread.GetProcess().GetTarget().GetDebugger().GetCommandInterpreter().HandleCommand(
|
||||
"bt", result
|
||||
)
|
||||
self.init_succeeded = result.Succeeded()
|
||||
|
||||
@staticmethod
|
||||
def get_description():
|
||||
return "Provider that calls HandleCommand('bt') in __init__"
|
||||
|
||||
def get_frame_at_index(self, idx):
|
||||
if idx < len(self.input_frames):
|
||||
return idx
|
||||
return None
|
||||
|
||||
|
||||
class EvaluateExpressionInGetFrameProvider(ScriptedFrameProvider):
|
||||
"""
|
||||
Provider that calls EvaluateExpression in get_frame_at_index.
|
||||
|
||||
This reproduces the circular dependency by evaluating an expression
|
||||
that accesses the thread's stack frames while the provider is fetching
|
||||
frames. Before the fix, EvaluateExpression would call
|
||||
Thread::GetStackFrameList() which would re-enter the
|
||||
SyntheticStackFrameList -> circular dependency.
|
||||
"""
|
||||
|
||||
def __init__(self, input_frames, args):
|
||||
super().__init__(input_frames, args)
|
||||
|
||||
@staticmethod
|
||||
def get_description():
|
||||
return "Provider that calls EvaluateExpression in get_frame_at_index"
|
||||
|
||||
def get_frame_at_index(self, idx):
|
||||
if idx < len(self.input_frames):
|
||||
frame = self.input_frames[idx]
|
||||
# Evaluating an expression that calls a function triggers frame
|
||||
# access on the thread, which before the fix would cause a
|
||||
# circular dependency.
|
||||
frame.EvaluateExpression("baz()")
|
||||
return idx
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
C_SOURCES := main.c
|
||||
|
||||
include Makefile.rules
|
||||
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Test that a frame provider can pass through all frames from its parent
|
||||
StackFrameList while modifying function names.
|
||||
"""
|
||||
|
||||
import os
|
||||
import lldb
|
||||
from lldbsuite.test.decorators import *
|
||||
from lldbsuite.test.lldbtest import TestBase
|
||||
from lldbsuite.test import lldbutil
|
||||
|
||||
|
||||
class FrameProviderPassThroughPrefixTestCase(TestBase):
|
||||
NO_DEBUG_INFO_TESTCASE = True
|
||||
|
||||
def setUp(self):
|
||||
TestBase.setUp(self)
|
||||
self.source = "main.c"
|
||||
|
||||
@expectedFailureAll(oslist=["linux"], archs=["arm$"])
|
||||
@expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
|
||||
def test_pass_through_with_prefix(self):
|
||||
"""
|
||||
Test that a provider can read every frame from its parent list and
|
||||
return them with a prefixed function name.
|
||||
|
||||
The call stack is main -> foo -> bar -> baz, with a breakpoint in
|
||||
baz. After registering the provider, every frame's function name
|
||||
should be prefixed with 'my_custom_'.
|
||||
"""
|
||||
self.build()
|
||||
|
||||
(target, process, thread, _) = lldbutil.run_to_source_breakpoint(
|
||||
self, "break here", lldb.SBFileSpec(self.source)
|
||||
)
|
||||
|
||||
# Verify the original backtrace: baz, bar, foo, main.
|
||||
expected_names = ["baz", "bar", "foo", "main"]
|
||||
for i, name in enumerate(expected_names):
|
||||
frame = thread.GetFrameAtIndex(i)
|
||||
self.assertEqual(
|
||||
frame.GetFunctionName(),
|
||||
name,
|
||||
f"Frame {i} should be '{name}' before provider",
|
||||
)
|
||||
|
||||
original_frame_count = thread.GetNumFrames()
|
||||
self.assertGreaterEqual(
|
||||
original_frame_count, 4, "Should have at least 4 frames"
|
||||
)
|
||||
|
||||
# Import and register the provider.
|
||||
script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
|
||||
self.runCmd("command script import " + script_path)
|
||||
|
||||
error = lldb.SBError()
|
||||
provider_id = target.RegisterScriptedFrameProvider(
|
||||
"frame_provider.PrefixPassThroughProvider",
|
||||
lldb.SBStructuredData(),
|
||||
error,
|
||||
)
|
||||
self.assertTrue(
|
||||
error.Success(), f"Should register provider successfully: {error}"
|
||||
)
|
||||
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
||||
|
||||
# Frame count should be unchanged since we're passing through.
|
||||
new_frame_count = thread.GetNumFrames()
|
||||
self.assertEqual(
|
||||
new_frame_count,
|
||||
original_frame_count,
|
||||
"Frame count should be unchanged (pass-through provider)",
|
||||
)
|
||||
|
||||
# Every frame should now have the 'my_custom_' prefix exactly once.
|
||||
prefix = "my_custom_"
|
||||
for i, name in enumerate(expected_names):
|
||||
frame = thread.GetFrameAtIndex(i)
|
||||
expected = prefix + name
|
||||
self.assertEqual(
|
||||
frame.GetFunctionName(),
|
||||
expected,
|
||||
f"Frame {i} should be '{expected}' after provider",
|
||||
)
|
||||
|
||||
@expectedFailureAll(oslist=["linux"], archs=["arm$"])
|
||||
@expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
|
||||
def test_provider_receives_parent_frames(self):
|
||||
"""
|
||||
Test that the provider's input_frames come from the parent
|
||||
StackFrameList, not from the provider's own output.
|
||||
|
||||
The provider peeks at input_frames[0] when constructing frame 1.
|
||||
If that name already carries the 'my_custom_' prefix, the provider
|
||||
knows it was given its own output list and flags the error by
|
||||
inserting a 'danger_will_robinson_' prefix. The test asserts that
|
||||
'danger_will_robinson_' never
|
||||
appears, proving the provider received the bare parent list.
|
||||
"""
|
||||
self.build()
|
||||
|
||||
(target, process, thread, _) = lldbutil.run_to_source_breakpoint(
|
||||
self, "break here", lldb.SBFileSpec(self.source)
|
||||
)
|
||||
|
||||
# Import and register the validating provider.
|
||||
script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
|
||||
self.runCmd("command script import " + script_path)
|
||||
|
||||
error = lldb.SBError()
|
||||
provider_id = target.RegisterScriptedFrameProvider(
|
||||
"frame_provider.ValidatingPrefixProvider",
|
||||
lldb.SBStructuredData(),
|
||||
error,
|
||||
)
|
||||
self.assertTrue(
|
||||
error.Success(), f"Should register provider successfully: {error}"
|
||||
)
|
||||
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
||||
|
||||
# No frame should contain 'danger_will_robinson_' — that would mean the provider
|
||||
# was handed its own output list instead of the parent list.
|
||||
expected_names = ["baz", "bar", "foo", "main"]
|
||||
prefix = "my_custom_"
|
||||
for i, name in enumerate(expected_names):
|
||||
frame = thread.GetFrameAtIndex(i)
|
||||
actual = frame.GetFunctionName()
|
||||
self.assertFalse(
|
||||
actual.startswith("danger_will_robinson_"),
|
||||
f"Frame {i}: provider got its own output list "
|
||||
f"(expected bare parent frames, got '{actual}')",
|
||||
)
|
||||
expected = prefix + name
|
||||
self.assertEqual(
|
||||
actual,
|
||||
expected,
|
||||
f"Frame {i} should be '{expected}' after provider",
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Frame provider that passes through all frames but prefixes their function names.
|
||||
|
||||
This exercises the provider's ability to consult its parent StackFrameList
|
||||
when constructing each frame, verifying that the push/pop mechanism correctly
|
||||
routes frame lookups to the parent list.
|
||||
"""
|
||||
|
||||
import lldb
|
||||
from lldb.plugins.scripted_process import ScriptedFrame
|
||||
from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
|
||||
|
||||
|
||||
class PrefixedFrame(ScriptedFrame):
|
||||
"""A frame that wraps a real frame but prefixes the function name."""
|
||||
|
||||
def __init__(self, thread, idx, pc, function_name, prefix):
|
||||
args = lldb.SBStructuredData()
|
||||
super().__init__(thread, args)
|
||||
|
||||
self.idx = idx
|
||||
self.pc = pc
|
||||
self.function_name = prefix + function_name
|
||||
|
||||
def get_id(self):
|
||||
return self.idx
|
||||
|
||||
def get_pc(self):
|
||||
return self.pc
|
||||
|
||||
def get_function_name(self):
|
||||
return self.function_name
|
||||
|
||||
def is_artificial(self):
|
||||
return False
|
||||
|
||||
def is_hidden(self):
|
||||
return False
|
||||
|
||||
def get_register_context(self):
|
||||
return None
|
||||
|
||||
|
||||
class PrefixPassThroughProvider(ScriptedFrameProvider):
|
||||
"""
|
||||
Provider that passes through every frame from its parent StackFrameList
|
||||
but adds a prefix to each function name.
|
||||
|
||||
This verifies that the provider can freely access its input_frames
|
||||
(the parent list) without hitting circular dependencies or deadlocks.
|
||||
"""
|
||||
|
||||
PREFIX = "my_custom_"
|
||||
|
||||
def __init__(self, input_frames, args):
|
||||
super().__init__(input_frames, args)
|
||||
|
||||
@staticmethod
|
||||
def get_description():
|
||||
return "Provider that prefixes all function names with 'my_custom_'"
|
||||
|
||||
def get_frame_at_index(self, idx):
|
||||
if idx < len(self.input_frames):
|
||||
frame = self.input_frames[idx]
|
||||
function_name = frame.GetFunctionName()
|
||||
pc = frame.GetPC()
|
||||
return PrefixedFrame(self.thread, idx, pc, function_name, self.PREFIX)
|
||||
return None
|
||||
|
||||
|
||||
class ValidatingPrefixProvider(ScriptedFrameProvider):
|
||||
"""
|
||||
Provider that prefixes function names AND validates it receives the
|
||||
parent StackFrameList (not its own output).
|
||||
|
||||
When constructing frame N (where N > 0), peeks at input_frames[N-1].
|
||||
If that younger frame's name already carries the prefix, the provider
|
||||
was incorrectly given its own output list — it flags this by prepending
|
||||
'danger_will_robinson_' to the function name.
|
||||
"""
|
||||
|
||||
PREFIX = "my_custom_"
|
||||
|
||||
def __init__(self, input_frames, args):
|
||||
super().__init__(input_frames, args)
|
||||
|
||||
@staticmethod
|
||||
def get_description():
|
||||
return "Validating provider that detects wrong input list"
|
||||
|
||||
def get_frame_at_index(self, idx):
|
||||
if idx >= len(self.input_frames):
|
||||
return None
|
||||
|
||||
frame = self.input_frames[idx]
|
||||
function_name = frame.GetFunctionName()
|
||||
pc = frame.GetPC()
|
||||
|
||||
# For frames after the first, peek at the younger (already-provided)
|
||||
# frame in input_frames. If it already has our prefix, we were handed
|
||||
# our own output list instead of the parent list.
|
||||
if idx > 0:
|
||||
younger = self.input_frames[idx - 1]
|
||||
if younger.GetFunctionName().startswith(self.PREFIX):
|
||||
return PrefixedFrame(
|
||||
self.thread, idx, pc, function_name, "danger_will_robinson_"
|
||||
)
|
||||
|
||||
return PrefixedFrame(self.thread, idx, pc, function_name, self.PREFIX)
|
||||
@@ -0,0 +1,21 @@
|
||||
#include <stdio.h>
|
||||
|
||||
int baz() {
|
||||
printf("baz\n");
|
||||
return 42; // break here.
|
||||
}
|
||||
|
||||
int bar() {
|
||||
printf("bar\n");
|
||||
return baz();
|
||||
}
|
||||
|
||||
int foo() {
|
||||
printf("foo\n");
|
||||
return bar();
|
||||
}
|
||||
|
||||
int main() {
|
||||
printf("main\n");
|
||||
return foo();
|
||||
}
|
||||
Reference in New Issue
Block a user