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.