将Roslyn编译的程序集加载到沙盒AppDomain中

10

我有一个代码片段,使用脚本引擎编译脚本并将程序集作为字节数组检索。

现在我想在沙盒中加载这个Assembly,这是我的代码:

Assembly _dynamicAssembly;
ScriptEngine _engine;
Session _session;

public string Execute(string code)
{
    // Setup sandbox
    var e = new Evidence();
    e.AddHostEvidence(new Zone(SecurityZone.Internet));
    var ps = SecurityManager.GetStandardSandbox(e);
    var setup = new AppDomainSetup 
                         { ApplicationBase = Environment.CurrentDirectory };
    var domain = 
        AppDomain.CreateDomain("Sandbox", 
                               AppDomain.CurrentDomain.Evidence, setup, ps);
    AppDomain.CurrentDomain.AssemblyResolve += DomainAssemblyResolve;

    // Process code
    var submission = _engine.CompileSubmission<object>(code, _session);
    submission.Compilation.Emit(memoryStream);
    var assembly = memoryStream.ToArray();

    _dynamicAssembly = Assembly.Load(assembly);

    var loaded = domain.Load(assembly);

    // Rest of the code...
}

这是AssemblyResolve事件处理程序:

Assembly DomainAssemblyResolve(object sender, ResolveEventArgs args)
{
    return _dynamicAssembly;
}
这意味着当我执行 domain.Load(assembly) 时,如果我没有订阅该事件并返回该 Assembly,我将得到 _dynamicAssembly, 如果不这样做,则会出现 FileNotFoundException

以上代码可以编译和运行,但问题在于在域程序集中执行的代码实际上并未在沙箱中执行。 当获取提交方法并调用其中的工厂后,返回此 AppDomain.CurrentDomain.FriendlyName,结果是:MyRoslynApplication.vshost.exe,而不是沙箱的 AppDomain
我是否以错误的方式加载了我的 byte[] 程序集?
2个回答

11
如果您想要加载一个在沙盒中运行的类型,并从您的主应用程序域中访问它,您需要使用像CreateInstanceFromAndUnwrap这样的方法。该类型将需要是MarshalByRefObject,以便它可以为访问创建调用应用程序域中的透明代理。
如果主应用程序域解析了该程序集,则它将被加载到主应用程序域(以及沙盒应用程序域)中,因此您最终会有两个已加载的副本。您的主应用程序域必须始终通过代理与沙盒隔离,以便只能访问MarshalByRefObject和可序列化对象。请注意,您引用的类型也不能在要加载到沙盒中的程序集中定义;您需要在第三个公共程序集中定义接口和可能的可序列化类型,然后将其加载到主应用程序域和沙盒应用程序域中。
我进行了一些额外的挖掘,看起来所有将程序集加载到另一个应用程序域并生成代理的方法都需要解析程序集名称。在这种情况下,我不确定是否可以使用byte [] 进行加载;您可能需要将程序集保存到磁盘并加载它。我会再仔细查一下。
我认为您可以这样做(未经测试,但似乎是可行的)。
这些需要在一个“接口”程序集中,该程序集可由您的主应用程序和沙盒访问(我将其称为Services.dll):
public interface IMyService
{
    //.... service-specific methods you'll be using
}

public interface IStubLoader
{
    Object CreateInstanceFromAndUnwrap(byte[] assemblyBytes, string typeName);
}

接下来是 StubLoader.dll 中的一个类。您不会直接引用此程序集;这是您将调用第一个 AppDomain.CreateInstanceFromAndUnwrap 的位置,将其作为程序集名称并将 StubLoader 作为类型名称提供。

public sealed class StubLoader: MarshalByRefObject, IStubLoader
    {
        public object CreateInstanceFromAndUnwrap(byte[] assemblyBytes, string typeName)
        {
            var assembly = Assembly.Load(assemblyBytes);
            return assembly.CreateInstance(typeName);
        }
    }

现在,要从您的主应用程序域中使用它,您需要执行以下操作:

//Create transparent proxy for the stub loader, which will live in the sandbox
var stubLoader = (IStubLoader)sandboxDomain.CreateInstanceFromAndUnwrap("Stubloader.dll", "StubLoader");

//Have the stub loader marshal a proxy to a dynamically loaded assembly (via byte[]) where MyService is the type name implementing MarshalByRefObject and IMyService
var myService = (IMyService)stubLoader.CreateInstanceFromAndUnwrap(assemblyBytes, "MyService");

很遗憾,AppDomains使用起来并不简单。这是因为它们提供了高度的隔离性,因此需要代理才能允许跨AppDomain边界的使用。


回答如何传递非可序列化和非MarshalByRefObject类的问题,以下是共享接口DLL中可能存在的例子:

public interface ISessionWrapper
{
    void DoSomethingWithSession();
}

public sealed class SessionWrapper : MarshalByRefObject, ISessionWrapper
{
    private readonly Session _session;

    public SessionWrapper(Session session)
    {
        _session = session;
    }

    public void DoSomethingWithSession()
    {
        //Do something with the wrapped session...
        //This executes inside the sandbox, even though it can be called (via proxy) from outside the sandbox
    }
}

现在,您原始服务所需的所有会话工作都可以使用ISessionWrapper传递,其调用将在幕后进行编排,以便所有实际代码在沙箱中执行,而实际的Session实例则生存在沙箱中。


当汇编不在磁盘上时,这该怎么办?CreateInstanceFromAndUnwrap需要一个物理文件,对吗?编辑针对您的编辑,我无法从磁盘加载,因为那将需要IO权限。 - Filip Ekberg
2
@Filip,目前我能想到的唯一方法是使用一些迂回的机制,通过某些跨进程或跨域机制(例如将其存储在隔离存储中或通过端口进行通信)来传递byte[],然后有一个“存根加载器”程序集,您可以按名称引用它。然后它会启动,通过这个外部介质访问byte[],然后返回包装代理。 - Dan Bryant
1
@Filip,我认为有一种稍微简单的方法,因为byte[]是可序列化的,所以您不需要使用外部数据传输机制。我提出了一种通用机制来解决BCL没有为AppDomain.CreateInstanceFromAndUnwrap包含byte[]重载的问题。如果您可以使其正常工作并发现我发布的任何问题,请继续编辑帖子。 - Dan Bryant
1
@Filip,我认为你可能需要在AppDomains和MarshalByRefObject上进行更多的研究。除了从byte[]加载的笨拙之外,你仍然需要做一些设计工作,以找出如何代理一个适应你的“脚本”供你的应用程序使用的接口。至少,你需要一个MarshalByRefObject和某种形式的接口来为你的服务提供独立于动态加载程序集的支持。 - Dan Bryant
1
@Filip,一个想法是你可以创建一个包装器MarshalByRefObject,封装你需要使用Session执行的操作,然后将该SessionWrapper传递给你的脚本。现在你可以在你的主AppDomain中构造SessionWrapper,将它传递给方法(它将其作为代理传递给脚本),然后脚本可以使用代理进行操作(其中它的方法被编组回到你的主AppDomain以执行)。 - Dan Bryant
显示剩余9条评论

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接