WindowChromeWorker._ExtendGlassFrame dereferences HwndSource.CompositionTarget without a null check (NullReferenceException during window creation)
- Dominant language
- C#
- Stars
- 7.7k
- Forks
- 1.3k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 61
Description
### 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:
```csharp
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:
```csharp
_isGlassEnabled = frameState && _chromeInfo.GlassFrameThickness != default(Thickness);
if (!_isGlassEnabled) { _SetRoundingRegion(null); }
else { _ClearRoundingRegion(); _ExtendGlassFrame(); } // <-- always reached while DWM composition is on
```
So, with a style such as:
```xml
```
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](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/ui/apply-rounded-corners), 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:
```csharp
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.
Contributor guide
Research direction
Start in WindowChromeWorker.cs by reading _ExtendGlassFrame and _RestoreGlassFrame, then compare their composition-target handling with the containment added for #11417. Confirm the existing _UpdateFrameState path and the window-creation entry point before making the change. Done means both methods return safely when CompositionTarget is null without changing normal glass-frame behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100