Skip to content
Merged
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
104 changes: 89 additions & 15 deletions tests/Opc.Ua.Types.Tests/Encoders/EncoderStackDepthTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ public enum Shape
private const int kBuilderStack = 256 * 1024 * 1024;
private const int kUnboundedLimit = 1_000_000;

// Twice the deepest nesting of any codec and shape that fits a one
// megabyte stack before the stack check fires.
private const int kBeyondTheStackDepth = 5000;

private static readonly Codec[] s_decoderCodecs =
[Codec.Binary, Codec.Json, Codec.Xml, Codec.XmlParser];
private static readonly Codec[] s_encoderCodecs = [Codec.Binary, Codec.Json, Codec.Xml];
Expand All @@ -86,6 +90,11 @@ public enum Shape
Shape.ExtensionObjectChain,
Shape.EncodeableTree
];

// A thread can get more stack than it asks for: on Linux, glibc runs it
// on the cached stack of an exited thread up to four times the requested
// size, and the stack check measures the stack the thread got. A quarter
// megabyte request can therefore run on a one megabyte stack.
private static readonly int[] s_stackSizes = [kOneMegabyte, kQuarterMegabyte];

private static IEnumerable<TestCaseData> DecoderCases()
Expand Down Expand Up @@ -140,30 +149,51 @@ public void EncodeBeyondDefaultNestingLimitDoesNotOverflowTheStack(Codec codec,
public void DecodeWithUnboundedNestingLimitFailsBeforeTheStackOverflows(Codec codec, Shape shape)
{
// Without a usable nesting limit only the stack check stands between
// a deep message and a crash. The JSON encoder cannot write more than
// 1000 levels, which still overflowed a small stack before.
int depth = codec == Codec.Json ? 450 : 2000;
object input = EncodeOnLargeStack(codec, Build(shape, depth));
// a deep message and a crash. The JsonEncoder cannot write more than
// 1000 levels, so the JSON input is written by hand.
object input = codec == Codec.Json
? CreateNestedJson(shape, kBeyondTheStackDepth)
: EncodeOnLargeStack(codec, Build(shape, kBeyondTheStackDepth));

ServiceResultException sre = RunOnThread(
kQuarterMegabyte,
() => Decode(codec, shape, input, CreateContext(kUnboundedLimit)));
foreach (int stackSize in s_stackSizes)
{
ServiceResultException sre = RunOnThread(
stackSize,
() => Decode(codec, shape, input, CreateContext(kUnboundedLimit)));

Assert.That(sre, Is.Not.Null);
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadEncodingLimitsExceeded));
Assert.That(sre, Is.Not.Null, $"Stack of {stackSize} bytes");
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadEncodingLimitsExceeded));
}
}

[TestCaseSource(nameof(EncoderCases))]
public void EncodeWithUnboundedNestingLimitFailsBeforeTheStackOverflows(Codec codec, Shape shape)
{
object graph = Build(shape, 2000);
object graph = Build(shape, kBeyondTheStackDepth);

ServiceResultException sre = RunOnThread(
kQuarterMegabyte,
() => Encode(codec, graph, CreateContext(kUnboundedLimit)));
foreach (int stackSize in s_stackSizes)
{
ServiceResultException sre = RunOnThread(
stackSize,
() => Encode(codec, graph, CreateContext(kUnboundedLimit)));

Assert.That(sre, Is.Not.Null);
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadEncodingLimitsExceeded));
Assert.That(sre, Is.Not.Null, $"Stack of {stackSize} bytes");
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadEncodingLimitsExceeded));
}
}

[Test]
public void NestedJsonMatchesTheJsonEncoder([Values] Shape shape)
{
// The JSON inputs nested beyond the encoder's reach are written by
// hand. They must stay what the JsonEncoder writes, or the decoder
// tests stop exercising the encoding a peer sends.
foreach (int depth in new[] { 1, 2, 7 })
{
string encoded = (string)EncodeOnLargeStack(Codec.Json, Build(shape, depth));

Assert.That(CreateNestedJson(shape, depth), Is.EqualTo(encoded), $"Depth {depth}");
}
}

[TestCase(Codec.Xml)]
Expand Down Expand Up @@ -345,6 +375,50 @@ private static object Build(Shape shape, int depth)
}
}

/// <summary>
/// Writes the JSON that the JsonEncoder writes for <see cref="Build"/>
/// at any depth. The encoder itself cannot nest deeper than 1000 JSON
/// levels.
/// </summary>
private static string CreateNestedJson(Shape shape, int depth)
{
const string int32 = "{\"UaType\":6,\"Value\":1}";
(string head, string open, string core, string close, string tail, int levels) = shape switch
{
Shape.VariantArray => ("{\"Value\":", "{\"UaType\":24,\"Value\":[", int32, "]}", "}", depth),
Shape.DataValue => ("{\"Value\":", "{\"UaType\":23,\"Value\":", int32, "}", "}", depth),
Shape.ExtensionObjectVariant => (
"{\"Value\":",
"{\"UaType\":22,\"Value\":{\"UaTypeId\":\"i=88901\",\"Value\":",
int32,
"}}",
"}",
depth),
Shape.ExtensionObjectChain => (
"{\"Value\":{\"UaType\":22,\"Value\":",
"{\"UaTypeId\":\"i=88911\",\"Child\":",
"null",
"}",
"}}",
depth + 1),
_ => ("{\"Value\":", "{\"Children\":[", "{\"Children\":null}", "]}", "}", depth)
};

var builder = new StringBuilder(
head.Length + core.Length + tail.Length + (levels * (open.Length + close.Length)));
builder.Append(head);
for (int ii = 0; ii < levels; ii++)
{
builder.Append(open);
}
builder.Append(core);
for (int ii = 0; ii < levels; ii++)
{
builder.Append(close);
}
return builder.Append(tail).ToString();
}

/// <summary>
/// Encodes the input for a decoder test on a large stack with a limit
/// that allows its depth.
Expand Down
Loading