Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid crashing with a StackOverflowException when iterating over the AllNodes property when it's infinitely recursive #223

Merged
merged 5 commits into from
Dec 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions YamlDotNet.Test/RepresentationModel/YamlStreamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using FluentAssertions;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Xunit;
using YamlDotNet.Core;
Expand All @@ -42,6 +45,28 @@ public void LoadSimpleDocument()
Assert.Equal(YamlNodeType.Scalar, stream.Documents[0].RootNode.NodeType);
}

[Fact]
public void AccessingAllNodesOnInfinitelyRecursiveDocumentThrows()
{
var stream = new YamlStream();
stream.Load(Yaml.ParserForText("&a [*a]"));

var accessAllNodes = new Action(() => stream.Documents.Single().AllNodes.ToList());

accessAllNodes.ShouldThrow<MaximumRecursionLevelReachedException>("because the document is infinitely recursive.");
}

[Fact]
public void InfinitelyRecursiveNodeToStringSucceeds()
{
var stream = new YamlStream();
stream.Load(Yaml.ParserForText("&a [*a]"));

var toString = stream.Documents.Single().RootNode.ToString();

toString.Should().Contain(YamlNode.MaximumRecursionLevelReachedToStringValue);
}

[Fact]
public void BackwardAliasReferenceWorks()
{
Expand Down
81 changes: 81 additions & 0 deletions YamlDotNet/Core/MaximumRecursionLevelReachedException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// This file is part of YamlDotNet - A .NET library for YAML.
// Copyright (c) Antoine Aubry and contributors

// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System;
using System.Runtime.Serialization;

namespace YamlDotNet.Core
{
/// <summary>
/// Exception that is thrown when an infinite recursion is detected.
/// </summary>
[Serializable]
public class MaximumRecursionLevelReachedException : YamlException
{
/// <summary>
/// Initializes a new instance of the <see cref="MaximumRecursionLevelReachedException"/> class.
/// </summary>
public MaximumRecursionLevelReachedException()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="MaximumRecursionLevelReachedException"/> class.
/// </summary>
/// <param name="message">The message.</param>
public MaximumRecursionLevelReachedException(string message)
: base(message)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="MaximumRecursionLevelReachedException"/> class.
/// </summary>
public MaximumRecursionLevelReachedException(Mark start, Mark end, string message)
: base(start, end, message)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="MaximumRecursionLevelReachedException"/> class.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="inner">The inner.</param>
public MaximumRecursionLevelReachedException(string message, Exception inner)
: base(message, inner)
{
}

#if !(PORTABLE || UNITY)
/// <summary>
/// Initializes a new instance of the <see cref="MaximumRecursionLevelReachedException"/> class.
/// </summary>
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext"/> that contains contextual information about the source or destination.</param>
/// <exception cref="T:System.ArgumentNullException">The <paramref name="info"/> parameter is null. </exception>
/// <exception cref="T:System.Runtime.Serialization.SerializationException">The class name is null or <see cref="P:System.Exception.HResult"/> is zero (0). </exception>
protected MaximumRecursionLevelReachedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
#endif
}
}
68 changes: 68 additions & 0 deletions YamlDotNet/Core/RecursionLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// This file is part of YamlDotNet - A .NET library for YAML.
// Copyright (c) Antoine Aubry and contributors

// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

namespace YamlDotNet.Core
{
/// <summary>
/// Keeps track of the <see cref="Current"/> recursion level,
/// and throws <see cref="MaximumRecursionLevelReachedException"/>
/// whenever <see cref="Maximum"/> is reached.
/// </summary>
internal class RecursionLevel
{
public int Current { get; private set; }
public int Maximum { get; }

public RecursionLevel(int maximum)
{
Maximum = maximum;
}

/// <summary>
/// Increments the <see cref="Current"/> recursion level,
/// and throws <see cref="MaximumRecursionLevelReachedException"/>
/// if <see cref="Maximum"/> is reached.
/// </summary>
internal void Increment()
{
Current++;
if (Current >= Maximum)
{
throw new MaximumRecursionLevelReachedException();
}
}

/// <summary>
/// Increments the <see cref="Current"/> recursion level,
/// and returns whether <see cref="Current"/> is still less than <see cref="Maximum"/>.
/// </summary>
internal bool TryIncrement()
{
Current++;
return Current < Maximum;
}

internal void Decrement()
{
Current--;
}
}
}
10 changes: 6 additions & 4 deletions YamlDotNet/RepresentationModel/YamlAliasNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,19 @@ public override int GetHashCode()
/// <returns>
/// A <see cref="System.String"/> that represents this instance.
/// </returns>
public override string ToString()
internal override string ToString(RecursionLevel level)
{
return "*" + Anchor;
}

/// <summary>
/// Gets all nodes from the document, starting on the current node.
/// Recursively enumerates all the nodes from the document, starting on the current node,
/// and throwing <see cref="MaximumRecursionLevelReachedException"/>
/// if <see cref="RecursionLevel.Maximum"/> is reached.
/// </summary>
public override IEnumerable<YamlNode> AllNodes
internal override IEnumerable<YamlNode> SafeAllNodes(RecursionLevel level)
{
get { yield return this; }
yield return this;
}

/// <summary>
Expand Down
15 changes: 12 additions & 3 deletions YamlDotNet/RepresentationModel/YamlDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,21 @@ internal YamlDocument(IParser parser)
state.ResolveAliases();

#if DEBUG
foreach (var node in AllNodes)
try
{
if (node is YamlAliasNode)
foreach (var node in AllNodes)
{
throw new InvalidOperationException("Error in alias resolution.");
if (node is YamlAliasNode)
{
throw new InvalidOperationException("Error in alias resolution.");
}
}
}
catch (MaximumRecursionLevelReachedException)
{
// Silently absorb this exception.
// This is required to make some unit tests pass in DEBUG mode.
}
#endif

parser.Expect<DocumentEnd>();
Expand Down Expand Up @@ -195,6 +203,7 @@ public void Accept(IYamlVisitor visitor)

/// <summary>
/// Gets all nodes from the document.
/// <see cref="MaximumRecursionLevelReachedException"/> is thrown if an infinite recursion is detected.
/// </summary>
public IEnumerable<YamlNode> AllNodes
{
Expand Down
38 changes: 23 additions & 15 deletions YamlDotNet/RepresentationModel/YamlMappingNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,25 +320,26 @@ public override int GetHashCode()
}

/// <summary>
/// Gets all nodes from the document, starting on the current node.
/// Recursively enumerates all the nodes from the document, starting on the current node,
/// and throwing <see cref="MaximumRecursionLevelReachedException"/>
/// if <see cref="RecursionLevel.Maximum"/> is reached.
/// </summary>
public override IEnumerable<YamlNode> AllNodes
internal override IEnumerable<YamlNode> SafeAllNodes(RecursionLevel level)
{
get
level.Increment();
yield return this;
foreach (var child in children)
{
yield return this;
foreach (var child in children)
foreach (var node in child.Key.SafeAllNodes(level))
{
foreach (var node in child.Key.AllNodes)
{
yield return node;
}
foreach (var node in child.Value.AllNodes)
{
yield return node;
}
yield return node;
}
foreach (var node in child.Value.SafeAllNodes(level))
{
yield return node;
}
}
level.Decrement();
}

/// <summary>
Expand All @@ -355,8 +356,13 @@ public override YamlNodeType NodeType
/// <returns>
/// A <see cref="System.String"/> that represents this instance.
/// </returns>
public override string ToString()
internal override string ToString(RecursionLevel level)
{
if (!level.TryIncrement())
{
return MaximumRecursionLevelReachedToStringValue;
}

var text = new StringBuilder("{ ");

foreach (var child in children)
Expand All @@ -365,11 +371,13 @@ public override string ToString()
{
text.Append(", ");
}
text.Append("{ ").Append(child.Key).Append(", ").Append(child.Value).Append(" }");
text.Append("{ ").Append(child.Key.ToString(level)).Append(", ").Append(child.Value.ToString(level)).Append(" }");
}

text.Append(" }");

level.Decrement();

return text.ToString();
}

