我可以在RealProxy实例中使用反射吗?

26

我很确定我错过了某些限制或警告,但这就是我的情况。假设我有一个类想要代理它,例如以下内容:

public class MyList : MarshalByRefObject, IList<string>
{
    private List<string> innerList;

    public MyList(IEnumerable<string> stringList)
    {
        this.innerList = new List<string>(stringList);
    }

    // IList<string> implementation omitted for brevity.
    // For the sake of this exercise, assume each method
    // implementation merely passes through to the associated
    // method on the innerList member variable.
}

我想为那个类创建一个代理,这样我就可以拦截方法调用并对底层对象执行一些处理。这是我的实现:

public class MyListProxy : RealProxy
{
    private MyList actualList;

    private MyListProxy(Type typeToProxy, IEnumerable<string> stringList)
        : base(typeToProxy)
    {
        this.actualList = new MyList(stringList);
    }

    public static object CreateProxy(IEnumerable<string> stringList)
    {
        MyListProxy listProxy = new MyListProxy(typeof(MyList), stringList);
        object foo =  listProxy.GetTransparentProxy();
        return foo;
    }

    public override IMessage Invoke(IMessage msg)
    {
        IMethodCallMessage callMsg = msg as IMethodCallMessage;
        MethodInfo proxiedMethod = callMsg.MethodBase as MethodInfo;
        return new ReturnMessage(proxiedMethod.Invoke(actualList, callMsg.Args), null, 0, callMsg.LogicalCallContext, callMsg);
    }
}

最后,我有一个消费代理类的类,并通过反射设置MyList成员的值。

public class ListConsumer
{
    public MyList MyList { get; protected set; }

    public ListConsumer()
    {
        object listProxy = MyListProxy.CreateProxy(new List<string>() { "foo", "bar", "baz", "qux" });
        PropertyInfo myListPropInfo = this.GetType().GetProperty("MyList");
        myListPropInfo.SetValue(this, listProxy);
    }
}

现在,如果我尝试使用反射来访问代理对象,就会遇到问题。以下是一个例子:

class Program
{
    static void Main(string[] args)
    {
        ListConsumer listConsumer = new ListConsumer();

        // These calls merely illustrate that the property can be
        // properly accessed and methods called through the created
        // proxy without issue.
        Console.WriteLine("List contains {0} items", listConsumer.MyList.Count);
        Console.WriteLine("List contents:");
        foreach(string stringValue in listConsumer.MyList)
        {
            Console.WriteLine(stringValue);
        }

        Type listType = listConsumer.MyList.GetType();
        foreach (Type interfaceType in listType.GetInterfaces())
        {
            if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(ICollection<>))
            {
                // Attempting to get the value of the Count property via
                // reflection throws an exception.
                Console.WriteLine("Checking interface {0}", interfaceType.Name);
                System.Reflection.PropertyInfo propInfo = interfaceType.GetProperty("Count");
                int count = (int)propInfo.GetValue(listConsumer.MyList, null);
            }
            else
            {
                Console.WriteLine("Skipping interface {0}", interfaceType.Name);
            }
        }

        Console.ReadLine();
    }
}
尝试通过反射调用Count属性上的GetValue方法会引发以下异常:

System.Reflection.TargetException 异常在 mscorlib.dll 中发生,但未在用户代码中处理

额外信息:对象不符合目标类型。

当尝试获取Count属性的值时,显然框架会向下调用System.Runtime.InteropServices.WindowsRuntime.IVector来调用get_Size方法。我不理解为什么基础代理对象(实际列表)上的此调用会失败,导致这种情况发生。如果我没有使用对象的代理,通过反射获取属性值是可以正常工作的。我做错了什么?我能否完成我正在尝试的操作?编辑: 此问题已在Microsoft Connect站点上开放了漏洞

1
请注意,这个实现并不是空穴来风。MbUnit的Assert.Count方法对于某些集合进行了处理。如果集合对象是代理,则调用Assert.Count会抛出异常。 - JimEvans
是否可以使 MyListProxy.CreateProxy 成为泛型函数,以便返回实际类型而非类型对象?对于测试,请问:如果将主函数中的调用 interfaceType.GetProperty("Count") 更改为 ((MyList)interfaceType).GetProperty("Count"),那么对 Count 的调用是否有效? - keenthinker
类似的问题在这里 - 使用Invoke()似乎会导致编组,从而导致执行进入VectorToCollectionAdapter,然后出现“对象与目标类型不匹配”的错误消息(因为IVector不是ICollection)。我认为这是一个错误。 - fusi
@JimEvans 你在 https://connect.microsoft.com/ 上报告这个bug了吗?如果没有,我们中的一个人可以… - codekaizen
3个回答

11

这是一个相当有趣的CLR漏洞,部分内部机制在故障中暴露了出来。从堆栈跟踪可以看出,它试图调用VectorToCollectionAdapter的Count属性。

