[Lldb-commits] [lldb] [lldb][Windows] Tolerate OS plugins that replace real threads with virtual ones (PR #215574)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Tue Aug 11 09:53:13 PDT 2026
https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/215574
>From 6f07da9ab026d1002a4313f558feda0bb71361f2 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Tue, 11 Aug 2026 16:27:50 +0200
Subject: [PATCH 1/2] [lldb][Windows] Tolerate OS plugins that replace real
threads with virtual ones
---
.../Process/Windows/Common/ProcessWindows.cpp | 52 ++++++--
.../windows_plugin_threads/Makefile | 4 +
.../TestWindowsOSPluginThreads.py | 125 ++++++++++++++++++
.../windows_plugin_threads/main.cpp | 40 ++++++
.../operating_system.py | 29 ++++
5 files changed, 236 insertions(+), 14 deletions(-)
create mode 100644 lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/Makefile
create mode 100644 lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/TestWindowsOSPluginThreads.py
create mode 100644 lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/main.cpp
create mode 100644 lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/operating_system.py
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
index 561710ccec3c8..179f7ead21501 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
@@ -109,6 +109,24 @@ static bool ShouldUseLLDBServer() {
return LLDB_ENABLE_LIBXML2;
}
+/// Maps a real OS thread ID onto the user-visible thread that stands for it,
+/// or returns it unchanged.
+static lldb::tid_t GetUserThreadID(ThreadList &thread_list, lldb::tid_t tid) {
+ if (thread_list.FindThreadByID(tid, /*can_update=*/false))
+ return tid;
+
+ const uint32_t num_threads = thread_list.GetSize(/*can_update=*/false);
+ for (uint32_t i = 0; i < num_threads; ++i) {
+ ThreadSP thread = thread_list.GetThreadAtIndex(i, /*can_update=*/false);
+ if (!thread)
+ continue;
+ ThreadSP backing_thread = thread->GetBackingThread();
+ if (backing_thread && backing_thread->GetID() == tid)
+ return thread->GetID();
+ }
+ return tid;
+}
+
void ProcessWindows::Initialize() {
if (!ShouldUseLLDBServer()) {
PluginManager::RegisterPlugin(GetPluginNameStatic(),
@@ -177,12 +195,12 @@ Status ProcessWindows::DoDetach(bool keep_stopped) {
GetPrivateState());
LLDB_LOG(log, "resuming {0} threads for detach.",
- m_thread_list.GetSize());
+ m_thread_list_real.GetSize());
bool failed = false;
- for (uint32_t i = 0; i < m_thread_list.GetSize(); ++i) {
+ for (uint32_t i = 0; i < m_thread_list_real.GetSize(); ++i) {
auto thread = std::static_pointer_cast<TargetThreadWindows>(
- m_thread_list.GetThreadAtIndex(i));
+ m_thread_list_real.GetThreadAtIndex(i));
Status result = thread->DoResume();
if (result.Fail()) {
failed = true;
@@ -255,12 +273,12 @@ Status ProcessWindows::DoResume(RunDirection direction) {
m_session_data->m_debugger->GetProcess().GetProcessId(),
GetPrivateState());
- LLDB_LOG(log, "resuming {0} threads.", m_thread_list.GetSize());
+ LLDB_LOG(log, "resuming {0} threads.", m_thread_list_real.GetSize());
bool failed = false;
- for (uint32_t i = 0; i < m_thread_list.GetSize(); ++i) {
+ for (uint32_t i = 0; i < m_thread_list_real.GetSize(); ++i) {
auto thread = std::static_pointer_cast<TargetThreadWindows>(
- m_thread_list.GetThreadAtIndex(i));
+ m_thread_list_real.GetThreadAtIndex(i));
Status result = thread->DoResume();
if (result.Fail()) {
failed = true;
@@ -349,11 +367,17 @@ void ProcessWindows::RefreshStateAfterStop() {
}
StopInfoSP stop_info;
- m_thread_list.SetSelectedThreadByID(active_exception->GetThreadID());
+ m_thread_list.SetSelectedThreadByID(
+ GetUserThreadID(m_thread_list, active_exception->GetThreadID()));
ThreadSP stop_thread = m_thread_list.GetSelectedThread();
if (!stop_thread)
return;
+ // Hardware breakpoint slots live on the real thread's register context.
+ ThreadSP real_stop_thread = stop_thread;
+ if (ThreadSP backing_thread = stop_thread->GetBackingThread())
+ real_stop_thread = backing_thread;
+
RegisterContextSP register_context = stop_thread->GetRegisterContext();
uint64_t pc = register_context->GetPC();
@@ -366,7 +390,7 @@ void ProcessWindows::RefreshStateAfterStop() {
switch (active_exception->GetExceptionValue()) {
case EXCEPTION_SINGLE_STEP: {
auto *reg_ctx = static_cast<RegisterContextWindows *>(
- stop_thread->GetRegisterContext().get());
+ real_stop_thread->GetRegisterContext().get());
uint32_t slot_id = reg_ctx->GetTriggeredHardwareBreakpointSlotId();
if (slot_id != LLDB_INVALID_INDEX32) {
int id = m_watchpoint_ids[slot_id];
@@ -931,8 +955,8 @@ Status ProcessWindows::EnableWatchpoint(WatchpointSP wp_sp, bool notify) {
info.read = wp_sp->WatchpointRead();
info.write = wp_sp->WatchpointWrite() || wp_sp->WatchpointModify();
- for (unsigned i = 0U; i < m_thread_list.GetSize(); i++) {
- Thread *thread = m_thread_list.GetThreadAtIndex(i).get();
+ for (unsigned i = 0U; i < m_thread_list_real.GetSize(); i++) {
+ Thread *thread = m_thread_list_real.GetThreadAtIndex(i).get();
auto *reg_ctx = static_cast<RegisterContextWindows *>(
thread->GetRegisterContext().get());
if (!reg_ctx->AddHardwareBreakpoint(info.slot_id, info.address, info.size,
@@ -944,8 +968,8 @@ Status ProcessWindows::EnableWatchpoint(WatchpointSP wp_sp, bool notify) {
}
}
if (error.Fail()) {
- for (unsigned i = 0U; i < m_thread_list.GetSize(); i++) {
- Thread *thread = m_thread_list.GetThreadAtIndex(i).get();
+ for (unsigned i = 0U; i < m_thread_list_real.GetSize(); i++) {
+ Thread *thread = m_thread_list_real.GetThreadAtIndex(i).get();
auto *reg_ctx = static_cast<RegisterContextWindows *>(
thread->GetRegisterContext().get());
reg_ctx->RemoveHardwareBreakpoint(info.slot_id);
@@ -976,8 +1000,8 @@ Status ProcessWindows::DisableWatchpoint(WatchpointSP wp_sp, bool notify) {
return error;
}
- for (unsigned i = 0U; i < m_thread_list.GetSize(); i++) {
- Thread *thread = m_thread_list.GetThreadAtIndex(i).get();
+ for (unsigned i = 0U; i < m_thread_list_real.GetSize(); i++) {
+ Thread *thread = m_thread_list_real.GetThreadAtIndex(i).get();
auto *reg_ctx = static_cast<RegisterContextWindows *>(
thread->GetRegisterContext().get());
if (!reg_ctx->RemoveHardwareBreakpoint(it->second.slot_id)) {
diff --git a/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/Makefile b/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/Makefile
new file mode 100644
index 0000000000000..c46619c662348
--- /dev/null
+++ b/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/Makefile
@@ -0,0 +1,4 @@
+CXX_SOURCES := main.cpp
+ENABLE_THREADS := YES
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/TestWindowsOSPluginThreads.py b/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/TestWindowsOSPluginThreads.py
new file mode 100644
index 0000000000000..f1c243af94afa
--- /dev/null
+++ b/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/TestWindowsOSPluginThreads.py
@@ -0,0 +1,125 @@
+"""
+Test that ProcessWindows keeps working when an OS plugin populates the
+user-facing thread list with virtual threads.
+"""
+
+import os
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+import lldbsuite.test.lldbutil as lldbutil
+
+# The tid the OS plugin in this directory reports for its virtual thread.
+OS_TID = 0x111111111
+
+
+ at requireWindows
+ at skipIfWindowsAndLLDBServer
+class TestWindowsOSPluginThreads(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ def setUp(self):
+ TestBase.setUp(self)
+ self.source = lldb.SBFileSpec("main.cpp")
+
+ def stop_and_load_os_plugin(self, stop_regex, args=None):
+ """Run to stop_regex, load the OS plugin, and return (target, process).
+
+ Asserts the premise the plugin sets up: the real thread the process
+ stopped on is gone from the user-facing list, replaced by a virtual
+ thread that is not the list's first entry.
+ """
+ self.build()
+ launch_info = None
+ if args:
+ launch_info = lldb.SBLaunchInfo(args)
+ launch_info.SetWorkingDirectory(self.get_process_working_directory())
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, stop_regex, self.source, launch_info=launch_info
+ )
+
+ # These paths only exist in the in-process plugin. LLDB_USE_LLDB_SERVER
+ # is not the only thing that can select lldb-server, so check what we
+ # actually got rather than trusting the decorator.
+ if process.GetPluginName() != "windows":
+ self.skipTest("test covers the in-process Windows process plugin")
+
+ # main is core 0; the worker thread keeps a second real thread around.
+ self.assertGreaterEqual(process.GetNumThreads(), 2)
+ real_tid = thread.GetThreadID()
+ self.assertEqual(process.GetThreadAtIndex(0).GetThreadID(), real_tid)
+
+ self.runCmd(
+ "settings set target.process.python-os-plugin-path '%s'"
+ % os.path.join(self.getSourceDir(), "operating_system.py")
+ )
+
+ os_thread = process.GetThreadByID(OS_TID)
+ self.assertTrue(os_thread.IsValid(), "the OS plugin thread showed up")
+ self.assertFalse(
+ process.GetThreadByID(real_tid).IsValid(),
+ "the real thread we stopped on is no longer user-visible",
+ )
+ self.assertNotEqual(
+ process.GetThreadAtIndex(0).GetThreadID(),
+ OS_TID,
+ "the virtual thread is not the first thread in the list",
+ )
+ return target, process
+
+ def test_breakpoint_on_backed_thread(self):
+ """A breakpoint hit on a real thread is reported on the virtual thread
+ standing in for it, not on whichever thread happens to be first."""
+ target, process = self.stop_and_load_os_plugin("// Break here")
+
+ breakpoint = target.BreakpointCreateBySourceRegex(
+ "// Second stop here", self.source
+ )
+ self.assertEqual(breakpoint.GetNumLocations(), 1)
+
+ # Resuming has to walk the real threads: the virtual thread has no OS
+ # thread to resume.
+ process.Continue()
+ self.assertState(process.GetState(), lldb.eStateStopped)
+
+ stopped = lldbutil.get_threads_stopped_at_breakpoint(process, breakpoint)
+ self.assertEqual(len(stopped), 1, "exactly one thread hit the breakpoint")
+ self.assertEqual(stopped[0].GetThreadID(), OS_TID)
+
+ def test_watchpoint_on_backed_thread(self):
+ """A watchpoint is programmed into the real threads' debug registers and
+ its hit is reported on the virtual thread."""
+ target, process = self.stop_and_load_os_plugin("// Break here")
+
+ self.runCmd("watchpoint set variable g_watched")
+ self.assertEqual(target.GetNumWatchpoints(), 1)
+
+ process.Continue()
+ self.assertState(process.GetState(), lldb.eStateStopped)
+
+ thread = lldbutil.get_stopped_thread(process, lldb.eStopReasonWatchpoint)
+ self.assertIsNotNone(thread, "stopped for the watchpoint")
+ self.assertEqual(thread.GetThreadID(), OS_TID)
+
+ self.runCmd("watchpoint delete 1")
+ process.Continue()
+ self.assertState(process.GetState(), lldb.eStateExited)
+
+ def test_detach_with_virtual_threads(self):
+ """Detaching resumes the real threads rather than the virtual one, so the
+ inferior runs on afterwards."""
+ marker = self.getBuildArtifact("detached.marker")
+ if os.path.exists(marker):
+ os.remove(marker)
+
+ _, process = self.stop_and_load_os_plugin("// Break here", args=[marker])
+
+ self.assertSuccess(process.Detach())
+ self.assertState(process.GetState(), lldb.eStateDetached)
+
+ # main writes the marker just before returning. A thread left suspended
+ # by the detach never gets there.
+ while not os.path.exists(marker):
+ time.sleep(0.1)
+ self.assertTrue(os.path.exists(marker), "the inferior ran to completion")
diff --git a/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/main.cpp b/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/main.cpp
new file mode 100644
index 0000000000000..6fd02ccb384bd
--- /dev/null
+++ b/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/main.cpp
@@ -0,0 +1,40 @@
+// Two real threads: main, which the test stops on, and a worker that parks on a
+// mutex main holds so it stays in the thread list for the whole test. The OS
+// plugin in this directory hides main (core 0) behind a virtual thread, which
+// leaves the worker thread as the first entry of the user-facing thread list.
+
+#include <atomic>
+#include <cstdio>
+#include <mutex>
+#include <thread>
+
+int g_watched = 0;
+
+static std::mutex g_mutex;
+static std::atomic<bool> g_worker_started(false);
+
+static void worker() {
+ g_worker_started = true;
+ std::lock_guard<std::mutex> lock(g_mutex);
+}
+
+int main(int argc, char *argv[]) {
+ std::unique_lock<std::mutex> lock(g_mutex);
+ std::thread worker_thread(worker);
+ while (!g_worker_started)
+ std::this_thread::yield();
+
+ g_watched = 1; // Break here
+ g_watched = 2; // Second stop here
+
+ lock.unlock();
+ worker_thread.join();
+
+ if (argc > 1) {
+ if (FILE *marker = fopen(argv[1], "w")) {
+ fputs("done\n", marker);
+ fclose(marker);
+ }
+ }
+ return 0;
+}
diff --git a/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/operating_system.py b/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/operating_system.py
new file mode 100644
index 0000000000000..e9ddefa191b6a
--- /dev/null
+++ b/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/operating_system.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+
+
+class OperatingSystemPlugIn(object):
+ """OS plugin that hides the process's first real thread behind a virtual one."""
+
+ def __init__(self, process):
+ self.process = process
+
+ def create_thread(self, tid, context):
+ return None
+
+ def get_thread_info(self):
+ return [
+ {
+ "tid": 0x111111111,
+ "name": "virtual",
+ "queue": "queue",
+ "state": "stopped",
+ "stop_reason": "none",
+ "core": 0,
+ }
+ ]
+
+ def get_register_info(self):
+ return None
+
+ def get_register_data(self, tid):
+ return None
>From 3c5e05c036753f5f1671a0780965f8709cf890a1 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Tue, 11 Aug 2026 17:52:55 +0100
Subject: [PATCH 2/2] make test generic
---
.../Makefile | 0
.../TestOSPluginBackingThreadEvents.py} | 18 +++++++-----------
.../main.cpp | 0
.../operating_system.py | 0
4 files changed, 7 insertions(+), 11 deletions(-)
rename lldb/test/API/functionalities/plugins/python_os_plugin/{windows_plugin_threads => backing_thread_events}/Makefile (100%)
rename lldb/test/API/functionalities/plugins/python_os_plugin/{windows_plugin_threads/TestWindowsOSPluginThreads.py => backing_thread_events/TestOSPluginBackingThreadEvents.py} (89%)
rename lldb/test/API/functionalities/plugins/python_os_plugin/{windows_plugin_threads => backing_thread_events}/main.cpp (100%)
rename lldb/test/API/functionalities/plugins/python_os_plugin/{windows_plugin_threads => backing_thread_events}/operating_system.py (100%)
diff --git a/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/Makefile b/lldb/test/API/functionalities/plugins/python_os_plugin/backing_thread_events/Makefile
similarity index 100%
rename from lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/Makefile
rename to lldb/test/API/functionalities/plugins/python_os_plugin/backing_thread_events/Makefile
diff --git a/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/TestWindowsOSPluginThreads.py b/lldb/test/API/functionalities/plugins/python_os_plugin/backing_thread_events/TestOSPluginBackingThreadEvents.py
similarity index 89%
rename from lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/TestWindowsOSPluginThreads.py
rename to lldb/test/API/functionalities/plugins/python_os_plugin/backing_thread_events/TestOSPluginBackingThreadEvents.py
index f1c243af94afa..26cc40e64f4f1 100644
--- a/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/TestWindowsOSPluginThreads.py
+++ b/lldb/test/API/functionalities/plugins/python_os_plugin/backing_thread_events/TestOSPluginBackingThreadEvents.py
@@ -1,6 +1,8 @@
"""
-Test that ProcessWindows keeps working when an OS plugin populates the
-user-facing thread list with virtual threads.
+Test that a process plugin keeps correctly resuming, detaching, and
+reporting events for the real threads backing an OS plugin's virtual
+threads, wherever the OS plugin replaces a real thread in the user-facing
+thread list.
"""
import os
@@ -14,9 +16,8 @@
OS_TID = 0x111111111
- at requireWindows
- at skipIfWindowsAndLLDBServer
-class TestWindowsOSPluginThreads(TestBase):
+ at skipIfTargetDoesNotSupportThreads()
+class TestOSPluginBackingThreadEvents(TestBase):
NO_DEBUG_INFO_TESTCASE = True
def setUp(self):
@@ -39,12 +40,6 @@ def stop_and_load_os_plugin(self, stop_regex, args=None):
self, stop_regex, self.source, launch_info=launch_info
)
- # These paths only exist in the in-process plugin. LLDB_USE_LLDB_SERVER
- # is not the only thing that can select lldb-server, so check what we
- # actually got rather than trusting the decorator.
- if process.GetPluginName() != "windows":
- self.skipTest("test covers the in-process Windows process plugin")
-
# main is core 0; the worker thread keeps a second real thread around.
self.assertGreaterEqual(process.GetNumThreads(), 2)
real_tid = thread.GetThreadID()
@@ -115,6 +110,7 @@ def test_detach_with_virtual_threads(self):
_, process = self.stop_and_load_os_plugin("// Break here", args=[marker])
+ self.assertFalse(os.path.exists(marker), "marker was not yet created")
self.assertSuccess(process.Detach())
self.assertState(process.GetState(), lldb.eStateDetached)
diff --git a/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/main.cpp b/lldb/test/API/functionalities/plugins/python_os_plugin/backing_thread_events/main.cpp
similarity index 100%
rename from lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/main.cpp
rename to lldb/test/API/functionalities/plugins/python_os_plugin/backing_thread_events/main.cpp
diff --git a/lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/operating_system.py b/lldb/test/API/functionalities/plugins/python_os_plugin/backing_thread_events/operating_system.py
similarity index 100%
rename from lldb/test/API/functionalities/plugins/python_os_plugin/windows_plugin_threads/operating_system.py
rename to lldb/test/API/functionalities/plugins/python_os_plugin/backing_thread_events/operating_system.py
More information about the lldb-commits
mailing list