隔离AppDomain中抛出的异常以免崩溃应用程序

时间:2015-06-12 16:48:29

标签: c# .net appdomain

TL; DR:如何隔离外接程序异常以杀死主进程?

我希望有一个非常稳定的.Net应用程序,它在AppDomain中运行不太稳定的代码。这似乎是AppDomain的首要目的之一(好吧,那和安全沙箱),但它似乎不起作用。

例如AddIn.exe

public static class Program
{
    public static void Main(string[] args)
    {
        throw new Exception("test")
    }
}

用我的'稳定'代码调用:

var domain = AppDomain.CreateDomain("sandbox");
domain.UnhandledException += (sender, e) => { 
    Console.WriteLine("\r\n ## Unhandled: " + ((Exception) e.ExceptionObject).Message);
};
domain.ExecuteAssemblyByName("AddIn.exe", "arg A", "arg B")

AppDomain中抛出的异常会直接传递给创建域的应用程序。我可以使用domain.UnhandledException记录它们并在包装器应用程序中捕获它们。

但是,抛出了更多有问题的异常,例如:

public static class Program
{
    public static void Main(string[] args)
    {
        Stackoverflow(1);
    }

    static int Stackoverflow(int x)
    {
        return Stackoverflow(++x);
    }
}

这将抛出stackoverflow异常,每次都会终止整个应用程序。它甚至不会触发domain.UnhandledException - 它只会直接杀死整个应用程序。

此外,在Environment.Exit()内调用AppDomain之类的内容也会导致父应用程序失效,请不要通过GO,不要收取200英镑,也不要运行任何~Finialiser或{ {1}}。

从这一点看来,Dispose()从根本上没有做它声称的(或者它看起来要声称的)要做的事情,因为它只是将所有异常直接传递到父域,使得它无用隔离并且对于任何类型的安全都非常弱(如果我可以取出父进程,我可能会破坏机器)。这在.Net中是一个非常根本的失败,所以我必须在我的代码中遗漏一些东西。

我错过了什么吗?是否有某种方法可以使AppDomain实际上隔离它正在运行的代码并在发生错误时卸载?我是否使用了错误的东西,是否有其他.Net功能可以提供异常隔离?

2 个答案:

答案 0 :(得分:6)

您无法对Environment.Exit()执行任何操作,就像您无法阻止用户在任务管理器中终止您的进程一样。对此的静态分析也可以被规避。我不会太担心这个。你可以做的事情,你真的不能做的事情。

AppDomain确实做了它声称要做的事情。但是,它实际上声称要执行的操作以及您认为声明要做的事情是两件不同的事情。

任何地方未处理的例外情况都会导致您的申请失效。 AppDomains不能防范这些。但是你可以阻止未处理的异常通过以下方式跨越AppDomain边界(抱歉,没有代码)

  1. 创建您的AppDomain
  2. 在此AppDomain中加载和解包您的插件控制器
  3. 通过此控制器控制插件,
  4. 通过将它们包装在try / catch块中来隔离对第三方插件的调用。
  5. 实际上,AppDomain为您提供的唯一功能是加载,隔离和卸载您在运行时期间不完全信任的程序集。您无法在执行的AppDomain中执行此操作。所有加载的程序集都会一直停留,直到执行暂停,并且它们享有与AppDomain中所有其他代码相同的权限集。

    为了更加清晰,这里有一些看起来像c#的伪代码,可以防止第三方代码在AppDomain边界上抛出异常。

    public class PluginHost : IPluginHost, IPlugin
    {
        private IPlugin _wrapped;
        void IPluginHost.Load(string filename, string typename)
        {
            // load the assembly (filename) into the AppDomain.
            // Activator.CreateInstance the typename to create 3rd party plugin
            // _wrapped = the plugin instance
        }
    
        void IPlugin.DoWork()
        {
            try
            {
                _wrapped.DoWork();
            }catch(Exception ex)
                // log
                // unload plugin whatevs
            }
    }
    

    此类型将在您的插件AppDomain中创建,其代理将在应用程序AppDomain中解包。你使用它来插入插件AppDomain中的插件。它可以防止异常越过AppDomain边界,执行加载任务等。将插件类型的代理拉入应用程序AppDomain是非常危险的,因为代理可能以某种方式进入您手中的任何非MarshalByRefObject的对象类型(例如, Throw new MyCustomException())将导致插件程序集在应用程序AppDomain中加载,从而使您的隔离工作无效。

    (这有点过于简单了)

答案 1 :(得分:4)

我会抛出一些随意的想法,但@Will所说的在权限,CAS,安全透明度和沙盒方面是正确的。 AppDomains不是超人。但是,关于异常,AppDomain能够处理大多数未处理的异常。它们不属于的异常类别称为异步异常。由于我们有async / await,but it exists,因此找到有关此类异常的文档有点困难,它们有三种常见形式:

  • StackOverflowException
  • OutOfMemoryException异常
  • ThreadAbortException

这些异常被认为是异步的,因为它们可以被抛出,甚至在CIL操作码之间。前两个是关于整个环境的死亡。 CLR缺乏凤凰的权力,它无法处理这些例外,因为这样做的手段已经死了。请注意,这些规则仅在CLR抛出它们时才存在。如果你只是新手和实例并自己抛出它们,它们就像普通的异常一样。

  

旁注:如果您曾查看托管CLR的进程的内存转储,您会看到总是 OutOfMemoryExceptionThreadAbortException和{{ 1}}在堆上,但它们没有你能看到的根,而且它们永远不会得到GCed。是什么赋予了?它们存在的原因是因为CLR预先分配它们 - 它不能在需要时分配它们。当我们内存不足时,它将无法分配StackOverflowException

有一个软件可以处理所有这些异常。从2005年开始,SQL就能够使用名为SQLCLR的功能运行.NET程序集。 SQL服务器是一个相当重要的过程,并且.NET程序集抛出一个OutOfMemoryException并且它打乱整个SQL进程似乎非常不可取,因此SQL团队不会让这种情况发生。

他们使用名为约束执行和关键区域的.NET 2.0功能来实现此目的。这就是ExecuteCodeWithGuaranteedCleanup之类的东西发挥作用的地方。如果您能够自己托管CLR,请从本机代码开始并自行启动CLR,然后您就可以更改升级策略:从本机代码中您可以处理这些托管异常。这就是SQL CLR处理这些情况的方式。