这个类非常特殊,它从未创建过任何实例。它是.NET 4.5中添加的语言投影的一部分,使WinRT接口类型看起来像.NET Framework类型。它非常类似于SZArrayHelper类,这是一个适配器类,帮助实现非泛型数组实现泛型接口类型(如IList<T>)的假象。

这里涉及到的接口映射是针对WinRT的IVector<T>接口。正如MSDN文章中所指出的那样,该接口类型被映射到IList<T>上。内部的VectorToListAdapter类处理IList<T>成员,而VectorToCollectionAdapter则处理ICollection<T>成员。

您的代码强制CLR查找ICollection<>.Count的实现,它可能是一个正常实现它的.NET类,也可能是一个将其作为IVector<>.Size公开的WinRT对象。显然,您创建的代理会导致它头疼,它错误地选择了WinRT版本。

它应该如何确定正确的选择方式相当模糊。毕竟,您的代理可以是实际WinRT对象的代理,那么它所做的选择就是正确的。这很可能是一个结构性问题。它表现得如此随意,代码在64位模式下是工作的,这并不令人鼓舞。VectorToCollectionAdapter非常危险,请注意JitHelpers.UnsafeCast调用,这个漏洞有潜在的可利用性。

好的,请通知有关部门,在 connect.microsoft.com 上提交错误报告。如果您没有时间,让我来处理。目前很难找到解决方法,使用以 WinRT 为中心的 TypeInfo 类进行反射并没有任何改变。去除强制运行在64位模式下的JIT编译器是一个权宜之计,但并不是百分之百保证。


11
我认为这可能是.Net框架中的一个错误。某种程度上,RuntimePropertyInfo.GetValue方法选择了错误的ICollection<>.Count属性实现,似乎与WindowsRuntime projections有关。也许当他们将WindowsRuntime互操作放入框架中时,重写了远程处理代码。

我将框架切换为目标.Net 2.0,因为我认为如果这是一个错误,它不应该存在于那个框架中。在转换时,Visual Studio删除了我的控制台exe项目中的“优先使用32位”复选框(因为在2.0中不存在)。当此选项不存在时,它可以正常运行,没有异常抛出。

总之,在.Net 2.0的32位和64位下都可以运行。在.Net 4.x 64位下可以运行。只有在.Net 4.x 32位下才会抛出异常。这肯定看起来像是一个错误。如果您可以以64位运行它,那将是一种解决方法。

请注意,我已安装了.Net 4.6,这替换了大部分.Net框架v4.x。问题可能是在这里引入的;在我得到一个没有.Net 4.6的机器之前,我无法测试。

更新:2015-09-08

这也发生在只安装了.Net 4.5.2(没有4.6)的计算机上。

更新:2015-09-07

这里有一个更小的重现,使用您相同的类:

static void Main(string[] args)
{
    var myList = MyListProxy.CreateProxy(new[] {"foo", "bar", "baz", "quxx"});
    var listType = myList.GetType();
    var interfaceType = listType.GetInterface("System.Collections.Generic.ICollection`1");
    var propInfo = interfaceType.GetProperty("Count");

    // TargetException thrown on 32-bit .Net 4.5.2+ installed
    int count = (int)propInfo.GetValue(myList, null); 
}

我还尝试了IsReadOnly属性,但它似乎有效(没有异常)。



