WindowChromeWorker._ExtendGlassFrame dereferences HwndSource.CompositionTarget without a null check (NullReferenceException during window creation)
Description
WindowChromeWorker._ExtendGlassFrame() dereferences _hwndSource.CompositionTarget without a null check. HwndSource.CompositionTarget returns null when the HwndSource is disposed or when its HwndTarget has been disposed independently, so this throws a NullReferenceException while a window is being created.
The guard in _UpdateFrameState does not cover this case:
if (IntPtr.Zero == _hwnd || _hwndSource.IsDisposed) // WindowChromeWorker.cs:703
{
return;
}It checks the HwndSource, but CompositionTarget also returns null based on the state of a different object (_hwndTarget). So _hwndSource.IsDisposed == false and CompositionTarget == null can hold at the same time, and the guard lets execution through.
This is the sibling of #11440. That issue reported a COMException from DwmExtendFrameIntoClientArea a few lines later in the same method, and was addressed by the crash containment in #11417. The null dereference that happens before that call is still unguarded.
Unguarded dereference sites on main:
| Location | Method |
|---|---|
WindowChromeWorker.cs:942 |
_ExtendGlassFrame — DWM-disabled branch |
WindowChromeWorker.cs:952 |
_ExtendGlassFrame — DWM-enabled branch |
WindowChromeWorker.cs:998 |
_ExtendGlassFrame |
WindowChromeWorker.cs:1142 |
_RestoreGlassFrame |
Reproduction Steps
I have not been able to reproduce this deterministically — it is a race, and it was observed in production rather than in a test.
What can be stated precisely is the exposure. Any window whose WindowChrome has a non-zero GlassFrameThickness runs the unguarded code on every window creation, because _ApplyNewCustomChrome() calls _UpdateFrameState(force: true), and force bypasses the state-change check:
_isGlassEnabled = frameState && _chromeInfo.GlassFrameThickness != default(Thickness);
if (!_isGlassEnabled) { _SetRoundingRegion(null); }
else { _ClearRoundingRegion(); _ExtendGlassFrame(); } // <-- always reached while DWM composition is onSo, with a style such as:
<Setter Property="shell:WindowChrome.WindowChrome">
<Setter.Value>
<shell:WindowChrome CaptionHeight="27" ResizeBorderThickness="5"
GlassFrameThickness="5" CornerRadius="5"/>
</Setter.Value>
</Setter>calling ShowDialog() at a moment when the render target is being torn down or recreated hits the null dereference. We suspect a DWM composition state change (remote-desktop reconnect, session lock/unlock, GPU driver reset) — the same circumstances reported in #11440.
Expected behavior
_ExtendGlassFrame() returns without doing anything when there is no composition target, consistent with the crash containment introduced in #11417.
Actual behavior
NullReferenceException. It is thrown inside Window.CreateSourceWindow, i.e. from within Window.ShowDialog(), so application code cannot meaningfully handle it.
System.NullReferenceException: Object reference not set to an instance of an object.
at System.Windows.Shell.WindowChromeWorker._ExtendGlassFrame()
at System.Windows.Shell.WindowChromeWorker._UpdateFrameState(Boolean force)
at System.Windows.Shell.WindowChromeWorker._ApplyNewCustomChrome()
at System.Windows.Window.CreateSourceWindow(Boolean duringShow)
at System.Windows.Window.ShowHelper(Object booleanBox)
at System.Windows.Window.ShowDialog()
at MyApp.CustomPopupWindow.ShowDialog() <-- application code below this point
at MyApp.SomeScreen.OpenDetailPopup()Regression?
No. This code path looks unchanged since the WPF Shell Integration Library was folded into the framework.
Known Workarounds
Set GlassFrameThickness="0" on the WindowChrome. _UpdateFrameState then computes _isGlassEnabled == false and _ExtendGlassFrame() is never called, so the crash path becomes unreachable.
The cost is losing the extended glass frame. _SetRoundingRegion is called instead, which applies a window region via SetWindowRgn — and per the rounded corners guidance, a window with a region can never be rounded by DWM, so this also opts the window out of Windows 11 corner rounding.
Impact
Affects any WPF application that uses WindowChrome with a non-zero GlassFrameThickness — a common pattern for custom title bars, including several third-party WPF UI libraries. Because _ApplyNewCustomChrome forces _UpdateFrameState(force: true), every window creation goes through the unguarded code, so the exposure is not limited to unusual windows. The crash cannot be handled by the application.
Configuration
- .NET 10.0 (
net10.0-windows), WPF - Windows 11, x64
- Appears correlated with environments where DWM composition state changes (remote desktop / VDI), but we cannot confirm this.
Other information
Suggested fix, in the same spirit as the containment added in #11417:
private void _ExtendGlassFrame()
{
Assert.IsNotNull(_window);
if (IntPtr.Zero == _hwnd)
{
// Can't do anything with this call until the Window has been shown.
return;
}
// The composition target can be gone even when the HwndSource itself is not disposed,
// so _UpdateFrameState's _hwndSource.IsDisposed check is not sufficient here.
if (_hwndSource?.CompositionTarget is null)
{
return;
}
// ... unchanged
}_RestoreGlassFrame (line 1142) needs the same treatment.
Related: #11440, #11417.
Source: dotnet/wpf