Continuing on from yesterday's post on creating partially-trusted AppDomains, I had a bit of an e-mail exchange with Robert Hurlbut of Hurlbut Consulting. He wanted me to divulge all my secrets about AppDomains to him over e-mail, but I do intend to post them here as blog entries sooner or later (although, for some reason, it might be later...).
Please don't everybody e-mail me and ask for more details; I can't scale to more than one request/week :-)
Anywho, one question he asked me that is easy to answer in a short blog is "Why do I have to put APTCA on my strong-named assemblies if they are going to be loaded into the partially-trusted AppDomain?"
That's a Good Question™
In my last blog, I hinted that you needed to setup AppDomain policy in the remote domain (ie, the one you just created) rather than in the host's domain to overcome a limitation in the way policy worked. Shawn posted today that (thankfully) this limitation is being removed in Whidbey RTM, so you will be able to setup policy from the host's domain and not have to worry about injecting helper assemblies to do it. Yay!
Nevertheless, it is still likely that you'll want to load some assemblies in the remote domain, and if you want them to be granted FullTrust you'll probably have strong-named them so that they match a custom group in your policy (note to self: blog why custom policies are bad). Being a security-conscious developer, you obviously don't want to put the AllowPartiallyTrustedCallersAttribute on your assembly because, well, there's no good reason to have those pesky little untrusted assemblies calling in to you.
But let's see what happens when you use AppDomain.CreateInstanceAndUnwrap to try and load your assembly into the new AppDomain (more or less; this is probably not how it actually works, since I don't have the code for it and don't care to look for it ;-) but it's gets the idea across):
1) The CLR locates the target assembly and loads it
2) Using reflection, the CLR locates the requested type and tries to create an instance of it
3) Since the assembly is not marked with APTCA, there is an implicit LinkDemand for FullTrust on the type
4) Reflection sees the LinkDemand and turns it into a full Demand
5) The Demand wanders through the CLR's reflection and marshalling code on the stack and eventually hits the AppDomain boundary
6) The AppDomain is only partially trusted, so the Demand fails
7) Reflection fails the request to create the type
8) CreateInstanceAndUnwrap fails the call
Notice that the assembly is still loaded in the remote domain; you just can't create any types inside it.
So, you have to put APTCA on the assembly so that it can be created inside the AppDomain, but now you've got a different kind of problem: the code inside your method is doing something "dangerous" (such as setting policy) that will require permissions not granted to the AppDomain itself. You have to Assert the right to use these permissions to avoid a Demand hitting the AppDomain boundary (as in the loading process above), but now you have an unprotected Assert for some really nasty permissions!
That's Bad News™
There are two solutions to this; the first (and ugliest) solution is to simply add a LinkDemand to the method for whatever permissions you are asserting (probably FullTrust). You can successfully call the method across the AppDomain boundary because the CLR will "do the right thing" with the LinkDemand and ensure that the "real" caller in the other AppDomain has the requested permissions (rather than the marshalling goo inbetween). This is different than the creation case above, because that (necessarily) uses reflection to create the type, whereas once the type is created you can early bind to its members and call them directly.
But as we know, LinkDemands are evil and their use is fraught with peril, so it's A Good Thing™ that there's a better way to do this. The "better" way is to have your dangerous method (with the Assert) be an internal method, and then have another "proxy" class inside the same assembly protected with a FullTrust Demand to call it. Now you create an instance of the proxy class in your local domain, an instance of the dangerous class in the remote domain, hook them both together, and get the proxy to invoke the call for you. Now you have a full Demand-protected method that can't be called by partially trusted code, and can't easily be involved in a luring attack, either.
Here's some (very simplified) code to demonstrate the technique:
// Create the strong name key file...
// Create the partially-trusted AppDomain
Evidence ev = new Evidence();
AppDomain ad = AppDomain.CreateDomain("PTDomain", ev);
// Load the helper assembly
// (As noted below, ideally this should be in a
// different assembly, but we're doing an all-in-one
// special to keep the code simple)
assemblyName = this.GetType().Assembly.GetName().Name;
DangerousType dt = ad.CreateInstanceAndUnwrap(assemblyName,
// Load the Proxy and let it do its thing!
Proxy p = new Proxy();
// Ideally these next two types would be in a separate assembly
// (so that the whole program isn't loaded in both AppDomains)
// but to keep the code simple we put them in here
// This is the proxy class that will invoke the
// internal method for us. It can have a full Demand
// as it will only be instantiated in the FullTrust domain
// This is the "dangerous" type that needs to do things
// under the liberation of an Assert, and thus needs
// to be protected with an "internal" modifier
DangerousType : MarshalByRefObject
// Here's where the dangerous stuff happens
Console.WriteLine("In AppDomain " + AppDomain.CurrentDomain.FriendlyName);
// Assert the right to do whatever we want
StreamReader sr = File.OpenText("c:\\boot.ini");