.NET dynamic, Unity, and RuntimeBinder bug

Background


The new version of the project was successfully completed, tested, and was already installed by customers. Nothing boded ill, everything went according to plan and you could relax a bit.
Suddenly, all clients began to receive complaints about errors that fell when trying to use the new version of the program. It was very strange, because everything was checked and should have worked like a clock, but this did not happen.

The error that appeared on all workstations where the new version of the program was installed looked like this: And, as it turned out later, it appeared in a piece of code that was harmless at first glance:

System.IndexOutOfRangeException: Index was outside the bounds of the array
at Microsoft.CSharp.RuntimeBinder.ExpressionTreeCallRewriter.GetMethodInfoFromExpr(EXPRMETHODINFO methinfo)
...




public void FillFrom(dynamic launch)
{
  Log.ShowLog(launch.Id);
}

Due to the fact that clients on computers have Windows XP, we are limited in using the .NET Framework 4.0 version, because the version above on XP can no longer be delivered. Therefore, our project aims to use this particular version of the framework, despite the fact that VS 2012 and framework 4.5 have been installed on our computers for a long time. This influenced the lack of error with us. Therefore, I had to find out what all the same was the cause of this error and how we can deal with it.

The essence of the problem


Due to the use of a dynamic object when calling a method, the compiler turns this line of code into a whole piece. And in this generated code, instead of directly calling the method, the method is defined at runtime. The so-called "late binding . " That is, the Log object is in the process of executing, in a certain way, the “ShowLog” method is searched for a further call. The code generated by the compiler uses the RuntimeBinder to find the desired method. But, something went wrong and the wrong method was in the process of execution. It was during the analysis of its parameters that the above error occurred, because their number did not coincide with the expected one.

Cause of the problem


But how could this happen? It turns out that RuntimeBinder initially finds the desired method and then, in its depths, takes all available methods from a particular type and then compares their metadata token with the token of the found method. If the tokens coincide, the binder takes the matching method and tries to analyze its parameters, which in our case leads to an error.
Tokens could only match for methods from different assemblies, because within one assembly, token numbers could not intersect. And indeed, there was such an opportunity, because the class in which the problem occurred was the descendant of a class from another assembly.

Now everything seemed simple - quickly write a small test case of two assemblies, a hierarchy of two classes, and a pair of methods. Ensure that the token of the heir method and the base class method coincide, and call the heir method using a dynamic variable. But it was not there. Although the example turned out to be very small and the tokens coincided, nothing happened and everything worked properly.

Dropping a little deeper, it turned out that the RuntimeBinder to search for a suitable method does not take everything in a row, but only certain methods that fall under the action of a specific filter:

BindingFlags .Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic

Thus, it became clear that methods from the base class do not participate in any way in the search for the right one.

Digging further, it became clear that the method token did not coincide with the token of the “ordinary” method from the base class, but with the accessor of the property that was defined in the base class. That is, the token of the ShowLog method coincided with the token of the get_IsNotifying method. But this accessor already perfectly passed the above filter.

It seemed that the answer was finally found and the test case was slightly corrected - a property appeared in the base class, the get_ method token ... which coincided with the showLog token. But, despite all the attempts, the test case worked without errors. Although the get_ and set_ methods from the base class passed the filter, they were at the end of the list of all selected methods, at the beginning of which was the correct ShowLog method, which was successfully determined by RuntimeBinder.

Unity role

But not for nothing that the title of the topic contains the name Unity . This is dependency injection , a container from Microsoft, which we use in the project.
As it turned out, using the poke method, his participation somehow influenced the appearance of this problem. I had to watch what makes the container so unusual. After a little study, it became clear that the essence of his work is this: to create an instance of a specific type, he generates a special method.
The code for this method is populated with several predefined strategies. In our case, these were three strategies:
  • A strategy that went through all the constructors of a class for constructor injection
  • A strategy that went through all the properties of a class for for property injection
  • A strategy that went through all the class methods for method injection

They were carried out in exactly this sequence. As it turned out, it was in the order of this enumeration that the last piece of the puzzle was hidden.

If you simply create an instance of some type, and then list all the methods of this type, then, first of all, the usual methods will go, and only then the accessors of properties and events. But, if before the first creation of an object of this type we list, for example, all the properties, then the accessors of which they consist will go to the top of the list.

It turns out that in order to provoke an error and force RuntimeBinder to choose the wrong method, it is necessary that an accessor with a suitable token go along the list before the correct method. In order for the accessor to be at the top of the list, it was enough to call, before the first creation of an object of this type, the following code:

