Fix Window.DispatcherQueue use-after-free after XAML shutdown (#12346)

## Fixes

Internal Issue: https://task.ms/55026189

## PR Type

- [x] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no API changes)
- [ ] Build related changes
- [ ] Documentation content changes
- [ ] Other (please describe):

## Description

### Current Behavior

`Window.DispatcherQueue` is meant to be read from any thread; its
generated getter has no thread check. But
`Window::get_DispatcherQueueImpl` walked
`DesktopWindowImpl::m_dxamlCoreNoRef`, a raw `DXamlCore*` cached in the
constructor and never cleared.

A desktop app shuts XAML down on a thread when its last window closes,
which deletes that thread's core. If the app still holds the `Window`,
and a background thread then reads `Window.DispatcherQueue` (for
example, to marshal work back to the UI thread during shutdown), the
getter walks freed memory, and the app can crash with an access
violation. Crash dumps show exactly this: a closed window kept alive by
a managed reference, read from a thread-pool worker during process exit.

### New Behavior

- `WindowImpl` gets `get_DispatcherQueueImpl`, and `Window` delegates to
it.
- `DesktopWindowImpl` captures a weak reference to its thread's
`DispatcherQueue` once, in its constructor, and resolves it in the
getter. The getter never touches the core.
- While the window's XAML is running, the core's dispatcher holds the
queue, so the getter returns the same object as before.
- After XAML shuts down, it returns the shut-down queue while anything
still holds it, and `TryEnqueue` returns false. Once the queue is
destroyed it returns null, which the getter's
`_Outptr_result_maybenull_` contract already allows.
- The reference is weak because `DispatcherQueue` keeps its
`ShutdownStarting`/`ShutdownCompleted` handlers until it's destroyed. A
strong reference would leak any window captured by such a handler.
- `UWPWindowImpl` keeps its existing lookup.

The new regression test
`XamlIslandTests::GetDispatcherQueueFromThreadpoolAfterCoreShutdown`:

1. closes a `Window` that it keeps alive;
2. shuts that thread's XAML down and verifies the core is gone;
3. reads `Window.DispatcherQueue` from an MTA thread-pool worker.

It checks that the same queue is returned before `Close`, after `Close`
and after shutdown, and that `TryEnqueue` then returns false.

## Customer Impact

Apps that read `Window.DispatcherQueue` from a background thread around
shutdown no longer crash in the getter. Once the window's queue has been
destroyed, the getter returns null, so callers on other threads should
null-check it. Behavior while a window's XAML is running is unchanged.

## Regression Potential

- [x] Low risk — isolated change, limited scope
- [ ] Medium risk — touches shared components or public APIs
- [ ] High risk — architectural or breaking API change

While XAML is running, the getter returns the same object as before.
Behavior changes only where the old code read freed memory, or during
core teardown, where the old lookup could return null. The weak
reference adds no lifetime extension or reference cycle. The member is
immutable after construction, so reading it from other threads is safe.

## How Has This Been Tested?

- [x] I have performed a self-review of my own code
- [x] I have added tests to cover my changes
- [ ] Existing tests pass locally

Validated on amd64chk on an internal branch that predates #12268 and
#11982, with and without the fix:

| | With fix | Without fix |
|---|---|---|
| New regression test | Passes | Access violation in the getter's core
walk |
| XamlIslandTests, Helix query, Win32Explicit (46 tests on that branch)
| 40 pass | 39 pass; the extra failure is the new test |
| WindowIntegrationTests, Helix query, WPF (42 tests on that branch) |
42 pass | 42 pass |

Six other XamlIslandTests fail on that machine both with and without the
fix; they are unrelated to this change.

Among the files this fix touches or depends on, that branch differs from
`main` only in these, none of which affect the fix:
- Its `DesktopWindowImpl.cpp`/`.h` lack #12268, which changes only the
Width/Height setters after `Close`.
- Its
`WindowsXamlManager_Partial.cpp`/`FrameworkApplication_Partial.cpp` lack
#11982's shutdown changes. On `main` the core is still deleted at
shutdown, and the test keeps the queue alive.
- Its `XamlIslandTests.cpp`/`.h` lack #11982's new tests, hence the
different test count.
- `Window_Partial.cpp`, `UWPWindowImpl.*`, `WindowImpl.h`,
`DXamlCore.cpp` and `xcpwindow.cpp` are byte-identical.

