简体   繁体   English

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

[英]Isolate exceptions thrown in an AppDomain to not Crash the Application

TL;DR: How do you isolate add-in exceptions from killing the main process? TL; DR:如何通过杀死主进程来隔离加载项异常?

I want to have a very stable .Net application that runs less stable code in an AppDomain . 我想要一个非常稳定的.Net应用程序,它在AppDomain中运行不太稳定的代码。 This would appear to be one of the prime purposes of the AppDomain in the first place (well, that and security sandboxing) but it doesn't appear to work. 这似乎是AppDomain的主要目的之一(嗯,那和安全沙箱)但它似乎不起作用。

For instance in AddIn.exe : 例如在AddIn.exe

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

Called in my 'stable' code with: 在我的“稳定”代码中调用:

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")

The exception thrown in the AppDomain gets passed straight to the application that created the domain. AppDomain抛出的异常会直接传递给创建域的应用程序。 I can log these with domain.UnhandledException and catch them in the wrapper application. 我可以使用domain.UnhandledException记录这些并在包装器应用程序中捕获它们。

However, there are more problematic exceptions thrown, for instance: 但是,抛出更多有问题的异常,例如:

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

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

This will throw a stackoverflow exception that kills the entire application every time. 这将抛出stackoverflow异常,每次都会终止整个应用程序。 It doesn't even fire domain.UnhandledException - it just goes straight to killing the entire application. 它甚至没有触发domain.UnhandledException - 它只是直接杀死整个应用程序。

In addition calling things like Environment.Exit() from inside the AppDomain also kill the parent application, do not pass GO, do not collect £200 and don't run any ~Finialiser or Dispose() . 此外,从AppDomain内部调用Environment.Exit()类的东西也会杀死父应用程序,不要传递GO,不要收取200英镑并且不要运行任何~FinialiserDispose()

It seems from this that AppDomain fundamentally doesn't do what it claims (or at lease what it appears to claim) to do, as it just passes all exceptions straight to the parent domain, making it useless for isolation and pretty weak for any kind of security (if I can take out the parent process I can probably compromise the machine). 从这一点看, AppDomain从根本上没有做它声称的(或者它看起来要声称的)要做的事情,因为它只是将所有异常直接传递到父域,使得它对于隔离无用而且对于任何类型来说都很弱安全性(如果我可以取出父进程,我可能会破坏机器)。 That would be a pretty fundamental failure in .Net, so I must be missing something in my code. 这在.Net中是一个非常根本的失败,所以我必须在我的代码中遗漏一些东西。

Am I missing something? 我错过了什么吗? Is there some way to make AppDomain actually isolate the code that it's running and unload when something bad happens? 是否有一些方法可以让AppDomain实际上隔离它正在运行的代码并在发生错误时卸载? Am I using the wrong thing and is there some other .Net feature that does provide exception isolation? 我使用了错误的东西,是否有其他.Net功能提供异常隔离?

You can't do anything about Environment.Exit() , just like you can't prevent a user from killing your process in Task Manager. 您无法对Environment.Exit()执行任何操作,就像您无法阻止用户在任务管理器中终止您的进程一样。 Static analysis for this could be circumvented, as well. 对此的静态分析也可以被规避。 I wouldn't worry too much about that. 我不会太担心这个。 There are things you can do, and things you really can't. 你可以做的事情,你真的不能做的事情。

The AppDomain does do what it claims to do. AppDomain确实做了它声称要做的事情。 However, what it actually claims to do and what you believe it claims to do are two different things. 然而,它实际上声称做,你相信它声称做是两回事什么。

Unhandled exceptions anywhere will take down your application. 任何地方未处理的异常都将取消您的应用程序。 AppDomains don't protect against these. AppDomains不能防范这些。 But you can prevent unhandled exceptions from crossing AppDomain boundaries by the following (sorry, no code) 但是你可以通过以下方法防止未处理的异常越过AppDomain边界(抱歉,没有代码)

  1. Create your AppDomain 创建您的AppDomain
  2. Load and unwrap your plugin controller in this AppDomain 在此AppDomain中加载和解包您的插件控制器
  3. Control plugins through this controller, which 通过这个控制器控制插件
  4. Isolates calls to 3rd party plugins by wrapping them in try/catch blocks. 通过将它们包装在try / catch块中来隔离对第三方插件的调用。

Really, the only thing an AppDomain gives you is the ability to load, isolate and unload assemblies that you do not fully trust during runtime . 实际上,AppDomain为您提供的唯一功能是能够加载,隔离和卸载在运行时期间您不完全信任的程序集 You cannot do this within the executing AppDomain. 您无法在执行的AppDomain中执行此操作。 All loaded assemblies stay until execution halts, and they enjoy the same permission set as all other code in the AppDomain. 所有加载的程序集一直保持到执行暂停,并且它们享有与AppDomain中所有其他代码相同的权限集。


To be a touch clearer, here's some pseudocode that looks like c# that prevents 3rd-party code from throwing exceptions across the AppDomain boundary. 为了更清楚,这里有一些看起来像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
        }
}

