What's wrong
YamlDotNet resolves each alias to the same object reference, and an alias can point back at its own anchor. That means a deserialized frontmatter value can be a cyclic List<object> / Dictionary<object, object>.
YamlSerializer.ConvertValue (Frontmatter/YamlSerializer.cs:158-170) and DeepCloneValue (:139-153) copy this graph recursively, with no visited set, depth limit or node budget. TryParseYamlObject (:72-90) reaches them for every block.
- With a cycle: the recursion runs until the stack overflows. .NET cannot catch a
StackOverflowException, so the whole host process dies. The existing catch (YamlException/InvalidOperationException/ArgumentException) blocks never get a chance to run.
- Without a cycle: each alias is expanded in full at every reference, so nested aliases grow exponentially ("billion laughs"). Serialization then writes the fully expanded tree back out.
Reproduction
Cycle:
Frontmatter.CombineFrontmatter("---\na: &x [1, *x]\n---\nbody\n",
FrontmatterNaming.AsIs, FrontmatterOrder.AsIs, FrontmatterMergeStrategy.None);
// or
Frontmatter.ExtractFrontmatter("---\na: &x {b: *x}\n---\nbody\n");
The test host exited with code 134 and Stack overflow.. The stack frames alternate between YamlSerializer.ConvertValue and Enumerable.ToList / ToDictionary.
Expansion: I used a block of about 350 bytes with 6 alias levels:
a: &a [x, x, x, x, x, x, x, x, x, x]
b: &b [*a, *a, *a, *a, *a, *a, *a, *a, *a, *a]
# … up to f
ExtractFrontmatter took about 150 ms and built 1,000,000 list nodes.
CombineFrontmatter took about 2.3 s and returned a 15.3M-character string.
- Each extra level multiplies both figures by about 10, so 8–9 levels runs out of memory.
Why it matters
The library is built for processing markdown files in bulk, such as site generators and vault tools. One malicious or accidental file can kill the host process or exhaust its memory, and the caller has no way to catch the failure.
Suggested fix
- Reject cycles. Track the containers on the current recursion path with a
ReferenceEqualityComparer set in ConvertValue / DeepCloneValue. If a cycle is found, TryParseYamlObject should return false.
- Cap expansion. Enforce a total node budget, e.g. 100k. Alternatively, reject aliases outright, since frontmatter almost never uses them. One way is to scan the
IParser event stream for AnchorAlias before deserializing.
- Add regression tests for a self-referencing sequence, a self-referencing mapping, and the 6-level alias bomb. Each should return quickly, leave the block unchanged or report it as unreadable, and not crash.
What's wrong
YamlDotNet resolves each alias to the same object reference, and an alias can point back at its own anchor. That means a deserialized frontmatter value can be a cyclic
List<object>/Dictionary<object, object>.YamlSerializer.ConvertValue(Frontmatter/YamlSerializer.cs:158-170) andDeepCloneValue(:139-153) copy this graph recursively, with no visited set, depth limit or node budget.TryParseYamlObject(:72-90) reaches them for every block.StackOverflowException, so the whole host process dies. The existingcatch (YamlException/InvalidOperationException/ArgumentException)blocks never get a chance to run.Reproduction
Cycle:
The test host exited with code 134 and
Stack overflow.. The stack frames alternate betweenYamlSerializer.ConvertValueandEnumerable.ToList/ToDictionary.Expansion: I used a block of about 350 bytes with 6 alias levels:
ExtractFrontmattertook about 150 ms and built 1,000,000 list nodes.CombineFrontmattertook about 2.3 s and returned a 15.3M-character string.Why it matters
The library is built for processing markdown files in bulk, such as site generators and vault tools. One malicious or accidental file can kill the host process or exhaust its memory, and the caller has no way to catch the failure.
Suggested fix
ReferenceEqualityComparerset inConvertValue/DeepCloneValue. If a cycle is found,TryParseYamlObjectshould returnfalse.IParserevent stream forAnchorAliasbefore deserializing.