Introduction
Imagine the perfect user for your application. He/she never does anything unexpected, never clicks any wrong buttons and always chooses the right steps. That would be an awesome world to live in. But we all know that people make mistakes, click the wrong button, use software in the manner it wasn’t intended to, etc. All these fault conditions lead to an undesired state of the application. It is precisely such anomaly conditions in application behavior that leads to programmers using exceptions to make their applications resilient. However in their quest to make their software more resilient, they also end up trapping exceptions which ideally should not be handled by their code.
Consider the following example:
try { string fileName = FunctionToGetFileName(); FileStream fs = File.Create(fileName); } catch(Exception ex) { Console.WriteLine("File could not be created"); }
Here an exception can be raised for either of the following reasons:
- FunctionToGetFileName might throw an exception
- File.Create fails because of invalid file name or lack of disk space
- While executing the second argument, we run out of stack space to store the local “fs”.
In essence, throwing an exception in the above code should not be interpreted as failure to create the file. However we see this pattern in a lot of applications today. Application developers in their quest to make their applications fool-proof fall prey to the trap of not checking for specific exceptions.
This behavior also resulted in use of “catch(Exception ex)
” to trap exceptions which were not in user code but in application hygiene (like state of stack, access violations). This resulted in applications running while being in the possible corrupt state where an outright failure would be more preferable to prevent loss of data integrity.
Types of Exceptions
Managed applications can have two types of exceptions. One is the user mode exceptions, which arise of faulty logic in application code and are raised by the application. The other kind of exceptions is SEH exceptions (Structured Exception Handling) which refer to context outside of the application. Examples would include access violation and stack overflow. SEH exceptions indicate that the process integrity is compromised and the application can no longer be considered to be in a stable state.
Corrupted State Exceptions
In .NET Framework 4.0, the CLR is now marking those exceptions which can corrupt the process state as corrupted state exceptions and are treated differently from other exceptions. By default, such exceptions are no longer delivered to the CLR. This implies that these exceptions can no longer be trapped in a catch block by user code. This is done to ensure process and data integrity. If the state of process is compromised, you don’t want to continue running lest your data gets corrupted.
How it worked in earlier versions of the .NET Frramework – To see the change in the behavior, let us consider the behavior of an SEH exception – access violation in pinvoked code.
Consider that we have native dll with the following code:
#include <Windows.h> #include <stdio.h> #include <tchar.h> #include <strsafe.h> #include <cstdio> #define BUF_SIZE 255 #define MAX_THREADS 8 typedef struct MyData { int val1; int val2; } MYDATA, *PMYDATA; BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch(ul_reason_for_call) { case DLL_PROCESS_ATTACH: break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; } return TRUE; } extern public "C" __declspec(dllexport) int PinvokeAV() { int *p ; p = 0; *p = 12; return 0; }
Listing 1 – pinvokeav.cpp
To compile the above code, open Microsoft Visual Studio Command Prompt and type the code above into a file called pinvokeav.cpp
and type the following:
cl /ld pinvokeav.cpp
This will create a native DLL called pinvokeav.dl
l
Now create a v2.0 managed application which refers to this DLL.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Runtime.InteropServices; namespace CorruptedStateExceptionsDemo { class OldBehavior { [DllImport("PinvokeAV.dll")] public static extern void PinvokeAV(); static void Main(string[] args) { try { PinvokeAV(); } catch(Exception ex) { Console.WriteLine("Caught an exception of type" + ex.GetType().Name); } } } }
Listing 2 – OldBehavior.cs
To compile this, execute the following command line (double check to ensure that you compile this against v2.0 CLR and not V4.0
c:windowsmicrosoft.netframeworkv3.5csc.exe OldBehavior.cs
Now make a clone of OldBehavior.cs
and call it NewBehavior.cs
. To differentiate it from OldBehavior.cs
, rename the class in NewBehavior.cs
to NewBehavior
.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Runtime.InteropServices; namespace CorruptedStateExceptionsDemo { class NewBehavior { [DllImport("PinvokeAV.dll")] public static extern void PinvokeAV(); static void Main(string[] args) { try { PinvokeAV(); } catch(Exception ex) { Console.WriteLine("Caught an exception of type" + ex.GetType().Name); } } } }
Listing 3 – NewBehavior.cs
Compile NewBehavior.cs against v4.0 CLR using the command line below.
c:windowsmicrosoft.netframeworkv4.0.30319csc.exe newBehavior.cs
Observing the change in behavior between pre- V4.0 and v4.0 managed application,
now put Pinvokeav.dll
in the same folder as OldBehavior.exe
and NewBehavior.exe
.
Execute OldBehavior.exe
You will see that the AV generated inside of PinvokeAV.dll
is catch in the user code and the following line is printed.
"Caught an exception of typeAccessViolationException"
However when you execute NewBehavior.exe
, the thrown inside of pinvokeav.dll
does not get handled by the catch block and the application terminates with the following output.
Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt. at CorruptedStateExceptionsDemo.NewBehavior.PinvokeAV() at CorruptedStateExceptionsDemo.NewBehavior.Main(String[] args)
What changed? The SEH exception in v4.0 is a Corrupted State exception and hence the user mode code is no longer allowed to handle this exception inside a catch block and hence the application is terminated.
Maintaining Legacy Behavior via HandleProcessCorruptedStateExceptionsAttribute
If you have a dependency on the legacy behavior, there is an attribute you must use to tag your code to ensure that you get the legacy behavior and your code can continue to trap SEH exceptions when compiled for v4.0. It is a very strong recommendation that you do not do that since it is against good programming practices.
In lieu of the attribute, you can also add an element <legacycorruptedstateexceptionspolicy>
in the application configuration file to preserve the legacy behavior.
Summary
CLR’s handling of SEH exceptions which compromise process state have changed in .NET 4.0 and you can no longer trap these exceptions in your code. Hopefully this article has provided enough information to code your application defensively.