Skip to content

Commit d53ff25

Browse files
author
Robert Stam
committed
Implemented CSHARP-424. Circular references now throw a BsonSerializationException which can be caught instead of a StackOverflowException. Default maximum serialization depth is 100, but can be set higher if required.
1 parent 1e667c4 commit d53ff25

File tree

8 files changed

+164
-4
lines changed

8 files changed

+164
-4
lines changed

Bson/BsonDefaults.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public static class BsonDefaults
2828
// private static fields
2929
private static GuidRepresentation __guidRepresentation = GuidRepresentation.CSharpLegacy;
3030
private static int __maxDocumentSize = 4 * 1024 * 1024; // 4MiB
31+
private static int __maxSerializationDepth = 100;
3132

3233
// public static properties
3334
/// <summary>
@@ -49,5 +50,14 @@ public static int MaxDocumentSize
4950
get { return __maxDocumentSize; }
5051
set { __maxDocumentSize = value; }
5152
}
53+
54+
/// <summary>
55+
/// Gets or sets the default max serialization depth (used to detect circular references during serialization). The default is 100.
56+
/// </summary>
57+
public static int MaxSerializationDepth
58+
{
59+
get { return __maxSerializationDepth; }
60+
set { __maxSerializationDepth = value; }
61+
}
5262
}
5363
}

Bson/IO/BsonBinaryWriter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ public override void WriteEndArray()
254254
ThrowInvalidContextType("WriteEndArray", _context.ContextType, ContextType.Array);
255255
}
256256

257+
base.WriteEndArray();
257258
_buffer.WriteByte(0);
258259
BackpatchSize(); // size of document
259260

@@ -276,6 +277,7 @@ public override void WriteEndDocument()
276277
ThrowInvalidContextType("WriteEndDocument", _context.ContextType, ContextType.Document, ContextType.ScopeDocument);
277278
}
278279

280+
base.WriteEndDocument();
279281
_buffer.WriteByte(0);
280282
BackpatchSize(); // size of document
281283

@@ -478,6 +480,7 @@ public override void WriteStartArray()
478480
ThrowInvalidState("WriteStartArray", BsonWriterState.Value);
479481
}
480482

483+
base.WriteStartArray();
481484
_buffer.WriteByte((byte)BsonType.Array);
482485
WriteNameHelper();
483486
_context = new BsonBinaryWriterContext(_context, ContextType.Array, _buffer.Position);
@@ -497,6 +500,7 @@ public override void WriteStartDocument()
497500
ThrowInvalidState("WriteStartDocument", BsonWriterState.Initial, BsonWriterState.Value, BsonWriterState.ScopeDocument, BsonWriterState.Done);
498501
}
499502

503+
base.WriteStartDocument();
500504
if (State == BsonWriterState.Value)
501505
{
502506
_buffer.WriteByte((byte)BsonType.Document);

Bson/IO/BsonDocumentWriter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ public override void WriteEndArray()
161161
ThrowInvalidContextType("WriteEndArray", _context.ContextType, ContextType.Array);
162162
}
163163

164+
base.WriteEndArray();
164165
var array = _context.Array;
165166
_context = _context.ParentContext;
166167
WriteValue(array);
@@ -182,6 +183,7 @@ public override void WriteEndDocument()
182183
ThrowInvalidContextType("WriteEndDocument", _context.ContextType, ContextType.Document, ContextType.ScopeDocument);
183184
}
184185

186+
base.WriteEndDocument();
185187
if (_context.ContextType == ContextType.ScopeDocument)
186188
{
187189
var scope = _context.Document;
@@ -376,6 +378,7 @@ public override void WriteStartArray()
376378
ThrowInvalidState("WriteStartArray", BsonWriterState.Value);
377379
}
378380

381+
base.WriteStartArray();
379382
_context = new BsonDocumentWriterContext(_context, ContextType.Array, new BsonArray());
380383
State = BsonWriterState.Value;
381384
}
@@ -391,6 +394,7 @@ public override void WriteStartDocument()
391394
ThrowInvalidState("WriteStartDocument", BsonWriterState.Initial, BsonWriterState.Value, BsonWriterState.ScopeDocument, BsonWriterState.Done);
392395
}
393396

