From c90b33221feb67f850800373de9dcf896d94de78 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 31 Aug 2026 04:19:37 +0000
Subject: [PATCH 1/4] Initial plan
From bb556c74812bd222a739371071e953cc99ad0cf4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 31 Aug 2026 04:24:08 +0000
Subject: [PATCH 2/4] Fix flaky background Gen2 GC detection in
GcRestrictedCalloutReversePInvoke test
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
---
.../GcRestrictedCalloutReversePInvoke.csproj | 5 ++++
.../Program.cs | 23 ++++---------------
2 files changed, 10 insertions(+), 18 deletions(-)
diff --git a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj
index 7c230bb041ed9d..9d1126895c8892 100644
--- a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj
+++ b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj
@@ -15,6 +15,11 @@
+
+
+
+
+
diff --git a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs
index 5b7677ebe0c56f..e004605ad72b67 100644
--- a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs
+++ b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs
@@ -32,37 +32,24 @@ public static void TestEntryPoint()
private static void ForceBackgroundGen2Collection(TimeSpan timeout)
{
- int initialGen2Collections = GC.CollectionCount(2);
+ long initialBackgroundGcIndex = GC.GetGCMemoryInfo(GCKind.Background).Index;
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)))
+ if (SpinWait.SpinUntil(() => GC.GetGCMemoryInfo(GCKind.Background).Index > initialBackgroundGcIndex, TimeSpan.FromMilliseconds(500)))
{
- continue;
- }
-
- GCMemoryInfo memoryInfo = GC.GetGCMemoryInfo();
- if (memoryInfo.Generation == 2 && memoryInfo.Concurrent)
- {
- observedConcurrentGen2 = true;
- break;
+ return;
}
}
- 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)}.");
- }
+ long finalBackgroundGcIndex = GC.GetGCMemoryInfo(GCKind.Background).Index;
+ throw new Exception($"Timed out after {timeout} waiting for a background GC. Initial index: {initialBackgroundGcIndex}, final index: {finalBackgroundGcIndex}.");
}
}
From 6d5801efc4d2c6cca92aa130e2181f3dfe90d842 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 31 Aug 2026 04:57:11 +0000
Subject: [PATCH 3/4] Poll background GC index with a short sleep instead of a
tight spin
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
---
.../GcRestrictedCalloutReversePInvoke/Program.cs | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs
index e004605ad72b67..9b500d2a23b6cd 100644
--- a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs
+++ b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs
@@ -42,9 +42,20 @@ private static void ForceBackgroundGen2Collection(TimeSpan timeout)
{
GC.Collect(2, GCCollectionMode.Forced, blocking: false, compacting: false);
- if (SpinWait.SpinUntil(() => GC.GetGCMemoryInfo(GCKind.Background).Index > initialBackgroundGcIndex, TimeSpan.FromMilliseconds(500)))
+ // Poll with a short sleep rather than a tight spin. GC.GetGCMemoryInfo(GCKind.Background)
+ // reads state that the GC engine updates non-atomically as a background collection
+ // finishes; querying it as fast as possible increases the odds of observing a
+ // transiently inconsistent record and hitting an unrelated GC-engine assert in Checked
+ // builds. A short delay between polls avoids that without noticeably slowing detection.
+ Stopwatch pollStopwatch = Stopwatch.StartNew();
+ while (pollStopwatch.Elapsed < TimeSpan.FromMilliseconds(500))
{
- return;
+ if (GC.GetGCMemoryInfo(GCKind.Background).Index > initialBackgroundGcIndex)
+ {
+ return;
+ }
+
+ Thread.Sleep(15);
}
}
From ba0a5153180a435183adadc30ed0e9d67de1f140 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 31 Aug 2026 06:46:47 +0000
Subject: [PATCH 4/4] Delete GcRestrictedCalloutReversePInvoke test per
maintainer request
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
---
.../GcRestrictedCalloutReversePInvoke.csproj | 26 ----
.../Program.cs | 126 ------------------
2 files changed, 152 deletions(-)
delete mode 100644 src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj
delete mode 100644 src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs
diff --git a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj
deleted file mode 100644
index 9d1126895c8892..00000000000000
--- a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/GcRestrictedCalloutReversePInvoke.csproj
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
- 0
- true
-
- true
- true
-
- true
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs b/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs
deleted file mode 100644
index 9b500d2a23b6cd..00000000000000
--- a/src/tests/Interop/COM/ComWrappers/GcRestrictedCalloutReversePInvoke/Program.cs
+++ /dev/null
@@ -1,126 +0,0 @@
-// 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;
-using ComWrappersTests.Common;
-using Xunit;
-using TestLibrary;
-
-public class Program
-{
- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))]
- [SkipOnCoreClr("This test is not compatible with GC stress.", RuntimeTestModes.AnyGCStress)]
- 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);
- }
-
- private static void ForceBackgroundGen2Collection(TimeSpan timeout)
- {
- long initialBackgroundGcIndex = GC.GetGCMemoryInfo(GCKind.Background).Index;
- using AllocationPressure pressure = new();
- pressure.Start();
-
- Stopwatch stopwatch = Stopwatch.StartNew();
-
- while (stopwatch.Elapsed < timeout)
- {
- GC.Collect(2, GCCollectionMode.Forced, blocking: false, compacting: false);
-
- // Poll with a short sleep rather than a tight spin. GC.GetGCMemoryInfo(GCKind.Background)
- // reads state that the GC engine updates non-atomically as a background collection
- // finishes; querying it as fast as possible increases the odds of observing a
- // transiently inconsistent record and hitting an unrelated GC-engine assert in Checked
- // builds. A short delay between polls avoids that without noticeably slowing detection.
- Stopwatch pollStopwatch = Stopwatch.StartNew();
- while (pollStopwatch.Elapsed < TimeSpan.FromMilliseconds(500))
- {
- if (GC.GetGCMemoryInfo(GCKind.Background).Index > initialBackgroundGcIndex)
- {
- return;
- }
-
- Thread.Sleep(15);
- }
- }
-
- long finalBackgroundGcIndex = GC.GetGCMemoryInfo(GCKind.Background).Index;
- throw new Exception($"Timed out after {timeout} waiting for a background GC. Initial index: {initialBackgroundGcIndex}, final index: {finalBackgroundGcIndex}.");
- }
-}
-
-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)
- {
- }
-}