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

Isolate exceptions thrown in an AppDomain to not Crash the Application

TL;DR:如何将加载项异常与终止主进程隔离开来?

我想要一个非常稳定的 .Net 应用程序 运行 在 AppDomain 中使用不太稳定的代码。这似乎首先是 AppDomain 的主要目的之一(以及安全沙箱),但它似乎不起作用。

例如AddIn.exe

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

在我的 'stable' 代码中调用:

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)
    {
        Whosebug(1);
    }

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

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

此外,从 AppDomain 内部调用 Environment.Exit() 之类的东西也会终止父应用程序,不要通过 GO,不要收取 200 英镑,也不要 运行 任何 ~FinialiserDispose().

从这里看来 AppDomain 根本没有做它声称的(或者至少是它看起来声称的)做的事情,因为它只是将所有异常直接传递给父域,使其成为对隔离毫无用处,对任何类型的安全性都非常薄弱(如果我可以删除父进程,我可能会危及机器)。这在 .Net 中将是一个非常根本的失败,所以我的代码中肯定遗漏了一些东西。

我错过了什么吗?有没有什么方法可以让 AppDomain 实际上隔离正在 运行 的代码,并在出现问题时卸载?我是不是用错了东西,还有其他一些 .Net 功能可以提供异常隔离吗?

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

AppDomain 确实做到了它声称要做的。然而,它声称实际要做的和你相信它声称要做的是两件不同的事情。

任何地方未处理的异常都会取消您的应用程序。 AppDomains 不防范这些。但是您可以通过以下方式防止 未处理的异常跨越 AppDomain 边界(抱歉,没有代码)

  1. 创建您的 AppDomain
  2. 在此 AppDomain 中加载并解包您的插件控制器
  3. 通过这个控制器控制插件,
  4. 通过将调用包装在 try/catch 块中来隔离对第 3 方插件的调用。

实际上,AppDomain 给您的唯一功能是加载、隔离和卸载您在运行时不完全信任的程序集。您不能在正在执行的 AppDomain 中执行此操作。所有加载的程序集都会保留,直到执行停止,并且它们与 AppDomain 中的所有其他代码享有相同的权限集。


为了更清楚一点,这里有一些伪代码看起来像 c#,可以防止第 3 方代码在 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 中展开。您可以使用它在 Plugin AppDomain 中操纵插件。它可以防止异常跨越 AppDomain 边界、执行加载任务等。将插件类型的代理拉入应用程序 AppDomain 是非常危险的,因为任何不是 MarshalByRefObject 的对象类型代理都可以以某种方式进入您的手中(例如, Throw new MyCustomException()) 将导致 插件程序集被加载到应用程序 AppDomain 中,从而使您的隔离工作无效。

(这个有点过于简单了)

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

  • WhosebugException
  • OutOfMemoryException
  • ThreadAbortException

这些异常被认为是异步的,因为它们可以在任何地方抛出,甚至在 CIL 操作码之间。前两个是关于整个环境的死亡。 CLR 缺乏 Phoenix 的能力,它无法处理这些异常,因为这样做的方法已经死了。请注意,这些规则仅在 CLR 抛出它们时才存在。如果您只是 new-up 和实例并自己抛出,它们的行为就像正常的异常。

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 WhosebugException on the heap, but they have no roots you can see, and they never get 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. It wouldn't be able to allocate an OutOfMemoryException when we're out of memory.

有一款软件可以处理所有这些异常。从 2005 年开始,SQL 已经能够 运行 具有称为 SQLCLR 的功能的 .NET 程序集。 SQL 服务器是一个相当重要的进程,让 .NET 程序集抛出 OutOfMemoryException 并导致整个 SQL 进程崩溃似乎非常不受欢迎,因此 SQL 团队没有让那发生吧。

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