Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Loading a Roslyn compiled assembly into a sandbox AppDomain

Tags:

c#

roslyn

I've got a code-snippet that compiles a script with the script engine and I retreiv the assembly as a byte array.

Now I want to load this Assembly in a Sandbox, this is what I've got:

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...
}

This is the event handler for AssemblyResolve:

Assembly DomainAssemblyResolve(object sender, ResolveEventArgs args)
{
    return _dynamicAssembly;
}

This means that when I do domain.Load(assembly) I will get the _dynamicAssembly, if I don't subscribe to that event and return that Assembly, I get a FileNotFoundException.

The above compiles and runs, but the problem is that code that is executed in the domain-assembly is not actually executed in the sandbox. When I get the submission-method and invoke the factory in it and return this AppDomain.CurrentDomain.FriendlyName the result is: MyRoslynApplication.vshost.exe which is not the sandbox AppDomain

Am I loading my byte[]-assembly wrong?

like image 835
Filip Ekberg Avatar asked Nov 29 '11 15:11

Filip Ekberg


1 Answers

If you want to load a type that runs in the sandbox and access it from your main AppDomain, you'll need to use a method like CreateInstanceFromAndUnwrap. The type will need to be a MarshalByRefObject so that it can create a transparent proxy in the calling AppDomain for access.

If the main AppDomain resolves the assembly, it will be loaded into the main AppDomain (as well as the sandbox AppDomain) so that you end up with two copies loaded. Your main AppDomain must always remain insulated from the sandbox via proxies, so that only MarshalByRefObject and serializable objects can be accessed. Note that the type you're referencing also cannot be defined in the assembly you want to load into the sandbox; you'll want to define interfaces and possibly serializable types in a third common assembly, which will then be loaded into both the main and sandbox AppDomains.


I've done some additional digging and it looks like all the methods for loading an assembly into another AppDomain and generating a proxy require an assembly name to resolve. I'm not sure if it's possible to load via a byte[] in this case; you might need to save the assembly to disk and load that. I'll dig a bit more.


I think you can do this (this is untested, but it seems plausible).

These would need to be in an 'interface' assembly accessible to both your main app and the sandbox (I'll refer to it as Services.dll):

public interface IMyService
{
    //.... service-specific methods you'll be using
}

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

Next is a class in a StubLoader.dll. You won't reference this assembly directly; this is where you'll call the first AppDomain.CreateInstanceFromAndUnwrap, providing this as the assembly name and StubLoader as the type name.

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

Now, to use it from your main AppDomain, you do this:

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

Unfortunately, AppDomains are not simple to use. This is because they provide a high degree of insulation and therefore require proxying to allow for usage across AppDomain boundaries.


In response to how you could marshal a non-serializable and non-MarshalByRefObject class, here is a rough example of what might be in the shared interface 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
    }
}

Now everywhere your original service needed to work with Session, it can instead pass ISessionWrapper, whose calls will be marshalled behind the scenes so that all of the actual code executes in the sandbox on a real Session instance living in the sandbox.

like image 173
Dan Bryant Avatar answered Oct 05 '22 10:10

Dan Bryant