Skip to content

ObjectReferenceWithContext.Release() dispatched into an apartment that is uninitializing, faulting inside the callee #2532

Description

@FrayxRulez

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 ApartmentUninitializeWaitForPendingGitRegistrations, 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:

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions