Fix endless loop in illink analyzer (#86449)
authorVitek Karas <10670590+vitek-karas@users.noreply.github.com>
Tue, 23 May 2023 07:50:10 +0000 (00:50 -0700)
committerGitHub <noreply@github.com>
Tue, 23 May 2023 07:50:10 +0000 (00:50 -0700)
The root cause is the fact that `ArrayValue` is mutable, so it needs to be `DeepCopy` everywhere. So the first set of fixes is to make sure that even hoisted locals are correctly copied.
The second problem is that we use `HashSet` to store multiple values in `ValueSet`/`MultiValue`. HashSet assumes that items don't change identity (equality group) while they're stored inside. But `ArrayValue` does exactly that. So we can't rely on pure `HashSet` functionality to implement `Equals`.

Changes:
* Make Maybe<T> an IDeepCopyValue and make sure it deep copies its value - because it can store values which require a deep copy
* Make ValueSet<T> an IDeepCopyValue since it can store values which require a deep copy (this causes an effective rename of MultiValue.Clone to MultiValue.DeepCopy - lot of files touched)
* Modify ValueSet<T> to be careful when comparing for equality - we can't rely on immutability of values and since we store them in a hashset, the hashset itself effectively breaks. Maybe we should consider switching to a simple List? But then we want to to unify same values.
* Modify array equality comparison - previously it caused an allocation of a `HashSet` - we compare values quite a bit (in analyzer during multi-pass analysis). Currently we rely on values in arrays to be immutable.

Future looking:
The data flow analysis simply doesn't have a solution for mutable values (mostly mutable refence types, but I think similar set of problems might occur with structs as well). ILLink implements by-ref values, which are probably the closest approximation yet, but the full CFG data flow doesn't support those yet. And ultimately it's probably more complex then just by-ref values.
Proper fix is complex, and requires some design work first.

Note that we will want to backport this to .NET 7 very likely - I haven't looked yet how well it will port.

15 files changed:
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/ArrayValue.cs
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/InterproceduralState.cs
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/TrimAnalysisAssignmentPattern.cs
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/TrimAnalysisMethodCallPattern.cs
src/tools/illink/src/ILLink.RoslynAnalyzer/DataFlow/InterproceduralState.cs
src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/ArrayValue.cs
src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimAnalysisAssignmentPattern.cs
src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimAnalysisMethodCallPattern.cs
src/tools/illink/src/ILLink.Shared/DataFlow/MaybeLattice.cs
src/tools/illink/src/ILLink.Shared/DataFlow/ValueSet.cs
src/tools/illink/src/linker/Linker.Dataflow/ArrayValue.cs
src/tools/illink/src/linker/Linker.Dataflow/InterproceduralState.cs
src/tools/illink/src/linker/Linker.Dataflow/TrimAnalysisAssignmentPattern.cs
src/tools/illink/src/linker/Linker.Dataflow/TrimAnalysisMethodCallPattern.cs
src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/ArrayDataFlow.cs

index f39e83d..2676873 100644 (file)
@@ -72,11 +72,15 @@ namespace ILLink.Shared.TrimAnalysis
             if (!equals)
                 return false;
 
-            // If both sets T and O are the same size and "T intersect O" is empty, then T == O.
-            HashSet<KeyValuePair<int, ValueBasicBlockPair>> thisValueSet = new(IndexValues);
-            HashSet<KeyValuePair<int, ValueBasicBlockPair>> otherValueSet = new(otherArr.IndexValues);
-            thisValueSet.ExceptWith(otherValueSet);
-            return thisValueSet.Count == 0;
+            // Here we rely on the assumption that we can't store mutable values in arrays. The only mutable value
+            // which we currently support are array values, but those are not allowed in an array (to avoid complexity).
+            // As such we can rely on the values to be immutable, and thus if the counts are equal
+            // then the arrays are equal if items from one can be directly found in the other.
+            foreach (var kvp in IndexValues)
+                if (!otherArr.IndexValues.TryGetValue(kvp.Key, out ValueBasicBlockPair value) || !kvp.Value.Equals(value))
+                    return false;
+
+            return true;
         }
 
         public override SingleValue DeepCopy()
@@ -93,7 +97,7 @@ namespace ILLink.Shared.TrimAnalysis
                     System.Diagnostics.Debug.Assert(v is not ArrayValue);
                 }
 #endif
-                newValue.IndexValues.Add(kvp.Key, new ValueBasicBlockPair(kvp.Value.Value.Clone(), kvp.Value.BasicBlockIndex));
+                newValue.IndexValues.Add(kvp.Key, new ValueBasicBlockPair(kvp.Value.Value.DeepCopy(), kvp.Value.BasicBlockIndex));
             }
 
             return newValue;
index 0126819..6a322ca 100644 (file)
@@ -51,7 +51,7 @@ namespace ILCompiler.Dataflow
         public override int GetHashCode() => base.GetHashCode();
 
         public InterproceduralState Clone()
-            => new(_ilProvider, MethodBodies.Clone(), HoistedLocals.Clone(), lattice);
+            => new(_ilProvider, MethodBodies.DeepCopy(), HoistedLocals.Clone(), lattice);
 
         public void TrackMethod(MethodDesc method)
         {
index e168331..e9ce207 100644 (file)
@@ -22,8 +22,8 @@ namespace ILCompiler.Dataflow
 
         internal TrimAnalysisAssignmentPattern(MultiValue source, MultiValue target, MessageOrigin origin, string reason)
         {
-            Source = source.Clone();
-            Target = target.Clone();
+            Source = source.DeepCopy();
+            Target = target.DeepCopy();
             Origin = origin;
             Reason = reason;
         }
index fd3ee33..9f2caf2 100644 (file)
@@ -38,7 +38,7 @@ namespace ILCompiler.Dataflow
             Operation = operation;
             Offset = offset;
             CalledMethod = calledMethod;
-            Instance = instance.Clone();
+            Instance = instance.DeepCopy();
             if (arguments.IsEmpty)
             {
                 Arguments = ImmutableArray<MultiValue>.Empty;
@@ -47,7 +47,7 @@ namespace ILCompiler.Dataflow
             {
                 var builder = ImmutableArray.CreateBuilder<MultiValue>();
                 foreach (var argument in arguments)
-                    builder.Add(argument.Clone());
+                    builder.Add(argument.DeepCopy());
                 Arguments = builder.ToImmutableArray();
             }
             Origin = origin;
index 11aba46..875f4e1 100644 (file)
@@ -45,7 +45,7 @@ namespace ILLink.RoslynAnalyzer.DataFlow
                        => throw new NotImplementedException ();
 
                public InterproceduralState<TValue, TValueLattice> Clone ()
-                       => new (Methods.Clone (),
+                       => new (Methods.DeepCopy (),
                        HoistedLocals.Clone (), lattice);
 
                public void TrackMethod (MethodBodyValue method)
index 3df3575..58ebbff 100644 (file)
@@ -53,10 +53,15 @@ namespace ILLink.Shared.TrimAnalysis
                        if (!equals)
                                return false;
 
-                       // If both sets T and O are the same size and "T intersect O" is empty, then T == O.
-                       HashSet<KeyValuePair<int, MultiValue>> thisValueSet = new (IndexValues);
-                       thisValueSet.ExceptWith (otherArr.IndexValues);
-                       return thisValueSet.Count == 0;
+                       // Here we rely on the assumption that we can't store mutable values in arrays. The only mutable value
+                       // which we currently support are array values, but those are not allowed in an array (to avoid complexity).
+                       // As such we can rely on the values to be immutable, and thus if the counts are equal
+                       // then the arrays are equal if items from one can be directly found in the other.
+                       foreach (var kvp in IndexValues)
+                               if (!otherArr.IndexValues.TryGetValue (kvp.Key, out MultiValue value) || !kvp.Value.Equals (value))
+                                       return false;
+
+                       return true;
                }
 
                // Lattice Meet() is supposed to copy values, so we need to make a deep copy since ArrayValue is mutable through IndexValues
@@ -73,7 +78,7 @@ namespace ILLink.Shared.TrimAnalysis
                                }
 #endif
 
-                               newArray.IndexValues.Add (kvp.Key, kvp.Value.Clone ());
+                               newArray.IndexValues.Add (kvp.Key, kvp.Value.DeepCopy ());
                        }
 
                        return newArray;
