Skip to content

Commit 8297e62

Browse files
authored
CSHARP-6040: Fix StackOverflowException in convention CouldApply on self-referencing IEnumerable<T> types (#2013)
1 parent 7f8777c commit 8297e62

4 files changed

Lines changed: 77 additions & 16 deletions

File tree

src/MongoDB.Bson/Serialization/Conventions/EnumRepresentationConvention.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,27 @@ IBsonSerializer Reconfigure(IBsonSerializer s)
9090
=> s.ValueType.IsEnum ? (s as IRepresentationConfigurable)?.WithRepresentation(_representation) : null;
9191

9292
bool CouldApply(Type type)
93-
=> type.IsEnum ||
94-
type.IsNullableEnum() ||
95-
(type.IsArray && CouldApply(type.GetElementType())) || // This is covered by IEnumerable<T>, but it's a good short-circuit
96-
type.GetInterfaces().Prepend(type).Any(
97-
i =>
98-
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>) && CouldApply(i.GetGenericArguments()[0]) || // IEnumerable<T>
99-
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>) && (CouldApply(i.GetGenericArguments()[0]) || CouldApply(i.GetGenericArguments()[1])) // IDictionary<TKey, TValue>
100-
);
93+
{
94+
var visited = new HashSet<Type>();
95+
return CouldApplyCore(type);
96+
97+
bool CouldApplyCore(Type t)
98+
{
99+
if (!visited.Add(t))
100+
{
101+
return false;
102+
}
103+
104+
return t.IsEnum ||
105+
t.IsNullableEnum() ||
106+
(t.IsArray && CouldApplyCore(t.GetElementType())) || // This is covered by IEnumerable<T>, but it's a good short-circuit
107+
t.GetInterfaces().Prepend(t).Any(
108+
i =>
109+
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>) && CouldApplyCore(i.GetGenericArguments()[0]) || // IEnumerable<T>
110+
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>) && (CouldApplyCore(i.GetGenericArguments()[0]) || CouldApplyCore(i.GetGenericArguments()[1])) // IDictionary<TKey, TValue>
111+
);
112+
}
113+
}
101114
}
102115

103116
// private methods

src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,27 @@ public void Apply(BsonMemberMap memberMap)
170170
}
171171

172172
bool CouldApply(Type type)
173-
=> type == typeof(object) ||
174-
(type.IsArray && CouldApply(type.GetElementType())) || // This is covered by IEnumerable<T>, but it's a good short-circuit
175-
IsNonGenericEnumerable(type) ||
176-
type.GetInterfaces().Prepend(type).Any(
177-
i =>
178-
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>) && CouldApply(i.GetGenericArguments()[0]) || // IEnumerable<T>
179-
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>) && (CouldApply(i.GetGenericArguments()[0]) || CouldApply(i.GetGenericArguments()[1])) // IDictionary<TKey, TValue>
180-
);
173+
{
174+
var visited = new HashSet<Type>();
175+
return CouldApplyCore(type);
176+
177+
bool CouldApplyCore(Type t)
178+
{
179+
if (!visited.Add(t))
180+
{
181+
return false;
182+
}
183+
184+
return t == typeof(object) ||
185+
(t.IsArray && CouldApplyCore(t.GetElementType())) || // This is covered by IEnumerable<T>, but it's a good short-circuit
186+
IsNonGenericEnumerable(t) ||
187+
t.GetInterfaces().Prepend(t).Any(
188+
i =>
189+
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>) && CouldApplyCore(i.GetGenericArguments()[0]) || // IEnumerable<T>
190+
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>) && (CouldApplyCore(i.GetGenericArguments()[0]) || CouldApplyCore(i.GetGenericArguments()[1])) // IDictionary<TKey, TValue>
191+
);
192+
}
193+
}
181194

182195
// A type that implements IEnumerable but not IEnumerable<T> (e.g. ArrayList, Hashtable).
183196
static bool IsNonGenericEnumerable(Type type)

tests/MongoDB.Bson.Tests/Serialization/Conventions/EnumRepresentationConventionTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
using System;
17+
using System.Collections;
1718
using System.Collections.Generic;
1819
using System.Linq.Expressions;
1920
using FluentAssertions;
@@ -52,6 +53,7 @@ public class C
5253
public IEnumerable<E> IEnumerableEnum { get; set; }
5354
public IDictionary<string, E> IDictionaryEnumValue { get; set; }
5455
public IDictionary<E, string> IDictionaryEnumKey { get; set; }
56+
public SelfEnumerable SelfEnumerableProp { get; set; }
5557
// public Dictionary<E, C> RecursiveDictionary { get; set; } - this is not supported.
5658
}
5759

@@ -60,6 +62,12 @@ public class IC
6062
public C C { get; set; }
6163
}
6264

65+
public class SelfEnumerable : IEnumerable<SelfEnumerable>
66+
{
67+
public IEnumerator<SelfEnumerable> GetEnumerator() => throw new NotImplementedException();
68+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
69+
}
70+
6371
[Theory]
6472
[InlineData(BsonType.Int32)]
6573
[InlineData(BsonType.Int64)]
@@ -295,6 +303,17 @@ public void Convention_should_work_with_recursive_type_when_top_level_is_false()
295303
ConventionRegistry.Remove("enumRecursive");
296304
}
297305

306+
[Theory]
307+
[InlineData(true)]
308+
[InlineData(false)]
309+
public void Convention_should_not_stack_overflow_with_self_referencing_enumerable_type(bool topLevelOnly)
310+
{
311+
var subject = new EnumRepresentationConvention(BsonType.String, topLevelOnly);
312+
var memberMap = CreateMemberMap(c => c.SelfEnumerableProp);
313+
314+
subject.Apply(memberMap);
315+
}
316+
298317
[Theory]
299318
[InlineData((BsonType)0)]
300319
[InlineData(BsonType.Int32)]

tests/MongoDB.Bson.Tests/Serialization/Conventions/ObjectSerializerAllowedTypesConventionTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ private class TestClass
4949
public IDictionary<string, object> IDictionaryStringObjectProp { get; set; }
5050
public IDictionary<object, string> IDictionaryObjectStringProp { get; set; }
5151
public ArrayList ArrayListProp { get; set; }
52+
public SelfEnumerable SelfEnumerableProp { get; set; }
5253
// public Dictionary<TestClass, object> RecursivePropDictionary { get; set; } - this is not supported.
5354
}
5455

@@ -57,6 +58,12 @@ private class IC
5758
public TestClass TestClass { get; set; }
5859
}
5960

61+
private class SelfEnumerable : IEnumerable<SelfEnumerable>
62+
{
63+
public IEnumerator<SelfEnumerable> GetEnumerator() => throw new NotImplementedException();
64+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
65+
}
66+
6067
[Fact]
6168
public void Apply_should_configure_serializer_when_building_with_constructor_single_delegate()
6269
{
@@ -508,6 +515,15 @@ public void Convention_should_work_with_recursive_type()
508515
ConventionRegistry.Remove("objectRecursive");
509516
}
510517

518+
[Fact]
519+
public void Convention_should_not_stack_overflow_with_self_referencing_enumerable_type()
520+
{
521+
var subject = new ObjectSerializerAllowedTypesConvention();
522+
var memberMap = CreateMemberMap(c => c.SelfEnumerableProp);
523+
524+
subject.Apply(memberMap);
525+
}
526+
511527
// private methods
512528
private static BsonMemberMap CreateMemberMap<TMember>(Expression<Func<TestClass, TMember>> member)
513529
{

0 commit comments

Comments
 (0)