Skip to content

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

Description

@m-smkim

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 on

So, 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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