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