From bba3f1d0ba71da61b443de35df61c27fdb36e6a5 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 1 Sep 2026 18:07:31 -0700 Subject: [PATCH 1/4] Deterministically collect the EventTest RCW in the Dynamic COM test Removing the last handler for a COM event source unadvises the sink but keeps it in the RCW's sink container so it can be reused. Finalizing that container unconditionally unadvises every sink it holds, which trips the 'Can not unadvise from empty connection point' assert on the finalizer thread. Run the event test in a non-inlined helper and force a collection afterwards so the RCW and its sink container are reclaimed at a deterministic point, and prove the RCW is gone with a WeakReference. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/tests/Interop/COM/Dynamic/EventTest.cs | 38 ++++++++++++++++++---- src/tests/Interop/COM/Dynamic/Program.cs | 2 +- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/tests/Interop/COM/Dynamic/EventTest.cs b/src/tests/Interop/COM/Dynamic/EventTest.cs index a48879cf094a55..ebb62a0a4cf43c 100644 --- a/src/tests/Interop/COM/Dynamic/EventTest.cs +++ b/src/tests/Interop/COM/Dynamic/EventTest.cs @@ -4,27 +4,53 @@ namespace Dynamic { using System; + using System.Runtime.CompilerServices; using Xunit; internal class EventTest { private dynamic obj; private Random rand; + private readonly WeakReference _rcwReference; public EventTest(int seed = 123) { Type t = Type.GetTypeFromCLSID(Guid.Parse(ServerGuids.EventTest)); - obj = Activator.CreateInstance(t); + object rcw = Activator.CreateInstance(t); + obj = rcw; + _rcwReference = new WeakReference(rcw); rand = new Random(seed); } - public void Run() + public static void Run() { Console.WriteLine($"Running {nameof(EventTest)}"); - FireEvent(); - DynamicEventHandler(); - MultipleHandlers(); - MultipleSources(); + + WeakReference rcwReference = RunCore(); + + // RunCore removed every handler it added, so each event source's sink has already + // been unadvised and is kept in the RCW's sink container only so it can be reused. + // Collecting the RCW finalizes that container, which must not unadvise those sinks + // a second time. + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Assert.False(rcwReference.IsAlive); + } + + // The RCW is created and used entirely within this method so that no local in Run can + // extend its lifetime past the collection there, regardless of the JIT's lifetime and + // codegen behavior in debug and release builds. + [MethodImpl(MethodImplOptions.NoInlining)] + private static WeakReference RunCore() + { + var test = new EventTest(); + test.FireEvent(); + test.DynamicEventHandler(); + test.MultipleHandlers(); + test.MultipleSources(); + return test._rcwReference; } private void FireEvent() diff --git a/src/tests/Interop/COM/Dynamic/Program.cs b/src/tests/Interop/COM/Dynamic/Program.cs index f34e1ae2fb746b..155a89c216571a 100644 --- a/src/tests/Interop/COM/Dynamic/Program.cs +++ b/src/tests/Interop/COM/Dynamic/Program.cs @@ -25,7 +25,7 @@ public static int TestEntryPoint() { new BasicTest().Run(); new CollectionTest().Run(); - new EventTest().Run(); + EventTest.Run(); new ParametersTest().Run(); new NETServerTest().Run(); } From 4b7c541eb271c566bee7e4397022cf1531196ea4 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 1 Sep 2026 22:57:52 -0700 Subject: [PATCH 2/4] Remove invalid Unadvise assert on an already-unadvised COM event sink Removing the last handler for a source interface unadvises the sink but keeps it in its container so it can be reused, so the container finalizer legitimately unadvises it a second time and trips the assert, fail-fasting the finalizer thread with 0x80131623 in checked/debug builds. Asserting that a null connection point implies a cleared source interface id is not a valid alternative: Initialize assigns the source interface id before calling Advise, so a failure inside Advise on a reused sink leaves a non-empty id with a null connection point, and RemoveHandler runs outside the container lock so the window between Unadvise and clearing the id is observable. Drop the assert and keep the existing null check. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/System/Runtime/InteropServices/ComEventsSink.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/src/System/Runtime/InteropServices/ComEventsSink.cs b/src/libraries/Common/src/System/Runtime/InteropServices/ComEventsSink.cs index ff804a6f9fae0d..af78e005179d32 100644 --- a/src/libraries/Common/src/System/Runtime/InteropServices/ComEventsSink.cs +++ b/src/libraries/Common/src/System/Runtime/InteropServices/ComEventsSink.cs @@ -258,7 +258,10 @@ private void Advise(object rcw) private void Unadvise() { - Debug.Assert(_connectionPoint != null, "Can not unadvise from empty connection point"); + // Once the last handler for a source interface is removed, the sink is unadvised and may be + // marked as reusable by clearing its source interface id and keeping it in its container + // so a later handler can advise it again. Finalizing that container unadvises every + // sink it holds, so encountering an already unadvised sink here is expected. if (_connectionPoint == null) return; From a304e69174ace06b54c642d444ffb4b246bd983f Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 2 Sep 2026 09:17:08 -0700 Subject: [PATCH 3/4] Revert "Disable failing COM dynamic test on CoreCLR (#133037)" This reverts commit 600c5b67ae4680acf39f6e85529973930bda4168. --- src/tests/Interop/COM/Dynamic/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/Interop/COM/Dynamic/Program.cs b/src/tests/Interop/COM/Dynamic/Program.cs index 0fb33f9a5aeddf..155a89c216571a 100644 --- a/src/tests/Interop/COM/Dynamic/Program.cs +++ b/src/tests/Interop/COM/Dynamic/Program.cs @@ -11,7 +11,6 @@ namespace Dynamic public class Program { [SkipOnCoreClr("This test is very slow under some GC stress variations, especially with DOTNET_HeapVerify=1, and can time out in CI. See https://github.com/dotnet/runtime/issues/39584.", RuntimeTestModes.AnyGCStress)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/132947", TestRuntimes.CoreCLR)] [Fact] [Xunit.SkipOnCoreClrAttribute("Depends on COM behavior that is not correct in interpreter", RuntimeTestModes.InterpreterActive)] public static int TestEntryPoint() From bccaf2d90f5bba58d82ba7ea892409d370d90850 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 2 Sep 2026 17:36:24 -0700 Subject: [PATCH 4/4] Update ComEventsSink.cs Co-authored-by: Aaron R Robinson --- .../Common/src/System/Runtime/InteropServices/ComEventsSink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/src/System/Runtime/InteropServices/ComEventsSink.cs b/src/libraries/Common/src/System/Runtime/InteropServices/ComEventsSink.cs index af78e005179d32..d39c8d8d5557e7 100644 --- a/src/libraries/Common/src/System/Runtime/InteropServices/ComEventsSink.cs +++ b/src/libraries/Common/src/System/Runtime/InteropServices/ComEventsSink.cs @@ -259,7 +259,7 @@ private void Advise(object rcw) private void Unadvise() { // Once the last handler for a source interface is removed, the sink is unadvised and may be - // marked as reusable by clearing its source interface id and keeping it in its container + // marked as reusable by clearing its source interface ID and keeping it in its container // so a later handler can advise it again. Finalizing that container unadvises every // sink it holds, so encountering an already unadvised sink here is expected. if (_connectionPoint == null)