dotnet / dotnet/wpf

WindowChromeWorker._ExtendGlassFrame dereferences HwndSource.CompositionTarget without a null check (NullReferenceException during window creation)

Open
#11,906 1 comment 0 reactions 0 assignees View on GitHub
Bug Cost:S Priority:1
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.