397+
base.WriteStartDocument();
394398
switch (State)
395399
{
396400
case BsonWriterState.Initial:

Bson/IO/BsonWriter.cs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public abstract class BsonWriter : IDisposable
3535
private string _name;
3636
private bool _checkElementNames;
3737
private bool _checkUpdateDocument;
38+
private int _serializationDepth;
3839

3940
// constructors
4041
/// <summary>
@@ -66,6 +67,14 @@ public bool CheckUpdateDocument
6667
set { _checkUpdateDocument = value; }
6768
}
6869

70+
/// <summary>
71+
/// Gets the current serialization depth.
72+
/// </summary>
73+
public int SerializationDepth
74+
{
75+
get { return _serializationDepth; }
76+
}
77+
6978
/// <summary>
7079
/// Gets the settings of the writer.
7180
/// </summary>
@@ -324,12 +333,18 @@ public void WriteDouble(string name, double value)
324333
/// <summary>
325334
/// Writes the end of a BSON array to the writer.
326335
/// </summary>
327-
public abstract void WriteEndArray();
336+
public virtual void WriteEndArray()
337+
{
338+
_serializationDepth--;
339+
}
328340

329341
/// <summary>
330342
/// Writes the end of a BSON document to the writer.
331343
/// </summary>
332-
public abstract void WriteEndDocument();
344+
public virtual void WriteEndDocument()
345+
{
346+
_serializationDepth--;
347+
}
333348

334349
/// <summary>
335350
/// Writes a BSON Int32 to the writer.
@@ -506,7 +521,14 @@ public void WriteRegularExpression(string name, string pattern, string options)
506521
/// <summary>
507522
/// Writes the start of a BSON array to the writer.
508523
/// </summary>
509-
public abstract void WriteStartArray();
524+
public virtual void WriteStartArray()
525+
{
526+
_serializationDepth++;
527+
if (_serializationDepth > _settings.MaxSerializationDepth)
528+
{
529+
throw new BsonSerializationException("Maximum serialization depth exceeded (does the object being serialized have a circular reference?).");
530+
}
531+
}
510532

511533
/// <summary>
512534
/// Writes the start of a BSON array element to the writer.
@@ -521,7 +543,14 @@ public void WriteStartArray(string name)
521543
/// <summary>
522544
/// Writes the start of a BSON document to the writer.
523545
/// </summary>
524-
public abstract void WriteStartDocument();
546+
public virtual void WriteStartDocument()
547+
{
548+
_serializationDepth++;
549+
if (_serializationDepth > _settings.MaxSerializationDepth)
550+
{
551+
throw new BsonSerializationException("Maximum serialization depth exceeded (does the object being serialized have a circular reference?).");
552+
}
553+
}
525554

526555
/// <summary>
527556
/// Writes the start of a BSON document element to the writer.

Bson/IO/BsonWriterSettings.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public abstract class BsonWriterSettings
2929
// private fields
3030
private GuidRepresentation _guidRepresentation = BsonDefaults.GuidRepresentation;
3131
private bool _isFrozen;
32+
private int _maxSerializationDepth = BsonDefaults.MaxSerializationDepth;
3233

3334
// constructors
3435
/// <summary>
@@ -69,6 +70,19 @@ public bool IsFrozen
6970
get { return _isFrozen; }
7071
}
7172

73+
/// <summary>
74+
/// Gets or sets the max serialization depth allowed (used to detect circular references).
75+
/// </summary>
76+
public int MaxSerializationDepth
77+
{
78+
get { return _maxSerializationDepth; }
79+
set
80+
{
81+
if (_isFrozen) { ThrowFrozenException(); }
82+
_maxSerializationDepth = value;
83+
}
84+
}
85+
7286
// public methods
7387
/// <summary>
7488
/// Creates a clone of the settings.

Bson/IO/JsonWriter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ public override void WriteEndArray()
271271
ThrowInvalidState("WriteEndArray", BsonWriterState.Value);
272272
}
273273

274+
base.WriteEndArray();
274275
_textWriter.Write("]");
275276

276277
_context = _context.ParentContext;
@@ -288,6 +289,7 @@ public override void WriteEndDocument()
288289
ThrowInvalidState("WriteEndDocument", BsonWriterState.Name);
289290
}
290291

