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.

IS4
  • 11,945
  • 2
  • 47
  • 86
  • 3
    You'd need real [MCVE] 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... – Alexei Levenkov Aug 12 '17 at 01: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 Aug 12 '17 at 03: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 Aug 12 '17 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 Aug 12 '17 at 10:57
  • Hmm, I wonder what bug `new double[0]` might be hiding. Exceptions are your friend. – Hans Passant Aug 12 '17 at 11:41
  • @HansPassant Where? If an empty array gets passed to the dynamically created constructor, accessing any index will throw an exception. – IS4 Aug 12 '17 at 11:47
  • I have not run your code and this is just an observation, but I can not understand how you could use a `BinaryFormatter` on a type that is not marked `Serializable`. `var vtyp = mod.DefineType("Vectors.Dynamic.Vect", TypeAttributes.Public, typeof(ValueType))` - As far as I know, you should need to apply the `TypeAttributes.Serializable` in this statement for the BF to serialize it unless you are providing you own surrogate selector. – TnTinMn Aug 12 '17 at 17:21
  • @TnTinMn I didn't know it could be added this way. Check `DefineDeserialization`, I add the custom attribute manually there. It works this way too. However, even when using *TypeAttributes*, it doesn't fix it. – IS4 Aug 12 '17 at 18:38
  • These comments were related to the original text and example. They are no longer relevant. – IS4 Oct 09 '17 at 14:22
  • I've tested your code and both x86 and x64 output two 4. I'm running from VS 2017 with the latest framework installed. Maybe a bug that was fixed. – Simon Mourier Oct 13 '17 at 06:34
  • @SimonMourier Good to hear it is fixed in later versions, but I wonder if I there is some workaround possible in older versions. – IS4 Oct 13 '17 at 11:15
  • Ditto what @SimonMourier writes, although mine is a console app. So mine prints 4 twice compiled for AnyCPU. Used VS2017 and .NET 4.5.2 – Clay Ver Valen Oct 17 '17 at 23:02
  • @ClayVerValen For x86, I get the same results for all versions of .NET, but now I get 4 twice when I run it in x64. Strange... – IS4 Nov 18 '17 at 21:34
  • @IllidanS4 I also tested the code in Visual Studio 2017 (Community Edition) and it works perfectly on both x86 / x64. I think that a reinstall for the framework will do the trick. Note: The assembly resolve never gets called :) – George Lica Dec 14 '17 at 09:03
  • @GeorgeLica Are you sure you followed correctly "the assembly is not included in the executable directory"? "DummyAssembly.dll" **must not** be in the folder with the exe, otherwise the resolve will never be called, as you pointed out. In that case, 4, 4 will be obviously printed. – IS4 Dec 14 '17 at 19:28

1 Answers1

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.

ghord
  • 13,260
  • 6
  • 44
  • 69
  • Thank you. Since I cannot limit the struct's size, I am probably left to using references. – IS4 Feb 26 '18 at 20:41