[Lldb-commits] [lldb] [lldb] Fix deadlock between the command thread and event-handler thread in scripted frame providers (PR #208242)

2026-07-11 Thread Med Ismail Bennani via lldb-commits

https://github.com/medismailben updated 
https://github.com/llvm/llvm-project/pull/208242

>From e4f942057251517f2e74359ceb6630ba582db40b Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani 
Date: Fri, 10 Jul 2026 15:32:20 -0700
Subject: [PATCH] [lldb] Fix scripted frame provider cross-thread re-entrant
 deadlock

GetStoppedExecutionContext unconditionally blocked acquiring the
target's API mutex. A thread already holding that mutex (e.g. a `bt`
command thread, via CommandObjectParsed's eCommandTryTargetAPILock) can
end up waiting on a StackFrameList lock held by another thread (e.g. the
debugger's event-handler thread) that is itself blocked re-acquiring the
API mutex from inside a scripted frame provider's Python code that
touches SB API -- a classic AB-BA deadlock.

Introduce Policy::Capabilities::can_reenter_target_api_mutex, pushed
around every scripted-extension callback in ScriptedPythonInterface::Dispatch
and CallStaticMethod. The point isn't "avoid this deadlock" as a goal in itself:
a thread running one of these callbacks isn't servicing a client-facing SB API
entry point, it's doing internal work on the callback's behalf, so it doesn't
need the same locking guarantees a top-level SB API call does. Avoiding the
deadlock is a beneficial side effect of giving the thread the permissions that
match what it's actually doing.

Encapsulate the policy check in a new Target::GetAPIMutexLock(), next
to GetAPIMutex() (which already picks between the public/private mutex
based on the policy's view), instead of leaving a free-standing
`PolicyStack::Get()...` check at the call site. It returns a real lock
normally, or a no-op unlocked one when the current thread's policy says
it doesn't need one.

Adds a regression test for the deadlock. It's a genuine cross-thread
race (the command thread vs. the debugger's event-handler thread), so
like the sibling runlock_reentrant_deadlock/was_hit_deadlock tests, it
raises the odds of hitting it within a single invocation but can't
guarantee it.

Also updates PolicyTest's DumpPublicState/DumpPrivateState expectations
for the new reenter_api_mutex field in Policy::Dump().

Signed-off-by: Med Ismail Bennani 
---
 lldb/include/lldb/Target/ExecutionContext.h   | 11 ++-
 lldb/include/lldb/Target/Target.h |  4 +
 lldb/include/lldb/Utility/Policy.h| 11 +++
 .../Interfaces/ScriptedPythonInterface.h  |  7 ++
 lldb/source/Target/ExecutionContext.cpp   |  4 +-
 lldb/source/Target/Target.cpp |  8 ++
 lldb/source/Utility/Policy.cpp|  7 ++
 .../Makefile  |  2 +
 ...ProviderRegisterCommandAPIMutexDeadlock.py | 89 +++
 .../frame_provider.py | 28 ++
 .../main.c|  7 ++
 lldb/unittests/Utility/PolicyTest.cpp |  6 +-
 12 files changed, 176 insertions(+), 8 deletions(-)
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/Makefile
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/main.c

diff --git a/lldb/include/lldb/Target/ExecutionContext.h 
b/lldb/include/lldb/Target/ExecutionContext.h
index bf976f4db8c87..b155b2ce436f3 100644
--- a/lldb/include/lldb/Target/ExecutionContext.h
+++ b/lldb/include/lldb/Target/ExecutionContext.h
@@ -14,6 +14,7 @@
 #include "lldb/Host/ProcessRunLock.h"
 #include "lldb/Target/StackID.h"
 #include "lldb/Target/SyntheticFrameProvider.h"
+#include "lldb/Utility/Policy.h"
 #include "lldb/lldb-private.h"
 
 namespace lldb_private {
@@ -561,9 +562,9 @@ class ExecutionContext {
 };
 
 /// A wrapper class representing an execution context with non-null Target
-/// and Process pointers, a locked API mutex and a locked ProcessRunLock.
-/// The locks are private by design: to unlock them, destroy the
-/// StoppedExecutionContext.
+/// and Process pointers, a locked ProcessRunLock, and (unless the current
+/// thread's policy allows skipping it) a locked API mutex. The locks are
+/// private by design; to unlock them, destroy the StoppedExecutionContext.
 struct StoppedExecutionContext : ExecutionContext {
   StoppedExecutionContext(lldb::TargetSP &target_sp,
   lldb::ProcessSP &process_sp,
@@ -574,7 +575,9 @@ struct StoppedExecutionContext : ExecutionContext {
   : m_api_lock(std::move(api_lock)), m_stop_locker(std::move(stop_locker)) 
{
 assert(target_sp);
 assert(process_sp);
-assert(m_api_lock.owns_lock());
+assert(
+m_api_lock.owns_lock() ||
+
PolicyStack::Get().Current().capabilities.can_reenter_target_api_mutex);
 assert(

[Lldb-commits] [lldb] [lldb] Fix deadlock between the command thread and event-handler thread in scripted frame providers (PR #208242)

2026-07-11 Thread Med Ismail Bennani via lldb-commits

https://github.com/medismailben updated 
https://github.com/llvm/llvm-project/pull/208242

>From e4f942057251517f2e74359ceb6630ba582db40b Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani 
Date: Fri, 10 Jul 2026 15:32:20 -0700
Subject: [PATCH 1/2] [lldb] Fix scripted frame provider cross-thread
 re-entrant deadlock

GetStoppedExecutionContext unconditionally blocked acquiring the
target's API mutex. A thread already holding that mutex (e.g. a `bt`
command thread, via CommandObjectParsed's eCommandTryTargetAPILock) can
end up waiting on a StackFrameList lock held by another thread (e.g. the
debugger's event-handler thread) that is itself blocked re-acquiring the
API mutex from inside a scripted frame provider's Python code that
touches SB API -- a classic AB-BA deadlock.

Introduce Policy::Capabilities::can_reenter_target_api_mutex, pushed
around every scripted-extension callback in ScriptedPythonInterface::Dispatch
and CallStaticMethod. The point isn't "avoid this deadlock" as a goal in itself:
a thread running one of these callbacks isn't servicing a client-facing SB API
entry point, it's doing internal work on the callback's behalf, so it doesn't
need the same locking guarantees a top-level SB API call does. Avoiding the
deadlock is a beneficial side effect of giving the thread the permissions that
match what it's actually doing.

Encapsulate the policy check in a new Target::GetAPIMutexLock(), next
to GetAPIMutex() (which already picks between the public/private mutex
based on the policy's view), instead of leaving a free-standing
`PolicyStack::Get()...` check at the call site. It returns a real lock
normally, or a no-op unlocked one when the current thread's policy says
it doesn't need one.

Adds a regression test for the deadlock. It's a genuine cross-thread
race (the command thread vs. the debugger's event-handler thread), so
like the sibling runlock_reentrant_deadlock/was_hit_deadlock tests, it
raises the odds of hitting it within a single invocation but can't
guarantee it.

Also updates PolicyTest's DumpPublicState/DumpPrivateState expectations
for the new reenter_api_mutex field in Policy::Dump().

Signed-off-by: Med Ismail Bennani 
---
 lldb/include/lldb/Target/ExecutionContext.h   | 11 ++-
 lldb/include/lldb/Target/Target.h |  4 +
 lldb/include/lldb/Utility/Policy.h| 11 +++
 .../Interfaces/ScriptedPythonInterface.h  |  7 ++
 lldb/source/Target/ExecutionContext.cpp   |  4 +-
 lldb/source/Target/Target.cpp |  8 ++
 lldb/source/Utility/Policy.cpp|  7 ++
 .../Makefile  |  2 +
 ...ProviderRegisterCommandAPIMutexDeadlock.py | 89 +++
 .../frame_provider.py | 28 ++
 .../main.c|  7 ++
 lldb/unittests/Utility/PolicyTest.cpp |  6 +-
 12 files changed, 176 insertions(+), 8 deletions(-)
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/Makefile
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/main.c

diff --git a/lldb/include/lldb/Target/ExecutionContext.h 
b/lldb/include/lldb/Target/ExecutionContext.h
index bf976f4db8c87..b155b2ce436f3 100644
--- a/lldb/include/lldb/Target/ExecutionContext.h
+++ b/lldb/include/lldb/Target/ExecutionContext.h
@@ -14,6 +14,7 @@
 #include "lldb/Host/ProcessRunLock.h"
 #include "lldb/Target/StackID.h"
 #include "lldb/Target/SyntheticFrameProvider.h"
+#include "lldb/Utility/Policy.h"
 #include "lldb/lldb-private.h"
 
 namespace lldb_private {
@@ -561,9 +562,9 @@ class ExecutionContext {
 };
 
 /// A wrapper class representing an execution context with non-null Target
-/// and Process pointers, a locked API mutex and a locked ProcessRunLock.
-/// The locks are private by design: to unlock them, destroy the
-/// StoppedExecutionContext.
+/// and Process pointers, a locked ProcessRunLock, and (unless the current
+/// thread's policy allows skipping it) a locked API mutex. The locks are
+/// private by design; to unlock them, destroy the StoppedExecutionContext.
 struct StoppedExecutionContext : ExecutionContext {
   StoppedExecutionContext(lldb::TargetSP &target_sp,
   lldb::ProcessSP &process_sp,
@@ -574,7 +575,9 @@ struct StoppedExecutionContext : ExecutionContext {
   : m_api_lock(std::move(api_lock)), m_stop_locker(std::move(stop_locker)) 
{
 assert(target_sp);
 assert(process_sp);
-assert(m_api_lock.owns_lock());
+assert(
+m_api_lock.owns_lock() ||
+
PolicyStack::Get().Current().capabilities.can_reenter_target_api_mutex);
 ass

[Lldb-commits] [lldb] [lldb] Fix deadlock between the command thread and event-handler thread in scripted frame providers (PR #208242)

2026-07-09 Thread Med Ismail Bennani via lldb-commits

https://github.com/medismailben updated 
https://github.com/llvm/llvm-project/pull/208242

>From a79bd79cae1a18e493f8f4e7936b57e669ef1a6e Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani 
Date: Wed, 8 Jul 2026 16:34:48 -0700
Subject: [PATCH 1/2] [lldb] Fix scripted frame provider cross-thread
 re-entrant deadlock

GetStoppedExecutionContext unconditionally blocked acquiring the
target's API mutex. A thread already holding that mutex (e.g. a `bt`
command thread, via CommandObjectParsed's eCommandTryTargetAPILock) can
end up waiting on a StackFrameList lock held by another thread (e.g. the
debugger's event-handler thread) that is itself blocked re-acquiring the
API mutex from inside a scripted frame provider's Python code that
touches SB API -- a classic AB-BA deadlock.

Introduce Policy::Capabilities::can_reenter_target_api_mutex, pushed
around every scripted-extension callback in
ScriptedPythonInterface::Dispatch and CallStaticMethod. The point isn't
"avoid this deadlock" as a goal in itself: a thread running one of these
callbacks isn't servicing a client-facing SB API entry point, it's doing
internal work on the callback's behalf, so it doesn't need the same
locking guarantees a top-level SB API call does. Avoiding the deadlock
is a beneficial side effect of giving the thread the permissions that
match what it's actually doing.

This also required making PolicyStack::Get() out-of-line:
Dispatch/CallStaticMethod live in a header included by both liblldb and
the Python script interpreter plugin, and LLDB builds with hidden
visibility by default. An inline function's function-local static is NOT
shared across shared library boundaries, so each dylib that included
Policy.h and called an inline Get() would get its own private copy of
the thread_local stack, silently splitting a single logical per-thread
stack into two. Push/Pop calls from the plugin would then operate on a
different object than Guard's destructor's Pop (already out-of-line,
compiled into liblldb), draining liblldb's copy out from under callers
that never pushed to it and tripping the "can't pop the base policy"
assert. Making Get() out-of-line ensures every dylib resolves to the
single instance defined in Policy.cpp.

Adds a regression test for the deadlock. It's a genuine cross-thread
race (the command thread vs. the debugger's event-handler thread), so
like the sibling runlock_reentrant_deadlock/was_hit_deadlock tests, it
raises the odds of hitting it within a single invocation but can't
guarantee it. The frame provider deliberately returns dict-based
synthetic frames rather than forwarding a frame under its own index, to
keep this test isolated from the separate frame-aliasing bug fixed on
top of this commit.

Also updates PolicyTest's DumpPublicState/DumpPrivateState expectations
for the new reenter_api_mutex field in Policy::Dump().
---
 lldb/include/lldb/Target/ExecutionContext.h   | 12 ++-
 lldb/include/lldb/Utility/Policy.h| 29 +-
 .../Interfaces/ScriptedPythonInterface.h  |  7 ++
 lldb/source/Target/ExecutionContext.cpp   |  5 +-
 lldb/source/Utility/Policy.cpp| 12 +++
 .../Makefile  |  2 +
 ...ProviderRegisterCommandAPIMutexDeadlock.py | 89 +++
 .../frame_provider.py | 30 +++
 .../main.c|  7 ++
 lldb/unittests/Utility/PolicyTest.cpp |  6 +-
 10 files changed, 187 insertions(+), 12 deletions(-)
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/Makefile
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/main.c

diff --git a/lldb/include/lldb/Target/ExecutionContext.h 
b/lldb/include/lldb/Target/ExecutionContext.h
index bf976f4db8c87..a3935ac23df9f 100644
--- a/lldb/include/lldb/Target/ExecutionContext.h
+++ b/lldb/include/lldb/Target/ExecutionContext.h
@@ -14,6 +14,7 @@
 #include "lldb/Host/ProcessRunLock.h"
 #include "lldb/Target/StackID.h"
 #include "lldb/Target/SyntheticFrameProvider.h"
+#include "lldb/Utility/Policy.h"
 #include "lldb/lldb-private.h"
 
 namespace lldb_private {
@@ -561,9 +562,10 @@ class ExecutionContext {
 };
 
 /// A wrapper class representing an execution context with non-null Target
-/// and Process pointers, a locked API mutex and a locked ProcessRunLock.
-/// The locks are private by design: to unlock them, destroy the
-/// StoppedExecutionContext.
+/// and Process pointers, a locked ProcessRunLock, and (unless
+/// Policy::Capabilities::can_reenter_target_api_mutex is set for the
+/// current thread) a locked API mutex. The locks are private b

[Lldb-commits] [lldb] [lldb] Fix deadlock between the command thread and event-handler thread in scripted frame providers (PR #208242)

2026-07-09 Thread Med Ismail Bennani via lldb-commits

https://github.com/medismailben updated 
https://github.com/llvm/llvm-project/pull/208242

>From cf3801d3b105a101dd6e136b59a528b6b617e770 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani 
Date: Wed, 8 Jul 2026 16:34:48 -0700
Subject: [PATCH 1/2] [lldb] Fix scripted frame provider cross-thread
 re-entrant deadlock

GetStoppedExecutionContext unconditionally blocked acquiring the
target's API mutex. A thread already holding that mutex (e.g. a `bt`
command thread, via CommandObjectParsed's eCommandTryTargetAPILock) can
end up waiting on a StackFrameList lock held by another thread (e.g. the
debugger's event-handler thread) that is itself blocked re-acquiring the
API mutex from inside a scripted frame provider's Python code that
touches SB API -- a classic AB-BA deadlock.

Introduce Policy::Capabilities::can_reenter_target_api_mutex, pushed
around every scripted-extension callback in
ScriptedPythonInterface::Dispatch and CallStaticMethod. The point isn't
"avoid this deadlock" as a goal in itself: a thread running one of these
callbacks isn't servicing a client-facing SB API entry point, it's doing
internal work on the callback's behalf, so it doesn't need the same
locking guarantees a top-level SB API call does. Avoiding the deadlock
is a beneficial side effect of giving the thread the permissions that
match what it's actually doing.

This also required making PolicyStack::Get() out-of-line:
Dispatch/CallStaticMethod live in a header included by both liblldb and
the Python script interpreter plugin, and LLDB builds with hidden
visibility by default. An inline function's function-local static is NOT
shared across shared library boundaries, so each dylib that included
Policy.h and called an inline Get() would get its own private copy of
the thread_local stack, silently splitting a single logical per-thread
stack into two. Push/Pop calls from the plugin would then operate on a
different object than Guard's destructor's Pop (already out-of-line,
compiled into liblldb), draining liblldb's copy out from under callers
that never pushed to it and tripping the "can't pop the base policy"
assert. Making Get() out-of-line ensures every dylib resolves to the
single instance defined in Policy.cpp.

Adds a regression test for the deadlock. It's a genuine cross-thread
race (the command thread vs. the debugger's event-handler thread), so
like the sibling runlock_reentrant_deadlock/was_hit_deadlock tests, it
raises the odds of hitting it within a single invocation but can't
guarantee it. The frame provider deliberately returns dict-based
synthetic frames rather than forwarding a frame under its own index, to
keep this test isolated from the separate frame-aliasing bug fixed on
top of this commit.

Also updates PolicyTest's DumpPublicState/DumpPrivateState expectations
for the new reenter_api_mutex field in Policy::Dump().
---
 lldb/include/lldb/Target/ExecutionContext.h   | 12 ++-
 lldb/include/lldb/Utility/Policy.h| 29 +-
 .../Interfaces/ScriptedPythonInterface.h  |  7 ++
 lldb/source/Target/ExecutionContext.cpp   |  5 +-
 lldb/source/Utility/Policy.cpp| 12 +++
 .../Makefile  |  2 +
 ...ProviderRegisterCommandAPIMutexDeadlock.py | 89 +++
 .../frame_provider.py | 30 +++
 .../main.c|  7 ++
 lldb/unittests/Utility/PolicyTest.cpp |  6 +-
 10 files changed, 187 insertions(+), 12 deletions(-)
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/Makefile
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/main.c

diff --git a/lldb/include/lldb/Target/ExecutionContext.h 
b/lldb/include/lldb/Target/ExecutionContext.h
index bf976f4db8c87..a3935ac23df9f 100644
--- a/lldb/include/lldb/Target/ExecutionContext.h
+++ b/lldb/include/lldb/Target/ExecutionContext.h
@@ -14,6 +14,7 @@
 #include "lldb/Host/ProcessRunLock.h"
 #include "lldb/Target/StackID.h"
 #include "lldb/Target/SyntheticFrameProvider.h"
+#include "lldb/Utility/Policy.h"
 #include "lldb/lldb-private.h"
 
 namespace lldb_private {
@@ -561,9 +562,10 @@ class ExecutionContext {
 };
 
 /// A wrapper class representing an execution context with non-null Target
-/// and Process pointers, a locked API mutex and a locked ProcessRunLock.
-/// The locks are private by design: to unlock them, destroy the
-/// StoppedExecutionContext.
+/// and Process pointers, a locked ProcessRunLock, and (unless
+/// Policy::Capabilities::can_reenter_target_api_mutex is set for the
+/// current thread) a locked API mutex. The locks are private b

[Lldb-commits] [lldb] [lldb] Fix deadlock between the command thread and event-handler thread in scripted frame providers (PR #208242)

2026-07-08 Thread Med Ismail Bennani via lldb-commits

https://github.com/medismailben updated 
https://github.com/llvm/llvm-project/pull/208242

>From 931fd27c92a8eac144385be0dae0d5bc71075b9f Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani 
Date: Wed, 8 Jul 2026 09:03:23 -0700
Subject: [PATCH] [lldb] Fix scripted frame provider cross-thread re-entrant
 deadlock

GetStoppedExecutionContext unconditionally blocked acquiring the
target's API mutex. A thread already holding that mutex (e.g. a `bt`
command thread, via CommandObjectParsed's eCommandTryTargetAPILock) can
end up waiting on a StackFrameList lock held by another thread (e.g. the
debugger's event-handler thread) that is itself blocked re-acquiring the
API mutex from inside a scripted frame provider's Python code that
touches SB API -- a classic AB-BA deadlock.

Introduce Policy::Capabilities::can_reenter_target_api_mutex, pushed
around every scripted-extension callback in
ScriptedPythonInterface::Dispatch and CallStaticMethod, so
GetStoppedExecutionContext can skip the mutex instead of blocking when
running under a scripted callback -- these callbacks are already running
under whatever protections the caller set up, and blocking again is both
redundant and, in this case, deadlock-prone.

This also required making PolicyStack::Get() out-of-line:
Dispatch/CallStaticMethod live in a header included by both liblldb and
the Python script interpreter plugin, and LLDB builds with hidden
visibility by default. An inline function's function-local static is NOT
shared across shared library boundaries, so each dylib that included
Policy.h and called an inline Get() would get its own private copy of
the thread_local stack, silently splitting a single logical per-thread
stack into two. Push/Pop calls from the plugin would then operate on a
different object than Guard's destructor's Pop (already out-of-line,
compiled into liblldb), draining liblldb's copy out from under callers
that never pushed to it and tripping the "can't pop the base policy"
assert. Making Get() out-of-line ensures every dylib resolves to the
single instance defined in Policy.cpp.

Adds a regression test for the deadlock. It's a genuine cross-thread
race (the command thread vs. the debugger's event-handler thread), so
like the sibling runlock_reentrant_deadlock/was_hit_deadlock tests, it
raises the odds of hitting it within a single invocation but can't
guarantee it. The frame provider deliberately returns dict-based
synthetic frames rather than forwarding a frame under its own index, to
keep this test isolated from the separate frame-aliasing bug fixed on
top of this commit.

Signed-off-by: Med Ismail Bennani 
---
 lldb/include/lldb/Target/ExecutionContext.h   | 12 ++-
 lldb/include/lldb/Utility/Policy.h| 28 +-
 .../Interfaces/ScriptedPythonInterface.h  |  7 ++
 lldb/source/Target/ExecutionContext.cpp   |  5 +-
 lldb/source/Utility/Policy.cpp| 12 +++
 .../Makefile  |  2 +
 ...ProviderRegisterCommandAPIMutexDeadlock.py | 89 +++
 .../frame_provider.py | 30 +++
 .../main.c|  7 ++
 lldb/unittests/Utility/PolicyTest.cpp |  6 +-
 10 files changed, 186 insertions(+), 12 deletions(-)
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/Makefile
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/main.c

diff --git a/lldb/include/lldb/Target/ExecutionContext.h 
b/lldb/include/lldb/Target/ExecutionContext.h
index bf976f4db8c87..3a53a7755ee72 100644
--- a/lldb/include/lldb/Target/ExecutionContext.h
+++ b/lldb/include/lldb/Target/ExecutionContext.h
@@ -14,6 +14,7 @@
 #include "lldb/Host/ProcessRunLock.h"
 #include "lldb/Target/StackID.h"
 #include "lldb/Target/SyntheticFrameProvider.h"
+#include "lldb/Utility/Policy.h"
 #include "lldb/lldb-private.h"
 
 namespace lldb_private {
@@ -561,9 +562,10 @@ class ExecutionContext {
 };
 
 /// A wrapper class representing an execution context with non-null Target
-/// and Process pointers, a locked API mutex and a locked ProcessRunLock.
-/// The locks are private by design: to unlock them, destroy the
-/// StoppedExecutionContext.
+/// and Process pointers, a locked ProcessRunLock, and (unless
+/// Policy::Capabilities::can_reenter_target_api_mutex is set for the
+/// current thread) a locked API mutex. The locks are private by design: to
+/// unlock them, destroy the StoppedExecutionContext.
 struct StoppedExecutionContext : ExecutionContext {
   StoppedExecutionContext(lldb::TargetSP &target_sp,
   lldb::ProcessSP &process_sp,
@@ 

[Lldb-commits] [lldb] [lldb] Fix deadlock between the command thread and event-handler thread in scripted frame providers (PR #208242)

2026-07-08 Thread via lldb-commits

github-actions[bot] wrote:


# :penguin: Linux x64 Test Results

* 33993 tests passed
* 545 tests skipped
* 2 tests failed

## Failed Tests
(click on a test name to see its output)

### lldb-unit

lldb-unit.Utility/_/UtilityTests/PolicyTest/DumpPrivateState

```
Script:
--
/home/gha/actions-runner/_work/llvm-project/llvm-project/build/tools/lldb/unittests/Utility/./UtilityTests
 --gtest_filter=PolicyTest.DumpPrivateState
--
/home/gha/actions-runner/_work/llvm-project/llvm-project/lldb/unittests/Utility/PolicyTest.cpp:157
Expected equality of these values:
  s.GetString()
Which is: "policy: view=private, capabilities={eval_expr=true run_all=true 
try_all=true bp_actions=true frame_providers=true frame_recognizers=true 
reenter_api_mutex=false}"
  "policy: view=private, capabilities={" "eval_expr=true run_all=true 
try_all=true " "bp_actions=true frame_providers=true frame_recognizers=true}"
Which is: 0x283a84


```


lldb-unit.Utility/_/UtilityTests/PolicyTest/DumpPublicState

```
Script:
--
/home/gha/actions-runner/_work/llvm-project/llvm-project/build/tools/lldb/unittests/Utility/./UtilityTests
 --gtest_filter=PolicyTest.DumpPublicState
--
/home/gha/actions-runner/_work/llvm-project/llvm-project/lldb/unittests/Utility/PolicyTest.cpp:148
Expected equality of these values:
  s.GetString()
Which is: "policy: view=public, capabilities={eval_expr=true run_all=true 
try_all=true bp_actions=true frame_providers=true frame_recognizers=true 
reenter_api_mutex=false}"
  "policy: view=public, capabilities={" "eval_expr=true run_all=true 
try_all=true " "bp_actions=true frame_providers=true frame_recognizers=true}"
Which is: 0x228f1e


```


If these failures are unrelated to your changes (for example tests are broken 
or flaky at HEAD), please open an issue at 
https://github.com/llvm/llvm-project/issues and add the `infrastructure` label.

https://github.com/llvm/llvm-project/pull/208242
___
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits


[Lldb-commits] [lldb] [lldb] Fix deadlock between the command thread and event-handler thread in scripted frame providers (PR #208242)

2026-07-08 Thread via lldb-commits

github-actions[bot] wrote:


# :window: Windows x64 Test Results

* 33346 tests passed
* 903 tests skipped
* 2 tests failed

## Failed Tests
(click on a test name to see its output)

### lldb-unit

lldb-unit.Utility/_/UtilityTests_exe/PolicyTest/DumpPrivateState

```
Script:
--
C:\_work\llvm-project\llvm-project\build\tools\lldb\unittests\Utility\.\UtilityTests.exe
 --gtest_filter=PolicyTest.DumpPrivateState
--
C:\_work\llvm-project\llvm-project\lldb\unittests\Utility\PolicyTest.cpp:157
Expected equality of these values:
  s.GetString()
Which is: "policy: view=private, capabilities={eval_expr=true run_all=true 
try_all=true bp_actions=true frame_providers=true frame_recognizers=true 
reenter_api_mutex=false}"
  "policy: view=private, capabilities={" "eval_expr=true run_all=true 
try_all=true " "bp_actions=true frame_providers=true frame_recognizers=true}"
Which is: 7FF72189B257


```


lldb-unit.Utility/_/UtilityTests_exe/PolicyTest/DumpPublicState

```
Script:
--
C:\_work\llvm-project\llvm-project\build\tools\lldb\unittests\Utility\.\UtilityTests.exe
 --gtest_filter=PolicyTest.DumpPublicState
--
C:\_work\llvm-project\llvm-project\lldb\unittests\Utility\PolicyTest.cpp:148
Expected equality of these values:
  s.GetString()
Which is: "policy: view=public, capabilities={eval_expr=true run_all=true 
try_all=true bp_actions=true frame_providers=true frame_recognizers=true 
reenter_api_mutex=false}"
  "policy: view=public, capabilities={" "eval_expr=true run_all=true 
try_all=true " "bp_actions=true frame_providers=true frame_recognizers=true}"
Which is: 7FF72189B2E1


```


If these failures are unrelated to your changes (for example tests are broken 
or flaky at HEAD), please open an issue at 
https://github.com/llvm/llvm-project/issues and add the `infrastructure` label.

https://github.com/llvm/llvm-project/pull/208242
___
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits


[Lldb-commits] [lldb] [lldb] Fix deadlock between the command thread and event-handler thread in scripted frame providers (PR #208242)

2026-07-08 Thread via lldb-commits

github-actions[bot] wrote:




:warning: Python code formatter, darker found issues in your code. :warning:



You can test this locally with the following command:


``bash
darker --check --diff -r origin/main...HEAD 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py
 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py
``

:warning:
The reproduction instructions above might return results for more than one PR
in a stack if you are using a stacked PR workflow. You can limit the results by
changing `origin/main` to the base branch/commit you want to compare against.
:warning:





View the diff from darker here.


``diff
--- frame_provider.py   2026-07-08 15:41:06.00 +
+++ frame_provider.py   2026-07-08 15:47:43.977166 +
@@ -24,7 +24,5 @@
 # exercises GetStoppedExecutionContext.
 frame = self.input_frames[index]
 if frame is None:
 return None
 return {"idx": index, "pc": frame.GetPC()}
-
-

``




https://github.com/llvm/llvm-project/pull/208242
___
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits


[Lldb-commits] [lldb] [lldb] Fix deadlock between the command thread and event-handler thread in scripted frame providers (PR #208242)

2026-07-08 Thread via lldb-commits

llvmorg-github-actions[bot] wrote:




@llvm/pr-subscribers-lldb

Author: Med Ismail Bennani (medismailben)


Changes

`GetStoppedExecutionContext` unconditionally blocked acquiring the target's API 
mutex. A thread already holding that mutex (e.g. a `bt` command thread, via 
`CommandObjectParsed's eCommandTryTargetAPILock`) can end up waiting on a 
StackFrameList lock held by another thread (e.g. the debugger's `event-handler` 
thread) that is itself blocked re-acquiring the API mutex from inside a 
scripted frame provider's Python code that touches SB API -- a classic AB-BA 
deadlock.

Introduce `Policy::Capabilities::can_reenter_target_api_mutex`, pushed around 
every scripted-extension callback in
`ScriptedPythonInterface::Dispatch` and `CallStaticMethod`, so 
`GetStoppedExecutionContext` can skip the mutex instead of blocking when 
running under a scripted callback -- these callbacks are already running under 
whatever protections the caller set up, and blocking again is both redundant 
and, in this case, deadlock-prone.

This also required making `PolicyStack::Get()` out-of-line: 
`Dispatch`/`CallStaticMethod` live in a header included by both liblldb and the 
Python script interpreter plugin, and LLDB builds with hidden visibility by 
default. An inline function's function-local static is NOT shared across shared 
library boundaries, so each dylib that included Policy.h and called an inline 
`Get()` would get its own private copy of the `thread_local` stack, silently 
splitting a single logical per-thread stack into two. `Push`/`Pop` calls from 
the plugin would then operate on a different object than Guard's destructor's 
Pop (already out-of-line, compiled into liblldb), draining liblldb's copy out 
from under callers that never pushed to it and tripping the "can't pop the base 
policy" assert. Making `Get()` out-of-line ensures every dylib resolves to the 
single instance defined in Policy.cpp.

Adds a regression test for the deadlock. It's a genuine cross-thread race (the 
command thread vs. the debugger's event-handler thread), so like the sibling 
`runlock_reentrant_deadlock`/`was_hit_deadlock` tests, it raises the odds of 
hitting it within a single invocation but can't guarantee it.

Fixes https://github.com/llvm/llvm-project/issues/194108

---
Full diff: https://github.com/llvm/llvm-project/pull/208242.diff


9 Files Affected:

- (modified) lldb/include/lldb/Target/ExecutionContext.h (+8-4) 
- (modified) lldb/include/lldb/Utility/Policy.h (+24-4) 
- (modified) 
lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
 (+7) 
- (modified) lldb/source/Target/ExecutionContext.cpp (+3-2) 
- (modified) lldb/source/Utility/Policy.cpp (+12) 
- (added) 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/Makefile
 (+2) 
- (added) 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py
 (+89) 
- (added) 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py
 (+30) 
- (added) 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/main.c
 (+7) 


``diff
diff --git a/lldb/include/lldb/Target/ExecutionContext.h 
b/lldb/include/lldb/Target/ExecutionContext.h
index bf976f4db8c87..3a53a7755ee72 100644
--- a/lldb/include/lldb/Target/ExecutionContext.h
+++ b/lldb/include/lldb/Target/ExecutionContext.h
@@ -14,6 +14,7 @@
 #include "lldb/Host/ProcessRunLock.h"
 #include "lldb/Target/StackID.h"
 #include "lldb/Target/SyntheticFrameProvider.h"
+#include "lldb/Utility/Policy.h"
 #include "lldb/lldb-private.h"
 
 namespace lldb_private {
@@ -561,9 +562,10 @@ class ExecutionContext {
 };
 
 /// A wrapper class representing an execution context with non-null Target
-/// and Process pointers, a locked API mutex and a locked ProcessRunLock.
-/// The locks are private by design: to unlock them, destroy the
-/// StoppedExecutionContext.
+/// and Process pointers, a locked ProcessRunLock, and (unless
+/// Policy::Capabilities::can_reenter_target_api_mutex is set for the
+/// current thread) a locked API mutex. The locks are private by design: to
+/// unlock them, destroy the StoppedExecutionContext.
 struct StoppedExecutionContext : ExecutionContext {
   StoppedExecutionContext(lldb::TargetSP &target_sp,
   lldb::ProcessSP &process_sp,
@@ -574,7 +576,9 @@ struct StoppedExecutionContext : ExecutionContext {
   : m_api_lock(std::move(api_lock)), m_stop_locker(std::move(stop_locker)) 
{
 assert(target_sp);
 assert(process_sp);
-assert(m_api_lock.owns_lock());
+assert(
+m_api_lock.owns_lock() ||
+
PolicyStack::Get().Current().capabilities.can_reenter_target_api_mutex);
 assert(m_stop_locker.IsLocked());
 SetTargetSP(target_sp);
 SetProcessSP(process_sp);
diff --git a/lldb/include/lldb/Utility/Policy.h 
b/lldb/inc

[Lldb-commits] [lldb] [lldb] Fix deadlock between the command thread and event-handler thread in scripted frame providers (PR #208242)

2026-07-08 Thread Med Ismail Bennani via lldb-commits

https://github.com/medismailben updated 
https://github.com/llvm/llvm-project/pull/208242

>From ee8bb54ac1a3c0ab233b1f0cf3aa6db8917f556a Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani 
Date: Wed, 8 Jul 2026 08:41:06 -0700
Subject: [PATCH] [lldb] Fix deadlock between the command thread and
 event-handler thread in scripted frame providers

GetStoppedExecutionContext unconditionally blocked acquiring the
target's API mutex. A thread already holding that mutex (e.g. a `bt`
command thread, via CommandObjectParsed's eCommandTryTargetAPILock) can
end up waiting on a StackFrameList lock held by another thread (e.g. the
debugger's event-handler thread) that is itself blocked re-acquiring the
API mutex from inside a scripted frame provider's Python code that
touches SB API -- a classic AB-BA deadlock.

Introduce Policy::Capabilities::can_reenter_target_api_mutex, pushed
around every scripted-extension callback in
ScriptedPythonInterface::Dispatch and CallStaticMethod, so
GetStoppedExecutionContext can skip the mutex instead of blocking when
running under a scripted callback -- these callbacks are already running
under whatever protections the caller set up, and blocking again is both
redundant and, in this case, deadlock-prone.

This also required making PolicyStack::Get() out-of-line:
Dispatch/CallStaticMethod live in a header included by both liblldb and
the Python script interpreter plugin, and LLDB builds with hidden
visibility by default. An inline function's function-local static is NOT
shared across shared library boundaries, so each dylib that included
Policy.h and called an inline Get() would get its own private copy of
the thread_local stack, silently splitting a single logical per-thread
stack into two. Push/Pop calls from the plugin would then operate on a
different object than Guard's destructor's Pop (already out-of-line,
compiled into liblldb), draining liblldb's copy out from under callers
that never pushed to it and tripping the "can't pop the base policy"
assert. Making Get() out-of-line ensures every dylib resolves to the
single instance defined in Policy.cpp.

Adds a regression test for the deadlock. It's a genuine cross-thread
race (the command thread vs. the debugger's event-handler thread), so
like the sibling runlock_reentrant_deadlock/was_hit_deadlock tests, it
raises the odds of hitting it within a single invocation but can't
guarantee it. The frame provider deliberately returns dict-based
synthetic frames rather than forwarding a frame under its own index, to
keep this test isolated from the separate frame-aliasing bug fixed on
top of this commit.

Signed-off-by: Med Ismail Bennani 
---
 lldb/include/lldb/Target/ExecutionContext.h   | 12 ++-
 lldb/include/lldb/Utility/Policy.h| 28 +-
 .../Interfaces/ScriptedPythonInterface.h  |  7 ++
 lldb/source/Target/ExecutionContext.cpp   |  5 +-
 lldb/source/Utility/Policy.cpp| 12 +++
 .../Makefile  |  2 +
 ...ProviderRegisterCommandAPIMutexDeadlock.py | 89 +++
 .../frame_provider.py | 30 +++
 .../main.c|  7 ++
 9 files changed, 182 insertions(+), 10 deletions(-)
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/Makefile
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py
 create mode 100644 
lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/main.c

diff --git a/lldb/include/lldb/Target/ExecutionContext.h 
b/lldb/include/lldb/Target/ExecutionContext.h
index bf976f4db8c87..3a53a7755ee72 100644
--- a/lldb/include/lldb/Target/ExecutionContext.h
+++ b/lldb/include/lldb/Target/ExecutionContext.h
@@ -14,6 +14,7 @@
 #include "lldb/Host/ProcessRunLock.h"
 #include "lldb/Target/StackID.h"
 #include "lldb/Target/SyntheticFrameProvider.h"
+#include "lldb/Utility/Policy.h"
 #include "lldb/lldb-private.h"
 
 namespace lldb_private {
@@ -561,9 +562,10 @@ class ExecutionContext {
 };
 
 /// A wrapper class representing an execution context with non-null Target
-/// and Process pointers, a locked API mutex and a locked ProcessRunLock.
-/// The locks are private by design: to unlock them, destroy the
-/// StoppedExecutionContext.
+/// and Process pointers, a locked ProcessRunLock, and (unless
+/// Policy::Capabilities::can_reenter_target_api_mutex is set for the
+/// current thread) a locked API mutex. The locks are private by design: to
+/// unlock them, destroy the StoppedExecutionContext.
 struct StoppedExecutionContext : ExecutionContext {
   StoppedExecutionContext(lldb::TargetSP &target_sp,
   lldb::ProcessSP &process_sp,
@@ -574,7 +576,9 @@ struct 

[Lldb-commits] [lldb] [lldb] Fix deadlock between the command thread and event-handler thread in scripted frame providers (PR #208242)

2026-07-08 Thread Med Ismail Bennani via lldb-commits

https://github.com/medismailben created 
https://github.com/llvm/llvm-project/pull/208242

`GetStoppedExecutionContext` unconditionally blocked acquiring the target's API 
mutex. A thread already holding that mutex (e.g. a `bt` command thread, via 
`CommandObjectParsed's eCommandTryTargetAPILock`) can end up waiting on a 
StackFrameList lock held by another thread (e.g. the debugger's `event-handler` 
thread) that is itself blocked re-acquiring the API mutex from inside a 
scripted frame provider's Python code that touches SB API -- a classic AB-BA 
deadlock.

Introduce `Policy::Capabilities::can_reenter_target_api_mutex`, pushed around 
every scripted-extension callback in
`ScriptedPythonInterface::Dispatch` and `CallStaticMethod`, so 
`GetStoppedExecutionContext` can skip the mutex instead of blocking when 
running under a scripted callback -- these callbacks are already running under 
whatever protections the caller set up, and blocking again is both redundant 
and, in this case, deadlock-prone.

This also required making `PolicyStack::Get()` out-of-line: 
`Dispatch`/`CallStaticMethod` live in a header included by both liblldb and the 
Python script interpreter plugin, and LLDB builds with hidden visibility by 
default. An inline function's function-local static is NOT shared across shared 
library boundaries, so each dylib that included Policy.h and called an inline 
`Get()` would get its own private copy of the `thread_local` stack, silently 
splitting a single logical per-thread stack into two. `Push`/`Pop` calls from 
the plugin would then operate on a different object than Guard's destructor's 
Pop (already out-of-line, compiled into liblldb), draining liblldb's copy out 
from under callers that never pushed to it and tripping the "can't pop the base 
policy" assert. Making `Get()` out-of-line ensures every dylib resolves to the 
single instance defined in Policy.cpp.

Adds a regression test for the deadlock. It's a genuine cross-thread race (the 
command thread vs. the debugger's event-handler thread), so like the sibling 
`runlock_reentrant_deadlock`/`was_hit_deadlock` tests, it raises the odds of 
hitting it within a single invocation but can't guarantee it.

Fixes https://github.com/llvm/llvm-project/issues/194108

>From 3e336b4958d69d804ddc452dbffd7b1ccf661e85 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani 
Date: Wed, 8 Jul 2026 07:19:07 -0700
Subject: [PATCH] [lldb] Fix deadlock between the command thread and
 event-handler thread in scripted frame providers

GetStoppedExecutionContext unconditionally blocked acquiring the
target's API mutex. A thread already holding that mutex (e.g. a `bt`
command thread, via CommandObjectParsed's eCommandTryTargetAPILock) can
end up waiting on a StackFrameList lock held by another thread (e.g. the
debugger's event-handler thread) that is itself blocked re-acquiring the
API mutex from inside a scripted frame provider's Python code that
touches SB API -- a classic AB-BA deadlock.

Introduce Policy::Capabilities::can_reenter_target_api_mutex, pushed
around every scripted-extension callback in
ScriptedPythonInterface::Dispatch and CallStaticMethod, so
GetStoppedExecutionContext can skip the mutex instead of blocking when
running under a scripted callback -- these callbacks are already running
under whatever protections the caller set up, and blocking again is both
redundant and, in this case, deadlock-prone.

This also required making PolicyStack::Get() out-of-line:
Dispatch/CallStaticMethod live in a header included by both liblldb and
the Python script interpreter plugin, and LLDB builds with hidden
visibility by default. An inline function's function-local static is NOT
shared across shared library boundaries, so each dylib that included
Policy.h and called an inline Get() would get its own private copy of
the thread_local stack, silently splitting a single logical per-thread
stack into two. Push/Pop calls from the plugin would then operate on a
different object than Guard's destructor's Pop (already out-of-line,
compiled into liblldb), draining liblldb's copy out from under callers
that never pushed to it and tripping the "can't pop the base policy"
assert. Making Get() out-of-line ensures every dylib resolves to the
single instance defined in Policy.cpp.

Adds a regression test for the deadlock. It's a genuine cross-thread
race (the command thread vs. the debugger's event-handler thread), so
like the sibling runlock_reentrant_deadlock/was_hit_deadlock tests, it
raises the odds of hitting it within a single invocation but can't
guarantee it. The frame provider deliberately returns dict-based
synthetic frames rather than forwarding a frame under its own index, to
keep this test isolated from the separate frame-aliasing bug fixed on
top of this commit.
---
 lldb/include/lldb/Target/ExecutionContext.h   | 12 ++-
 lldb/include/lldb/Utility/Policy.h| 28 +-
 .../Interfaces/ScriptedPythonInterface.h  |  7 ++
 lldb/source/Target/ExecutionCon