关于错误的来源,属性周围有两层间接性,一层是远程调用,另一层是称为MethodDef的元数据结构的映射与实际运行时方法(在内部称为MethodDesc)。该映射专门用于属性(以及事件),其中添加了额外的MethodDesc来支持属性的get/set PropertyInfo实例,这些实例被称为Associates。通过调用PropertyInfo.GetValue,我们通过其中一个Associate MethodDesc指针进入底层方法实现,而远程调用会进行一些指针数学运算,以获取通道另一侧的正确MethodDesc。在此处,CLR代码非常复杂,我对保存这些MethodDesc记录的MethodTable的内存布局(或它用于到达MethodTable的映射)不够了解,但我认为可以猜测,远程调用正在通过一些错误的指针数学获取错误的MethodDesc。这就是为什么我们看到类似但与程序无关的MethodDesc - 在调用中调用IVector<T>UInt32 get_Size
System.Reflection.RuntimeMethodInfo.CheckConsistency(Object target)
System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
ConsoleApplication1.MyListProxy.Invoke(IMessage msg) Program.cs: line: 60
System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
System.Runtime.InteropServices.WindowsRuntime.IVector`1.get_Size()
System.Runtime.InteropServices.WindowsRuntime.VectorToCollectionAdapter.Count[T]()

4

我们目前正在使用这种脆弱的干预方式来解决这个问题(对于代码表示歉意):

public class ProxyBase : RealProxy
{
    // ... stuff ...

    public static T Cast<T>(object o)
    {
        return (T)o;
    }

    public static object Create(Type interfaceType, object coreInstance, 
        IEnforce enforce, string parentNamingSequence)
    {
        var x = new ProxyBase(interfaceType, coreInstance, enforce, 
            parentNamingSequence);

        MethodInfo castMethod = typeof(ProxyBase).GetMethod(
            "Cast").MakeGenericMethod(interfaceType);

        return castMethod.Invoke(null, new object[] { x.GetTransparentProxy() });
    }

    public override IMessage Invoke(IMessage msg)
    {
        IMethodCallMessage methodCall = (IMethodCallMessage)msg;
        var method = (MethodInfo)methodCall.MethodBase;

        if(method.DeclaringType.IsGenericType
        && method.DeclaringType.GetGenericTypeDefinition().FullName.Contains(
            "System.Runtime.InteropServices.WindowsRuntime"))
        {
            Dictionary<string, string> methodMap = new Dictionary<string, string>
            {   // add problematic methods here
                { "Append", "Add" },
                { "GetAt", "get_Item" }
            };

            if(methodMap.ContainsKey(method.Name) == false)
            {
                throw new Exception("Unable to resolve '" + method.Name + "'.");
            }
            // thanks microsoft
            string correctMethod = methodMap[method.Name];
            method = m_baseInterface.GetInterfaces().Select(
                i => i.GetMethod(correctMethod)).Where(
                    mi => mi != null).FirstOrDefault();

            if(method == null)
            {
                throw new Exception("Unable to resolve '" + method.Name + 
                    "' to '" + correctMethod + "'.");
            }
        }

        try
        {
            if(m_coreInstance == null)
            {
                var errorMessage = Resource.CoreInstanceIsNull;
                WriteLogs(errorMessage, TraceEventType.Error);
                throw new NullReferenceException(errorMessage);
            }

            var args = methodCall.Args.Select(a =>
            {
                object o;

                if(RemotingServices.IsTransparentProxy(a))
                {
                    o = (RemotingServices.GetRealProxy(a) 
                        as ProxyBase).m_coreInstance;
                }
                else
                {
                    o = a;
                }

                if(method.Name == "get_Item")
                {   // perform parameter conversions here
                    if(a.GetType() == typeof(UInt32))
                    { 
                        return Convert.ToInt32(a);
                    }

                    return a;                            
                }

                return o;
            }).ToArray();
            // this is where it barfed
            var result = method.Invoke(m_coreInstance, args);
            // special handling for GetType()
            if(method.Name == "GetType")
            {
                result = m_baseInterface;
            }
            else
            {
                // special handling for interface return types
                if(method.ReturnType.IsInterface)
                {
                    result = ProxyBase.Create(method.ReturnType, result, m_enforce, m_namingSequence);
                }
            }

            return new ReturnMessage(result, args, args.Length, methodCall.LogicalCallContext, methodCall);
        }
        catch(Exception e)
        {
            WriteLogs("Exception: " + e, TraceEventType.Error);
            if(e is TargetInvocationException && e.InnerException != null)
            {
                return new ReturnMessage(e.InnerException, msg as IMethodCallMessage);
            }
            return new ReturnMessage(e, msg as IMethodCallMessage);
        }
    }

    // ... stuff ...
}

m_coreInstance是代理所包装的对象实例。

m_baseInterface是该对象要用作的接口。

这段代码拦截了VectorToListAdapter和VectorToCollectionAdapter中进行的调用,并通过methodMap字典将其转换回原始状态。

条件语句的一部分:

method.DeclaringType.GetGenericTypeDefinition().FullName.Contains(
        "System.Runtime.InteropServices.WindowsRuntime")

确保它仅拦截来自System.Runtime.InteropServices.WindowsRuntime命名空间中的调用——理想情况下,我们应该直接针对类型进行目标定位,但是它们是不可访问的——这可能应该更改为针对命名空间中特定的类名。

然后,将参数转换为适当的类型并调用方法。参数转换似乎是必要的,因为传入的参数类型基于System.Runtime.InteropServices.WindowsRuntime命名空间中对象的方法调用参数类型,而不是原始对象类型方法调用的参数;即,在System.Runtime.InteropServices.WindowsRuntime命名空间对象劫持机制之前的原始类型。

例如,WindowsRuntime组件拦截了对get_Item的原始调用,并将其转换为对Indexer_Get方法的调用:http://referencesource.microsoft.com/#mscorlib/system/runtime/interopservices/windowsruntime/vectortolistadapter.cs,de8c78a8f98213a0,references。然后,此方法使用不同参数类型调用GetAt成员,然后在我们的对象上调用GetAt(再次使用不同参数类型)——这就是我们在Invoke()中劫持并将其转换回原始方法调用与原始参数类型的调用。

很希望能够反映VectorToListAdapter和VectorToCollectionAdapter以提取它们的所有方法和嵌套调用,但是这些类不幸地被标记为内部。

虽然这对我们有用,但我确信它存在漏洞——这是一个试错的过程,运行它以查看哪些失败,然后添加所需的字典条目/参数转换。我们正在寻找更好的解决方案。

希望这可以帮到你。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接