15

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.

15
  • 3
    You'd need real minimal reproducible example for someone to look at your problem... Side note: the fact you have struct Vect without any value type fields in it is quite confusing as you presumably have good understanding of .Net internals including assembly identity issues related to loading assemblies from bytes... Commented Aug 12, 2017 at 1:48
  • Why not just define your struct as unsafe struct { double coordinates[DIMENSION_COUNT]; }? Then you can just take its address and pass it as a long or something to the other AppDomain, which will be able to read it just fine as long as it lives in the same process.
    – hoodaticus
    Commented Aug 12, 2017 at 3:38
  • @AlexeiLevenkov The code I provided can be used to verify the problem, and contains all necessary code to reproduce it. The dummy Vect type only has to store the coordinates and the serialize them to the dynamic appdomain. It also originally had an indexer, but I removed it to decrease the size of the code here. The real vector operations happen on the dynamic type, which of course has a fixed number of double fields (vtyp.DefineField), which you can access using dynamic with a way I have added now to the question.
    – IS4
    Commented Aug 12, 2017 at 10:55
  • @hoodaticus I would like to access any method or property that uses Vect in the dynamic appdomain without having to address the serialization at the call site. I could have passed double[] directly to the method as well. Also DIMENSION_COUNT cannot be a compile-type constant, because the user has to be able to change it at runtime.
    – IS4
    Commented Aug 12, 2017 at 10:57
  • Hmm, I wonder what bug new double[0] might be hiding. Exceptions are your friend. Commented Aug 12, 2017 at 11:41

1 Answer 1

2

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.

1
  • Thank you. Since I cannot limit the struct's size, I am probably left to using references.
    – IS4
    Commented Feb 26, 2018 at 20:41

Not the answer you're looking for? Browse other questions tagged or ask your own question.