index 316b9f9..9a41f47 100644 (file)
@@ -20,8 +20,8 @@ namespace ILLink.RoslynAnalyzer.TrimAnalysis
 
                public TrimAnalysisAssignmentPattern (MultiValue source, MultiValue target, IOperation operation)
                {
-                       Source = source.Clone ();
-                       Target = target.Clone ();
+                       Source = source.DeepCopy ();
+                       Target = target.DeepCopy ();
                        Operation = operation;
                }
 
index d9276c8..c8e8d6d 100644 (file)
@@ -29,13 +29,13 @@ namespace ILLink.RoslynAnalyzer.TrimAnalysis
                        ISymbol owningSymbol)
                {
                        CalledMethod = calledMethod;
-                       Instance = instance.Clone ();
+                       Instance = instance.DeepCopy ();
                        if (arguments.IsEmpty) {
                                Arguments = ImmutableArray<MultiValue>.Empty;
                        } else {
                                var builder = ImmutableArray.CreateBuilder<MultiValue> ();
                                foreach (var argument in arguments) {
-                                       builder.Add (argument.Clone ());
+                                       builder.Add (argument.DeepCopy ());
                                }
                                Arguments = builder.ToImmutableArray ();
                        }
index 6c2e6d6..65726b2 100644 (file)
@@ -10,7 +10,7 @@ namespace ILLink.Shared.DataFlow
        // Wrapper for Nullable<T> which implements IEquatable so that this may
        // be used as a lattice value. Nullable types can't satisfy interface constraints;
        // see for example https://docs.microsoft.com/dotnet/csharp/misc/cs0313.
-       public struct Maybe<T> : IEquatable<Maybe<T>>
+       public struct Maybe<T> : IEquatable<Maybe<T>>, IDeepCopyValue<Maybe<T>>
                where T : struct, IEquatable<T>
        {
                public T? MaybeValue;
@@ -18,7 +18,7 @@ namespace ILLink.Shared.DataFlow
                public bool Equals (Maybe<T> other) => MaybeValue?.Equals (other.MaybeValue) ?? other.MaybeValue == null;
                public override bool Equals (object? obj) => obj is Maybe<T> other && Equals (other);
                public override int GetHashCode () => MaybeValue?.GetHashCode () ?? 0;
-               public Maybe<T> Clone ()
+               public Maybe<T> DeepCopy ()
                {
                        if (MaybeValue is not T value)
                                return default;
@@ -45,9 +45,9 @@ namespace ILLink.Shared.DataFlow
                public Maybe<T> Meet (Maybe<T> left, Maybe<T> right)
                {
                        if (left.MaybeValue is not T leftValue)
-                               return right.Clone ();
+                               return right.DeepCopy ();
                        if (right.MaybeValue is not T rightValue)
-                               return left.Clone ();
+                               return left.DeepCopy ();
                        return new Maybe<T> (ValueLattice.Meet (leftValue, rightValue));
                }
        }
index d4d5af4..199a276 100644 (file)
@@ -12,7 +12,7 @@ using System.Text;
 
 namespace ILLink.Shared.DataFlow
 {
-       public readonly struct ValueSet<TValue> : IEquatable<ValueSet<TValue>>, IEnumerable<TValue>
+       public readonly struct ValueSet<TValue> : IEquatable<ValueSet<TValue>>, IEnumerable<TValue>, IDeepCopyValue<ValueSet<TValue>>
                where TValue : notnull
        {
                // Since we're going to do lot of type checks for this class a lot, it is much more efficient
@@ -28,12 +28,45 @@ namespace ILLink.Shared.DataFlow
                                        hashCode = HashUtils.Combine (hashCode, item);
                                return hashCode;
                        }
+
+                       public bool Equals (EnumerableValues other)
+                       {
+                               // Unfortunately if some of the values are ArrayValues then they can mutate
+                               // after being added to the set, in which case their "hashing" is broken
+                               // The set will self-heal on every Meet since we recreate the HashSet
+                               // but equality is not guaranteed in the interim state.
+                               // So to workaround this for now, iterate over both sets and check
+                               // that the item can be found in the other set.
+                               foreach (TValue item in this)
+                                       if (!other.Contains (item))
+                                               return false;
+
+                               foreach (TValue item in other)
+                                       if (!Contains (item))
+                                               return false;
+
+                               return true;
+                       }
+
+                       public bool Equals (TValue other)
+                       {
+                               // As described above, it's possible to end up with a hashset which has multiple
+                               // values which are equal (due to mutability). So we can't rely on item count.
+                               bool found = false;
+                               foreach (TValue item in this) {
+                                       if (!item.Equals (other))
+                                               return false;
+                                       found = true;
+                               }
+
+                               return found;
+                       }
                }
 
                public struct Enumerator : IEnumerator<TValue>, IDisposable, IEnumerator
                {
                        private readonly object? _value;
-                       private int _state;  // 0 before begining, 1 at item, 2 after end
+                       private int _state;  // 0 before beginning, 1 at item, 2 after end
                        private readonly IEnumerator<TValue>? _enumerator;
 
                        internal Enumerator (object? values)
@@ -108,12 +141,12 @@ namespace ILLink.Shared.DataFlow
 
                        if (_values is EnumerableValues enumerableValues) {
                                if (other._values is EnumerableValues otherValuesSet) {
-                                       return enumerableValues.SetEquals (otherValuesSet);
+                                       return enumerableValues.Equals (otherValuesSet);
                                } else
-                                       return false;
+                                       return enumerableValues.Equals ((TValue) other._values);
                        } else {
-                               if (other._values is EnumerableValues) {
-                                       return false;
+                               if (other._values is EnumerableValues otherEnumerableValues) {
+                                       return otherEnumerableValues.Equals ((TValue) _values);
                                }
 
                                return EqualityComparer<TValue>.Default.Equals ((TValue) _values, (TValue) other._values);
@@ -149,18 +182,18 @@ namespace ILLink.Shared.DataFlow
                internal static ValueSet<TValue> Meet (ValueSet<TValue> left, ValueSet<TValue> right)
                {
                        if (left._values == null)
-                               return right.Clone ();
+                               return right.DeepCopy ();
                        if (right._values == null)
-                               return left.Clone ();
+                               return left.DeepCopy ();
 
                        if (left._values is not EnumerableValues && right.Contains ((TValue) left._values))
-                               return right.Clone ();
+                               return right.DeepCopy ();
 
                        if (right._values is not EnumerableValues && left.Contains ((TValue) right._values))
-                               return left.Clone ();
+                               return left.DeepCopy ();
 
-                       var values = new EnumerableValues (left.Clone ());
-                       values.UnionWith (right.Clone ());
+                       var values = new EnumerableValues (left.DeepCopy ());
+                       values.UnionWith (right.DeepCopy ());
                        return new ValueSet<TValue> (values);
                }
 
@@ -177,7 +210,7 @@ namespace ILLink.Shared.DataFlow
 
                // Meet should copy the values, but most SingleValues are immutable.
                // Clone returns `this` if there are no mutable SingleValues (SingleValues that implement IDeepCopyValue), otherwise creates a new ValueSet with copies of the copiable Values
-               public ValueSet<TValue> Clone ()
+               public ValueSet<TValue> DeepCopy ()
                {
                        if (_values is null)
                                return this;
index 9e816bf..6b914c1 100644 (file)
@@ -68,11 +68,15 @@ namespace ILLink.Shared.TrimAnalysis
                        if (!equals)
                                return false;
 
-                       // If both sets T and O are the same size and "T intersect O" is empty, then T == O.
-                       HashSet<KeyValuePair<int, ValueBasicBlockPair>> thisValueSet = new (IndexValues);
-                       HashSet<KeyValuePair<int, ValueBasicBlockPair>> otherValueSet = new (otherArr.IndexValues);
-                       thisValueSet.ExceptWith (otherValueSet);
-                       return thisValueSet.Count == 0;
+                       // Here we rely on the assumption that we can't store mutable values in arrays. The only mutable value
+                       // which we currently support are array values, but those are not allowed in an array (to avoid complexity).
+                       // As such we can rely on the values to be immutable, and thus if the counts are equal
+                       // then the arrays are equal if items from one can be directly found in the other.
+                       foreach (var kvp in IndexValues)
+                               if (!otherArr.IndexValues.TryGetValue (kvp.Key, out ValueBasicBlockPair value) || !kvp.Value.Equals (value))
+                                       return false;
+
+                       return true;
                }
 
                public override SingleValue DeepCopy ()
@@ -88,7 +92,7 @@ namespace ILLink.Shared.TrimAnalysis
                                }
 #endif
 
-                               newValue.IndexValues.Add (kvp.Key, new ValueBasicBlockPair (kvp.Value.Value.Clone (), kvp.Value.BasicBlockIndex));
+                               newValue.IndexValues.Add (kvp.Key, new ValueBasicBlockPair (kvp.Value.Value.DeepCopy (), kvp.Value.BasicBlockIndex));
                        }
 
                        return newValue;
index 1c277ac..2366733 100644 (file)
@@ -35,7 +35,7 @@ namespace Mono.Linker.Dataflow
                public override int GetHashCode () => HashUtils.Combine (MethodBodies.GetHashCode (), HoistedLocals.GetHashCode ());
 
                public InterproceduralState Clone ()
-                       => new (MethodBodies.Clone (), HoistedLocals.Clone (), lattice);
+                       => new (MethodBodies.DeepCopy (), HoistedLocals.Clone (), lattice);
 
                public void TrackMethod (MethodDefinition method)
                {
index 1517afa..829fefd 100644 (file)
@@ -17,8 +17,8 @@ namespace Mono.Linker.Dataflow
 
                public TrimAnalysisAssignmentPattern (MultiValue source, MultiValue target, MessageOrigin origin)
                {
-                       Source = source.Clone ();
-                       Target = target.Clone ();
+                       Source = source.DeepCopy ();
+                       Target = target.DeepCopy ();
                        Origin = origin;
                }
 
index 7f20ba6..a7b42a0 100644 (file)
@@ -30,13 +30,13 @@ namespace Mono.Linker.Dataflow
                        Debug.Assert (origin.Provider is MethodDefinition);
                        Operation = operation;
                        CalledMethod = calledMethod;
-                       Instance = instance.Clone ();
+                       Instance = instance.DeepCopy ();
                        if (arguments.IsEmpty) {
                                Arguments = ImmutableArray<MultiValue>.Empty;
                        } else {
                                var builder = ImmutableArray.CreateBuilder<MultiValue> ();
                                foreach (var argument in arguments)
-                                       builder.Add (argument.Clone ());
+                                       builder.Add (argument.DeepCopy ());
                                Arguments = builder.ToImmutableArray ();
                        }
                        Origin = origin;
index 1f06afb..8a24181 100644 (file)
@@ -48,6 +48,8 @@ namespace Mono.Linker.Tests.Cases.DataFlow
                        WriteCapturedArrayElement.Test ();
 
                        ConstantFieldValuesAsIndex.Test ();
+
+                       HoistedArrayMutation.Test ();
                }
 
                [ExpectedWarning ("IL2062", nameof (DataFlowTypeExtensions.RequiresPublicMethods))]
@@ -638,6 +640,38 @@ namespace Mono.Linker.Tests.Cases.DataFlow
                        }
                }
 
+               class HoistedArrayMutation
+               {
+                       static void LoopAssignmentWithInitAfter ()
+                       {
+                               // This is a repro for https://github.com/dotnet/runtime/issues/86379
+                               // The array value is a hoisted local
+                               // It's first used in the main method (in the for loop)
+                               // this doesn't get a deep clone of the value, it takes the value from
+                               // the hoisted locals dictionary - and modifies it.
+                               // The local function Initialize then creates a deep copy and uses that.
+                               // Because of a bug, the changes done in the main body method are also
+                               // visible in the "old" interprocedural state. So the state never settles
+                               // and this causes an endless loop in the analyzer.
+                               int[] arr;
+
+                               Initialize ();
+                               for (int i = 0; i < arr.Length; i++) {
+                                       arr[i] = 0;
+                               }
+
+                               void Initialize ()
+                               {
+                                       arr = new int[10];
+                               }
+                       }
+
+                       public static void Test ()
+                       {
+                               LoopAssignmentWithInitAfter ();
+                       }
+               }
+
                [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors)]
                private static Type GetTypeWithPublicConstructors ()
                {