A security audit flagged classic YAML alias-expansion ("billion laughs") as a load-time DoS for any ruamel.yaml consumer, hedged as "Potential (not exercised)". Exercising it matters: ruamel's round-trip loader resolves aliases to shared references, so the canonical bomb does not blow up memory at all.
Tested on ruamel.yaml 0.19.1 / Python 3.12 with a 9-level 9-way alias bomb (334 bytes, ~1.4GB logical expansion): YAML().load() returned in 0.00s with baseline ~20MB RSS. Same for a self-referential anchor (a: &a {next: *a}). Aliases are deduplicated object references, not copies.
What actually DoS-es a ruamel loader, verified live:
- Unbounded input read — if the caller reads the file with no cap (
open(path,'rb').read()) before parsing,--file /dev/zerohangs/OOMs at the read, never reaching YAML. Cap bytes before load (PP-style:read(MAX+1)then refuse). - Composer recursion on deep nesting —
'[' * 2000 + ']' * 2000raisesRecursionErrorinsideruamel.yaml.composer(parse-time stack, not Python-list depth). The scanner-level event stream survives where the composer does not:YAML().parse('['*5000+']'*5000)iterates 10,004 events fine.
So the correct cheap pre-load guard is an iterative pass over yaml.parse(contents) counting CollectionStartEvent/CollectionEndEvent depth and rejecting AliasEvent (if the format never legitimately uses aliases) — not a text grep for &/*, which false-positives on ordinary prose values, and not an alias-count cap, which solves a non-problem in ruamel.
General rule: when an audit (or CVE writeup) claims a parser DoS "potential, not exercised", write the 5-line bomb harness first — for ruamel the memory-expansion claim is simply false, and the fix you'd ship for it (anchor/alias rejection alone) misses the two vectors that are real.