Why does Environment.Exit() not terminate the program any more?

This is something I discovered just a few days ago, I got confirmation that it isn't just limited to my machine from this question.

The easiest way to repro it is by starting a Windows Forms application, add a button and write this code:

    private void button1_Click(object sender, EventArgs e) {
        MessageBox.Show("yada");
        Environment.Exit(1);         // Kaboom!
    }

The program fails after the Exit() statement executes. On Windows Forms you get "Error creating window handle".

Enabling unmanaged debugging makes it somewhat clear what's going on. The COM modal loop is executing and allows a WM_PAINT message to be delivered. That's fatal on a disposed form.

The only facts I've gathered so far are:

  • It isn't just limited to running with the debugger. This also fails without one. Rather poorly as well, the WER crash dialog shows up twice.
  • It doesn't have anything to do with the bitness of the process. The wow64 layer is pretty notorious, but an AnyCPU build crashes the same way.
  • It doesn't have anything to do with the .NET version, 4.5 and 3.5 crash the same way.
  • The exit code doesn't matter.
  • Calling Thread.Sleep() before calling Exit() doesn't fix it.
  • This happens on the 64-bit version of Windows 8, and Windows 7 does not seem to be affected the same way.
  • This should be relatively new behavior, I haven't seen this before. I see no relevant updates delivered through Windows Update, albeit that the update history isn't accurate on my machine any more.
  • This is grossly breaking behavior. You would write code like this in an event handler for AppDomain.UnhandledException, and it crashes the same way.

I'm particularly interested in what you could possibly do to avoid this crash. Particularly the AppDomain.UnhandledException scenario stumps me; there are not a lot of ways to terminate a .NET program. Please do note that calling Application.Exit() or Form.Close() are not valid in an event handler for UnhandledException, so they are not workarounds.


UPDATE: Mehrdad pointed out that the finalizer thread could be part of the problem. I think I'm seeing this and am also seeing some evidence for the 2 second timeout that the CLR gives the finalizer thread to finish executing.

The finalizer is inside NativeWindow.ForceExitMessageLoop(). There's an IsWindow() Win32 function there that roughly corresponds with the code location, offset 0x3c when looking at the machine code in 32-bit mode. It seems that IsWindow() is deadlocking. I cannot get a good stack trace for the internals however, the debugger thinks the P/Invoke call just returned. This is hard to explain. If you can get a better stack trace then I'd love to see it. Mine:

System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.ForceExitMessageLoop() + 0x3c bytes
System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.Finalize() + 0x16 bytes
[Native to Managed Transition]
kernel32.dll!@BaseThreadInitThunk@12()  + 0xe bytes
ntdll.dll!___RtlUserThreadStart@8()  + 0x27 bytes
ntdll.dll!__RtlUserThreadStart@8()  + 0x1b bytes

Nothing above the ForceExitMessageLoop call, unmanaged debugger enabled.


Solution 1:

I contacted Microsoft about this problem and that seemed to have paid off. At least I'd like to think it did :). Although I didn't get a confirmation of a resolution back from them, the Windows group is difficult to contact directly and I had to use an intermediary.

An update delivered through Windows Update solved the problem. The noticeable 2 second delay before the crash is no longer present, strongly suggesting that the IsWindow() deadlock got solved. And the program shuts down cleanly and reliably. The update installed patches for Windows Defender, wdboot.sys, wdfilter.sys, tcpip.sys, rpcrt4.dll, uxtheme.dll, crypt32.dll and wintrust.dll

Uxtheme.dll is the odd-duck out. It implements the Visual Styles theming API and is used by this test program. I can't be sure, but my money is on that one as the source of the problem. The copy in C:\WINDOWS\system32 has version number 6.2.9200.16660, created on August 14th, 2013 on my machine.

Case closed.

Solution 2:

I don't know why it doesn't work "any more", but I think Environment.Exit executes pending finalizers. Environment.FailFast doesn't.

It might be that (for some bizarre reason) you have weird pending finalizers that must run afterward, causing this to happen.