This type would be created in your Plugin AppDomain, and its proxy unwrapped in the application AppDomain. 此类型将在您的插件AppDomain中创建,其代理将在应用程序AppDomain中解包。 You use it to puppet the plugin within the Plugin AppDomain. 你使用它来插入插件AppDomain中的插件。 It prevents exceptions from crossing AppDomain boundaries, performs loading tasks, etc etc. Pulling a proxy of the plugin type into the application AppDomain is very risky, as any object types that are NOT MarshalByRefObject that the proxy can somehow get into your hands (eg, Throw new MyCustomException() ) will result in the plugin assembly being loaded in the application AppDomain, thus rendering your isolation efforts null and void. 它可以防止异常越过AppDomain边界,执行加载任务等。将插件类型的代理拉入应用程序AppDomain是非常危险的,因为代理可能以某种方式进入您手中的任何非MarshalByRefObject的对象类型(例如, Throw new MyCustomException() )将导致插件程序集在应用程序AppDomain中加载,从而使您的隔离工作无效。

(this is a bit oversimplified) (这有点过于简化了)

I'll throw on some random thoughts, but what @Will has said is correct regarding permissions, CAS, security transparency, and sandboxing. 我会抛出一些随意的想法,但@Will所说的在权限,CAS,安全透明度和沙盒方面是正确的。 AppDomains are not quite superman. AppDomains不是超人。 Regarding exceptions though, an AppDomain is capable of handling most unhandled exceptions. 但是,关于异常,AppDomain能够处理大多数未处理的异常。 The category of exceptions that they are not is called an asynchronous exception . 它们不属于的异常类别称为异步异常 Finding documentation on such exceptions is a little more difficult now that we have async/await, but it exists , and they come in three common forms: 现在我们有异步/等待, 但是它存在 ,找到有关此类异常的文档有点困难,它们有三种常见形式:

  • StackOverflowException StackOverflowException
  • OutOfMemoryException OutOfMemoryException异常
  • ThreadAbortException ThreadAbortException

These exceptions are said to be asynchronous because they can be thrown anywhere , even between CIL opcodes. 这些异常被认为是异步的,因为它们可以被抛出到任何地方 ,甚至在CIL操作码之间。 The first two are about the whole environment dying. 前两个是关于整个环境的死亡。 The CLR lacks the powers of a Phoenix, it cannot handle these exceptions because the means of doing so are already dead. CLR缺乏凤凰的权力,它无法处理这些例外,因为这样做的手段已经死了。 Note that these rules only exist when the CLR throws them. 请注意,这些规则仅在CLR抛出它们时才存在。 If you just new-up and instance and throw it yourself, they behave like normal exceptions. 如果你只是新手和实例并自己抛出它们,它们就像普通的异常一样。

Sidenote: If you ever peek at a memory dump of a process that is hosting the CLR, you will see there are always OutOfMemoryException , ThreadAbortException , and StackOverflowException on the heap, but they have no roots you can see, and they never get GCed. 旁注:如果你曾经看过托管CLR的进程的内存转储,你会发现堆上总是有 OutOfMemoryExceptionThreadAbortExceptionStackOverflowException ,但是它们没有你能看到的根,并且它们永远不会被GCed。 What gives? 是什么赋予了? The reason they are there is because the CLR preallocates them - it wouldn't be able to allocate them at the time they are needed. 它们存在的原因是因为CLR预分配它们 - 它们无法在需要时分配它们。 It wouldn't be able to allocate an OutOfMemoryException when we're out of memory. 当我们内存不足时,它将无法分配OutOfMemoryException

There is a piece of software that is able to handle all of these exceptions. 有一个软件能够处理所有这些异常。 Starting in 2005, SQL has had the ability to run .NET assemblies with a feature called SQLCLR. 从2005年开始,SQL就能够使用名为SQLCLR的功能运行.NET程序集。 SQL server is a rather important process, and having a .NET assembly throw an OutOfMemoryException and it bringing down the entire SQL process seemed tremendously undesirable, so the SQL team doesn't let that happen. SQL服务器是一个相当重要的过程,并且.NET程序集抛出OutOfMemoryException并且它使整个SQL进程失效似乎非常不可取,因此SQL团队不会让这种情况发生。

They do this using a .NET 2.0 feature called constrained execution and critical regions. 他们使用称为约束执行和关键区域的.NET 2.0功能来实现此目的。 This is where things like ExecuteCodeWithGuaranteedCleanup come into play. 这就是ExecuteCodeWithGuaranteedCleanup东西。 If you are able to host the CLR yourself, start with native code and spin up the CLR yourself, you are then able to change the escalation policy: from native code you are able to handle those managed exceptions. 如果您能够自己托管CLR,请从本机代码开始并自行启动CLR,然后您就可以更改升级策略:从本机代码中您可以处理这些托管异常。 This is how SQL CLR handles those situations. 这就是SQL CLR处理这些情况的方式。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM