[Lldb-commits] [lldb] [lldb] Make ProcessRunLock recursive on the read side per-thread (PR #201685)
Med Ismail Bennani via lldb-commits
lldb-commits at lists.llvm.org
Fri Jun 5 18:54:58 PDT 2026
https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/201685
>From 5b3a5272b1b3ccea9b1f6f3813895f80d968ae89 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Fri, 5 Jun 2026 18:54:41 -0700
Subject: [PATCH] [lldb] Make ProcessRunLock recursive on the read side
per-thread
When SB API code enters a Python callback (frame provider, breakpoint
callback, ...) and the callback re-enters the SB API, the inner call
tries to re-acquire the ProcessRunLock read lock. Recursive shared
acquisition deadlocks against a queued writer on a write-preferring
rwlock (POSIX), and is UB on Windows SRW.
Each ProcessRunLock now keeps a per-instance map of lldb::thread_t ->
recursion count under its own std::mutex. ProcessRunLocker::TryLock
checks that map first: if this thread already has a non-zero count it
bumps and returns, skipping the rwlock. Unlock decrements; only the
final Unlock releases the rwlock. Per-instance (not thread_local) so
the bookkeeping is colocated with the lock and concurrent readers
from multiple threads each get the fast path.
Thread identity is tracked via lldb::thread_t / Host::GetCurrentThread()
to match the rest of LLDB (HostThread, PrivateStateThread, etc.). That
type already has a DenseMapInfo specialization so DenseMap works out of
the box.
ProcessRunLocker move policy: unlocked moves are always fine; moving
a held locker is only valid on the owning thread. Cross-thread move
asserts in debug and llvm::report_fatal_error in release. The
destructor enforces the same invariant.
rdar://176223894
Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
lldb/include/lldb/Host/ProcessRunLock.h | 110 ++++++++++------
lldb/source/Host/common/ProcessRunLock.cpp | 118 ++++++++++++++++-
.../runlock_reentrant_deadlock/Makefile | 3 +
.../TestRunLockReentrantDeadlock.py | 124 ++++++++++++++++++
.../bkpt_resolver.py | 44 +++++++
.../frame_provider.py | 32 +++++
.../runlock_reentrant_deadlock/main.c | 18 +++
lldb/unittests/Host/CMakeLists.txt | 1 +
lldb/unittests/Host/ProcessRunLockTest.cpp | 113 ++++++++++++++++
9 files changed, 521 insertions(+), 42 deletions(-)
create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/Makefile
create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/TestRunLockReentrantDeadlock.py
create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/bkpt_resolver.py
create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/frame_provider.py
create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/main.c
create mode 100644 lldb/unittests/Host/ProcessRunLockTest.cpp
diff --git a/lldb/include/lldb/Host/ProcessRunLock.h b/lldb/include/lldb/Host/ProcessRunLock.h
index 27d10942ddb4c..43308fb9d608c 100644
--- a/lldb/include/lldb/Host/ProcessRunLock.h
+++ b/lldb/include/lldb/Host/ProcessRunLock.h
@@ -9,27 +9,59 @@
#ifndef LLDB_HOST_PROCESSRUNLOCK_H
#define LLDB_HOST_PROCESSRUNLOCK_H
+#include <cassert>
#include <cstdint>
#include <ctime>
+#include <mutex>
+
+#include "llvm/ADT/DenseMap.h"
#include "lldb/lldb-defines.h"
+#include "lldb/lldb-types.h"
/// Enumerations for broadcasting.
namespace lldb_private {
/// \class ProcessRunLock ProcessRunLock.h "lldb/Host/ProcessRunLock.h"
-/// A class used to prevent the process from starting while other
-/// threads are accessing its data, and prevent access to its data while it is
+/// A class used to prevent the process from starting while other threads
+/// are accessing its data, and prevent access to its data while it is
/// running.
+///
+/// The lock is read/write internally, but readers and writers reach it
+/// through different APIs:
+/// - Readers (any thread that wants the process to stay stopped) use the
+/// RAII ProcessRunLocker. The underlying rwlock primitives are private
+/// because they are not safe to call directly: bypassing the locker
+/// skips the per-instance recursion bookkeeping and can deadlock on
+/// re-entrant acquisition.
+/// - Writers (the private state thread, when transitioning the process
+/// between running and stopped) call SetRunning / SetStopped.
+/// There is intentionally no public TryWriteLock / WriteUnlock entry point;
+/// `ProcessRunLocker` is therefore a read-lock-only helper.
+///
+/// Re-entrant read acquisition on the same thread (e.g. SB API re-entry
+/// from a frame-provider Python callback) is supported via ProcessRunLocker.
+/// Each lock keeps a per-instance map of `lldb::thread_t -> recursion count`
+/// so a nested TryLock on the same lock skips the underlying rwlock and
+/// just bumps the count. The outer reader keeps any writer blocked, so
+/// the process state cannot change while the count is non-zero. The map
+/// is per-instance (not thread_local) so it correctly handles concurrent
+/// readers from multiple threads: each thread has its own entry in the
+/// same lock's map, and a re-entrant acquire from any of them takes the
+/// fast path.
+///
+/// Cross-thread move of a held ProcessRunLocker is forbidden: the
+/// recursion bookkeeping is keyed by the acquiring thread, and POSIX
+/// requires the same thread to call pthread_rwlock_unlock as called
+/// pthread_rwlock_rdlock. Move operations on an unlocked locker are
+/// supported. Move operations on a held locker are valid only on the
+/// owning thread (asserted in debug builds, fatal in release builds).
class ProcessRunLock {
public:
ProcessRunLock();
~ProcessRunLock();
- bool ReadTryLock();
- bool ReadUnlock();
-
/// Set the process to running. Returns true if the process was stopped.
/// Return false if the process was running.
bool SetRunning();
@@ -38,51 +70,37 @@ class ProcessRunLock {
/// Returns false if the process was stopped.
bool SetStopped();
+ /// RAII helper around the read-lock side of ProcessRunLock. Supports
+ /// same-thread recursion (see class doc).
+ ///
+ /// Move policy:
+ /// - Moving an unlocked locker is always allowed (and free).
+ /// - Moving a held locker is allowed *only* on the owning thread.
+ /// Cross-thread move of a held locker is a programming error and
+ /// triggers an assertion in debug builds / a fatal error in release
+ /// builds.
class ProcessRunLocker {
public:
ProcessRunLocker() = default;
- ProcessRunLocker(ProcessRunLocker &&other) : m_lock(other.m_lock) {
- other.m_lock = nullptr;
- }
- ProcessRunLocker &operator=(ProcessRunLocker &&other) {
- if (this != &other) {
- Unlock();
- m_lock = other.m_lock;
- other.m_lock = nullptr;
- }
- return *this;
- }
-
+ ProcessRunLocker(ProcessRunLocker &&other);
+ ProcessRunLocker &operator=(ProcessRunLocker &&other);
~ProcessRunLocker() { Unlock(); }
bool IsLocked() const { return m_lock; }
- // Try to lock the read lock, but only do so if there are no writers.
- bool TryLock(ProcessRunLock *lock) {
- if (m_lock) {
- if (m_lock == lock)
- return true; // We already have this lock locked
- else
- Unlock();
- }
- if (lock) {
- if (lock->ReadTryLock()) {
- m_lock = lock;
- return true;
- }
- }
- return false;
- }
+ /// Try to acquire the read lock. If this thread already holds the
+ /// read lock on this ProcessRunLock, the underlying rwlock is bypassed
+ /// and the per-instance recursion count for this thread is bumped
+ /// instead.
+ bool TryLock(ProcessRunLock *lock);
protected:
- void Unlock() {
- if (m_lock) {
- m_lock->ReadUnlock();
- m_lock = nullptr;
- }
- }
+ void Unlock();
ProcessRunLock *m_lock = nullptr;
+ /// Set in TryLock when m_lock becomes non-null. Used to detect
+ /// cross-thread destruction or move of a locker that holds the lock.
+ lldb::thread_t m_thread = LLDB_INVALID_HOST_THREAD;
private:
ProcessRunLocker(const ProcessRunLocker &) = delete;
@@ -96,6 +114,20 @@ class ProcessRunLock {
private:
ProcessRunLock(const ProcessRunLock &) = delete;
const ProcessRunLock &operator=(const ProcessRunLock &) = delete;
+
+ /// Low-level rwlock primitives. Private because calling them outside the
+ /// ProcessRunLocker bookkeeping bypasses the per-instance recursion
+ /// count and can deadlock on re-entrant acquisition.
+ bool ReadTryLock();
+ bool ReadUnlock();
+ friend class ProcessRunLocker;
+
+ /// Per-instance recursion bookkeeping. Maps lldb::thread_t -> active
+ /// TryLock count for that thread on this lock. A thread with a non-zero
+ /// count is already holding the rwlock as a reader and may TryLock
+ /// re-entrantly without going through the rwlock.
+ std::mutex m_recursion_mu;
+ llvm::DenseMap<lldb::thread_t, uint32_t> m_recursion;
};
} // namespace lldb_private
diff --git a/lldb/source/Host/common/ProcessRunLock.cpp b/lldb/source/Host/common/ProcessRunLock.cpp
index 8e7ef45e1e350..e1ec876a09e79 100644
--- a/lldb/source/Host/common/ProcessRunLock.cpp
+++ b/lldb/source/Host/common/ProcessRunLock.cpp
@@ -6,11 +6,19 @@
//
//===----------------------------------------------------------------------===//
-#ifndef _WIN32
#include "lldb/Host/ProcessRunLock.h"
+#include "lldb/Host/Host.h"
+
+#include "llvm/Support/ErrorHandling.h"
+
+#include <cassert>
+#include <cstdint>
+
namespace lldb_private {
+#ifndef _WIN32
+
ProcessRunLock::ProcessRunLock() {
int err = ::pthread_rwlock_init(&m_rwlock, nullptr);
(void)err;
@@ -51,6 +59,110 @@ bool ProcessRunLock::SetStopped() {
return was_running;
}
-} // namespace lldb_private
+#endif // !_WIN32
+
+ProcessRunLock::ProcessRunLocker::ProcessRunLocker(ProcessRunLocker &&other)
+ : m_lock(other.m_lock), m_thread(other.m_thread) {
+ if (m_lock && m_thread != Host::GetCurrentThread()) {
+ assert(false && "ProcessRunLocker moved across threads while held");
+ llvm::report_fatal_error(
+ "ProcessRunLocker moved across threads while held");
+ }
+ other.m_lock = nullptr;
+}
+
+ProcessRunLock::ProcessRunLocker &
+ProcessRunLock::ProcessRunLocker::operator=(ProcessRunLocker &&other) {
+ if (this != &other) {
+ if (other.m_lock && other.m_thread != Host::GetCurrentThread()) {
+ assert(false &&
+ "ProcessRunLocker move-assigned across threads while held");
+ llvm::report_fatal_error(
+ "ProcessRunLocker move-assigned across threads while held");
+ }
+ Unlock();
+ m_lock = other.m_lock;
+ m_thread = other.m_thread;
+ other.m_lock = nullptr;
+ }
+ return *this;
+}
+
+bool ProcessRunLock::ProcessRunLocker::TryLock(ProcessRunLock *lock) {
+ if (m_lock) {
+ if (m_lock == lock)
+ return true;
+ Unlock();
+ }
+ if (!lock)
+ return false;
-#endif
+ lldb::thread_t self = Host::GetCurrentThread();
+
+ // Fast path: this thread already holds the rwlock as a reader. Bump
+ // the per-instance recursion count for this thread; the outer reader
+ // keeps any writer blocked, so the process state can't change.
+ {
+ std::lock_guard<std::mutex> guard(lock->m_recursion_mu);
+ auto it = lock->m_recursion.find(self);
+ if (it != lock->m_recursion.end()) {
+ ++it->second;
+ m_lock = lock;
+ m_thread = self;
+ return true;
+ }
+ }
+
+ // Real acquire. Done outside m_recursion_mu so a blocking
+ // pthread_rwlock_rdlock doesn't serialize unrelated acquires through
+ // the recursion mutex.
+ if (!lock->ReadTryLock())
+ return false;
+
+ std::lock_guard<std::mutex> guard(lock->m_recursion_mu);
+ lock->m_recursion[self] = 1;
+ m_lock = lock;
+ m_thread = self;
+ return true;
+}
+
+void ProcessRunLock::ProcessRunLocker::Unlock() {
+ if (!m_lock)
+ return;
+
+ lldb::thread_t self = Host::GetCurrentThread();
+ if (m_thread != self) {
+ // Cross-thread destruction defense. pthread_rwlock_unlock from a
+ // different thread than the one that called pthread_rwlock_rdlock
+ // is UB. Move ctor / operator= already trap this case at the
+ // source; this is the last-line check on the destructor path.
+ assert(false &&
+ "ProcessRunLocker destroyed on a different thread while held");
+ llvm::report_fatal_error(
+ "ProcessRunLocker destroyed on a different thread while held");
+ }
+
+ bool release_rwlock = false;
+ {
+ std::lock_guard<std::mutex> guard(m_lock->m_recursion_mu);
+ auto it = m_lock->m_recursion.find(self);
+ assert(it != m_lock->m_recursion.end() &&
+ "ProcessRunLocker released without a matching successful TryLock "
+ "on this thread");
+ if (it == m_lock->m_recursion.end()) {
+ m_lock = nullptr;
+ return;
+ }
+ assert(it->second > 0 && "ProcessRunLocker recursion underflow");
+ if (--it->second == 0) {
+ m_lock->m_recursion.erase(it);
+ release_rwlock = true;
+ }
+ }
+
+ if (release_rwlock)
+ m_lock->ReadUnlock();
+ m_lock = nullptr;
+}
+
+} // namespace lldb_private
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/Makefile b/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/Makefile
new file mode 100644
index 0000000000000..0b710c6e298ae
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+CFLAGS_EXTRAS := -std=c99
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/TestRunLockReentrantDeadlock.py b/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/TestRunLockReentrantDeadlock.py
new file mode 100644
index 0000000000000..12d6c473954f0
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/TestRunLockReentrantDeadlock.py
@@ -0,0 +1,124 @@
+"""
+Test that ProcessRunLock re-entrant read locks don't deadlock when a frame
+provider's get_frame_at_index calls SB API methods.
+
+This reproduces the deadlock seen in lldb-rpc-server where:
+
+ - An RPC client thread holds a ProcessRunLock read lock (from the outer
+ SB API call) and enters a provider's get_frame_at_index, which calls
+ SBFrame.IsValid -> GetStoppedExecutionContext -> ReadTryLock (re-entrant).
+
+ - The override PST is exiting RunPrivateStateThread and calls SetStopped
+ -> pthread_rwlock_wrlock (blocked by client thread's read lock).
+
+ - The client thread's re-entrant ReadTryLock blocks because the pending
+ writer prevents new readers on a write-preferring rwlock.
+
+ - The original PST is blocked joining the override thread.
+"""
+
+import os
+import threading
+import lldb
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+
+
+class TestRunLockReentrantDeadlock(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ def test_runlock_reentrant_no_deadlock(self):
+ """
+ Test that a frame provider calling SBFrame.IsValid from
+ get_frame_at_index does not deadlock with the override PST's
+ SetStopped when another thread triggers EvaluateExpression.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_name_breakpoint(self, "main")
+
+ 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)
+
+ error = lldb.SBError()
+ provider_id = target.RegisterScriptedFrameProvider(
+ "frame_provider.SBAPIAccessInGetFrameProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(
+ error.Success(),
+ f"Should register frame provider: {error}",
+ )
+ self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+ 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"
+ )
+
+ # Spawn a thread that repeatedly accesses frames through the provider.
+ # This simulates an RPC client thread that holds the ProcessRunLock
+ # read lock and enters get_frame_at_index -> SBFrame.IsValid.
+ stop_frame_access = threading.Event()
+ frame_access_error = [None]
+
+ def access_frames():
+ try:
+ while not stop_frame_access.is_set():
+ t = process.GetSelectedThread()
+ if t.IsValid():
+ for i in range(t.GetNumFrames()):
+ f = t.GetFrameAtIndex(i)
+ f.IsValid()
+ except Exception as e:
+ frame_access_error[0] = str(e)
+
+ frame_thread = threading.Thread(target=access_frames, daemon=True)
+ frame_thread.start()
+
+ # Continue into the breakpoint. was_hit calls EvaluateExpression on
+ # the PST, which triggers RunThreadPlan -> override PST.
+ # The frame access thread is concurrently calling GetFrameAtIndex ->
+ # provider get_frame_at_index -> SBFrame.IsValid.
+ # Without the fix, this deadlocks.
+ process.Continue()
+ self.assertState(process.GetState(), lldb.eStateStopped)
+
+ stop_frame_access.set()
+ frame_thread.join(timeout=5)
+ self.assertFalse(frame_thread.is_alive(), "Frame access thread should exit")
+ self.assertIsNone(
+ frame_access_error[0],
+ f"Frame access thread hit an error: {frame_access_error[0]}",
+ )
+
+ thread = process.GetSelectedThread()
+ self.assertTrue(thread.IsValid(), "Thread should be valid")
+ self.assertEqual(
+ thread.GetStopReason(),
+ lldb.eStopReasonBreakpoint,
+ "Should stop at breakpoint",
+ )
+
+ 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",
+ )
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/bkpt_resolver.py b/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/bkpt_resolver.py
new file mode 100644
index 0000000000000..06641295c534f
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/bkpt_resolver.py
@@ -0,0 +1,44 @@
+"""
+Scripted breakpoint resolver whose was_hit callback calls EvaluateExpression.
+
+Used to trigger the deadlock scenario described in
+TestRunLockReentrantDeadlock.py: was_hit -> EvaluateExpression spins up the
+override PST and starts an expression, setting up the conditions under
+which a re-entrant ReadTryLock from a frame provider would deadlock against
+the pending writer.
+"""
+
+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/runlock_reentrant_deadlock/frame_provider.py b/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/frame_provider.py
new file mode 100644
index 0000000000000..8392c90c7b317
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/frame_provider.py
@@ -0,0 +1,32 @@
+"""
+Frame provider whose get_frame_at_index calls SBFrame::IsValid on input frames.
+
+Used to trigger the deadlock scenario described in
+TestRunLockReentrantDeadlock.py: when a client thread accesses frames
+through this provider, get_frame_at_index re-enters the SB API and tries
+to re-acquire the ProcessRunLock read lock, which (without the recursion
+fix in ProcessRunLocker) deadlocks against the override PST's pending
+writer.
+"""
+
+import lldb
+from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
+
+
+class SBAPIAccessInGetFrameProvider(ScriptedFrameProvider):
+ """Provider that calls SBFrame.IsValid from get_frame_at_index."""
+
+ @staticmethod
+ def get_description():
+ return "Provider that accesses SB API in get_frame_at_index"
+
+ def get_frame_at_index(self, idx):
+ if idx < len(self.input_frames):
+ frame = self.input_frames.GetFrameAtIndex(idx)
+ # This call triggers GetStoppedExecutionContext ->
+ # ProcessRunLock::ReadTryLock. If the current thread already
+ # holds the read lock (from the outer SB API entry point),
+ # and a writer is pending, this re-entrant read lock blocks.
+ frame.IsValid()
+ return idx
+ return None
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/main.c b/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_deadlock/main.c
new file mode 100644
index 0000000000000..9ebeaf3380ed9
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/runlock_reentrant_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;
+}
diff --git a/lldb/unittests/Host/CMakeLists.txt b/lldb/unittests/Host/CMakeLists.txt
index 73dfe13ee54ad..87a5ffcb6dbe1 100644
--- a/lldb/unittests/Host/CMakeLists.txt
+++ b/lldb/unittests/Host/CMakeLists.txt
@@ -10,6 +10,7 @@ set (FILES
NativeProcessProtocolTest.cpp
PipeTest.cpp
ProcessLaunchInfoTest.cpp
+ ProcessRunLockTest.cpp
SocketAddressTest.cpp
SocketTest.cpp
StripConPTYSequencesTest.cpp
diff --git a/lldb/unittests/Host/ProcessRunLockTest.cpp b/lldb/unittests/Host/ProcessRunLockTest.cpp
new file mode 100644
index 0000000000000..ace28a01d1129
--- /dev/null
+++ b/lldb/unittests/Host/ProcessRunLockTest.cpp
@@ -0,0 +1,113 @@
+//===-- ProcessRunLockTest.cpp --------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Host/ProcessRunLock.h"
+
+#include "gtest/gtest.h"
+
+#include <thread>
+#include <utility>
+
+using namespace lldb_private;
+
+TEST(ProcessRunLockTest, BasicLockUnlock) {
+ ProcessRunLock lock;
+ ProcessRunLock::ProcessRunLocker locker;
+ EXPECT_FALSE(locker.IsLocked());
+ EXPECT_TRUE(locker.TryLock(&lock));
+ EXPECT_TRUE(locker.IsLocked());
+ // Destructor releases.
+}
+
+TEST(ProcessRunLockTest, SameThreadRecursion) {
+ ProcessRunLock lock;
+ ProcessRunLock::ProcessRunLocker outer;
+ EXPECT_TRUE(outer.TryLock(&lock));
+
+ // Re-entrant acquisition on the same thread must succeed without
+ // deadlocking on the underlying rwlock.
+ ProcessRunLock::ProcessRunLocker inner;
+ EXPECT_TRUE(inner.TryLock(&lock));
+ EXPECT_TRUE(inner.IsLocked());
+}
+
+TEST(ProcessRunLockTest, ConcurrentReaders) {
+ ProcessRunLock lock;
+ ProcessRunLock::ProcessRunLocker outer;
+ EXPECT_TRUE(outer.TryLock(&lock));
+
+ // Another thread concurrently acquires the same lock — and is also
+ // able to recurse on its own. Both threads must get the same-thread
+ // recursion fast-path; the per-instance map distinguishes them.
+ bool other_outer_ok = false, other_inner_ok = false;
+ std::thread t([&] {
+ ProcessRunLock::ProcessRunLocker thread_outer;
+ other_outer_ok = thread_outer.TryLock(&lock);
+ ProcessRunLock::ProcessRunLocker thread_inner;
+ other_inner_ok = thread_inner.TryLock(&lock);
+ });
+ t.join();
+ EXPECT_TRUE(other_outer_ok);
+ EXPECT_TRUE(other_inner_ok);
+}
+
+TEST(ProcessRunLockTest, MoveUnlocked) {
+ // Moving an unlocked locker is always allowed.
+ ProcessRunLock::ProcessRunLocker a;
+ ProcessRunLock::ProcessRunLocker b(std::move(a));
+ EXPECT_FALSE(b.IsLocked());
+
+ ProcessRunLock::ProcessRunLocker c;
+ c = std::move(b);
+ EXPECT_FALSE(c.IsLocked());
+}
+
+TEST(ProcessRunLockTest, MoveLockedSameThread) {
+ // Moving a held locker on the owning thread is allowed.
+ ProcessRunLock lock;
+ ProcessRunLock::ProcessRunLocker a;
+ EXPECT_TRUE(a.TryLock(&lock));
+ ProcessRunLock::ProcessRunLocker b(std::move(a));
+ EXPECT_TRUE(b.IsLocked());
+ EXPECT_FALSE(a.IsLocked());
+}
+
+TEST(ProcessRunLockTest, MoveUnlockedAcrossThreads) {
+ // Moving an unlocked locker between threads is allowed (no held state
+ // to be inconsistent about).
+ ProcessRunLock::ProcessRunLocker a;
+ std::thread t([locker = std::move(a)]() mutable {
+ EXPECT_FALSE(locker.IsLocked());
+ ProcessRunLock lock;
+ EXPECT_TRUE(locker.TryLock(&lock));
+ });
+ t.join();
+}
+
+#if GTEST_HAS_DEATH_TEST
+// Crossing threads with a held locker is a programming error: the
+// recursion bookkeeping is keyed by the acquiring thread, and POSIX
+// requires the same thread to call pthread_rwlock_unlock as called
+// pthread_rwlock_rdlock. The check fires from whichever path actually
+// runs on the destination thread -- the move ctor (if the move
+// happens cross-thread) or the destructor (if the move happens
+// same-thread but destruction runs on the destination thread, which
+// is the typical lambda-capture pattern).
+TEST(ProcessRunLockDeathTest, MoveLockedAcrossThreads) {
+ ProcessRunLock lock;
+ ProcessRunLock::ProcessRunLocker a;
+ ASSERT_TRUE(a.TryLock(&lock));
+ EXPECT_DEATH(
+ {
+ std::thread t([locker = std::move(a)]() mutable { (void)locker; });
+ t.join();
+ },
+ "ProcessRunLocker (moved|move-assigned|destroyed) (across threads|on a "
+ "different thread)");
+}
+#endif
More information about the lldb-commits
mailing list