SystemEvents can contain reference to invalid thread and InvalidAsynchronousStateException
.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.
@CortiWins when trying to switch .NET SDK 10 there have the exception pops up as below screenshot.
@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
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 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!