Type.InvokeMember bug - a small Rotor debugging exercise

I figured I’d go exploring through the Rotor debugger today – we had a bug come in for Type.InvokeMember() where we throw a System.IndexOutOfRangeException unexpectedly. If you’re trying to invoke a method on a class which has an overload that takes “params” args, chances are you’ll run in to it. To firm this up, take a look at the C# file – you’ll notice a params string[] args, and a string arg1, string arg2. What happens if we try to call the method with two arguments?

using System;

using System.Reflection;

public class MainClass

{

    public static void Main(string[] args)

    {

            typeof(MainClass).InvokeMember("MyMethod", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.InvokeMethod, null, null, new object[] { "test", "me" });

      }

      public static void MyMethod(params string[] args)

      {

            foreach (string s in args)

                  Console.WriteLine(s);

      }

      public static void MyMethod(string arg1, string arg2)

      {

            Console.WriteLine(arg1);

            Console.WriteLine(arg2);

      }

}

As it turns out, C# will just call MyMethod(string arg1, string arg2). However, Type.InvokeMember behaves differently. We get an exception:

 

C:\repro>clix invokememberbug.exe

Unhandled Exception: System.IndexOutOfRangeException: Index was outside the bounds of the array.

   at System.DefaultBinder.FindMostSpecific(ParameterInfo[] p1, Int32[] paramOrder1, ParameterInfo[] p2, Int32[] paramOrder2, Type[] types, Object[] args)

   at System.DefaultBinder.FindMostSpecificMethod(MethodBase m1, Int32[] paramOrder1, MethodBase m2, Int32[] paramOrder2, Type[] types, Object[] args)

   at System.DefaultBinder.BindToMethod(BindingFlags bindingAttr, MethodBase[] match, Object[]& args, ParameterModifier[] modifiers, CultureInfo cultureInfo, String[] names, Object& state)

   at System.RuntimeType.InvokeMember(String name, BindingFlags invokeAttr, Binder binder, Object target, Object[] args, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParameters)

   at System.Type.InvokeMember(String name, BindingFlags invokeAttr, Binder binder, Object target, Object[] args)

   at MainClass.Main(String[] args) in C:\repro\invokememberbug.cs:line 8

 

IndexOutOfRangeException? That can’t be right. Either it hit the correct method (it’s up for debate which method it should hit), or throw an AmbigousMatchException, like usual. Our preference is for AmbigousMatchException.

 

Well, the object of the blog post is to prove it’s a bug, and not bad client code: lets cook this up in cordbg. We end up throwing the exception here:

 

C:\repro>cordbg clix invokememberbug.exe

Microsoft (R) Shared Source CLI Test Debugger Shell Version 1.0.0003.0

Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

(cordbg) run clix invokememberbug.exe

(cordbg) g

 

DefaultBinder.cs

671: private static int FindMostSpecific(ParameterInfo[] p1, int[] paramOrder1,

672: ParameterInfo[] p2, int[] paramOrder2,

673: Type[] types, Object[] args)

674: {

...

679: if (args != null && args[i] == Type.Missing)

680: continue;

681: Type c1 = p1[paramOrder1[i]].ParameterType;

682:* Type c2 = p2[paramOrder2[i]].ParameterType;

 

Lets check the length and the index:

(cordbg) print p1

p1=(0x00b77e70) array with dims=[2]

  p1[0] = (0x00b77b20) <System.Reflection.ParameterInfo>

  p1[1] = (0x00b77b88) <System.Reflection.ParameterInfo>

(cordbg) print p2

p2=(0x00b77e88) array with dims=[1]

  p2[0] = (0x00b77c70) <System.Reflection.ParameterInfo>

(cordbg) print i

i=0x00000001

(cordbg) print paramOrder1[1]

paramOrder1[1]=0x00000001

(cordbg) print paramOrder2[1]

paramOrder2[1]=0x00000001

(cordbg) print p2[1]

Array index out of range.

Variable unavailable, or not valid

As it turns out, indexing into p2[1] is takes you out of range of the array – there’s our bug. Okay, so we have a bug in the default binder – this is great, my client code should work, it’s the binder that blew up – we’ve found our bug. But why?

 

Turns out that part is a little more tricky. What does FindMostSpecific do? Intuition tells you that it’s probably got a couple of methods parameters, from methods that look similar and it needs to find the best fit (verification of this means looking at the defaultbinder.cs source code, but we’ll keep going…). What is it passing in? Let us look up the stack:

 

(cordbg) up

821: int res = FindMostSpecific(m1.GetParameters(), paramOrder1, m2.GetParameters(), paramOrder2, types, args);

(cordbg) funceval System.Object::ToString m1

Function evaluation complete.

$result=(0x00b8101c) "Void MyMethod(System.String, System.String)"

(cordbg) up

821: int res = FindMostSpecific(m1.GetParameters(), paramOrder1, m2.GetParameters(), paramOrder2, types, args);

(cordbg) funceval System.Object::ToString m2

Function evaluation complete.

$result=(0x00b81084) "Void MyMethod(System.String[])"

We looked up the stack (into FindMostSpecificMethod), found that we were invoking FindMostSpecific with the parameters of two methods, m1 and m2. Surprise surprise. Turns out the code wrapped around FindMostSpecificMethod passes in two methods that could be matches, and asks FindMostSpecificMethod to figure that out. It then calls a helper method FindMostSpecific.

 

It looks like FindMostSpecific relies on the fact that the two methods parameters it has to sort and identify as the most specific, are the same length as the InvokeMember object args list. In this case, MyMethod(params string[] args) is clearly only length 1, and the InvokeMember object array is length 2. Quite simply -  the binder found a couple of matches, asked the helper methods to go figure it out, but the assumption was that the helper methods would always receive a parameter list the same length as the args list. Clearly a bug.

 

Of course, you could dig a little deeper to find out where in the binder the logic exists to collect the methods that look like they could be a target invocation match – but I’ll leave that exercise up to the reader. I hope you caught the general theme – think you’ve found a bug? Okay, go away and take a peek. ;)

 

Have fun