Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cross-AppDomain call corrupts the runtime

This was originally a much more lengthy question, but now I have constructed a smaller usable example code, so the original text is no longer relevant.

I have two projects, one containing a single struct with no members, named TestType. This project is referenced by the main project, but the assembly is not included in the executable directory. The main project creates a new app-domain, where it registers the AssemblyResolve event with the name of the included assembly. In the main app-domain, the same event is handled, but it loads the assembly from the project resources, manually.

The new app-domain then constructs its own version of TestType, but with more fields than the original one. The main app-domain uses the dummy version, and the new app-domain uses the generated version.

When calling methods that have TestType in their signature (even simply returning it is sufficient), it appears that it simply destabilizes the runtime and corrupts the memory.

I am using .NET 4.5, running under x86.

DummyAssembly:

using System;

[Serializable]
public struct TestType
{

}

Main project:

using System;
using System.Reflection;
using System.Reflection.Emit;

internal sealed class Program
{
    [STAThread]
    private static void Main(string[] args)
    {
        Assembly assemblyCache = null;

        AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs rargs)
        {
            var name = new AssemblyName(rargs.Name);
            if(name.Name == "DummyAssembly")
            {
                return assemblyCache ?? (assemblyCache = TypeSupport.LoadDummyAssembly(name.Name));
            }
            return null;
        };

        Start();
    }

    private static void Start()
    {
        var server = ServerObject.Create();

        //prints 155680
        server.TestMethod1("Test");
        //prints 0
        server.TestMethod2("Test");
    }
}

public class ServerObject : MarshalByRefObject
{
    public static ServerObject Create()
    {
        var domain = AppDomain.CreateDomain("TestDomain");
        var t = typeof(ServerObject);
        return (ServerObject)domain.CreateInstanceAndUnwrap(t.Assembly.FullName, t.FullName);
    }

    public ServerObject()
    {
        Assembly genAsm = TypeSupport.GenerateDynamicAssembly("DummyAssembly");

        AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs rargs)
        {
            var name = new AssemblyName(rargs.Name);
            if(name.Name == "DummyAssembly")
            {
                return genAsm;
            }
            return null;
        };
    }

    public TestType TestMethod1(string v)
    {
        Console.WriteLine(v.Length);
        return default(TestType);
    }

    public void TestMethod2(string v)
    {
        Console.WriteLine(v.Length);
    }
}

public static class TypeSupport
{
    public static Assembly LoadDummyAssembly(string name)
    {
        var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name);
        if(stream != null)
        {
            var data = new byte[stream.Length];
            stream.Read(data, 0, data.Length);
            return Assembly.Load(data);
        }
        return null;
    }

    public static Assembly GenerateDynamicAssembly(string name)
    {
        var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
            new AssemblyName(name), AssemblyBuilderAccess.Run
        );

        var mod = ab.DefineDynamicModule(name+".dll");

        var tb = GenerateTestType(mod);

        tb.CreateType();

        return ab;
    }

    private static TypeBuilder GenerateTestType(ModuleBuilder mod)
    {
        var tb = mod.DefineType("TestType", TypeAttributes.Public | TypeAttributes.Serializable, typeof(ValueType));

        for(int i = 0; i < 3; i++)
        {
            tb.DefineField("_"+i.ToString(), typeof(int), FieldAttributes.Public);
        }

        return tb;
    }
}

While both TestMethod1 and TestMethod2 should print 4, the first one accesses some weird parts of the memory, and seems to corrupt the call stack well enough to influence the call to the second method. If I remove the call to the first method, everything is fine.

If I run the code under x64, the first method throws NullReferenceException.

The amount of fields of both structs seems to be important. If the second struct is larger in total than the first one (if I generate only one field or none), everything also works fine, same if the struct in DummyAssembly contains more fields. This leads me to believe that the JITter either incorrectly compiles the method (not using the generated assembly), or that the incorrect native version of the method gets called. I have checked that typeof(TestType) returns the correct (generated) version of the type.

All in all, I am not using any unsafe code, so this shouldn't happen.

like image 530
IS4 Avatar asked Aug 12 '17 01:08

IS4


1 Answers

I was able to reproduce this problem on my machine with newest framework.

I've added check for the version in default appdomain's assembly resolve:

if (name.Name == "DummyAssembly" && name.Version.Major == 1)

And I got following exception:

System.Runtime.Serialization.SerializationException: Cannot find assembly 'DummyAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'



Server stack trace:
   w System.Runtime.Serialization.Formatters.Binary.BinaryAssemblyInfo.GetAssembly()
   w System.Runtime.Serialization.Formatters.Binary.ObjectReader.GetType(BinaryAssemblyInfo assemblyInfo, String name)
   w System.Runtime.Serialization.Formatters.Binary.ObjectMap..ctor(String objectName, String[] memberNames, BinaryTypeEnum[] binaryTypeEnumA, Object[] typeInformationA, Int32[] memberAssemIds, ObjectReader objectReader, Int32 objectId, BinaryAssemblyInfo assemblyInfo, SizedArray assemIdToAssemblyTable)
   w System.Runtime.Serialization.Formatters.Binary.__BinaryParser.ReadObjectWithMapTyped(BinaryObjectWithMapTyped record)
   w System.Runtime.Serialization.Formatters.Binary.__BinaryParser.ReadObjectWithMapTyped(BinaryHeaderEnum binaryHeaderEnum)
   w System.Runtime.Serialization.Formatters.Binary.__BinaryParser.Run()
   w System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler handler, __BinaryParser serParser, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
   w System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream, HeaderHandler handler, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
   w System.Runtime.Remoting.Channels.CrossAppDomainSerializer.DeserializeObject(MemoryStream stm)
   w System.Runtime.Remoting.Messaging.SmuggledMethodReturnMessage.FixupForNewAppDomain()
   w System.Runtime.Remoting.Channels.CrossAppDomainSink.SyncProcessMessage(IMessage reqMsg)

Exception rethrown at [0]:
   w System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
   w System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
   w ServerObject.TestMethod1(TestType& result, String v)

Binary formatter is used for Marshaling here, and it finds value types of different sizes from different AppDomains. Note that it tries to load your DummyAssembly with version 0.0.0.0 when you call TestMethod1, and you pass it the dummy version 1.0.0.0 you cached earliesr, where TestType has different size.

Because of different sizes of structs, when you return by value from your method, something goes wrong with marshaling between AppDomains and stack gets unbalanced (probably a bug in the runtime?). Returning by reference seems to work without issues (size of reference is always the same).

Making structs equal in size in both assemblies / returning by reference should work around this issue.

like image 174
ghord Avatar answered Nov 12 '22 07:11

ghord