typeof(Log).GetEvents();
typeof(Log).GetProperties();
new Log();

Thanks to this sequence of actions, the list of methods of this type is first filled with event accessors, then properties. And then, after creating the object, it is filled with everything else. After that, reproducing the problem with a small test case turned out to be simple.

Differences in frameworks


Why did the problem appear only on those machines where there was 4 framework, but not on others, despite the fact that the project is aimed at 4 frameworks? As it turned out, in version 4.5 the version of Microsoft.CSharp.dll differs from the version in version 4 of the framework, albeit slightly: in version 4 it is version number 4.0.30319.1 , and version 4.5 it is version 4.0.30319.17929 , in which they apparently managed to fix some errors.

If you look at the code of the problematic method, then it has changed quite a bit, it was:

MethodInfo[] methods = type.GetMethods(BindingFlags.Instance ...);
for (int i = 0; i < methods.Length; i++)
{
if (methods[i].MetadataToken == methodInfo.MetadataToken)
...

became:

MethodInfo[] methods = type.GetMethods(BindingFlags.Instance...);
for (int i = 0; i < methods.Length; i++)
{
if (methods[i].MetadataToken == methodInfo.MetadataToken && !(methods[i].Module != methodInfo.Module))
...

So, with this double negation, this bug was fixed in 4.5 framework.

Consequences of the error


As it turned out experimentally, the consequences of such an error can be different, because accessors have not only properties, but also events. And some accessors have options. And accessors of indexers - parameters can generally have a different amount.

It turns out that the following errors may occur if the RuntimeBinder chooses the wrong method:
  • if a method with fewer parameters is selected, an IndexOutOfRangeException error occurs
  • if a method with the same number of parameters is selected, the types of which completely coincide with the expected ones, then the wrong method will be simply called, and if the method returns something, the result of the incorrect method will be returned.
  • if a method with a large number of parameters is selected, and the types of the required number of parameters completely coincide with the expected ones, the wrong method will be called and an ArgumentException error will occur: Incorrect number of arguments supplied for call to method

How to live with it?


On connect.microsoft.com this problem was registered, and, judging by what was written, there is no fix for the 4 framework and there will not be one. Most likely, the majority of this problem may never occur because in order for it to happen, a large set of circumstances is needed.

But, if the problem still occurred, then the answer to this question is ambiguous and there may be several approaches. For example, to go over initially all the methods of all types of all assemblies, etc., so that they are listed in the right order, although this is rather strange.

For ourselves, we decided, at the moment, to make a small utility in the form of MSBuild task , which we added to the build process. This utility analyzes assemblies using Mono.Cecilto be able to conveniently view the instructions of the methods. In the process of analysis, the utility looks for a specific sequence of instructions, examining their operands, gets the type and name of the method that RuntimeBinder will look for and checks if the described problem can occur. If such a problem call is found, an error will appear during the project build.

In other words, you can avoid errors at all only by setting users to the framework version 4.5. If this is not possible (as in our case), you will either have to not use dynamic, or use it with caution.

Test case
To see the error, this code must be run on a computer with the .NET Framework version 4.0 installed.
For contrast, on computers with the framework version above everything will work as it should.

Assembly A:
A.cs
public class A
{
public void MethodForTokenOffset() {}
public event EventHandler Event
{
add { Console.WriteLine("Event, add");}
remove {}
}
public object this[long id]
{
get
{
Console.WriteLine("Indexator, get {0}", id);
return new { Name = "ThisIsSomeObject" };
}
set { Console.WriteLine("Indexator, set {0}", id); }
}
}

AssemblyB
Program.cs
class Program
{
static void Main()
{
typeof(B).GetEvents();
typeof(B).GetProperties();
new B();
Console.ReadLine();
}
}

B.cs
public class B : A
{
public B()
{
try
{
dynamic obj = new { Handler = new EventHandler((s, e) => Console.WriteLine("EventHandler")), Id = 1L };
MethodForEvent(obj.Handler);
var result = MethodForIndexator(obj.Id);
Console.WriteLine("Method result, {0}", result);
MethodForProperty(obj.Id);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
public void MethodForEvent(EventHandler handler)
{
Console.WriteLine("MethodForEvent, {0}", handler);
}
public void StubMethodForOffset()
{
}
public long MethodForIndexator(long id)
{
Console.WriteLine("MethodForIndexator, {0}", id);
return 0;
}
public void MethodForProperty(long id)
{
Console.WriteLine("MethodForProperty, {0}", id);
}
}



Also popular now: