In .NET 4.0, how do I 'sandbox' an in-memory assembly and execute a method?

OK, first things first: there's no actual way to use the CSharpCodeProvider to do dynamic compilation of C# source entirely in memory. There are methods that seem to support that functionality, but since the C# compiler is a native executable that cannot run in-process, the source string is saved to a temporary file, the compiler is invoked on that file, and then the resulting assembly is saved to disk and then loaded for you using Assembly.Load.

Secondly, as you've discovered, you should be able to use the Compile method from within the AppDomain to load the assembly and give it the desired permissions. I ran into this same unusual behavior, and after a lot of digging found that it was a bug in the framework. I filed an issue report for it on MS Connect.

Since the framework is already writing to the filesystem anyway, the workaround is to have the assembly written to a temporary file and then loaded as needed. When you load it however, you'll need to temporarily assert permissions in the AppDomain, since you've disallowed access to the file system. Here's an example snippet of that:

new FileIOPermission(FileIOPermissionAccess.Read | FileIOPermissionAccess.PathDiscovery, assemblyPath).Assert();
var assembly = Assembly.LoadFile(assemblyPath);
CodeAccessPermission.RevertAssert();

From there you can use the assembly and reflection to invoke your method. Note that this method lets you hoist the compilation process outside of the sandboxed AppDomain, which is a plus in my opinion.

For reference, here is my Sandbox class created to facilitate the launching of script assemblies in a nice clean separate AppDomain that has limited permissions and can be easily unloaded when necessary:

class Sandbox : MarshalByRefObject
{
    const string BaseDirectory = "Untrusted";
    const string DomainName = "Sandbox";

    public Sandbox()
    {
    }

    public static Sandbox Create()
    {
        var setup = new AppDomainSetup()
        {
            ApplicationBase = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, BaseDirectory),
            ApplicationName = DomainName,
            DisallowBindingRedirects = true,
            DisallowCodeDownload = true,
            DisallowPublisherPolicy = true
        };

        var permissions = new PermissionSet(PermissionState.None);
        permissions.AddPermission(new ReflectionPermission(ReflectionPermissionFlag.RestrictedMemberAccess));
        permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));

        var domain = AppDomain.CreateDomain(DomainName, null, setup, permissions,
            typeof(Sandbox).Assembly.Evidence.GetHostEvidence<StrongName>());

        return (Sandbox)Activator.CreateInstanceFrom(domain, typeof(Sandbox).Assembly.ManifestModule.FullyQualifiedName, typeof(Sandbox).FullName).Unwrap();
    }

    public string Execute(string assemblyPath, string scriptType, string method, params object[] parameters)
    {
        new FileIOPermission(FileIOPermissionAccess.Read | FileIOPermissionAccess.PathDiscovery, assemblyPath).Assert();
        var assembly = Assembly.LoadFile(assemblyPath);
        CodeAccessPermission.RevertAssert();

        Type type = assembly.GetType(scriptType);
        if (type == null)
            return null;

        var instance = Activator.CreateInstance(type);
        return string.Format("{0}", type.GetMethod(method).Invoke(instance, parameters));
    }
}

Quick note: if you use this method to supply security evidence for the new AppDomain, you need to sign your assembly to give it a strong name.

Note that this works fine when run in process, but if you really want a bullet-proof script environment, you need to go one step further and isolate the script in a separate process to ensure that scripts that do malicious (or just stupid) things like stack overflows, fork bombs, and out of memory situations don't bring down the whole application process. I can give you more information on doing that if you need it.