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.0 ref 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
WaitForPendingGitRegistrations deliberately 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
ObjectReferenceWithContext detect 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
AppContext switch 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.
Summary
When a UWP secondary view is closed,
ObjectReferenceWithContext<T>.Release()running on the finalizer thread marshals theReleaseback into that view's apartment throughIContextCallback. The apartment is insideApartmentUninitialize→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
10.0.26100.0ref pack), x64Windows.UI.Xaml),UseUwp, not NativeAOT in this dumpCoreApplication.CreateNewView(), i.e. its own ASTA threadThe two stacks
Finalizer thread (MTA), making the call:
The view's thread, servicing it while uninitializing:
Analysis
The object being released is a
CDependencyObjectcreated throughXamlDirect(IXamlDirectObject), so it has no framework peer. XAML's shutdown (DXamlCore::DeinitializeInstance) severs every entry in its peer table inShutdownAllPeers()— explicitly "so that when we start to release everything, we're not bothering to call back into the framework from the core" — and then releasesCCoreServices. Objects that were never in the peer table are not severed, so a laterReleaseon one still runs XAML code, and that code dereferences the now-destroyed core.Every participant is behaving reasonably in isolation:
WaitForPendingGitRegistrationsdeliberately pumps to drain pending work.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:
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.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.((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.