1
votes

I have a HttpHandler which looks like this:

public class GoodIISHandler : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        try
        {
            try
            {
                context.Response.End();
            }
            catch (Exception)
            {
            }
        }
        finally
        {
            Console.WriteLine("Inside finally block");
        }
    }

    public bool IsReusable => true;
}

It works great! Except when compiled in Release mode with optimizations enabled and then executed (on Windows Server 2012 running IIS 8 with .NET 4.5.2). Then it crashes IIS.

An unhandled exception occurred and the process was terminated.

Application ID: /LM/W3SVC/1592535739/ROOT/HttpHandlerApp

Process ID: 648

Exception: System.Threading.ThreadAbortException

Message: Thread was being aborted.

StackTrace: at System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context)

at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)

at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)

and

Faulting application name: w3wp.exe, version: 8.0.9200.16384, time stamp: 0x50108835

Faulting module name: KERNELBASE.dll, version: 6.2.9200.17366, time stamp: 0x554d4531

Exception code: 0xe0434352

Fault offset: 0x000000000004aea8

Faulting process id: 0x288

Faulting application start time: 0x01d1ec2c7db735a6

Faulting application path: c:\windows\system32\inetsrv\w3wp.exe

Faulting module path: C:\Windows\system32\KERNELBASE.dll

Report Id: bb7471fa-581f-11e6-93f1-00155d461b1c

Faulting package full name:

Faulting package-relative application ID:

I have narrowed the issue down to a particular bit of IL which changes when optimization is enabled/disabled.

Here is the IL for ProcessRequest which does not crash IIS:

.method public hidebysig newslot virtual final 
        instance void  ProcessRequest(class [System.Web]System.Web.HttpContext context) cil managed
{
  // Code size       30 (0x1e)
  .maxstack  1
  .try
  {
    .try
    {
      IL_0000:  ldarg.1
      IL_0001:  callvirt   instance class [System.Web]System.Web.HttpResponse [System.Web]System.Web.HttpContext::get_Response()
      IL_0006:  callvirt   instance void [System.Web]System.Web.HttpResponse::End()
      IL_000b:  leave.s    IL_0010

    }  // end .try
    catch [mscorlib]System.Exception 
    {
      IL_000d:  pop
      IL_000e:  leave.s    IL_0010

    }  // end handler
    IL_0010:  leave.s    IL_001d

  }  // end .try
  finally
  {
    IL_0012:  ldstr      "Inside finally block"
    IL_0017:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_001c:  endfinally
  }  // end handler
  IL_001d:  ret
} // end of method GoodIISHandler::ProcessRequest

To make IIS crash, change line 19 to

        IL_000e:  leave.s    IL_001d

This change makes the nested catch block exit directly to the method return instruction without visiting the single instruction remaining in the try block of the try..finally.

My hypothesis is that the crash relates to the magic auto-rethrow behaviour of ThreadAbortException (one of which context.Response.End() will throw) and how the CLR and/or IIS and/or ASP.NET manages that; and so when the optimised IL exits from the catch block directly to the return instruction, some hypothetical abort reset magic which the CLR/IIS/ASP.NET slotted in at IL_0010 gets missed out and the ThreadAbortException takes down the whole of IIS.

There are clues scattered around the internet about this behaviour (e.g. these msdn docs state ThreadAbortException is a special exception that can be caught, but it will automatically be raised again at the end of the catch block. and this blog mentions ... notably ASP.NET, even aborts individual threads routinely without unloading the domain. They backstop the ThreadAbortExceptions, call ResetAbort on the thread and reuse it or return it to the CLR ThreadPool.) but I cannot find anything concrete about the mechanism.

So I guess my question is: what is going on here? Have I really discovered a CLR or IIS or ASP.NET bug to do with this thread abort mechanism being defeated by an IL optimisation?


Fully scripted repro can be found at https://github.com/jamezor/HttpHandlerRepro for anyone interested.

1
Which version of .NET Framework did you use? 4.6.0 is known to have some issues, and you should disable RyuJIT to workaround some.Lex Li
We are running with .NET 4.5.2 so cannot blame 4.6 unfortunately :)Jamezor

1 Answers

1
votes

Don't use Response.End() to stop processing the request, it's better to use HttpApplication.CompleteRequest(). Take a look at these posts: