From cbf2e04150a79b6e4b1f36ed4ad765047f59cace Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:36:04 -0400 Subject: [PATCH 1/3] Look up TEB from OS thread ID instead of DacpThreadData.teb The !Threads command was reading the COM apartment state (STA/MTA/NTA) by using the TEB address from DacpThreadData.teb. This couples the SOS command to a runtime-internal field that is being removed from the DAC/cDAC Thread data contract. Instead, look up the TEB via the debugger services API (IDebuggerServices::GetThreadTeb) using the OS thread ID that is already available in DacpThreadData.osThreadId. This is the same mechanism SOS uses elsewhere for TEB access and does not depend on the DAC carrying the TEB pointer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/SOS/Strike/strike.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/SOS/Strike/strike.cpp b/src/SOS/Strike/strike.cpp index 2309e8173d..09d8e885e7 100644 --- a/src/SOS/Strike/strike.cpp +++ b/src/SOS/Strike/strike.cpp @@ -4456,8 +4456,12 @@ HRESULT PrintThreadsFromThreadStore(BOOL bMiniDump, BOOL bPrintLiveThreadsOnly) // Apartment state #ifndef FEATURE_PAL DWORD_PTR OleTlsDataAddr; + ULONG64 teb = 0; + IDebuggerServices* debuggerServices = GetDebuggerServices(); if (IsWindowsTarget() && !bSwitchedOutFiber - && SafeReadMemory(TO_TADDR(Thread.teb + offsetof(TEB, ReservedForOle)), + && debuggerServices != nullptr + && SUCCEEDED(debuggerServices->GetThreadTeb(Thread.osThreadId, &teb)) && teb != 0 + && SafeReadMemory(TO_TADDR(teb + offsetof(TEB, ReservedForOle)), &OleTlsDataAddr, sizeof(OleTlsDataAddr), NULL) && OleTlsDataAddr != 0) { From 74a0b0174d514c36896a79d54e3b23194bc809ba Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:27:03 -0400 Subject: [PATCH 2/3] Add ThreadApartment test for !Threads COM apartment state display Add a new SOS integration test that validates the !Threads (clrthreads) command properly displays COM apartment state (STA/MTA) in the Apt column. The test creates a debuggee with explicit STA and MTA threads using Thread.SetApartmentState before Thread.Start, then verifies clrthreads output contains both STA and MTA values. This test is Windows-only (guarded in both the test method and script) since COM apartment state is a Windows concept. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ThreadApartment/ThreadApartment.cs | 50 +++++++++++++++++++ .../ThreadApartment/ThreadApartment.csproj | 9 ++++ src/tests/SOS.UnitTests/SOS.cs | 11 ++++ .../Scripts/ThreadApartment.script | 16 ++++++ 4 files changed, 86 insertions(+) create mode 100644 src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.cs create mode 100644 src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.csproj create mode 100644 src/tests/SOS.UnitTests/Scripts/ThreadApartment.script diff --git a/src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.cs b/src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.cs new file mode 100644 index 0000000000..c8dff61694 --- /dev/null +++ b/src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Threading; + +internal sealed class ThreadApartment +{ + private static readonly ManualResetEventSlim s_staReady = new(); + private static readonly ManualResetEventSlim s_mtaReady = new(); + + private static void Main() + { + // Create an STA thread using SetApartmentState before Start. + // The runtime will call CoInitializeEx(COINIT_APARTMENTTHREADED) when the thread starts. + Thread staThread = new Thread(() => + { + s_staReady.Set(); + Thread.Sleep(Timeout.Infinite); + }); + staThread.SetApartmentState(ApartmentState.STA); + staThread.IsBackground = true; + staThread.Start(); + + // Create an MTA thread using SetApartmentState before Start. + Thread mtaThread = new Thread(() => + { + s_mtaReady.Set(); + Thread.Sleep(Timeout.Infinite); + }); + mtaThread.SetApartmentState(ApartmentState.MTA); + mtaThread.IsBackground = true; + mtaThread.Start(); + + s_staReady.Wait(); + s_mtaReady.Wait(); + + // If a debugger is attached, break into it. Otherwise just wait. + if (Debugger.IsAttached) + { + Debugger.Break(); + } + else + { + Console.WriteLine("Ready for dump capture"); + Thread.Sleep(Timeout.Infinite); + } + } +} diff --git a/src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.csproj b/src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.csproj new file mode 100644 index 0000000000..5438f6aa45 --- /dev/null +++ b/src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.csproj @@ -0,0 +1,9 @@ + + + Exe + $(BuildProjectFramework) + $(SupportedSubProcessTargetFrameworks) + + $(NoWarn);CA1416 + + diff --git a/src/tests/SOS.UnitTests/SOS.cs b/src/tests/SOS.UnitTests/SOS.cs index 59de9f14ec..88350a0b00 100644 --- a/src/tests/SOS.UnitTests/SOS.cs +++ b/src/tests/SOS.UnitTests/SOS.cs @@ -463,6 +463,17 @@ public async Task SimpleThrow(TestConfiguration config) await SOSTestHelpers.RunTest(config, debuggeeName: "SimpleThrow", scriptName: "SimpleThrow.script", Output, testTriage: true); } + [SkippableTheory, MemberData(nameof(Configurations))] + public async Task ThreadApartment(TestConfiguration config) + { + if (OS.Kind != OSKind.Windows) + { + throw new SkipTestException("Apartment state is a Windows COM concept"); + } + + await SOSTestHelpers.RunTest(config, debuggeeName: "ThreadApartment", scriptName: "ThreadApartment.script", Output); + } + [SkippableTheory, MemberData(nameof(Configurations))] public async Task AsyncMain(TestConfiguration config) { diff --git a/src/tests/SOS.UnitTests/Scripts/ThreadApartment.script b/src/tests/SOS.UnitTests/Scripts/ThreadApartment.script new file mode 100644 index 0000000000..0fc258c478 --- /dev/null +++ b/src/tests/SOS.UnitTests/Scripts/ThreadApartment.script @@ -0,0 +1,16 @@ +# +# Tests that clrthreads properly displays COM apartment state (STA/MTA). +# This test is Windows-only since apartment state is a Windows COM concept. +# + +LOADSOS + +# Verify that clrthreads shows the apartment state column with STA and MTA values. +# The ThreadApartment debuggee creates one STA and one MTA thread before breaking. +IFDEF:WINDOWS +SOSCOMMAND:clrthreads +VERIFY:\s*ThreadCount:\s+\s+ +VERIFY:\s+ID\s+OSID\s+ThreadOBJ\s+State.*Apt.* +VERIFY:.*\bSTA\b.* +VERIFY:.*\bMTA\b.* +ENDIF:WINDOWS From 2539add3e6219499bb63a7ba6cb0770f186c65da Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:48:05 -0400 Subject: [PATCH 3/3] fix test script --- src/SOS/Strike/strike.cpp | 18 ++++++++++++------ .../ThreadApartment/ThreadApartment.cs | 13 +++---------- .../Scripts/ThreadApartment.script | 2 ++ 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/SOS/Strike/strike.cpp b/src/SOS/Strike/strike.cpp index 09d8e885e7..40ad2d5fa9 100644 --- a/src/SOS/Strike/strike.cpp +++ b/src/SOS/Strike/strike.cpp @@ -4408,10 +4408,9 @@ HRESULT PrintThreadsFromThreadStore(BOOL bMiniDump, BOOL bPrintLiveThreadsOnly) } BOOL bSwitchedOutFiber = Thread.osThreadId == SWITCHED_OUT_FIBER_OSID; + ULONG id = 0; if (!IsKernelDebugger()) { - ULONG id = 0; - if (bSwitchedOutFiber) { table.WriteColumn(0, "<<<< "); @@ -4457,10 +4456,17 @@ HRESULT PrintThreadsFromThreadStore(BOOL bMiniDump, BOOL bPrintLiveThreadsOnly) #ifndef FEATURE_PAL DWORD_PTR OleTlsDataAddr; ULONG64 teb = 0; - IDebuggerServices* debuggerServices = GetDebuggerServices(); - if (IsWindowsTarget() && !bSwitchedOutFiber - && debuggerServices != nullptr - && SUCCEEDED(debuggerServices->GetThreadTeb(Thread.osThreadId, &teb)) && teb != 0 + if (IsWindowsTarget() && !bSwitchedOutFiber && id != 0) + { + ULONG curId; + if (SUCCEEDED(g_ExtSystem->GetCurrentThreadId(&curId)) && + SUCCEEDED(g_ExtSystem->SetCurrentThreadId(id))) + { + g_ExtSystem->GetCurrentThreadTeb(&teb); + g_ExtSystem->SetCurrentThreadId(curId); + } + } + if (teb != 0 && SafeReadMemory(TO_TADDR(teb + offsetof(TEB, ReservedForOle)), &OleTlsDataAddr, sizeof(OleTlsDataAddr), NULL) && OleTlsDataAddr != 0) diff --git a/src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.cs b/src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.cs index c8dff61694..7e1045aceb 100644 --- a/src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.cs +++ b/src/tests/SOS.UnitTests/Debuggees/ThreadApartment/ThreadApartment.cs @@ -36,15 +36,8 @@ private static void Main() s_staReady.Wait(); s_mtaReady.Wait(); - // If a debugger is attached, break into it. Otherwise just wait. - if (Debugger.IsAttached) - { - Debugger.Break(); - } - else - { - Console.WriteLine("Ready for dump capture"); - Thread.Sleep(Timeout.Infinite); - } + Debugger.Break(); + + throw new Exception("ThreadApartment test complete"); } } diff --git a/src/tests/SOS.UnitTests/Scripts/ThreadApartment.script b/src/tests/SOS.UnitTests/Scripts/ThreadApartment.script index 0fc258c478..9fdf82186d 100644 --- a/src/tests/SOS.UnitTests/Scripts/ThreadApartment.script +++ b/src/tests/SOS.UnitTests/Scripts/ThreadApartment.script @@ -3,6 +3,8 @@ # This test is Windows-only since apartment state is a Windows COM concept. # +CONTINUE + LOADSOS # Verify that clrthreads shows the apartment state column with STA and MTA values.