Gracefully handling corrupted state exceptions

前端 未结 3 1760
遇见更好的自我
遇见更好的自我 2020-11-27 17:50

Related to this question, I would like to force CLR to let my .NET 4.5.2 app catch Corrupted State Exceptions, for the sole purpose of logging them and then terminating the

相关标签:
3条回答
  • 2020-11-27 18:32

    Instead of using <legacyCorruptedStateExceptionsPolicy> it would be better to use [HandleProcessCorruptedStateExceptions] (and [SecurityCritical]) as stated here:

    https://msdn.microsoft.com/en-us/magazine/dd419661.aspx

    Following that, your Main method should look something like this:

    [HandleProcessCorruptedStateExceptions, SecurityCritical]
    static void Main(string[] args)
    {
        try
        {
            ...
        }
        catch (Exception ex)
        {
            // Log the CSE.
        }
    }
    

    But be aware that this doesn't catch the more serious exceptions like StackOverflowException and ExecutionEngineException.

    Also finally of involved try blocks will not be executed:

    https://csharp.2000things.com/2013/08/30/920-a-finally-block-is-not-executed-when-a-corrupted-state-exception-occurs/

    For other unhandled appdomain exceptions you can use :

    • AppDomain.CurrentDomain.UnhandledException
    • Application.Current.DispatcherUnhandledException
    • TaskScheduler.UnobservedTaskException

    (Please do a search for the details when a specific handler is appropriate for your situation. TaskScheduler.UnobservedTaskException for example is a bit tricky.)

    If you don't have access to the Main method, you can also mark your AppDomain exception handler to catch the CSE:

    AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
    
    ...
    
    [HandleProcessCorruptedStateExceptions, SecurityCritical]
    private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        // AccessViolationExceptions will get caught here but you cannot stop
        // the termination of the process if e.IsTerminating is true.
    }
    

    The last line of defense could be an unmanaged UnhandledExceptionFilter like this:

    [DllImport("kernel32"), SuppressUnmanagedCodeSecurity]
    private static extern int SetUnhandledExceptionFilter(Callback cb);
    // This has to be an own non generic delegate because generic delegates cannot be marshalled to unmanaged code.
    private delegate uint Callback(IntPtr ptrToExceptionInfo);
    

    And then somewhere at the beginning of your process:

    SetUnhandledExceptionFilter(ptrToExceptionInfo =>
    {
        var errorCode = "0x" + Marshal.GetExceptionCode().ToString("x2");
        ...
        return 1;
    });
    

    You can find more information about the possible return codes here:

    https://msdn.microsoft.com/en-us/library/ms680634(VS.85).aspx

    A "specialty" of the UnhandledExceptionFilter is that it isn't called if a debugger is attached. (At least not in my case of having a WPF app.) So be aware of that.

    If you set all the appropriate ExceptionHandlers from above, you should be logging all exceptions that can be logged. For the more serious exceptions (like StackOverflowException and ExecutionEngineException) you have to find another way because the whole process is unusable after they happened. A possible way could perhaps be another process that watches the main process and logs any fatal errors.

    Additional hints:

    • In the AppDomain.CurrentDomain.UnhandledException you can safely cast the e.ExceptionObject to Exception without having to worry - at least if you don't have any IL code that throws other objects than Exception: Why is UnhandledExceptionEventArgs.ExceptionObject an object and not an Exception?
    • If you want to suppress the Windows Error Reporting dialog, you can take a look here: How to terminate a program when it crashes? (which should just fail a unit test instead of getting stuck forever)
    • If you have a WPF application with multiple dispatchers you can also use a Dispatcher.UnhandledException for the other dispatchers.
    0 讨论(0)
  • 2020-11-27 18:32

    Thanks to @haindl for pointing out that you can also decorate handler methods with the [HandleProcessCorruptedStateExceptions]1 attribute, so I made a little test app just to confirm if things really work as they are supposed to.

    1 Note: Most answers state that I should also include the [SecurityCritical] attribute, although in the tests below omitting it didn't change the behavior (the [HandleProcessCorruptedStateExceptions] alone seemed to work just fine). However, I will leave both attributes below since I am presuming all these folks knew what they were saying. That's a school example of "Copied from StackOverflow" pattern in action.

    The idea is, obviously, to remove the <legacyCorruptedStateExceptionsPolicy> setting from app.config, i.e. only allow our outermost (entry-level) handler(s) to catch the exception, log it, and then fail. Adding the setting will allow your app to continue, if you catch the exception in some inner handler, and this is not what you want: the idea is just to get the accurate exception info and then die miserably.

    I used the following method to throw the exception:

    static void DoSomeAccessViolation()
    {
        // if you have any questions about why this throws,
        // the answer is "42", of course
    
        var ptr = new IntPtr(42);
        Marshal.StructureToPtr(42, ptr, true);
    }
    

    1. Catching exceptions from Main:

    [SecurityCritical]
    [HandleProcessCorruptedStateExceptions]
    static void Main(string[] args)
    {
        try
        {
            DoSomeAccessViolation();
        }
        catch (Exception ex)
        {
            // this will catch all CSEs in the main thread
            Log(ex);
        }
    }
    

    2. Catching all exceptions, including background threads/tasks:

    // no need to add attributes here
    static void Main(string[] args)
    {
        AppDomain.CurrentDomain.UnhandledException += UnhandledException;
    
        // throw on a background thread
        var t = new Task(DoSomeAccessViolation);
        t.Start();
        t.Wait();
    }
    
    // but it's important that this method is marked
    [SecurityCritical]
    [HandleProcessCorruptedStateExceptions]
    private static void UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        // this will catch all unhandled exceptions, including CSEs
        Log(e.ExceptionObject as Exception);
    }
    

    I would recommend using just the latter approach, and removing the [HandleProcessCorruptedStateExceptions] from all other places to make sure the exception doesn't get caught at the wrong place. I.e. if you have a try/catch block somewhere and an AccessViolationException is thrown, you want CLR to skip the catch block and propagate to the UnhandledException before ending the app.

    0 讨论(0)
  • 2020-11-27 18:46

    Is party over? not so fast

    Microsoft: "Use application domains to isolate tasks that might bring down a process."

    The program below will protect your main application/thread from unrecoverable failures without risks associated with use of HandleProcessCorruptedStateExceptions and <legacyCorruptedStateExceptionsPolicy>

    public class BoundaryLessExecHelper : MarshalByRefObject
    {
        public void DoSomething(MethodParams parms, Action action)
        {
            if (action != null)
                action();
            parms.BeenThere = true; // example of return value
        }
    }
    
    public struct MethodParams
    {
        public bool BeenThere { get; set; }
    }
    
    class Program
    {
        static void InvokeCse()
        {
            IntPtr ptr = new IntPtr(123);
            System.Runtime.InteropServices.Marshal.StructureToPtr(123, ptr, true);
        }
        // This is a plain code that will prove that CSE is thrown and not handled
        // this method is not a solution. Solution is below 
        private static void ExecInThisDomain()
        {
            try
            {
                var o = new BoundaryLessExecHelper();
                var p = new MethodParams() { BeenThere = false };
                Console.WriteLine("Before call");
    
                o.DoSomething(p, CausesAccessViolation);
                Console.WriteLine("After call. param been there? : " + p.BeenThere.ToString()); //never stops here
            }
            catch (Exception exc)
            {
                Console.WriteLine($"CSE: {exc.ToString()}");
            }
            Console.ReadLine();
        }
    
        // This is a solution for CSE not to break your app. 
        private static void ExecInAnotherDomain()
        {
            AppDomain dom = null;
    
            try
            {
                dom = AppDomain.CreateDomain("newDomain");
                var p = new MethodParams() { BeenThere = false };
                var o = (BoundaryLessExecHelper)dom.CreateInstanceAndUnwrap(typeof(BoundaryLessExecHelper).Assembly.FullName, typeof(BoundaryLessExecHelper).FullName);         
                Console.WriteLine("Before call");
    
                o.DoSomething(p, CausesAccessViolation);
                Console.WriteLine("After call. param been there? : " + p.BeenThere.ToString()); // never gets to here
            }
            catch (Exception exc)
            {
                Console.WriteLine($"CSE: {exc.ToString()}");
            }
            finally
            {
                AppDomain.Unload(dom);
            }
    
            Console.ReadLine();
        }
    
    
        static void Main(string[] args)
        {
            ExecInAnotherDomain(); // this will not break app
            ExecInThisDomain();  // this will
        }
    }
    
    0 讨论(0)
提交回复
热议问题