[Lldb-commits] [lldb] [lldb] Fix deadlock when scripted frame providers load on private state thread (PR #191913)
Med Ismail Bennani via lldb-commits
lldb-commits at lists.llvm.org
Tue Apr 14 17:07:52 PDT 2026
https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/191913
>From bcaa03876b0c2e1bee1aef34a592d0a00d640704 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Tue, 14 Apr 2026 17:06:06 -0700
Subject: [PATCH] [lldb] Fix deadlock when scripted frame providers load on
private state thread
Frame providers are an overlay on top of the parent reality (the unwinder or
scripted process stack). The private state thread (PST) manages the stop of
that parent reality, so the correct view for PST logic should be the parent
-- providers should only be applied once the process has settled and
clients query the stopped state.
When a scripted breakpoint's `was_hit` callback calls
`EvaluateExpression` on the PST, `RunThreadPlan` spawns an override
PST (Thread B) and reassigns `m_current_private_state_thread_sp` to
it. Two threads then need to see parent frames:
- Thread B (override PST): processes stop events via
`HandlePrivateEvent` -> `ShouldStop` -> `GetStackFrameList`.
If it loads a provider, the provider's Python code can acquire
locks held by Thread A, causing a deadlock.
- Thread A (original PST): processes events inline via
`FindNextEventInternal` -> `DoOnRemoval` -> `GetStackFrameList`.
After the override is created, `CurrentThreadIsPrivateStateThread`
no longer recognizes Thread A (it checks against
`m_current_private_state_thread_sp`, which now points to Thread B).
The existing re-entrancy guard in `GetStackFrameList` (e1cd558) only
triggers when a provider is already active. In the deadlock scenario,
provider loading is being initiated for the first time, so the guard
does not trigger.
This patch introduces `PrivateStateThreadGuard`, an RAII guard that
sets a `thread_local` flag checked by `GetStackFrameList` to return
parent frames instead of loading providers. The guard is activated in
two places:
- `RunPrivateStateThread`: for override PSTs only (identified via a
new `is_override` flag on `PrivateStateThread`). The original PST
does not set the guard here, so normal stepping still sees
provider-augmented frames.
- `RunThreadPlan`: for the original PST, scoped to the event
processing window when an override has been spawned.
rdar://174679105
Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
lldb/include/lldb/Target/Process.h | 32 ++++-
lldb/source/Target/Process.cpp | 25 +++-
lldb/source/Target/Thread.cpp | 72 ++++++++---
.../was_hit_deadlock/Makefile | 4 +
.../TestWasHitWithFrameProviderDeadlock.py | 119 ++++++++++++++++++
.../was_hit_deadlock/bkpt_resolver.py | 47 +++++++
.../was_hit_deadlock/frame_provider.py | 41 ++++++
.../was_hit_deadlock/main.c | 18 +++
8 files changed, 335 insertions(+), 23 deletions(-)
create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/Makefile
create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/TestWasHitWithFrameProviderDeadlock.py
create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/bkpt_resolver.py
create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/frame_provider.py
create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/main.c
diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h
index e32a484198668..19b5ae3041826 100644
--- a/lldb/include/lldb/Target/Process.h
+++ b/lldb/include/lldb/Target/Process.h
@@ -3230,9 +3230,10 @@ void PruneThreadPlans();
struct PrivateStateThread {
PrivateStateThread(Process &process, lldb::StateType public_state,
lldb::StateType private_state,
- llvm::StringRef thread_name)
+ llvm::StringRef thread_name, bool is_override = false)
: m_process(process), m_public_state(public_state),
- m_private_state(private_state), m_thread_name(thread_name) {}
+ m_private_state(private_state), m_is_override(is_override),
+ m_thread_name(thread_name) {}
// This returns false if we couldn't start up the thread. If that happens,
// you won't be doing any debugging today.
bool StartupThread();
@@ -3250,6 +3251,8 @@ void PruneThreadPlans();
bool IsRunning() { return m_is_running; }
+ bool IsOverride() const { return m_is_override; }
+
void SetThreadName(llvm::StringRef new_name) { m_thread_name = new_name; }
lldb::StateType GetPrivateState() const {
@@ -3319,6 +3322,7 @@ void PruneThreadPlans();
ProcessRunLock m_public_run_lock;
ProcessRunLock m_private_run_lock;
bool m_is_running = false;
+ bool m_is_override = false;
///< This will be the thread name given to the Private State HostThread when
///< it gets spun up.
std::string m_thread_name;
@@ -3561,7 +3565,7 @@ void PruneThreadPlans();
// Starts up the private state thread that will watch for events from the
// debugee.
- lldb::thread_result_t RunPrivateStateThread();
+ lldb::thread_result_t RunPrivateStateThread(bool is_override);
protected:
void HandlePrivateEvent(lldb::EventSP &event_sp);
@@ -3657,6 +3661,28 @@ class UtilityFunctionScope {
}
};
+/// RAII guard that marks the current thread as a private state thread.
+///
+/// When RunThreadPlan detects it is running on the private state thread, it
+/// spins up an override thread and reassigns m_current_private_state_thread_sp
+/// to it. The original PST continues processing events via DoOnRemoval
+/// callbacks, but CurrentThreadIsPrivateStateThread() no longer recognizes it.
+/// This guard sets a thread_local flag so that GetStackFrameList can identify
+/// the original PST and return parent frames instead of provider-augmented
+/// frames.
+struct PrivateStateThreadGuard {
+ PrivateStateThreadGuard() { g_is_private_state_thread = true; }
+ ~PrivateStateThreadGuard() { g_is_private_state_thread = false; }
+ static bool IsPrivateStateThread() { return g_is_private_state_thread; }
+
+ // Non-copyable, non-movable.
+ PrivateStateThreadGuard(const PrivateStateThreadGuard &) = delete;
+ PrivateStateThreadGuard &operator=(const PrivateStateThreadGuard &) = delete;
+
+private:
+ static thread_local bool g_is_private_state_thread;
+};
+
} // namespace lldb_private
#endif // LLDB_TARGET_PROCESS_H
diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp
index e725f9eac7d5f..f8bd43a775c28 100644
--- a/lldb/source/Target/Process.cpp
+++ b/lldb/source/Target/Process.cpp
@@ -3925,7 +3925,8 @@ bool Process::ShouldBroadcastEvent(Event *event_ptr) {
bool Process::PrivateStateThread::StartupThread() {
llvm::Expected<HostThread> private_state_thread =
ThreadLauncher::LaunchThread(
- m_thread_name, [this] { return m_process.RunPrivateStateThread(); },
+ m_thread_name,
+ [this] { return m_process.RunPrivateStateThread(m_is_override); },
8 * 1024 * 1024);
if (!private_state_thread) {
LLDB_LOG_ERROR(GetLog(LLDBLog::Host), private_state_thread.takeError(),
@@ -3983,7 +3984,8 @@ bool Process::StartPrivateStateThread(
// place already, so do that first:
*backup_ptr = m_current_private_state_thread_sp;
m_current_private_state_thread_sp.reset(new PrivateStateThread(
- *this, GetPublicState(), GetPrivateState(), thread_name));
+ *this, GetPublicState(), GetPrivateState(), thread_name,
+ /*is_override=*/true));
} else
m_current_private_state_thread_sp->SetThreadName(thread_name);
@@ -4198,7 +4200,15 @@ Status Process::HaltPrivate() {
return error;
}
-thread_result_t Process::RunPrivateStateThread() {
+thread_local bool PrivateStateThreadGuard::g_is_private_state_thread = false;
+
+thread_result_t Process::RunPrivateStateThread(bool is_override) {
+ // Override PSTs exist solely to service RunThreadPlan expression evaluation.
+ // They must see parent frames, not provider-augmented frames.
+ std::optional<PrivateStateThreadGuard> pst_guard;
+ if (is_override)
+ pst_guard.emplace();
+
bool control_only = true;
Log *log = GetLog(LLDBLog::Process);
@@ -5386,6 +5396,13 @@ Process::RunThreadPlan(ExecutionContext &exe_ctx,
// still succeed.
bool miss_first_event = true;
#endif
+
+ // If we spawned an override PST, mark the current (original) PST so
+ // GetStackFrameList returns parent frames during event processing.
+ std::optional<PrivateStateThreadGuard> private_state_thread_guard;
+ if (backup_private_state_thread)
+ private_state_thread_guard.emplace();
+
while (true) {
// We usually want to resume the process if we get to the top of the
// loop. The only exception is if we get two running events with no
@@ -5728,6 +5745,8 @@ Process::RunThreadPlan(ExecutionContext &exe_ctx,
}
} // END WAIT LOOP
+ private_state_thread_guard.reset();
+
// If we had to start up a temporary private state thread to run this
// thread plan, shut it down now.
if (backup_private_state_thread &&
diff --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp
index c199fd236f5cd..1dd0d31fba4e3 100644
--- a/lldb/source/Target/Thread.cpp
+++ b/lldb/source/Target/Thread.cpp
@@ -1491,30 +1491,56 @@ bool Thread::IsAnyProviderActive() {
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.
+ // Determine if we must return the parent frames instead of the
+ // provider-augmented frames on this call.
+ //
+ // Frame providers are a public illusion layered on top of the private
+ // reality (the unwinder stack, or a scripted process playing that
+ // role). The private state thread (PST) manages the stop of that private
+ // reality, so the correct view for its logic IS the private reality
+ // -- the public illusion is only applied once the process has settled
+ // and clients query the stopped state.
+ //
+ // When RunThreadPlan spawns an override PST, the original PST changes
+ // role: it becomes the public event listener for the override, but it is
+ // still working to manage the private side of the process. So it also should
+ // see the private reality and not the public illusion. Feeding it the public
+ // illusion instead of the private reality is incorrect and can lead to
+ // deadlocks as a side effect, since provider code may try to acquire locks
+ // already held further up the call stack.
+ //
+ // We return parent frames in two situations:
+ //
+ // 1. Re-entrancy: a provider is already active on some host thread.
+ // - Same thread: the provider's get_frame_at_index() calls
+ // HandleCommand("bt") or accesses input_frames, which re-enters
+ // GetStackFrameList() -> infinite recursion.
+ // - Private state thread: the provider called EvaluateExpression()
+ // which resumed the process via RunThreadPlan; the private state
+ // thread must process the resulting stop event, but if it tries to
+ // build the synthetic frame list it will re-enter the provider ->
+ // deadlock.
+ // - Any other thread: would run the provider concurrently with the
+ // thread that is already mid-construction.
+ //
+ // 2. Current thread is a private state thread that should see the
+ // private reality (PrivateStateThreadGuard::IsPrivateStateThread).
+ //
+ // For case 1, if a provider is active we return its input (parent)
+ // frames. For case (2), we return/create the unwinder frame list
+ // without caching it in m_curr_frames_sp so that non-private-state
+ // callers still get the public illusion once the process settles.
+ ProcessSP process_sp = GetProcess();
{
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.
+ // Case 1a: 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();
+ // Case 1b: private state thread while a provider is active elsewhere.
if (process_sp && process_sp->CurrentThreadIsPrivateStateThread())
return m_active_frame_providers_by_thread.begin()->second.back();
}
@@ -1523,9 +1549,21 @@ StackFrameListSP Thread::GetStackFrameList() {
if (m_curr_frames_sp)
return m_curr_frames_sp;
+ // For case 2, PST must see the private reality, not the public illusion.
+ // PrivateStateThreadGuard is a thread_local flag set by RunThreadPlan
+ // (for the original PST) and RunPrivateStateThread (for override PSTs).
+ // We cannot use CurrentThreadIsPrivateStateThread() here because
+ // RunThreadPlan reassigns m_current_private_state_thread_sp to the
+ // override, so the original PST is no longer recognized.
+ if (PrivateStateThreadGuard::IsPrivateStateThread()) {
+ if (!m_unwinder_frames_sp)
+ m_unwinder_frames_sp = std::make_shared<StackFrameList>(
+ *this, m_prev_frames_sp, true, /*provider_id=*/0);
+ return m_unwinder_frames_sp;
+ }
+
// First, try to load frame providers if we don't have any yet.
if (m_frame_providers.empty()) {
- ProcessSP process_sp = GetProcess();
if (process_sp) {
Target &target = process_sp->GetTarget();
const auto &descriptors = target.GetScriptedFrameProviderDescriptors();
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/Makefile b/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/Makefile
new file mode 100644
index 0000000000000..695335e068c0c
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/Makefile
@@ -0,0 +1,4 @@
+C_SOURCES := main.c
+CFLAGS_EXTRAS := -std=c99
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/TestWasHitWithFrameProviderDeadlock.py b/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/TestWasHitWithFrameProviderDeadlock.py
new file mode 100644
index 0000000000000..febbef49de9e7
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/TestWasHitWithFrameProviderDeadlock.py
@@ -0,0 +1,119 @@
+"""
+Test that a scripted breakpoint's was_hit callback calling EvaluateExpression
+does not deadlock when a scripted frame provider is also registered.
+
+This reproduces the deadlock from:
+ lldb-rpc-server sample showing two private state threads in mutual wait:
+
+ Thread A (lldb.process.internal-state):
+ BreakpointResolverScripted::WasHit -> Python -> SBFrame.EvaluateExpression
+ -> RunThreadPlan -> Halt -> WaitForProcessToStop
+ (holds mutex, waits for state change event)
+
+ Thread B (lldb.process.internal-state-override):
+ HandlePrivateEvent -> ShouldStop -> GetStackFrameList
+ -> LoadScriptedFrameProvider -> ScriptedFrameProvider.__init__
+ -> Python -> SBThread.__bool__ -> GetStoppedExecutionContext
+ -> recursive_mutex::lock (BLOCKED - mutex held by Thread A)
+
+ Thread A waits for the event that Thread B would deliver, but Thread B
+ is blocked on the mutex Thread A holds.
+"""
+
+import os
+import lldb
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+
+
+class TestWasHitWithFrameProviderDeadlock(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24528")
+ def test_was_hit_with_frame_provider_no_deadlock(self):
+ """
+ Test that a scripted breakpoint doing EvaluateExpression in was_hit
+ does not deadlock when a scripted frame provider is registered.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_name_breakpoint(self, "main")
+
+ # Import both the breakpoint resolver and the frame provider scripts.
+ resolver_path = os.path.join(self.getSourceDir(), "bkpt_resolver.py")
+ provider_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+ self.runCmd("command script import " + resolver_path)
+ self.runCmd("command script import " + provider_path)
+
+ # Register the scripted frame provider FIRST.
+ # This means that when any stop event is processed and the thread's
+ # stack frames are accessed, the provider will be loaded, calling
+ # its __init__ which accesses SBThread.
+ error = lldb.SBError()
+ provider_id = target.RegisterScriptedFrameProvider(
+ "frame_provider.SBAPIAccessInInitProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(
+ error.Success(),
+ f"Should register frame provider: {error}",
+ )
+ self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+ # Create the scripted breakpoint that calls EvaluateExpression in was_hit.
+ extra_args = lldb.SBStructuredData()
+ stream = lldb.SBStream()
+ stream.Print('{"symbol": "target_func"}')
+ extra_args.SetFromJSON(stream)
+
+ bkpt = target.BreakpointCreateFromScript(
+ "bkpt_resolver.ExprEvalResolver",
+ extra_args,
+ lldb.SBFileSpecList(),
+ lldb.SBFileSpecList(),
+ )
+ self.assertTrue(bkpt.IsValid(), "Scripted breakpoint should be valid")
+ self.assertGreater(
+ bkpt.GetNumLocations(), 0, "Breakpoint should have locations"
+ )
+
+ # Continue the process. When the breakpoint is hit:
+ # 1. was_hit runs on private state thread, calls EvaluateExpression
+ # 2. EvaluateExpression -> RunThreadPlan -> Halt -> WaitForProcessToStop
+ # (holds mutex, waits for state event)
+ # 3. Override state thread processes the stop, loads frame provider
+ # 4. Provider __init__ calls SBThread.__bool__ -> GetStoppedExecutionContext
+ # -> tries to acquire mutex held by step 2 -> DEADLOCK
+ #
+ # If this test completes without hanging, the deadlock is fixed.
+ process.Continue()
+ self.assertState(process.GetState(), lldb.eStateStopped)
+
+ thread = process.GetSelectedThread()
+ self.assertTrue(thread.IsValid(), "Thread should be valid")
+ self.assertEqual(
+ thread.GetStopReason(),
+ lldb.eStopReasonBreakpoint,
+ "Should stop at breakpoint",
+ )
+
+ # Verify that EvaluateExpression in was_hit actually ran successfully
+ # by checking that g_value was incremented.
+ g_value = target.FindFirstGlobalVariable("g_value")
+ self.assertTrue(g_value.IsValid(), "Should find g_value")
+ self.assertGreater(
+ g_value.GetValueAsUnsigned(),
+ 0,
+ "g_value should have been incremented by the was_hit callback",
+ )
+
+ # Continue to hit the breakpoint again to ensure it's not a one-off.
+ process.Continue()
+ self.assertState(process.GetState(), lldb.eStateStopped)
+ self.assertEqual(
+ thread.GetStopReason(),
+ lldb.eStopReasonBreakpoint,
+ "Should stop at breakpoint again",
+ )
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/bkpt_resolver.py b/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/bkpt_resolver.py
new file mode 100644
index 0000000000000..7fb7f8675f2a6
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/bkpt_resolver.py
@@ -0,0 +1,47 @@
+"""
+Scripted breakpoint resolver whose was_hit callback calls EvaluateExpression.
+
+This reproduces the deadlock seen in the sample where:
+1. BreakpointResolverScripted::WasHit runs on the private state thread
+2. was_hit calls SBFrame.EvaluateExpression -> RunThreadPlan -> Halt ->
+ WaitForProcessToStop (holds a mutex, waits for state event)
+3. The override private state thread handles the stop and loads a scripted
+ frame provider
+4. The provider's __init__ calls SBThread.__bool__ -> GetStoppedExecutionContext
+ -> tries to acquire the same mutex -> DEADLOCK
+"""
+
+import lldb
+
+
+class ExprEvalResolver:
+ """Scripted breakpoint resolver that evaluates an expression in was_hit."""
+
+ def __init__(self, bkpt, extra_args, dict):
+ self.bkpt = bkpt
+ sym_name = extra_args.GetValueForKey("symbol").GetStringValue(100)
+ self.sym_name = sym_name
+ self.facade_loc = None
+
+ def __callback__(self, sym_ctx):
+ sym = sym_ctx.module.FindSymbol(self.sym_name, lldb.eSymbolTypeCode)
+ if sym.IsValid():
+ self.bkpt.AddLocation(sym.GetStartAddress())
+ self.facade_loc = self.bkpt.AddFacadeLocation()
+
+ def get_short_help(self):
+ return f"ExprEvalResolver for {self.sym_name}"
+
+ def was_hit(self, frame, bp_loc):
+ # This runs on the private state thread. Calling EvaluateExpression
+ # here triggers RunThreadPlan -> Halt -> WaitForProcessToStop, which
+ # holds a mutex and waits for a state change event.
+ options = lldb.SBExpressionOptions()
+ options.SetStopOthers(True)
+ options.SetTryAllThreads(False)
+
+ result = frame.EvaluateExpression("increment()", options)
+ if not result.error.success:
+ return lldb.LLDB_INVALID_BREAK_ID
+
+ return self.facade_loc
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/frame_provider.py b/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/frame_provider.py
new file mode 100644
index 0000000000000..10d4fe3ab0fce
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/frame_provider.py
@@ -0,0 +1,41 @@
+"""
+Scripted frame provider that accesses the SB API in __init__.
+
+This provider checks SBThread validity during construction, which calls
+GetStoppedExecutionContext and tries to acquire a recursive_mutex.
+When combined with the scripted breakpoint's was_hit callback doing
+EvaluateExpression on the private state thread, this causes a deadlock:
+
+- Thread A (private state): holds the mutex in RunThreadPlan/WaitForProcessToStop
+- Thread B (override state): loads this provider, calls SBThread.__bool__ ->
+ GetStoppedExecutionContext -> tries to lock the same mutex -> DEADLOCK
+"""
+
+import lldb
+from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
+
+
+class SBAPIAccessInInitProvider(ScriptedFrameProvider):
+ """Provider that accesses SBThread (triggering mutex acquisition) in __init__."""
+
+ def __init__(self, input_frames, args):
+ super().__init__(input_frames, args)
+ # This is the key line that triggers the deadlock: checking if the
+ # thread is valid calls SBThread::operator bool() ->
+ # GetStoppedExecutionContext -> recursive_mutex::lock().
+ # When the private state thread holds that mutex (during
+ # RunThreadPlan/WaitForProcessToStop from a was_hit callback),
+ # this blocks forever.
+ if self.thread:
+ self.thread_is_valid = bool(self.thread)
+ else:
+ self.thread_is_valid = False
+
+ @staticmethod
+ def get_description():
+ return "Provider that accesses SB API in __init__ (deadlock reproducer)"
+
+ def get_frame_at_index(self, idx):
+ if idx < len(self.input_frames):
+ return idx
+ return None
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/main.c b/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/main.c
new file mode 100644
index 0000000000000..9ebeaf3380ed9
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/was_hit_deadlock/main.c
@@ -0,0 +1,18 @@
+#include <stdio.h>
+
+int g_value = 0;
+
+int increment() { return ++g_value; }
+
+int target_func() {
+ printf("target_func: %d\n", g_value);
+ return g_value;
+}
+
+int main() {
+ for (int i = 0; i < 10; i++) {
+ target_func();
+ }
+ increment();
+ return 0;
+}
More information about the lldb-commits
mailing list