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 <[email protected]>
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
+
+
+@requireWindows
+@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 <[email protected]>
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
 
 
-@requireWindows
-@skipIfWindowsAndLLDBServer
-class TestWindowsOSPluginThreads(TestBase):
+@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

_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to