winforms icon indicating copy to clipboard operation
winforms copied to clipboard

SystemEvents can contain reference to invalid thread and InvalidAsynchronousStateException

Open CortiWins opened this issue 7 months ago • 4 comments

.NET version

Net48, Net8

Did it work in .NET Framework?

No

Did it work in any of the earlier releases of .NET Core or .NET 5+?

No response

Issue description

Short:

Potential application crash by SystemEvents having a reference to an invalid thread used as a SynchronizationContext.

Long:

Microsoft.Win32.SystemEvents has a few different static events that winforms controls can register with.

Examples are:

  • UserPreferenceChanged
  • DisplaySettingsChanged

Internally it is not a pure list of delegates. The SystemEventInvokeInfo contains a delegate and an optional SynchronizationContext.

The SynchronizationContext is of two types

  • System.Threading.SynchronizationContext
  • System.Windows.Forms.WindowsFormsSynchronizationContext

The SynchronizationContext can contain a thread, or the thread can be null. In the constructor, the thread is requested by System.ComponentModel.AsyncOperationManager.SynchronizationContext

So whenever a display setting in Windows changes, the DisplaySettingsChanged event gets fired and the delegates get invoked with a potential to be invoked with a WindowsFormsSynchronizationContext.

The issue: These static events are not only used by instances of controls that register and deregister on disposal.

There are also static types that register and they only register once for that type. Examples are:

  • System.Windows.Forms.Screen
  • System.Drawing.Internal.SystemColorTracker
  • System.Windows.Forms.ProfessionalColors
  • System.Windows.Forms.SystemInforation

And when doing that they create a SynchronizationContext that depends on the thread and the state of the thread.

  • Start/Main Thread in Program.Main()
  • Start/Main Thread within System.Windows.Forms.Application.Run()
  • Any other thread

And while the main thread will stay for the lifetime of the application, other threads may not.

Result: If a thread other than the main thread happens to read from one of the static types like requesting System.Windows.Forms.Screen.PrimaryScreen.WorkingArea before the main thread does, it creates a SynchronizationContext with a dead thread and that can throw an exception later and crash the application when a Windows setting changes that triggers the event.

InvalidAsynchronousStateException(SR.ThreadNoLongerValid) from Stacktrace:

    System.Windows.Forms.dll!System.Windows.Forms.WindowsFormsSynchronizationContext.Send(System.Threading.SendOrPostCallback d, object state) Line 84	C#
    Microsoft.Win32.SystemEvents.dll!Microsoft.Win32.SystemEvents.SystemEventInvokeInfo.Invoke(bool checkFinalization, object[] args) Line 34	C#
    Microsoft.Win32.SystemEvents.dll!Microsoft.Win32.SystemEvents.RaiseEvent(bool checkFinalization, object key, object[] args) Line 844	C#
    Microsoft.Win32.SystemEvents.dll!Microsoft.Win32.SystemEvents.RaiseEvent(object key, object[] args) Line 819	C#
    Microsoft.Win32.SystemEvents.dll!Microsoft.Win32.SystemEvents.OnUserPreferenceChanging(int msg, nint wParam, nint lParam) Line 809	C#
    Microsoft.Win32.SystemEvents.dll!Microsoft.Win32.SystemEvents.WindowProc(nint hWnd, int msg, nint wParam, nint lParam) Line 955	C#
    [Native to Managed Transition]	
    [Managed to Native Transition]	
    Microsoft.Win32.SystemEvents.dll!Interop.User32.DispatchMessageW(ref Interop.User32.MSG msg) Line 859	C#
    Microsoft.Win32.SystemEvents.dll!Microsoft.Win32.SystemEvents.WindowThreadProc() Line 1033	C#

Related Code files:

  • Link: SystemEvents Class https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Win32.SystemEvents/src/Microsoft/Win32/SystemEvents.cs
  • Link: SystemEvents s_handlers https://github.com/dotnet/runtime/blob/e0620bc380fa291867568e653206a9ecb8923d64/src/libraries/Microsoft.Win32.SystemEvents/src/Microsoft/Win32/SystemEvents.cs#L62
  • Link: SystemEventInvokeInfo: https://github.com/dotnet/runtime/blob/e0620bc380fa291867568e653206a9ecb8923d64/src/libraries/Microsoft.Win32.SystemEvents/src/Microsoft/Win32/SystemEvents.cs#L1246

