From ffa6bd47ea548c98bc087a4c0d46bfd6fbf796ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 03:00:55 +0000 Subject: [PATCH 1/8] Initial plan From 594946e3b6742a99c46bf5489f30ce6dbd50af6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 03:33:49 +0000 Subject: [PATCH 2/8] Add NativeAOT GC callout reverse P/Invoke regression smoke test Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com> --- .../GcRestrictedCalloutReversePInvoke.csproj | 22 +++ .../Program.cs | 136 ++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj create mode 100644 src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/Program.cs diff --git a/src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj b/src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj new file mode 100644 index 00000000000000..644a9732cf18aa --- /dev/null +++ b/src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj @@ -0,0 +1,22 @@ + + + Exe + 0 + true + true + false + true + true + + true + true + + + + + + + + + + diff --git a/src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/Program.cs b/src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/Program.cs new file mode 100644 index 00000000000000..e7a7902ec4d2fa --- /dev/null +++ b/src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/Program.cs @@ -0,0 +1,136 @@ +// 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.Collections; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; + +ComWrappers.RegisterForTrackerSupport(TrackerComWrappers.Instance); + +IntPtr trackerObject = MockReferenceTrackerRuntime.CreateTrackerObject(); +object wrapper = TrackerComWrappers.Instance.GetOrCreateObjectForComInstance(trackerObject, CreateObjectFlags.TrackerObject); +Marshal.Release(trackerObject); + +// dotnet/runtime#110683 +// Before the runtime fix, assertion-enabled NativeAOT runtimes can fail with +// ASSERT(ThreadStore::IsTrapThreadsRequested()) when this managed GC restricted +// callout is reverse-invoked on a background GC thread. +ForceBackgroundGen2Collection(timeout: TimeSpan.FromSeconds(30)); +GC.KeepAlive(wrapper); + +return 100; + +static void ForceBackgroundGen2Collection(TimeSpan timeout) +{ + int initialGen2Collections = GC.CollectionCount(2); + using AllocationPressure pressure = new(); + pressure.Start(); + + Stopwatch stopwatch = Stopwatch.StartNew(); + bool observedConcurrentGen2 = false; + + while (stopwatch.Elapsed < timeout) + { + int previousGen2Collections = GC.CollectionCount(2); + GC.Collect(2, GCCollectionMode.Forced, blocking: false, compacting: false); + + if (!SpinWait.SpinUntil(() => GC.CollectionCount(2) > previousGen2Collections, TimeSpan.FromMilliseconds(500))) + { + continue; + } + + GCMemoryInfo memoryInfo = GC.GetGCMemoryInfo(); + if (memoryInfo.Generation == 2 && memoryInfo.Concurrent) + { + observedConcurrentGen2 = true; + break; + } + } + + pressure.Stop(); + + if (!observedConcurrentGen2) + { + throw new Exception($"Timed out after {timeout} waiting for a concurrent Gen2 GC. Initial count: {initialGen2Collections}, final count: {GC.CollectionCount(2)}."); + } +} + +sealed class AllocationPressure : IDisposable +{ + private readonly ManualResetEventSlim _stopSignal = new(initialState: false); + private readonly Thread _allocatorThread; + + public AllocationPressure() + { + _allocatorThread = new Thread(AllocateUntilStopped) + { + IsBackground = true, + Name = "GcRestrictedCalloutReversePInvoke_AllocationPressure" + }; + } + + public void Start() => _allocatorThread.Start(); + + public void Stop() + { + _stopSignal.Set(); + if (!_allocatorThread.Join(millisecondsTimeout: 5000)) + { + throw new Exception("Allocation pressure thread did not stop in time."); + } + } + + private void AllocateUntilStopped() + { + byte[][] ring = new byte[64][]; + int index = 0; + + while (!_stopSignal.IsSet) + { + ring[index] = new byte[256 * 1024]; + index = (index + 1) % ring.Length; + } + } + + public void Dispose() + { + Stop(); + _stopSignal.Dispose(); + } +} + +sealed class TrackerComWrappers : ComWrappers +{ + public static TrackerComWrappers Instance { get; } = new(); + + protected unsafe override ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count) + { + count = 0; + return null; + } + + protected override object CreateObject(IntPtr externalComObject, CreateObjectFlags flags) => new object(); + + protected override void ReleaseObjects(IEnumerable objects) + { + } +} + +static class MockReferenceTrackerRuntime +{ + [DllImport(nameof(MockReferenceTrackerRuntime), EntryPoint = "CreateTrackerObject_Unsafe")] + private static extern IntPtr CreateTrackerObjectUnsafe(IntPtr outer, out IntPtr inner); + + public static IntPtr CreateTrackerObject() + { + IntPtr result = CreateTrackerObjectUnsafe(IntPtr.Zero, out IntPtr inner); + if (inner != IntPtr.Zero) + { + Marshal.Release(inner); + } + + return result; + } +} From 6d8e5aee1cfc7c0886ff79f82e6f24a92bb6b14e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:24:06 +0000 Subject: [PATCH 3/8] Move GC restricted NativeAOT smoke test under ComWrappers Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com> --- .../GcRestrictedCalloutReversePInvoke.csproj | 2 +- .../ComWrappers}/GcRestrictedCalloutReversePInvoke/Program.cs | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/tests/{nativeaot/SmokeTests => Interop/COM/ComWrappers}/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj (88%) rename src/tests/{nativeaot/SmokeTests => Interop/COM/ComWrappers}/GcRestrictedCalloutReversePInvoke/Program.cs (100%) diff --git a/src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj similarity index 88% rename from src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj rename to src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj index 644a9732cf18aa..40a2412c88f790 100644 --- a/src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj +++ b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj @@ -17,6 +17,6 @@ - + diff --git a/src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/Program.cs b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs similarity index 100% rename from src/tests/nativeaot/SmokeTests/GcRestrictedCalloutReversePInvoke/Program.cs rename to src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs From 4413efc084d3f753f688fcb91ad2a79caee6025b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 07:40:26 +0000 Subject: [PATCH 4/8] Weaken NativeAOT reverse P/Invoke GC assertion Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com> --- src/coreclr/nativeaot/Runtime/thread.inl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/coreclr/nativeaot/Runtime/thread.inl b/src/coreclr/nativeaot/Runtime/thread.inl index 4345246a462283..332d531ae9c218 100644 --- a/src/coreclr/nativeaot/Runtime/thread.inl +++ b/src/coreclr/nativeaot/Runtime/thread.inl @@ -203,8 +203,7 @@ FORCEINLINE bool Thread::InlineTryFastReversePInvoke(ReversePInvokeFrame* pFrame // We will allow threads in DoNotTriggerGc mode to do reverse PInvoke regardless of their coop state. if (IsDoNotTriggerGcSet()) { - // We expect this scenario only when EE is stopped. - ASSERT(ThreadStore::IsTrapThreadsRequested()); + ASSERT(IsGCSpecial()); // no need to do anything return true; } From 920649edae11f29ec926558c4327080237f16539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Tue, 14 Jul 2026 21:53:25 -0700 Subject: [PATCH 5/8] Add assertion for DoNotTriggerGc threads --- src/coreclr/nativeaot/Runtime/thread.inl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/nativeaot/Runtime/thread.inl b/src/coreclr/nativeaot/Runtime/thread.inl index 332d531ae9c218..6c70800a4e899b 100644 --- a/src/coreclr/nativeaot/Runtime/thread.inl +++ b/src/coreclr/nativeaot/Runtime/thread.inl @@ -203,7 +203,8 @@ FORCEINLINE bool Thread::InlineTryFastReversePInvoke(ReversePInvokeFrame* pFrame // We will allow threads in DoNotTriggerGc mode to do reverse PInvoke regardless of their coop state. if (IsDoNotTriggerGcSet()) { - ASSERT(IsGCSpecial()); + // We expect this scenario only when EE is stopped. + ASSERT(ThreadStore::IsTrapThreadsRequested() || IsGCSpecial()); // no need to do anything return true; } From 975cfb976f96a9927f55a2bd7c85f4c811bed12f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:38:30 +0000 Subject: [PATCH 6/8] Reuse shared MockReferenceTrackerRuntime helper in ComWrappers regression test Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com> --- .../GcRestrictedCalloutReversePInvoke.csproj | 1 + .../Program.cs | 18 +----------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj index 40a2412c88f790..22dbf9f9d7c1f5 100644 --- a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj +++ b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj @@ -13,6 +13,7 @@ + diff --git a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs index e7a7902ec4d2fa..85e3a22c0d1817 100644 --- a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs +++ b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; +using ComWrappersTests.Common; ComWrappers.RegisterForTrackerSupport(TrackerComWrappers.Instance); @@ -117,20 +118,3 @@ protected override void ReleaseObjects(IEnumerable objects) { } } - -static class MockReferenceTrackerRuntime -{ - [DllImport(nameof(MockReferenceTrackerRuntime), EntryPoint = "CreateTrackerObject_Unsafe")] - private static extern IntPtr CreateTrackerObjectUnsafe(IntPtr outer, out IntPtr inner); - - public static IntPtr CreateTrackerObject() - { - IntPtr result = CreateTrackerObjectUnsafe(IntPtr.Zero, out IntPtr inner); - if (inner != IntPtr.Zero) - { - Marshal.Release(inner); - } - - return result; - } -} From c5af0453abc33148129915e0205d8db711a48d6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:28:22 +0000 Subject: [PATCH 7/8] Align ComWrappers regression test with xUnit pattern and temporarily restore original assert Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com> --- src/coreclr/nativeaot/Runtime/thread.inl | 2 +- .../GcRestrictedCalloutReversePInvoke.csproj | 2 - .../Program.cs | 82 ++++++++++--------- 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/src/coreclr/nativeaot/Runtime/thread.inl b/src/coreclr/nativeaot/Runtime/thread.inl index 6c70800a4e899b..4345246a462283 100644 --- a/src/coreclr/nativeaot/Runtime/thread.inl +++ b/src/coreclr/nativeaot/Runtime/thread.inl @@ -204,7 +204,7 @@ FORCEINLINE bool Thread::InlineTryFastReversePInvoke(ReversePInvokeFrame* pFrame if (IsDoNotTriggerGcSet()) { // We expect this scenario only when EE is stopped. - ASSERT(ThreadStore::IsTrapThreadsRequested() || IsGCSpecial()); + ASSERT(ThreadStore::IsTrapThreadsRequested()); // no need to do anything return true; } diff --git a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj index 22dbf9f9d7c1f5..86d1bdede5c46b 100644 --- a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj +++ b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj @@ -1,10 +1,8 @@ - Exe 0 true true - false true true diff --git a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs index 85e3a22c0d1817..d5a3263bc4e27c 100644 --- a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs +++ b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs @@ -7,54 +7,60 @@ using System.Runtime.InteropServices; using System.Threading; using ComWrappersTests.Common; +using Xunit; -ComWrappers.RegisterForTrackerSupport(TrackerComWrappers.Instance); - -IntPtr trackerObject = MockReferenceTrackerRuntime.CreateTrackerObject(); -object wrapper = TrackerComWrappers.Instance.GetOrCreateObjectForComInstance(trackerObject, CreateObjectFlags.TrackerObject); -Marshal.Release(trackerObject); - -// dotnet/runtime#110683 -// Before the runtime fix, assertion-enabled NativeAOT runtimes can fail with -// ASSERT(ThreadStore::IsTrapThreadsRequested()) when this managed GC restricted -// callout is reverse-invoked on a background GC thread. -ForceBackgroundGen2Collection(timeout: TimeSpan.FromSeconds(30)); -GC.KeepAlive(wrapper); - -return 100; - -static void ForceBackgroundGen2Collection(TimeSpan timeout) +public class Program { - int initialGen2Collections = GC.CollectionCount(2); - using AllocationPressure pressure = new(); - pressure.Start(); - - Stopwatch stopwatch = Stopwatch.StartNew(); - bool observedConcurrentGen2 = false; + [Fact] + public static void TestEntryPoint() + { + ComWrappers.RegisterForTrackerSupport(TrackerComWrappers.Instance); + + IntPtr trackerObject = MockReferenceTrackerRuntime.CreateTrackerObject(); + object wrapper = TrackerComWrappers.Instance.GetOrCreateObjectForComInstance(trackerObject, CreateObjectFlags.TrackerObject); + Marshal.Release(trackerObject); + + // dotnet/runtime#110683 + // Before the runtime fix, assertion-enabled NativeAOT runtimes can fail with + // ASSERT(ThreadStore::IsTrapThreadsRequested()) when this managed GC restricted + // callout is reverse-invoked on a background GC thread. + ForceBackgroundGen2Collection(timeout: TimeSpan.FromSeconds(30)); + GC.KeepAlive(wrapper); + } - while (stopwatch.Elapsed < timeout) + private static void ForceBackgroundGen2Collection(TimeSpan timeout) { - int previousGen2Collections = GC.CollectionCount(2); - GC.Collect(2, GCCollectionMode.Forced, blocking: false, compacting: false); + int initialGen2Collections = GC.CollectionCount(2); + using AllocationPressure pressure = new(); + pressure.Start(); - if (!SpinWait.SpinUntil(() => GC.CollectionCount(2) > previousGen2Collections, TimeSpan.FromMilliseconds(500))) - { - continue; - } + Stopwatch stopwatch = Stopwatch.StartNew(); + bool observedConcurrentGen2 = false; - GCMemoryInfo memoryInfo = GC.GetGCMemoryInfo(); - if (memoryInfo.Generation == 2 && memoryInfo.Concurrent) + while (stopwatch.Elapsed < timeout) { - observedConcurrentGen2 = true; - break; + int previousGen2Collections = GC.CollectionCount(2); + GC.Collect(2, GCCollectionMode.Forced, blocking: false, compacting: false); + + if (!SpinWait.SpinUntil(() => GC.CollectionCount(2) > previousGen2Collections, TimeSpan.FromMilliseconds(500))) + { + continue; + } + + GCMemoryInfo memoryInfo = GC.GetGCMemoryInfo(); + if (memoryInfo.Generation == 2 && memoryInfo.Concurrent) + { + observedConcurrentGen2 = true; + break; + } } - } - pressure.Stop(); + pressure.Stop(); - if (!observedConcurrentGen2) - { - throw new Exception($"Timed out after {timeout} waiting for a concurrent Gen2 GC. Initial count: {initialGen2Collections}, final count: {GC.CollectionCount(2)}."); + if (!observedConcurrentGen2) + { + throw new Exception($"Timed out after {timeout} waiting for a concurrent Gen2 GC. Initial count: {initialGen2Collections}, final count: {GC.CollectionCount(2)}."); + } } } From 49265d48d08463d345abcfa63bab3f9b7ca522c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Tue, 21 Jul 2026 17:15:05 +0900 Subject: [PATCH 8/8] Update GcRestrictedCalloutReversePInvoke.csproj --- .../GcRestrictedCalloutReversePInvoke.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj index 86d1bdede5c46b..7c230bb041ed9d 100644 --- a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj +++ b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj @@ -2,8 +2,8 @@ 0 true + true - true true true