为什么将2个.NET框架类相互比较会导致stackoverflow异常?

时间:2019-02-28 15:42:29

标签: c# reflection json.net

问题

我目前正在创建应用程序。在此应用程序中,我正在对Func进行序列化。这莫名其妙地使我的应用程序崩溃了。

崩溃无一例外地使我对wtf发生了好奇,所以我进行了一些深潜,经过一番挖掘终于发现在Newtonsoft.Json中某个地方发生了List.Contains,然后对它执行了相等检查2个属性。

显然,在此等于检查中会导致无限循环,从而导致堆栈溢出异常。

仅用C#复制问题

Expression<Func<string, int>> expr = (t) => t.Length;
Func<string, int> exprCompiled = expr.Compile();

var aa = exprCompiled.Method.Module;
var bb = exprCompiled.Method.Module.Assembly;

//This code results in either an infinite loop or a Stackoverflow Exception
var tempresult = aa.Equals(bb);

Console.WriteLine("This code is never executed");

使用Newtonsoft.Json重现该问题

Expression<Func<string, int>> expr = (t) => t.Length;
Func<string, int> exprCompiled = expr.Compile();

//This code results in either an infinite loop or a Stackoverflow Exception
var res = JsonConvert.SerializeObject(exprCompiled);

Console.WriteLine("This code is never executed");

实际潜在问题

进一步研究.NET框架的工作方式,我认为问题在于InternalAssemblyBuilder内部类和InternalModuleBuilder内部类的实现。他们两个都有一个Equals方法重写,如下所示:

public override bool Equals(object obj)
{
    if (obj == null)
    {
        return false;
    }
    if (obj is InternalAssemblyBuilder)
    {
        return this == obj;
    }
    return obj.Equals(this);
}

我认为应该是这样:

public override bool Equals(object obj)
{
    if (obj == null)
    {
        return false;
    }
    if (obj is InternalAssemblyBuilder)
    {
        return this == obj;
    }
    return base.Equals(this); //changed obj to base
}

1 个答案:

答案 0 :(得分:6)

如您的问题的实际潜在问题以及NineBerry中的comments所述,Microsoft的InternalAssemblyBuilder.Equals(object)InternalModuleBuilder.Equals(object)实现似乎被打破。具体来说,在检查InternalAssemblyBuilder类型的对象和InternalModuleBuilder类型的对象之间是否相等时,将发生无限递归。

要变通解决此问题,您可以在JsonSerializer.SettingsEqualityComparer上设置自定义IEqualityComparer,以将Equals()的合理实现替换为这些类型。下面是一个使用引用相等的示例:

public class CustomJsonEqualityComparer : IEqualityComparer
{
    public static readonly CustomJsonEqualityComparer Instance = new CustomJsonEqualityComparer();

    // Use ImmutableHashSet in later .net versions
    static readonly HashSet<string> naughtyTypes = new HashSet<string>
    {
        "System.Reflection.Emit.InternalAssemblyBuilder",
        "System.Reflection.Emit.InternalModuleBuilder"
    };

    static readonly IEqualityComparer baseComparer = EqualityComparer<object>.Default;

    static bool HasBrokenEquals(Type type)
    {
        return naughtyTypes.Contains(type.FullName);
    }

    #region IEqualityComparer Members

    public bool Equals(object x, object y)
    {
        // Check reference equality
        if ((object)x == y)
            return true;
        // Check null
        else if ((object)x == null || (object)y == null)
            return false;

        var xType = x.GetType();
        if (xType != y.GetType())
            // Types should be identical.
            // Note this check alone might be sufficient to fix the problem.
            return false;

        if (xType.IsClass && !xType.IsPrimitive) // IsPrimitive check for performance
        {
            if (HasBrokenEquals(xType))
            {
                // These naughty types should ONLY be compared via reference equality -- which we have already done.
                // So return false
                return false;
            }
        }
        return baseComparer.Equals(x, y);
    }

    public int GetHashCode(object obj)
    {
        return baseComparer.GetHashCode(obj);
    }

    #endregion
}

然后您将按以下方式使用它:

var settings = new JsonSerializerSettings
{
    EqualityComparer = CustomJsonEqualityComparer.Instance,
};
var json = JsonConvert.SerializeObject(exprCompiled, settings);

注意:

  • 如果其他CustomJsonEqualityComparer.HasBrokenEquals()类型具有类似的System.Reflection.Emit实现中断,则可能需要调整Equals()

  • 确保两个传入对象的System.Type具有相同的GetType()值就足够了,因为到目前为止发现的损坏的Equals()方法只会在堆栈中溢出比较两种不同类型的事件,它们都有相同的错误。

  • 虽然我能够再现无限递归并验证是否已使用某些模型对象对其进行了修复,但我无法确认Json.NET实际上可以序列化您的Func<string, int> exprCompiled

    < / li>