From 10867ba55203d76d43fc30ec6d7cb2cf750079cc Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Sun, 21 Sep 2025 17:48:06 -0700 Subject: [PATCH 1/8] Add test for resetting apartment state --- .../System.Threading.Thread/tests/ThreadTests.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs index 908d690e7e207d..687d5acd7ee9f3 100644 --- a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs +++ b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs @@ -247,8 +247,20 @@ public static void GetSetApartmentStateTest_ChangeAfterThreadStarted_Windows( Assert.Equal(ApartmentState.MTA, getApartmentState(t)); Assert.Equal(0, setApartmentState(t, ApartmentState.MTA)); Assert.Equal(ApartmentState.MTA, getApartmentState(t)); - Assert.Equal(setType == 0 ? 0 : 2, setApartmentState(t, ApartmentState.STA)); // cannot be changed after thread is started + Assert.Equal(setType == 0 ? 0 : 2, setApartmentState(t, ApartmentState.STA)); // MTA<->STA cannot be changed directly after thread is started Assert.Equal(ApartmentState.MTA, getApartmentState(t)); + + if (!PlatformDetection.IsWindowsNanoServer) + { + Assert.Equal(0, setApartmentState(t, ApartmentState.Unknown)); // Compat quirk: MTA<->STA can be changed by going throught Unknown + Assert.Equal(ApartmentState.MTA, getApartmentState(t)); + Assert.Equal(0, setApartmentState(t, ApartmentState.STA)); + Assert.Equal(ApartmentState.STA, getApartmentState(t)); + Assert.Equal(0, setApartmentState(t, ApartmentState.Unknown)); + Assert.Equal(ApartmentState.MTA, getApartmentState(t)); + Assert.Equal(0, setApartmentState(t, ApartmentState.MTA)); + Assert.Equal(ApartmentState.MTA, getApartmentState(t)); + } }); } From 6be2f901df21f5fda1887f73dcdff855b34ca431 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Mon, 22 Sep 2025 06:23:51 -0700 Subject: [PATCH 2/8] Fix --- .../Threading/Thread.NativeAot.Windows.cs | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index 37d6d14ab60f4e..d3c6ee524978bf 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -15,7 +15,7 @@ namespace System.Threading public sealed partial class Thread { [ThreadStatic] - private static ApartmentType t_apartmentType; + private static sbyte t_apartmentState; // ApartmentState shifted by ApartmentState.Unknonw to represent Unknown as the default value [ThreadStatic] private static ComState t_comState; @@ -235,14 +235,15 @@ public ApartmentState GetApartmentState() return _initialApartmentState; } - switch (GetCurrentApartmentType()) + switch (GetCurrentApartmentState()) { - case ApartmentType.STA: + case ApartmentState.STA: return ApartmentState.STA; - case ApartmentType.MTA: + case ApartmentState.MTA: return ApartmentState.MTA; default: - return ApartmentState.Unknown; + // If COM is uninitialized on the current thread, it is assumed to be implicit MTA. + return ApartmentState.MTA; } } @@ -275,14 +276,15 @@ private bool SetApartmentStateUnchecked(ApartmentState state, bool throwOnError) } else { + // Compat: Setting ApartmentState to Unknown uninitializes COM UninitializeCom(); } } // Clear the cache and check whether new state matches the desired state - t_apartmentType = ApartmentType.Unknown; + t_apartmentState = 0; - retState = GetApartmentState(); + retState = GetCurrentApartmentState(); } if (retState != state) @@ -396,24 +398,25 @@ internal static Thread EnsureThreadPoolThreadInitialized() public void Interrupt() { throw new PlatformNotSupportedException(); } internal static bool ReentrantWaitsEnabled => - GetCurrentApartmentType() == ApartmentType.STA; + GetCurrentApartmentState() == ApartmentState.STA; - internal static ApartmentType GetCurrentApartmentType() + internal static ApartmentState GetCurrentApartmentState() { - ApartmentType currentThreadType = t_apartmentType; - if (currentThreadType != ApartmentType.Unknown) - return currentThreadType; + sbyte current = t_apartmentState; + if (current != 0) + return (ApartmentState)(current + (sbyte)ApartmentState.Unknown); Interop.APTTYPE aptType; Interop.APTTYPEQUALIFIER aptTypeQualifier; int result = Interop.Ole32.CoGetApartmentType(out aptType, out aptTypeQualifier); - ApartmentType type = ApartmentType.Unknown; + ApartmentState state = ApartmentState.Unknown; switch (result) { case HResults.CO_E_NOTINITIALIZED: - type = ApartmentType.None; + Debug.Fail("COM is not initialized"); + state = ApartmentState.Unknown; break; case HResults.S_OK: @@ -421,24 +424,27 @@ internal static ApartmentType GetCurrentApartmentType() { case Interop.APTTYPE.APTTYPE_STA: case Interop.APTTYPE.APTTYPE_MAINSTA: - type = ApartmentType.STA; + state = ApartmentState.STA; break; case Interop.APTTYPE.APTTYPE_MTA: - type = ApartmentType.MTA; + state = ApartmentState.MTA; break; case Interop.APTTYPE.APTTYPE_NA: switch (aptTypeQualifier) { case Interop.APTTYPEQUALIFIER.APTTYPEQUALIFIER_NA_ON_MTA: + state = ApartmentState.MTA; + break; + case Interop.APTTYPEQUALIFIER.APTTYPEQUALIFIER_NA_ON_IMPLICIT_MTA: - type = ApartmentType.MTA; + state = ApartmentState.Unknown; break; case Interop.APTTYPEQUALIFIER.APTTYPEQUALIFIER_NA_ON_STA: case Interop.APTTYPEQUALIFIER.APTTYPEQUALIFIER_NA_ON_MAINSTA: - type = ApartmentType.STA; + state = ApartmentState.STA; break; default: @@ -450,21 +456,13 @@ internal static ApartmentType GetCurrentApartmentType() break; default: - Debug.Fail("bad return from CoGetApartmentType"); + Debug.Fail("bad return from CoGetApartmentState"); break; } - if (type != ApartmentType.Unknown) - t_apartmentType = type; - return type; - } - - internal enum ApartmentType : byte - { - Unknown = 0, - None, - STA, - MTA + if (state != ApartmentState.Unknown) + t_apartmentState = (sbyte)(state - ApartmentState.Unknown); + return state; } [Flags] From 26f1a2afde70edfbc65d609acbc721d808f9bef0 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Mon, 22 Sep 2025 11:45:27 -0700 Subject: [PATCH 3/8] Update src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs --- .../src/System/Threading/Thread.NativeAot.Windows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index d3c6ee524978bf..cac85353df6e6d 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -456,7 +456,7 @@ internal static ApartmentState GetCurrentApartmentState() break; default: - Debug.Fail("bad return from CoGetApartmentState"); + Debug.Fail("bad return from CoGetApartmentType"); break; } From 0d0b494b15b0b9dfea9a6a8eace21a293c2af7f1 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Mon, 22 Sep 2025 11:59:59 -0700 Subject: [PATCH 4/8] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/System/Threading/Thread.NativeAot.Windows.cs | 2 +- src/libraries/System.Threading.Thread/tests/ThreadTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index cac85353df6e6d..cd5f87692c9000 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -15,7 +15,7 @@ namespace System.Threading public sealed partial class Thread { [ThreadStatic] - private static sbyte t_apartmentState; // ApartmentState shifted by ApartmentState.Unknonw to represent Unknown as the default value + private static sbyte t_apartmentState; // ApartmentState shifted by ApartmentState.Unknown to represent Unknown as the default value [ThreadStatic] private static ComState t_comState; diff --git a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs index 687d5acd7ee9f3..f0cfa6591a0571 100644 --- a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs +++ b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs @@ -252,7 +252,7 @@ public static void GetSetApartmentStateTest_ChangeAfterThreadStarted_Windows( if (!PlatformDetection.IsWindowsNanoServer) { - Assert.Equal(0, setApartmentState(t, ApartmentState.Unknown)); // Compat quirk: MTA<->STA can be changed by going throught Unknown + Assert.Equal(0, setApartmentState(t, ApartmentState.Unknown)); // Compat quirk: MTA<->STA can be changed by going through Unknown Assert.Equal(ApartmentState.MTA, getApartmentState(t)); Assert.Equal(0, setApartmentState(t, ApartmentState.STA)); Assert.Equal(ApartmentState.STA, getApartmentState(t)); From 8f7a0400d90df5128fa1976531e7c701a3f7ca70 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Mon, 22 Sep 2025 12:01:35 -0700 Subject: [PATCH 5/8] Update src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs --- .../src/System/Threading/Thread.NativeAot.Windows.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index cd5f87692c9000..cbd44dbdc51493 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -400,6 +400,7 @@ internal static Thread EnsureThreadPoolThreadInitialized() internal static bool ReentrantWaitsEnabled => GetCurrentApartmentState() == ApartmentState.STA; + // Unlike the public API, this returns ApartmentState.Unknown when the COM is uninitialized on current thread internal static ApartmentState GetCurrentApartmentState() { sbyte current = t_apartmentState; From 150e9acc1923d0df47ac2b67d97edac11d3de265 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Mon, 22 Sep 2025 14:40:48 -0700 Subject: [PATCH 6/8] Reuse ComState enum --- .../Threading/Thread.NativeAot.Windows.cs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index cbd44dbdc51493..559a594619b19e 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -14,9 +14,6 @@ namespace System.Threading { public sealed partial class Thread { - [ThreadStatic] - private static sbyte t_apartmentState; // ApartmentState shifted by ApartmentState.Unknown to represent Unknown as the default value - [ThreadStatic] private static ComState t_comState; @@ -279,12 +276,17 @@ private bool SetApartmentStateUnchecked(ApartmentState state, bool throwOnError) // Compat: Setting ApartmentState to Unknown uninitializes COM UninitializeCom(); } - } - // Clear the cache and check whether new state matches the desired state - t_apartmentState = 0; + // Clear the cache and check whether new state matches the desired state + t_comState &= ~(ComState.STA | ComState.MTA); - retState = GetCurrentApartmentState(); + retState = GetCurrentApartmentState(); + } + else + { + Debug.Assert((t_comState & ComState.MTA) != 0); + retState = ApartmentState.MTA; + } } if (retState != state) @@ -318,7 +320,7 @@ private static void InitializeComForThreadPoolThread() // Process-wide COM is initialized very early before any managed code can run. // Assume it is done. // Prevent re-initialization of COM model on threadpool threads from the default one. - t_comState |= ComState.Locked; + t_comState |= ComState.Locked | ComState.MTA; } private static void InitializeCom(ApartmentState state = ApartmentState.MTA) @@ -403,9 +405,8 @@ internal static Thread EnsureThreadPoolThreadInitialized() // Unlike the public API, this returns ApartmentState.Unknown when the COM is uninitialized on current thread internal static ApartmentState GetCurrentApartmentState() { - sbyte current = t_apartmentState; - if (current != 0) - return (ApartmentState)(current + (sbyte)ApartmentState.Unknown); + if ((t_comState & (ComState.MTA | ComState.STA)) != 0) + return ((t_comState & ComState.STA) != 0) ? ApartmentState.STA : ApartmentState.MTA; Interop.APTTYPE aptType; Interop.APTTYPEQUALIFIER aptTypeQualifier; @@ -462,7 +463,7 @@ internal static ApartmentState GetCurrentApartmentState() } if (state != ApartmentState.Unknown) - t_apartmentState = (sbyte)(state - ApartmentState.Unknown); + t_comState |= (state == ApartmentState.STA) ? ComState.STA : ComState.MTA; return state; } @@ -471,6 +472,8 @@ internal enum ComState : byte { InitializedByUs = 1, Locked = 2, + MTA = 4, + STA = 8 } } } From c46bc5c036856aa3076eb4ff208932405af5d316 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Mon, 22 Sep 2025 14:41:51 -0700 Subject: [PATCH 7/8] Update src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs Co-authored-by: Aaron Robinson --- .../src/System/Threading/Thread.NativeAot.Windows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index 559a594619b19e..cee6c340c843f3 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -402,7 +402,7 @@ internal static Thread EnsureThreadPoolThreadInitialized() internal static bool ReentrantWaitsEnabled => GetCurrentApartmentState() == ApartmentState.STA; - // Unlike the public API, this returns ApartmentState.Unknown when the COM is uninitialized on current thread + // Unlike the public API, this returns ApartmentState.Unknown when COM is uninitialized on the current thread internal static ApartmentState GetCurrentApartmentState() { if ((t_comState & (ComState.MTA | ComState.STA)) != 0) From 25d78853993b4b3af9f8c8f6f9130419bd05506f Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Mon, 22 Sep 2025 22:04:15 -0700 Subject: [PATCH 8/8] Copy over yet another quirk from CoreCLR --- .../src/System/Threading/Thread.NativeAot.Windows.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index cee6c340c843f3..9933c6309fb43d 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -289,6 +289,15 @@ private bool SetApartmentStateUnchecked(ApartmentState state, bool throwOnError) } } + // Special case where we pass in Unknown and get back MTA. + // Once we CoUninitialize the thread, the OS will still + // report the thread as implicitly in the MTA if any + // other thread in the process is CoInitialized. + if ((state == ApartmentState.Unknown) && (retState == ApartmentState.MTA)) + { + return true; + } + if (retState != state) { if (throwOnError)