winforms: Hang during exit if `WM_SETTINGCHANGE` is received

.NET version

7.0.9-servicing.23321.6+dd121ee97234c7490f6111cf0c7bfa9235923f81

Did it work in .NET Framework?

Yes

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

Due to the race condition nature of the issue, it is unclear whether the issue exists at a lower frequency or does not exist on prior versions. I was unable to reproduce the issue in .Net 4.7.2 or net5. The issue occurs reliably with .net 7.

Issue description

When a WM_SETTINGCHANGE message is received after the main form of an application has started closing, the application hangs. The use case where this happens reliably is when the window is an App Bar, as it will always generate a desktop change when the window closes.

Apparent timeline of events:

  1. On main thread, in closing the only Form, a change to the desktop layout is triggered (specifically due to removing an AppBar with SHAppBarMessage(ABM_REMOVE, ...))
  2. The main thread begins shutting down (disposing controls, form, etc…)
  3. The “.NET System Events” thread receives WM_SETTINGCHANGE and Invokes to the main thread
  4. Since the main thread has not shutdown, the test at WindowsFormsSynchronizationContext.Send:82 succeeds
  5. The “.Net System Events” thread enters a WaitHandle
  6. The main thread, having had its main form closed, exits the message loop and Main returns
  7. Hang

Call stacks at hang:

Main Thread (no .Net stack, only native)

win32u.dll!NtUserMsgWaitForMultipleObjectsEx()
combase.dll!CCliModalLoop::BlockFn(void * * ahEvent, unsigned long cEvents, unsigned long * lpdwSignaled) Line 2108
combase.dll!ClassicSTAThreadWaitForHandles(unsigned long dwFlags, unsigned long dwTimeout, unsigned long cHandles, void * * pHandles, unsigned long * pdwIndex) Line 54
combase.dll!CoWaitForMultipleHandles(unsigned long dwFlags, unsigned long dwTimeout, unsigned long cHandles, void * * pHandles, unsigned long * lpdwindex) Line 126
[Inline Frame] hostpolicy.dll!coreclr_t::shutdown(int *) Line 152
hostpolicy.dll!run_app_for_context(const hostpolicy_context_t & context, int argc, const wchar_t * * argv) Line 264
...snip...

.NET System Events thread

System.Private.CoreLib.dll!System.Threading.WaitHandle.WaitOneNoCheck(int millisecondsTimeout) Line 139
	at /_/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs(139)
System.Windows.Forms.dll!System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle waitHandle) Line 3967
	at /_/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs(3967)
System.Windows.Forms.dll!System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control caller, System.Delegate method, object[] args, bool synchronous) Line 7141
	at /_/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs(7141)
System.Windows.Forms.dll!System.Windows.Forms.Control.Invoke(System.Delegate method, object[] args) Line 6587
	at /_/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs(6587)
System.Windows.Forms.dll!System.Windows.Forms.WindowsFormsSynchronizationContext.Send(System.Threading.SendOrPostCallback d, object state) Line 88
	at /_/src/System.Windows.Forms/src/System/Windows/Forms/WindowsFormsSynchronizationContext.cs(88)
Microsoft.Win32.SystemEvents.dll!Microsoft.Win32.SystemEvents.SystemEventInvokeInfo.Invoke(bool checkFinalization, object[] args) Line 35
	at Microsoft.Win32\SystemEvents.cs(35)
Microsoft.Win32.SystemEvents.dll!Microsoft.Win32.SystemEvents.RaiseEvent(bool checkFinalization, object key, object[] args) Line 850
	at Microsoft.Win32\SystemEvents.cs(850)
	locals:
		array[0] 
			._delegate = System.Windows.Forms.VisualStyles.VisualStyleRenderer.OnUserPreferenceChanging
			._syncCtx = System.Windows.Forms.WindowsFormsSynchronizationContext
Microsoft.Win32.SystemEvents.dll!Microsoft.Win32.SystemEvents.WindowProc(nint hWnd, int msg, nint wParam, nint lParam) Line 961
	at Microsoft.Win32\SystemEvents.cs(961)
	locals:
		msg = 8218
		wParam = 0x2f
		lParam = 0
[Native to Managed Transition]
[Managed to Native Transition]
Microsoft.Win32.SystemEvents.dll!Interop.User32.DispatchMessageW.____PInvoke|210_0(Interop.User32.MSG* msg)
Microsoft.Win32.SystemEvents.dll!Microsoft.Win32.SystemEvents.WindowThreadProc() Line 1038
	at Microsoft.Win32\SystemEvents.cs(1038)

Notes:

Steps to reproduce

Clone either the appbar sample or the minimal SendMessageTimeout sample. Run the application and close the window which opens. After waiting a few seconds, pause the debugger to observe that the main thread has exited .Net user code and is hung waiting for ???, and that .NET System Events is hung on an invoke to the now destroyed main thread.

The minimal sample can be prepared from an empty winforms project by adding an OnHandleDestroyed override to Form1:

    [DllImport("USER32.dll", ExactSpelling = true, EntryPoint = "SendMessageTimeoutW", SetLastError = true)]
    internal static extern nint SendMessageTimeout(nint hWnd, uint Msg, nint wParam, nint lParam, uint fuFlags, uint uTimeout, IntPtr lpdwResult);

    protected override void OnHandleDestroyed(EventArgs e)
    {
        base.OnHandleDestroyed(e);

        // Simulate bad luck in timing or an appbar exiting
        var lp = Marshal.StringToHGlobalUni("TapsEnabled");
        SendMessageTimeout(0x0000FFFF /* HWND_BROADCAST */, 26 /* WM_SETTINGCHANGE */, 0, lp, 0, 1000, (IntPtr)0);
    }

Alternatively, examine the dump file of the hung appbar sample.

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 17 (12 by maintainers)

Commits related to this issue

Most upvoted comments

Verified this issue on release/7.0 branch of Winforms repo, it was fixed. After closing form, it ends debug mode.

https://github.com/dotnet/winforms/assets/108860782/7e3d92a8-341d-47f0-b9c5-8518221f54ab

@mgaffigan Quick update, we were able to service this 😃 I believe this should be available in 7.0.12 in October.

@lonitra I re-tested it again in .Net 7.0 and .Net 8.0 builds several times, they are definitely different - Same as yours. I’m questioning the results I saw earlier(Maybe changing the target version to 8.0 didn’t work.).

.Net 7.0 behavior: after closing form, applicaion is keeping in debug mode. Net7 0Behavior

.Net 8.0 behavior: after closing form, it ends debug mode. Looks like this issue is not reproduced in .Net 8. Net8 0Behavior