ObjectReferenceWithContext.Release() dispatched into an apartment that is uninitializing, faulting inside the callee
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 665
- Forks
- 134
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 32
Description
Summary
When a UWP secondary view is closed, ObjectReferenceWithContext<T>.Release() running on the finalizer thread marshals the Release back into that view's apartment through IContextCallback. The apartment is inside ApartmentUninitialize → WaitForPendingGitRegistrations, which still pumps, so the call is serviced — but the callee (XAML) has already torn down its core, and the release faults with an access violation inside it.
From the caller's point of view the context still looks valid, so I can't see a way for the app to detect the situation. I'd like to know whether CsWinRT can, and what the recommended pattern is.
Environment
- .NET 10, CsWinRT 2.x (Windows SDK
10.0.26100.0ref pack), x64 - UWP XAML app (
Windows.UI.Xaml),UseUwp, not NativeAOT in this dump - Secondary view created with
CoreApplication.CreateNewView(), i.e. its own ASTA thread - Reproduces reliably a moment after closing that view
The two stacks
Finalizer thread (MTA), making the call:
System.GC.RunFinalizers()
WinRT.IObjectReference.Finalize()
WinRT.ObjectReferenceWithContext`1[[WinRT.Interop.IUnknownVftbl]].Release()
WinRT.Interop.Platform.CoGetContextToken(IntPtr*)
combase!CObjectContext::ContextCallback+0xa8
combase!CObjectContext::InternalContextCallback+0x1b1
combase!ObjectStubless+0x42
combase!ObjectStublessClient+0x146
rpcrt4!NdrpClientCall3+0x431
combase!CSyncClientCall::SendReceive+0x3fb
combase!MTAThreadDispatchCrossApartmentCall
combase!MTAThreadWaitForCall+0xf4 <-- blocked, waiting for the callee
The view's thread, servicing it while uninitializing:
SHCore!_WrapperThreadProc
twinapi_appcore!<lambda_...>::operator()
combase!RoUninitialize+0x9
combase!CoUninitialize+0x19f
combase!wCoUninitialize+0x2e0
combase!ApartmentUninitialize+0x253
combase!CComApartment::WaitForPendingGitRegistrations+0x4b
combase!ASTAState::WaitForPendingGitRegistrations+0x44
combase!CoWaitForMultipleHandles+0xe2
combase!ModernSTAThreadWaitForHandles+0x9a
combase!ModernSTAWaitInNewContext+0xd4
combase!ModernSTAWaitContext::Wait+0x61a
combase!ModernSTAState::HandleMessage+0x3c
combase!ThreadDispatch+0x3ef
combase!ComInvokeWithLockAndIPID+0xd0c
combase!ASTAInvokeInApartment+0xaf
combase!ServerCall::ContextInvoke+0x28f
combase!StubInvoke+0x138
combase!DefaultStubInvoke+0x376
combase!CStdStubBuffer_Invoke+0x7d
rpcrt4!NdrStubCall3+0xc0
rpcrt4!Ndr64StubWorker+0x6ee
rpcrt4!Invoke+0x73
combase!CRemoteUnknown::DoCallback+0xc7
[managed frames]
Windows_UI_Xaml!ctl::interface_forwarder<Windows::UI::Xaml::Core::Direct::IXamlDirectObject,CDependencyObject>::Release+0x45
Windows_UI_Xaml!CDependencyObject::Release+0x1cf
Windows_UI_Xaml!CDependencyObject::ReleaseImpl+0x19d
Windows_UI_Xaml!CDependencyObject::ResetReferencesFromChildren+0x17b
Windows_UI_Xaml!CDependencyObject::ResetReferenceFromChild+0xbb
Windows_UI_Xaml!CMultiParentShareableDependencyObject::RemoveParent+0x8c
Windows_UI_Xaml!CDependencyObject::OnParentChange+0x5
Windows_UI_Xaml!CDependencyObject::GetContext+0x5 <-- AV, core pointer is null
Analysis
The object being released is a CDependencyObject created through XamlDirect (IXamlDirectObject), so it has no framework peer. XAML's shutdown (DXamlCore::DeinitializeInstance) severs every entry in its peer table in ShutdownAllPeers() — explicitly "so that when we start to release everything, we're not bothering to call back into the framework from the core" — and then releases CCoreServices. Objects that were never in the peer table are not severed, so a later Release on one still runs XAML code, and that code dereferences the now-destroyed core.
Every participant is behaving reasonably in isolation:
- CsWinRT marshals the release into the creating apartment, which is the apartment-correct thing to do for a non-agile object.
- COM services the call, because
WaitForPendingGitRegistrationsdeliberately pumps to drain pending work. - XAML released its core after severing everything it knew about.
The combination is what fails, and the app has no visibility into any of it.
Why this does not reproduce on .NET Native
The same app on the .NET Native toolchain never hits this. Its interop releases the interface pointer directly from the finalizer thread, with no context callback and therefore no cross-apartment call — so nothing re-enters the dying apartment. The garbage and its timing are identical; only the release path differs.
Question
Is there anything CsWinRT can do here? Specifically:
- Can
ObjectReferenceWithContextdetect that the target apartment is uninitializing and skip the marshal (leaking the reference, which is harmless at that point)? From the app side the context token still resolves and the callback still succeeds, so I could not find a check that works. - Would an opt-out — an
AppContextswitch to release non-agile objects directly, matching the older behaviour — be considered? For an app whose objects all die with the process it would be strictly safer. - If neither, is deterministic disposal on the owning thread the recommended guidance?
((IWinRTObject)obj).NativeObject.Dispose()from the UI thread releases inline rather than through the context callback, and appears to avoid the problem entirely — but it requires the app to know every RCW it holds and drop them all before the view closes, which is hard to guarantee.
Happy to share the dump or a minimal repro if that would help.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing ObjectReferenceWithContext.Release, IContextCallback, and the two provided apartment-uninitialization stacks. Compare the direct NativeObject.Dispose() path with the marshaled release path, then investigate whether CsWinRT can detect shutdown or support the proposed opt-out; done means a supported mitigation or clear guidance, ideally validated with the offered dump or minimal repro.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- desktop, operating-systems
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 28/100