292+
base.WriteEndDocument();
291293
if (_jsonWriterSettings.Indent && _context.HasElements)
292294
{
293295
_textWriter.Write(_jsonWriterSettings.NewLineChars);
@@ -559,6 +561,7 @@ public override void WriteStartArray()
559561
ThrowInvalidState("WriteStartArray", BsonWriterState.Value, BsonWriterState.Initial);
560562
}
561563

564+
base.WriteStartArray();
562565
WriteNameHelper(Name);
563566
_textWriter.Write("[");
564567

@@ -577,6 +580,7 @@ public override void WriteStartDocument()
577580
ThrowInvalidState("WriteStartDocument", BsonWriterState.Value, BsonWriterState.Initial, BsonWriterState.ScopeDocument);
578581
}
579582

583+
base.WriteStartDocument();
580584
if (State == BsonWriterState.Value || State == BsonWriterState.ScopeDocument)
581585
{
582586
WriteNameHelper(Name);

BsonUnitTests/BsonUnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
<Compile Include="DefaultSerializer\SerializeFlagsTests.cs" />
9696
<Compile Include="DefaultSerializer\Serializers\AnimalHierarchyWithAttributesTests.cs" />
9797
<Compile Include="DefaultSerializer\Serializers\AnimalHierarchyWithoutAttributesTests.cs" />
98+
<Compile Include="DefaultSerializer\Serializers\CircularReferencesTests.cs" />
9899
<Compile Include="DefaultSerializer\Serializers\JaggedArraySerializerTests.cs" />
99100
<Compile Include="DefaultSerializer\Serializers\ObjectSerializerTests.cs" />
100101
<Compile Include="DefaultSerializer\Serializers\ThreeDimensionalArraySerializerTests.cs" />
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/* Copyright 2010-2012 10gen Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.IO;
19+
using System.Linq;
20+
using System.Text;
21+
using NUnit.Framework;
22+
23+
using MongoDB.Bson;
24+
using MongoDB.Bson.IO;
25+
using MongoDB.Bson.Serialization;
26+
27+
namespace MongoDB.BsonUnitTests.Serialization
28+
{
29+
[TestFixture]
30+
public class CircularReferencesTests
31+
{
32+
public class C
33+
{
34+
public int X { get; set; }
35+
public C NestedDocument { get; set; }
36+
public BsonArray BsonArray { get; set; }
37+
}
38+
39+
[Test]
40+
public void TestCircularBsonArray()
41+
{
42+
// note: setting a breakpoint in this method will crash the debugger if the locals window is open
43+
// because it tries to display the value of array (presumably it's getting an internal stack overflow)
44+
var array = new BsonArray();
45+
array.Add(array);
46+
var c1 = new C { X = 1, BsonArray = array };
47+
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToBson(); });
48+
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToBsonDocument(); });
49+
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToJson(); });
50+
}
51+
52+
[Test]
53+
public void TestCircularDocument()
54+
{
55+
var c1 = new C { X = 1 };
56+
c1.NestedDocument = c1;
57+
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToBson(); });
58+
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToBsonDocument(); });
59+
Assert.Throws<BsonSerializationException>(() => { var json = c1.ToJson(); });
60+
}
61+
62+
[Test]
63+
public void TestNoCircularReference()
64+
{
65+
var c2 = new C { X = 2 };
66+
var c1 = new C { X = 1, NestedDocument = c2 };
67+
68+
var json = c1.ToJson();
69+
var expected = "{ 'X' : 1, 'NestedDocument' : { 'X' : 2, 'NestedDocument' : null, 'BsonArray' : null }, 'BsonArray' : null }".Replace("'", "\"");
70+
Assert.AreEqual(expected, json);
71+
72+
var memoryWriter = new MemoryStream();
73+
using (var writer = BsonWriter.Create(memoryWriter))
74+
{
75+
BsonSerializer.Serialize(writer, c1);
76+
Assert.AreEqual(0, writer.SerializationDepth);
77+
}
78+
79+
var document = new BsonDocument();
80+
using (var writer = BsonWriter.Create(document))
81+
{
82+
BsonSerializer.Serialize(writer, c1);
83+
Assert.AreEqual(0, writer.SerializationDepth);
84+
}
85+
86+
var stringWriter = new StringWriter();
87+
using (var writer = BsonWriter.Create(stringWriter))
88+
{
89+
BsonSerializer.Serialize(writer, c1);
90+
Assert.AreEqual(0, writer.SerializationDepth);
91+
}
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)