Fix finalizer fail-fast when unadvising reused COM event sinks - #133107
Open
jkoritzinsky wants to merge 5 commits into
Open
Fix finalizer fail-fast when unadvising reused COM event sinks#133107jkoritzinsky wants to merge 5 commits into
jkoritzinsky wants to merge 5 commits into
Conversation
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>
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>
This reverts commit 600c5b6.
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
|
Tagging subscribers to this area: @dotnet/interop-contrib |
Contributor
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The change removes a debug-only assertion that was not a valid invariant and updates the test to deterministically validate the intended finalizer behavior without altering release semantics.
Review tier: Lite
Findings: None
What changed in this PR
This PR addresses an intermittent fail-fast on the finalizer thread in the Interop/COM/Dynamic test scenario by making ComEventsSink.Unadvise() tolerant of already-unadvised (reused) sinks, and it updates the Dynamic COM event test to deterministically collect/finalize the RCW so the regression is reliably exercised (and the prior skip can be removed).
Changes:
- Remove the debug-only assertion in
ComEventsSink.Unadvise()and rely on the existing_connectionPoint == nullearly-return. - Refactor
EventTestto run via a non-inlined helper that returns aWeakReference, then force GC/finalization and assert the RCW is reclaimed. - Re-enable the Dynamic test by removing the
[ActiveIssue]skip and switching to the new staticEventTest.Run()entrypoint.
| File | Description |
|---|---|
| src/tests/Interop/COM/Dynamic/Program.cs | Removes the CoreCLR [ActiveIssue] skip and invokes the updated static event test entrypoint. |
| src/tests/Interop/COM/Dynamic/EventTest.cs | Ensures deterministic RCW reclamation/finalizer execution via NoInlining helper + forced GC and WeakReference assertion. |
| src/libraries/Common/src/System/Runtime/InteropServices/ComEventsSink.cs | Drops the debug assert that could fail-fast on the finalizer thread when a sink is already unadvised, preserving idempotent behavior. |
AaronRobinsonMSFT
approved these changes
Sep 2, 2026
This was referenced Sep 3, 2026
Co-authored-by: Aaron R Robinson <arobins@microsoft.com>
elinor-fung
approved these changes
Sep 3, 2026
jkoritzinsky
enabled auto-merge (squash)
September 3, 2026 01:05
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The
Interop/COM/Dynamictest intermittently fail-fasts the finalizer thread with0x80131623on checked/debug CI legs. A dump from build 1573972 showed:Removing the last handler for a COM event source unadvises the sink but deliberately keeps it in the RCW's sink container so it can be reused:
FromRuntimeCallableWrapperlooks for a sink whose_iidSourceItfisGuid.Emptyand re-Initializes it. When the RCW is later collected, the container finalizer unadvises every sink it holds, including the already-unadvised ones, and tripsDebug.Assert(_connectionPoint != null). Because that runs on the finalizer thread it takes the whole process down rather than failing a test.What changed
Product: drop the assert in
ComEventsSink.Unadvise. The existingif (_connectionPoint == null) return;already handles this correctly; only the assert was wrong.I first tried the narrower
Debug.Assert(_connectionPoint != null || _iidSourceItf == Guid.Empty, ...)and confirmed it holds on the normal path, but it is not a real invariant:Initializeassigns_iidSourceItfbefore callingAdvise, so a failure insideAdviseon a reused sink (FindConnectionPointfailing, orcp.AdvisereturningCONNECT_E_ADVISELIMIT) leaves a non-empty iid with a null connection point, still sitting in the container.RemoveHandlerruns outsidelock (comEventSinks), so the window betweenUnadvise()and_iidSourceItf = Guid.Emptyis observable by a concurrent-=.Removing the assert also matches what
Unadvisealready does: it swallows all exceptions because "the host may not be available at this point".Test:
EventTestnow runs in a non-inlined helper and forces a collection afterwards, so the RCW and its sink container are reclaimed at a deterministic point, with aWeakReferenceproving the RCW is actually gone. Previously hitting this depended on GC timing.This also reverts #133037, which had disabled the test with
[ActiveIssue]for this same issue.Notes for reviewers
src/libraries/Common/src/System/Runtime/InteropServices/ComEventsSink.csis shared: it compiles into bothMicrosoft.CSharpand CoreCLR'sSystem.Private.CoreLib. Only the Microsoft.CSharp path reuses sinks. TheComEventsHelperpath removes a sink from the linked list before unadvising and never clears_iidSourceItf, so it never double-unadvises and is unaffected. NativeAOT'sComEventsHelperthrowsPlatformNotSupportedException.The assert is
[Conditional("DEBUG")], so there is no release behavior change. The repro only manifests with Debug-configuration libraries, which is what PR legs use (debugOnPrReleaseOnRolling); rolling builds use Release libraries and compile the assert out.Validation
Windows x64, checked CoreCLR + Debug libraries:
Dynamic.cmdfailed 3/3 with0x80131623and the stack above. The unmodified test passed 3/3, confirming the test change is what makes it deterministic.Dynamic.cmdpasses 3/3, andAssert.False(rcwReference.IsAlive)is now reached and passes.Interop/COMtree: 24 of 30 ran and passed. The 6NativeClients/*tests exit 9009 ("program not found") locally because they need the out-of-process harness, so CI is the first real check ofNativeClients/Events, which is the only coverage of theComEventsHelperpath in this shared file.Unrelated defect noticed
FromRuntimeCallableWrapperinComEventsSink.Extended.cshas nobreakafter reusing a sink, so with two emptied sinks it advises both to the same iid and returns the last one. A later-=then finds the first one via the_iidSourceItf == sourceIidfast path, whose_methodsis null, and returns early without removing the handler. Not touched here.Fixes #132947
Note
This pull request description was generated by GitHub Copilot.