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