Expand Down
27 changes: 25 additions & 2 deletions YamlDotNet/RepresentationModel/YamlNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ namespace YamlDotNet.RepresentationModel
[Serializable]
public abstract class YamlNode
{
private const int MaximumRecursionLevel = 1000;
internal const string MaximumRecursionLevelReachedToStringValue = "WARNING! INFINITE RECURSION!";

/// <summary>
/// Gets or sets the anchor of the node.
/// </summary>
Expand Down Expand Up @@ -196,14 +199,34 @@ protected static int CombineHashCodes(int h1, int h2)
return unchecked(((h1 << 5) + h1) ^ h2);
}

public override string ToString()
{
var level = new RecursionLevel(MaximumRecursionLevel);
return ToString(level);
}

internal abstract string ToString(RecursionLevel level);

/// <summary>
/// Gets all nodes from the document, starting on the current node.
/// <see cref="MaximumRecursionLevelReachedException"/> is thrown if an infinite recursion is detected.
/// </summary>
public abstract IEnumerable<YamlNode> AllNodes
public IEnumerable<YamlNode> AllNodes
{
get;
get
{
var level = new RecursionLevel(MaximumRecursionLevel);
return SafeAllNodes(level);
}
}

/// <summary>
/// When implemented, recursively enumerates all the nodes from the document, starting on the current node.
/// If <see cref="RecursionLevel.Maximum"/> is reached, a <see cref="MaximumRecursionLevelReachedException"/> is thrown
/// instead of continuing and crashing with a <see cref="StackOverflowException"/>.
/// </summary>
internal abstract IEnumerable<YamlNode> SafeAllNodes(RecursionLevel level);

/// <summary>
/// Gets the type of node.
/// </summary>
Expand Down
10 changes: 6 additions & 4 deletions YamlDotNet/RepresentationModel/YamlScalarNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,19 @@ public static explicit operator string(YamlScalarNode value)
/// <returns>
/// A <see cref="System.String"/> that represents this instance.
/// </returns>
public override string ToString()
internal override string ToString(RecursionLevel level)
{
return Value;
}

/// <summary>
/// Gets all nodes from the document, starting on the current node.
/// Recursively enumerates all the nodes from the document, starting on the current node,
/// and throwing <see cref="MaximumRecursionLevelReachedException"/>
/// if <see cref="RecursionLevel.Maximum"/> is reached.
/// </summary>
public override IEnumerable<YamlNode> AllNodes
internal override IEnumerable<YamlNode> SafeAllNodes(RecursionLevel level)
{
get { yield return this; }
yield return this;
}

/// <summary>
Expand Down
Loading