https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/201685
>From d6d5021f735536eeec39876dc85a70d1d95364b4 Mon Sep 17 00:00:00 2001 From: Med Ismail Bennani <[email protected]> Date: Fri, 5 Jun 2026 18:24:26 -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 <[email protected]> --- 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 | 109 +++++++++++++++ 9 files changed, 517 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..91234dabd3563 --- /dev/null +++ b/lldb/unittests/Host/ProcessRunLockTest.cpp @@ -0,0 +1,109 @@ +//===-- 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 move ctor and the destructor both fail +// fatally in this case. +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 across threads"); +} +#endif _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
