[Lldb-commits] [lldb] d426dcf - [lldb] Fix a statusline deadlock when flushing the process (#231719)
via lldb-commits
lldb-commits at lists.llvm.org
Thu Oct 15 07:54:27 PDT 2026
Author: Jonas Devlieghere
Date: 2026-10-15T07:54:20-07:00
New Revision: d426dcf7262d7edbdb76ede83429187949ab8ec7
URL: https://github.com/llvm/llvm-project/commit/d426dcf7262d7edbdb76ede83429187949ab8ec7
DIFF: https://github.com/llvm/llvm-project/commit/d426dcf7262d7edbdb76ede83429187949ab8ec7.diff
LOG: [lldb] Fix a statusline deadlock when flushing the process (#231719)
Since #188377, Process::Flush calls Debugger::FlushStatusLine, which
takes the statusline mutex. Process::Flush may run under the target API
mutex, while a statusline redraw holds the statusline mutex and then
waits on the API mutex, resulting in a deadlock.
For example, this happens when a scripted command runs "target modules
load" through SBDebugger::HandleCommand while a progress event redraws
the statusline.
Make FlushStatusLine set an atomic flag instead, and have the next
redraw clear the cached frame under the statusline mutex. This keeps the
stale frame protection without the risk of lock inversion.
rdar://189958732
Added:
lldb/test/API/functionalities/statusline/statusline_flush.py
Modified:
lldb/include/lldb/Core/Debugger.h
lldb/source/Core/Debugger.cpp
lldb/test/API/functionalities/statusline/TestStatusline.py
Removed:
################################################################################
diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index bcc14479c3360d..ca0374834cb2b3 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -9,6 +9,7 @@
#ifndef LLDB_CORE_DEBUGGER_H
#define LLDB_CORE_DEBUGGER_H
+#include <atomic>
#include <cstdint>
#include <memory>
@@ -479,7 +480,9 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
/// output is an escape-code-capable terminal.
bool StatuslineSupported();
- /// Flush cached state (e.g. stale execution context in the statusline).
+ /// Discard the statusline's cached frame at its next redraw. This function
+ /// is thread-safe and takes no lock, so callers may hold the target API
+ /// mutex.
void FlushStatusLine();
/// This is the correct way to query the state of Interruption.
@@ -815,6 +818,8 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
/// Mutex protecting the m_statusline member.
std::mutex m_statusline_mutex;
std::optional<Statusline> m_statusline;
+ /// Raised by FlushStatusLine to request a flush at the next redraw.
+ std::atomic<bool> m_flush_statusline = false;
llvm::StringMap<std::weak_ptr<LogHandler>> m_stream_handlers;
std::shared_ptr<CallbackLogHandler> m_callback_handler_sp;
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index db55a0d416b279..ad89ce4c91dd47 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -1319,20 +1319,16 @@ void Debugger::RedrawStatusline(
std::optional<ExecutionContextRef> exe_ctx_ref) {
std::lock_guard<std::mutex> guard(m_statusline_mutex);
+ const bool flush = m_flush_statusline.exchange(false);
if (!m_statusline)
return;
+ if (flush)
+ m_statusline->ClearExecutionContext();
m_statusline->Redraw(exe_ctx_ref);
}
-void Debugger::FlushStatusLine() {
- std::lock_guard<std::mutex> guard(m_statusline_mutex);
-
- if (!m_statusline)
- return;
-
- m_statusline->ClearExecutionContext();
-}
+void Debugger::FlushStatusLine() { m_flush_statusline = true; }
ExecutionContext
Debugger::GetSelectedExecutionContext(bool adopt_dummy_target) {
diff --git a/lldb/test/API/functionalities/statusline/TestStatusline.py b/lldb/test/API/functionalities/statusline/TestStatusline.py
index a035b5e20cd565..2050e998326857 100644
--- a/lldb/test/API/functionalities/statusline/TestStatusline.py
+++ b/lldb/test/API/functionalities/statusline/TestStatusline.py
@@ -235,6 +235,23 @@ def test_resize_height_shrink_makes_room(self):
# Room is made by scrolling up: a newline followed by a cursor-up.
self.assertIn(b"\n\x1b[1A", tee.data)
+ def test_flush_deadlock(self):
+ """Regression test for lock inversion between the statusline mutex and
+ the target API mutex."""
+ self.build()
+ self.launch()
+ self.do_setup()
+ self.expect('set set separator "| "')
+ self.expect(
+ "set set show-statusline true", ["a.out | main.c:2:11 | breakpoint 1.1"]
+ )
+
+ flush = self.getSourcePath("statusline_flush.py")
+ self.expect('command script import "{}"'.format(flush))
+ self.expect("statusline_flush 0x1000")
+ # The flush drops the cached frame, and with it the location.
+ self.child.expect_exact("a.out | breakpoint 1.1")
+
@skipUnlessPlatform(["linux"])
def test_target_symbols_add(self):
"""Regression test: adding symbols while the statusline is enabled
diff --git a/lldb/test/API/functionalities/statusline/statusline_flush.py b/lldb/test/API/functionalities/statusline/statusline_flush.py
new file mode 100644
index 00000000000000..c4fdba1649c92d
--- /dev/null
+++ b/lldb/test/API/functionalities/statusline/statusline_flush.py
@@ -0,0 +1,26 @@
+"""Helper for TestStatusline.test_flush_deadlock.
+
+Flushes the process while the event thread redraws the statusline and waits on
+the target API mutex.
+"""
+
+import time
+
+import lldb
+
+
+ at lldb.command("statusline_flush")
+def statusline_flush(debugger, command, result, internal_dict):
+ # SBDebugger.HandleCommand holds the target API mutex for the duration of
+ # the nested command.
+ debugger.HandleCommand("statusline_flush_locked " + command)
+
+
+ at lldb.command("statusline_flush_locked")
+def statusline_flush_locked(debugger, command, result, internal_dict):
+ # The progress event makes the event thread redraw the statusline, which
+ # waits on the target API mutex held by the caller.
+ lldb.SBProgress("flush", "redraw", debugger).Finalize()
+ time.sleep(0.5)
+ # Changing a load address flushes the process.
+ debugger.HandleCommand("target modules load -f a.out -s " + command)
More information about the lldb-commits
mailing list