Not built on `main` locally; relying on PR CI.

## Screenshots (if appropriate)

Not applicable; no visual change.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84bf8dd5-9217-47bc-a3f9-88bffebaff37
This commit is contained in:
Godly T.AliasandCopilot authored and GitHub committed 2026-10-27 07:39:16 +05:30
1 parent b22743a863
commit 79fe886bc8
8 files changed
+161 -25

No files matched your search

@@ -1659,6 +1659,97 @@ void XamlIslandTests::FailFastWhenMessagesNotPumpedAfterClosing()
WaitForSingleObjectWithTimeout(uiThread);
}
void XamlIslandTests::GetDispatcherQueueFromThreadpoolAfterCoreShutdown()
{
// Verifies that reading Window.DispatcherQueue from a thread-pool worker, on a closed Window that has outlived its
// thread's XAML core, returns the window's (now shut down) DispatcherQueue without touching the deleted core.
// Keep an Application alive on the test thread so Microsoft.UI.Xaml.dll stays loaded after the UI thread exits.
XamlIslandTestHelper testHelper(this);
testHelper.StartAppOnCurrentThread();
wrl::ComPtr<ABI::Microsoft::UI::Xaml::IWindow> survivingWindow;
DispatcherQueue^ uiThreadQueue; // The Window holds its queue weakly; keep it alive for the final check.
const auto identityOf = [](Platform::Object^ object) { return reinterpret_cast<uintptr_t>(CastHatTo<IUnknown>(object).Get()); };
// On a UI thread, show and close a Window while keeping a reference to it, then shut down that thread's
// DispatcherQueue, which deletes its XAML core.
auto uiThread = RunOnNewThread([&]() {
VERIFY_SUCCEEDED(::RoInitialize(RO_INIT_SINGLETHREADED));
{
auto dqc { DispatcherQueueController::CreateOnCurrentThread() };
WindowsXamlManager^ wxm = WindowsXamlManager::InitializeForCurrentThread();
uiThreadQueue = dqc->DispatcherQueue;
Window^ window = ref new Window();
CopyObjectTo(window, survivingWindow.ReleaseAndGetAddressOf());
VERIFY_IS_NOT_NULL(window->DispatcherQueue);
VERIFY_ARE_EQUAL(identityOf(uiThreadQueue), identityOf(window->DispatcherQueue));
bool closedRaised = false;
window->Closed += ref new ::Windows::Foundation::TypedEventHandler<Platform::Object^, WindowEventArgs^>(
[&closedRaised](Platform::Object^, WindowEventArgs^) { closedRaised = true; });
window->Activate();
DrainMessageQueue();
LOG_OUTPUT(L"Closing the Window while keeping a reference to it.");
window->Close();
DrainMessageQueue();
VERIFY_IS_TRUE(closedRaised);
VERIFY_IS_NOT_NULL(window->DispatcherQueue);
VERIFY_ARE_EQUAL(identityOf(uiThreadQueue), identityOf(window->DispatcherQueue));
window = nullptr;
LOG_OUTPUT(L"Shutting down the UI thread's DispatcherQueue, which deletes its XAML core.");
dqc->ShutdownQueue();
// WindowsXamlManager is cleared only after the core is deinitialized, so this confirms the core is gone.
VERIFY_IS_NULL(WindowsXamlManager::GetForCurrentThread());
}
::RoUninitialize();
});
WaitForSingleObjectWithTimeout(uiThread);
VERIFY_IS_NOT_NULL(survivingWindow.Get());
struct WorkerContext
{
ABI::Microsoft::UI::Xaml::IWindow* window;
HRESULT roInitializeHr = E_UNEXPECTED;
HRESULT hr = E_UNEXPECTED;
wrl::ComPtr<ABI::Microsoft::UI::Dispatching::IDispatcherQueue> queue;
} context { survivingWindow.Get() };
LOG_OUTPUT(L"Reading Window.DispatcherQueue from an MTA thread-pool worker after core shutdown.");
PTP_WORK work = ::CreateThreadpoolWork(
[](PTP_CALLBACK_INSTANCE, void* pv, PTP_WORK) {
auto* ctx = static_cast<WorkerContext*>(pv);
ctx->roInitializeHr = ::RoInitialize(RO_INIT_MULTITHREADED);
if (SUCCEEDED(ctx->roInitializeHr))
{
ctx->hr = ctx->window->get_DispatcherQueue(ctx->queue.ReleaseAndGetAddressOf());
::RoUninitialize();
}
},
&context,
nullptr);
VERIFY_IS_NOT_NULL(work);
::SubmitThreadpoolWork(work);
::WaitForThreadpoolWorkCallbacks(work, FALSE);
::CloseThreadpoolWork(work);
VERIFY_SUCCEEDED(context.roInitializeHr);
// The getter returns the window's own DispatcherQueue, which has shut down and so rejects new work.
LOG_OUTPUT(L"Window.DispatcherQueue returned hr=0x%08x without crashing.", context.hr);
VERIFY_SUCCEEDED(context.hr);
VERIFY_IS_NOT_NULL(context.queue.Get());
DispatcherQueue^ queue = safe_cast<DispatcherQueue^>(ToObject(context.queue.Get()));
VERIFY_ARE_EQUAL(identityOf(uiThreadQueue), identityOf(queue));
VERIFY_IS_FALSE(queue->TryEnqueue(ref new DispatcherQueueHandler([]() {})));
}
void XamlIslandTests::IslandWithMuxcDoesntPoisonThread()
{
@@ -158,6 +158,11 @@ public:
// Run it manually by using the -RunIgnoredTests flag
END_TEST_METHOD()
BEGIN_TEST_METHOD(GetDispatcherQueueFromThreadpoolAfterCoreShutdown)
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") // Shuts XAML down on a UI thread; isolate from other tests.
TEST_METHOD_PROPERTY(L"Description", L"Reading Window.DispatcherQueue from a thread-pool worker after the window's XAML core has shut down returns the window's DispatcherQueue.")
END_TEST_METHOD()
BEGIN_TEST_METHOD(PointerInput)
TEST_METHOD_PROPERTY(L"TestPass:MaxOSVer", WINDOWS_OS_VERSION_22H2) // This test is currently failing on 23h2.
END_TEST_METHOD()
+36 -4
View File
@@ -70,17 +70,34 @@ static int GetClampedWindowSizeInPixels(double clientSizeDips, double scale, int
return static_cast<int>(std::clamp(outerSizePixels, 0.0, static_cast<double>(c_maxWindowSizeInPixels)));
}
static wrl::WeakRef AsWeakDispatcherQueue(_In_opt_ msy::IDispatcherQueue* dispatcherQueue)
{
wrl::WeakRef weakDispatcherQueue;
if (dispatcherQueue)
{
IFCFAILFAST(wrl::AsWeak(dispatcherQueue, &weakDispatcherQueue));
}
return weakDispatcherQueue;
}
// Note that win32 class registration, win32 window and DesktopWindowXamlSource
// creation, and DWXS::Initialize failures are non-recoverable errors.
//
// Additionally, DXamlCore must exist before Microsoft::UI::XAML::Window can be
// created, and DXamlCore will outlive any Microsoft::UI::XAML::Window instances.
// DXamlCore::GetCurrent() will always return a valid DXamlCore instance when
// called from a non-static Microsoft::UI::XAML::Window method.
DesktopWindowImpl::DesktopWindowImpl(Window* parentWindow) : m_dxamlWindowInstance(parentWindow)
// created, so DXamlCore::GetCurrent() returns a valid instance here. The Window
// can outlive that DXamlCore, though: an app can keep a reference to it after its
// thread's XAML has shut down, and m_dxamlCoreNoRef is not cleared when that
// happens. Code that can run after the core is deleted (off-thread getters, or
// teardown of a Window that outlives its thread's XAML) must not dereference it.
DesktopWindowImpl::DesktopWindowImpl(Window* parentWindow)
: m_dxamlWindowInstance(parentWindow)
, m_weakDispatcherQueue(AsWeakDispatcherQueue(DirectUI::DXamlCore::GetCurrent()->GetDispatcherQueueNoRef()))
{
XamlTelemetry::CreateDesktopWindow(true, reinterpret_cast<uint64_t>(this));
// The XAML dispatcher fails fast at startup if the thread has no DispatcherQueue.
ASSERT(m_weakDispatcherQueue.Get() != nullptr);
// Cache DXamlCore instance for this thread
m_dxamlCoreNoRef = DirectUI::DXamlCore::GetCurrent();
@@ -322,6 +339,21 @@ _Check_return_ HRESULT DesktopWindowImpl::get_DispatcherImpl(_Outptr_result_mayb
return S_OK;
}
_Check_return_ HRESULT DesktopWindowImpl::get_DispatcherQueueImpl(_Outptr_result_maybenull_ msy::IDispatcherQueue** ppValue)
{
if (nullptr == ppValue) { IFC_RETURN(E_INVALIDARG); }
*ppValue = nullptr;
// Don't walk m_dxamlCoreNoRef: this getter can run on any thread, after the core has been deleted. The core holds
// the queue while it's alive, so this always resolves then; afterwards it resolves for as long as the queue lives.
if (m_weakDispatcherQueue)
{
IFC_RETURN(m_weakDispatcherQueue.CopyTo(ppValue));
}
return S_OK;
}
_Check_return_ HRESULT DesktopWindowImpl::get_TitleImpl(_Out_ HSTRING* pValue)
{
IFC_RETURN(CheckIsWindowClosed());
+7
View File
@@ -32,6 +32,7 @@ namespace DirectUI
_Check_return_ HRESULT put_ContentImpl(_In_opt_ xaml::IUIElement* value) override;
_Check_return_ HRESULT get_CoreWindowImpl(_Outptr_result_maybenull_ wuc::ICoreWindow** pValue) override;
_Check_return_ HRESULT get_DispatcherImpl(_Outptr_result_maybenull_ wuc::ICoreDispatcher** pValue) override;
_Check_return_ HRESULT get_DispatcherQueueImpl(_Outptr_result_maybenull_ msy::IDispatcherQueue** ppValue) override;
_Check_return_ HRESULT get_TitleImpl(_Out_ HSTRING* pValue) override;
_Check_return_ HRESULT put_TitleImpl(_In_opt_ HSTRING value) override;
IFACEMETHOD(add_Activated)(_In_ wf::ITypedEventHandler<IInspectable*, xaml::WindowActivatedEventArgs*>* pValue, _Out_ EventRegistrationToken* ptToken) override;
@@ -218,6 +219,12 @@ namespace DirectUI
wil::critical_section m_weakWindowLock;
ctl::WeakRefPtr m_weakWindow;
DXamlCore* m_dxamlCoreNoRef = nullptr;
// The thread's DispatcherQueue, captured in the constructor's initializer list (on the UI thread, while the
// DXamlCore is alive) and never modified. Window.DispatcherQueue has no thread check, so get_DispatcherQueueImpl
// resolves this from any thread, possibly after this window's DXamlCore has been deleted, which m_dxamlCoreNoRef
// cannot detect. It's weak so the window doesn't extend the queue's lifetime: a DispatcherQueue keeps its event
// handlers until it's destroyed, so a handler that captures the window would otherwise form a reference cycle.
const wrl::WeakRef m_weakDispatcherQueue;
bool m_bMinimizedOrHidden = false;
bool m_bInitialWindowActivation = true;
+17
View File
@@ -343,6 +343,23 @@ _Check_return_ HRESULT UWPWindowImpl::get_DispatcherImpl(_Outptr_result_maybenul
RRETURN(S_OK);
}
_Check_return_ HRESULT UWPWindowImpl::get_DispatcherQueueImpl(_Outptr_result_maybenull_ msy::IDispatcherQueue** ppValue)
{
if (nullptr == ppValue) { IFC_RETURN(E_INVALIDARG); }
*ppValue = nullptr;
// Same lookup as before this getter moved here. DXamlCore::ReleaseWindow clears m_pDXamlCoreNoRef on the UI
// thread before the core is deleted; an off-thread caller can still race that, but apps can only reach this
// window through Window.Current outside classic desktop (see WindowFactory::get_CurrentImpl).
if (DXamlCore* xamlCore = m_pDXamlCoreNoRef)
{
*ppValue = xamlCore->GetDispatcherQueueNoRef();
AddRefInterface(*ppValue);
}
return S_OK;
}
_Check_return_ HRESULT UWPWindowImpl::get_CompositorImpl(_Outptr_result_maybenull_ WUComp::ICompositor** compositor)
{
IFCPTR_RETURN(compositor);
+1
View File
@@ -31,6 +31,7 @@ namespace DirectUI
_Check_return_ HRESULT put_ContentImpl(_In_opt_ xaml::IUIElement* value) override;
_Check_return_ HRESULT get_CoreWindowImpl(_Outptr_result_maybenull_ wuc::ICoreWindow** pValue) override;
_Check_return_ HRESULT get_DispatcherImpl(_Outptr_result_maybenull_ wuc::ICoreDispatcher** pValue) override;
_Check_return_ HRESULT get_DispatcherQueueImpl(_Outptr_result_maybenull_ msy::IDispatcherQueue** ppValue) override;
_Check_return_ HRESULT get_TitleImpl(_Out_ HSTRING* pValue) override;
_Check_return_ HRESULT put_TitleImpl(_In_opt_ HSTRING value) override;
IFACEMETHOD(add_Activated)(_In_ wf::ITypedEventHandler<IInspectable*, xaml::WindowActivatedEventArgs*>* pValue, _Out_ EventRegistrationToken* ptToken) override;
+1
View File
@@ -29,6 +29,7 @@ namespace DirectUI
virtual _Check_return_ HRESULT put_ContentImpl(_In_opt_ xaml::IUIElement* value) = 0;
virtual _Check_return_ HRESULT get_CoreWindowImpl(_Outptr_result_maybenull_ wuc::ICoreWindow** pValue) = 0;
virtual _Check_return_ HRESULT get_DispatcherImpl(_Outptr_result_maybenull_ wuc::ICoreDispatcher** pValue) = 0;
virtual _Check_return_ HRESULT get_DispatcherQueueImpl(_Outptr_result_maybenull_ msy::IDispatcherQueue** ppValue) = 0;
virtual _Check_return_ HRESULT get_TitleImpl(_Out_ HSTRING* pValue) = 0;
virtual _Check_return_ HRESULT put_TitleImpl(_In_opt_ HSTRING value) = 0;
IFACEMETHOD(add_Activated)(_In_ wf::ITypedEventHandler<IInspectable*, xaml::WindowActivatedEventArgs*>* pValue, _Out_ EventRegistrationToken* ptToken) = 0;
+3 -21
View File
@@ -454,27 +454,9 @@ _Check_return_ HRESULT Window::SetCoreWindow(_In_ wuc::ICoreWindow* pCoreWindow)
_Check_return_ HRESULT Window::get_DispatcherQueueImpl(_Outptr_result_maybenull_ msy::IDispatcherQueue** ppValue)
{
if (nullptr == ppValue) { IFC_RETURN(E_INVALIDARG); }
*ppValue = nullptr;
auto xamlCore = GetDXamlCore();
if (xamlCore)
{
auto coreServices = xamlCore->GetHandle();
if (coreServices)
{
auto hostSite = coreServices->GetHostSite();
if (hostSite)
{
auto xcpDispatcher = hostSite->GetXcpDispatcher();
if (xcpDispatcher)
{
*ppValue = xcpDispatcher->GetDispatcherQueueNoRef();
AddRefInterface(*ppValue);
}
}
}
}
// Callable from any thread: the generated getter has no CheckThread, and apps do read it from background threads.
// m_spWindowImpl is only assigned in the constructor, so reading it here is safe.
IFC_RETURN(m_spWindowImpl->get_DispatcherQueueImpl(ppValue));
return S_OK;
}