Steps to reproduce

This demo application an enforce the behaviour that would otherwise be possible by accident. It is a mini version of the application where it actually happened and can be used to simulate how the event handlers are setup by changing the injection point in Program.Main.

In this case, it is the SplashScreen-Form on a separate thread that causes the problem because it accesses System.Windows.Forms.Screen before the ui thread does. While in this specific case, it can be avoided now that i know what causes it.

In general, a harmless looking read access to a property (like System.Windows.Forms.Screen.PrimaryScreen.WorkingArea) can link a thread permanently to static types.

DeadEventHandler_V1.zip

CortiWins avatar Jun 13 '25 09:06 CortiWins

@CortiWins when trying to switch .NET SDK 10 there have the exception pops up as below screenshot.

Image

Zheng-Li01 avatar Jun 13 '25 09:06 Zheng-Li01

@Zheng-Li01 Oh yeah, the type of the weakreference was changed. The risks of using reflection.

This one if updated for that DeadEventHandler_V2.zip

CortiWins avatar Jun 13 '25 11:06 CortiWins

Am I correct in understanding that the issue appears to be with both .NET Framework and .NET 8? Or is this something that has come up more recently? @Zheng-Li01 can you try to repro in earlier versions as well? And since we did some significant changes in the last release so confirm .NET 10 if you have a chance.

merriemcgaw avatar Jun 18 '25 20:06 merriemcgaw

@merriemcgaw Yes. Versions i tested:

  • Classic Net Framework 48
  • SDK Style Projects with targetframeworks net8.0-windows;net48

Btw. i "solved" it by forcing the static classes to create events from the mainThread by calling this from Program.Main() at the same spot as Application.EnableVisualStyles(). The hashcodes is to prevent stuff from being optimized away. No idea if that is absolutely required, but it works like that.

    public static int PrepareStaticHandlersOnMainThread()
    {
        // https://github.com/dotnet/runtime/blob/e0620bc380fa291867568e653206a9ecb8923d64/src/libraries/Microsoft.Win32.SystemEvents/src/Microsoft/Win32/SystemEvents.cs#L62

        //// SystemInformation.EnsureSystemEvents called on getting HighContrast.
        var highContrastValue = SystemInformation.HighContrast;

        // Hash existiert nur um zu verhindern, dass Teilergebnisse wegoptimiert werden.
        var hash = highContrastValue.GetHashCode();

        //// Screen.Screens -> SystemEvents.DisplaySettingsChanging
        var allScreensArray = Screen.AllScreens;
        hash |= allScreensArray.GetHashCode();

        //// Screen.DesktopChangedCount -> SystemEvents.UserPreferenceChanged
        var workingArea = Screen.PrimaryScreen?.WorkingArea;
        hash |= workingArea.GetHashCode();

        // System.Windows.Forms.DisplayInformation.DisplaySettingsChanging
        // Initialized in static ctor
        // internal System.Windows.Forms.DisplayInformation
        // https://github.com/dotnet/winforms/blob/main/src/System.Windows.Forms/System/Windows/Forms/Rendering/DisplayInformation.cs
        var colorTable = new ProfessionalColorTable()
        {
            UseSystemColors = true,
        };

        hash |= colorTable.GetHashCode();
        var color = colorTable.ToolStripContentPanelGradientBegin;
        hash |= color.GetHashCode();

        // System.Drawing.KnownColorTable.OnUserPreferenceChanging
        var knownColor = System.Drawing.Color.FromKnownColor(System.Drawing.KnownColor.ActiveBorder);
        hash |= knownColor.GetHashCode();
        var blueChannel = knownColor.B; //// Accesses Value, Accesses IsKnownColor
        hash |= blueChannel.GetHashCode();

        return hash;
    }

@KlausLoeffelmann Guten Morgen Klaus!

CortiWins avatar Jun 18 '25 22:06 CortiWins