Thursday, October 15, 2009

In the last post, I have covered an aspect of sandboxing – unexpected AppDomain switches caused by thread promotion. In this post, I will focus on sandboxing, too. However, this time the primary focus is not security but performance. Nevertheless, at the end of this post, I will present an idea that might cause you less pain if you think about the thread-promotion issue.

So what are the performance issues of executing plug-ins in sandboxed AppDomains? Creating and managing AppDomains internally is by far not the most expensive part here. Typically, calls across application domains have much more impact on performance. There are different options for calling across application domains:

  • AppDomain.DoCallback
  • Calling objects in other application domains via transparent proxies
  • ICLRRuntimeHost::ExecuteInAppDomain (a COM-based API internally used by msclr/appdomain.h and its various call_in_appdomain templates)

Compared to calls inside an appdomain, all of these options are dark slow. Even if you use an optimization described here, there is a significant cost involved in cross AppDomain calls. For scenarios that require a lot of calls between the host application and the plug-in and vice versa, this is often not acceptable.

A customer of mine recently came up with a simple but very interesting idea that I have not considered so far: Why should the plug-in be executed in a different AppDomain? Can’t we create a sandboxed AppDomain in which only our assemblies and system assemblies have full-trust permissions whereas the plug-ins can only use a restricted permission-set?

Like most interesting questions, this question is not easy to answer. I do not claim that I have the ultimate answer to this question, but I want to explain my opinion here. At the end of the day it comes down to the question what are the real benefits an AppDomain can give you? In a later post, I will likely address AppDomains in more detail. For now it is sufficient to know that AppDomains can provide isolation; they are boundaries especially for

  • assembly and type loading (and unloading)
  • configuration
  • CAS security assignments

The first use-case for AppDoamins was ASP.NET. I think it is in fact fair to say, that many features of AppDomains only exist because the ASP.NET team needed them. However, this does not mean that these features are needed in all plug-in scenarios. Let’s take a look at each item mentioned above:

Assembly and type loading (and unloading): If you need hot-pluggable plug-ins (those that can be plugged in and out at runtime) you have to use AppDomains. AppDomains offer shadow copying of loaded assemblies which allows you to overwrite the assembly while it is loaded and, as explained here, shutting down AppDomains is the only way to unload assemblies. However, for many pluggable applications, it is acceptable that a restart of the application is required to replace a plug-in.

Configuration: While it can be convenient if each plug-in can have its own configuration file, it is seldom a strict requirement. For many applications, it is acceptable if the app and the plug-ins have to share a configuration file. In my experience, a plug-in typically receives its configuration as creation-parameters from the host application and not via configuration files.

CAS permission assignments: For this aspect the answer is not that easy. Is it more secure if a plug-in is executed in a separate sandboxed application domain? Isn’t it sufficient if the host and the plug-in are executed in a sandboxed application domain in which the host’s code has full-trust permissions and the plug-in is executed with restricted permissions? Are there aspects that make it easier for a sandboxed plug-in assembly to exploit assemblies in its own application domain than to exploit assemblies in other application domains? I am not aware of such a case, but I do not dare to say “no” here. Maybe there is a CAS permission that I have not thought of, and maybe you can use it in a way that I have not considered yet. If such a permission exists and if this permission is granted to the sandboxed plug-in, it could bypass CAS. However, if your sandbox has only the permission to execute type-safe code (SecurityPermissionFlag.Execute) and if everything else the plug-in needs is provided by an types that the host application provides to the plug-in developer, the plug-in would not have such a permission. Therefore my personal answer to this question is: For a plug-in that has only the execution permission, I consider it safe to have the application and the plug-in executing in the same sandboxed application domain. If you disagree with me, I would be more than happy if you could tell me your opinion (my email address alias is my firstname the server name is heege.net).

If you agree with me, it is time to discuss how to create a sandboxed application domain. One option is to use the simple sandboxing API, however, there are two disadvantages in this case:

  • You would have to name each and every non-GACed assembly that should have full-trust permissions. For realistic applications this list could be quite long.
  • As discussed here, it is easy to write code that causes unintended switches to the (full-trusted) default application domain

Key to solving the latter issue is sandboxing the default application domain: If the default application domain is sandboxed, so that a plug-in executes only with the permission to execute type-safe code, and if the plug-in as well as the host application execute in the same application domain, there is no need to create another application domain and thread-promotion automatically ends up in the right application domain.

Sandboxing the default application domain requires a custom AppDomainManager implementation. In this implementation you can to specify a HostSecurityManager. This HostSecurityManager could then grant full-trust permissions to all assemblies with your public key and execution-only permissions to all other assemblies. (Notice that assemblies from the GAC are always full-trust assemblies). The following C# code shows how to implement and AppDomainsManager:

// AppDomainSandboxer.cs
// compile with: csc /t:library /keyfile:keyfile.snk AppDomainSandboxer.cs

using System;
using System.Reflection;
using System.Security;
using System.Security.Policy;
using System.Security.Permissions;

namespace AppDomainSandboxer
{
  public class SandboxingAppDomainManager : AppDomainManager
  {
    SandboxingHostSecurityManager hostSecurityManager =
             new SandboxingHostSecurityManager();

    public override HostSecurityManager HostSecurityManager
    {
      get
      {
        return hostSecurityManager;
      }
    }
  }

  public class SandboxingHostSecurityManager : HostSecurityManager
  {
    private static PermissionSet psExecute;

    private static PermissionSet psFullTrust;

    private static StrongNamePublicKeyBlob publicKeyBlob;

    static SandboxingHostSecurityManager()
    {
      psExecute = new PermissionSet( PermissionState.None);
      psExecute.AddPermission( new SecurityPermission(
                                     SecurityPermissionFlag.Execution));
      psFullTrust = new PermissionSet( PermissionState.Unrestricted);
      foreach ( object evObj in Assembly.GetExecutingAssembly().Evidence)
      {
        StrongName sn = evObj as StrongName;
        if (sn != null)
        {
          publicKeyBlob = sn.PublicKey;
          return;
        }
      }
      throw new Exception( "No public key found in AppDomainSandboxer assembly");
    }

    public override HostSecurityManagerOptions Flags
    {
      get
      {
        return HostSecurityManagerOptions.HostResolvePolicy;
      }
    }

    public override PermissionSet ResolvePolicy( Evidence evidence)
    {
      foreach ( object evObj in evidence)
      {
        StrongName sn = evObj as StrongName;
        if (sn != null)
        {
          if (sn.PublicKey.Equals(publicKeyBlob))
          {
            return psFullTrust;
          }
          return psExecute;
        }
      }
      return psExecute;
    }
  }
}

There are two options to use this SandboxingAppDomainManager inside an application: You can either start the application with two special environment variables named APPDOMAIN_MANAGER_ASM and APPDOMAIN_MANAGER_TYPE as described here, or you can create a native application that hosts the CLR.

10/15/2009 9:40:45 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |   |  Trackback