How to reproduce InvalidCastException when binding

2019-01-25 21:46发布

In Suzanne Cook's .NET CLR Notes she talks about the dangers of the "LoadFrom" context. Specifically,

  • If a Load context assembly tries to load this assembly by display name, it will fail to be found by default (e.g., when mscorlib.dll deserializes this assembly)
  • Worse, an assembly with the same identity but at a different path could be found on the probing path, causing an InvalidCastException, MissingMethodException, or unexpected method behavior later on.

How do you reproduce this behavior with deserialization, but without explicitly loading two different versions of the assembly?

1条回答
姐就是有狂的资本
2楼-- · 2019-01-25 22:20

I've created a Console application, A.exe, which indirectly loads (via `Assembly.LoadFrom) and calls (via reflection) code from a class library, B.dll.

  • A.exe does not (necessarily) have a reference to B.dll, but B.dll should exist in the same directory as A.exe
  • A copy of of B.dll should be placed into another directory (here I've used the subdirectory called LoadFrom), this is the location we will use Assembly.LoadFrom on.

A.exe

class Program
{
    static void Main(string[] args)
    {
        // I have a post build step that copies the B.dll to this sub directory.
        // but the B.dll also lives in the main directory alongside the exe:
        // mkdir LoadFrom
        // copy B.dll LoadFrom
        //
        var loadFromAssembly = Assembly.LoadFrom(@".\LoadFrom\B.dll");
        var mySerializableType = loadFromAssembly.GetType("B.MySerializable");

        object mySerializableObject = Activator.CreateInstance(mySerializableType);
        var copyMeBySerializationMethodInfo = mySerializableType.GetMethod("CopyMeBySerialization");

        try
        {
            copyMeBySerializationMethodInfo.Invoke(mySerializableObject, null);
        }
        catch (TargetInvocationException tie)
        {
            Console.WriteLine(tie.InnerException.ToString());
        }

        Console.ReadKey();
    }
}

B.dll

namespace B
{
    [Serializable]
    public class MySerializable
    {
        public MySerializable CopyMeBySerialization()
        {
            return DeepClone(this);
        }

        private static T DeepClone<T>(T obj)
        {
            using (var ms = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(ms, obj);
                ms.Position = 0;

                return (T)formatter.Deserialize(ms);
            }
        }
    }
}

Output

System.InvalidCastException: 
  [A]B.MySerializable cannot be cast to 
  [B]B.MySerializable. 
  Type A originates from 'B, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' 
    in the context 'Default' at location 'c:\Dev\bin\Debug\B.dll'. 
  Type B originates from 'B, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' 
    in the context 'LoadFrom' at location 'c:\Dev\bin\Debug\LoadFrom\B.dll'.

   at B.MySerializable.DeepClone[T](T obj)
   at B.MySerializable.CopyMeBySerialization()

Here's what is happening:

  • When the call to formatter.Deserialize(ms) is made, it uses the information stored in the MemoryStream to determine what type of object it needs to create (and which assembly it needs in order to create that object).
  • It finds that it needs B.dll and attempts to Load it (from the default "Load" context).
  • The currently loaded B.dll is not found (because it was loaded in the "LoadFrom" context).
  • Thus, an attempt is made to find B.dll in the usual locations--it is found in the ApplicationBase directory and is loaded.
  • All types in this B.dll are considered different types that those from the other B.dll. Thus the cast in the expression (T)formatter.Deserialize(ms) fails.

Additional notes:

  • If the B.dll had not existed somewhere where A.exe could find it using Assembly.Load, then instead of an InvalidCastException, there would be a SerializationException with the message Unable to find assembly 'B, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
  • The same problem occurs even with signed assemblies, but what is more alarming with signed assemblies is that it can load a different version of the signed assembly. That is, if B.dll in the "LoadFrom" context is 1.0.0.0, but the B.dll found in the main directory is 2.0.0.0, the serialization code will still load the wrong version B.dll to do deserialization.
  • The DeepClone code I've shown seems to be one of the more popular ways to do a deep clone on an object. See: Deep cloning objects in C#.

So, from any code that was loaded into the "LoadFrom" context, you cannot use deserialization successfully (without jumping through additional hoops to allow the assembly to successfully load in the default "Load" context).

查看更多
登录 后发表回答