Pure OCaml Yaml 1.2 reader and writer using Bytesrw

Fix alias depth limit tracking in resolve_aliases

The depth limit was not being enforced correctly because anchors
were registered with already-resolved nodes. When processing
`b: &b [*a, *a]`, the code would:

1. Resolve all members first (expanding *a at depth 0)
2. Register anchor "b" with the fully-resolved content

This meant alias chains like `result -> *e -> *d -> *c -> *b -> *a`
would find pre-resolved data at each step, so the depth counter
never accumulated.

The fix registers anchors with the original (unresolved) node
BEFORE resolving members. Now each expand_alias call resolves
the original unresolved node at incremented depth, properly
triggering the depth limit for billion laughs protection.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+18 -24
+17 -23
lib/yaml.ml
··· 120 120 v 121 121 | `Alias name -> expand_alias ~depth name 122 122 | `A seq -> 123 - (* First resolve all members in order *) 123 + (* Register anchor with ORIGINAL node BEFORE resolving members. 124 + This ensures that when this anchor is expanded later through 125 + an alias chain, the internal aliases still need resolution, 126 + allowing the depth counter to properly accumulate. *) 127 + Option.iter (fun name -> register_anchor name v) (Sequence.anchor seq); 128 + (* Now resolve all members in order *) 124 129 let resolved_members = 125 130 List.map (resolve ~depth) (Sequence.members seq) 126 131 in 127 - let resolved = 128 - `A 129 - (Sequence.make ?anchor:(Sequence.anchor seq) ?tag:(Sequence.tag seq) 130 - ~implicit:(Sequence.implicit seq) ~style:(Sequence.style seq) 131 - resolved_members) 132 - in 133 - (* Register anchor with resolved node *) 134 - Option.iter 135 - (fun name -> register_anchor name resolved) 136 - (Sequence.anchor seq); 137 - resolved 132 + `A 133 + (Sequence.make ?anchor:(Sequence.anchor seq) ?tag:(Sequence.tag seq) 134 + ~implicit:(Sequence.implicit seq) ~style:(Sequence.style seq) 135 + resolved_members) 138 136 | `O map -> 137 + (* Register anchor with ORIGINAL node BEFORE resolving members. 138 + This ensures proper depth tracking for alias chains. *) 139 + Option.iter (fun name -> register_anchor name v) (Mapping.anchor map); 139 140 (* Process key-value pairs in document order *) 140 141 let resolved_pairs = 141 142 List.map ··· 145 146 (resolved_k, resolved_v)) 146 147 (Mapping.members map) 147 148 in 148 - let resolved = 149 - `O 150 - (Mapping.make ?anchor:(Mapping.anchor map) ?tag:(Mapping.tag map) 151 - ~implicit:(Mapping.implicit map) ~style:(Mapping.style map) 152 - resolved_pairs) 153 - in 154 - (* Register anchor with resolved node *) 155 - Option.iter 156 - (fun name -> register_anchor name resolved) 157 - (Mapping.anchor map); 158 - resolved 149 + `O 150 + (Mapping.make ?anchor:(Mapping.anchor map) ?tag:(Mapping.tag map) 151 + ~implicit:(Mapping.implicit map) ~style:(Mapping.style map) 152 + resolved_pairs) 159 153 in 160 154 resolve ~depth:0 root 161 155
+1 -1
tests/cram/bomb.t
··· 14 14 Test depth limit with a nested alias chain: 15 15 16 16 $ yamlcat --max-depth 2 --json depth_bomb.yml | head -c 50 17 - {"a": ["x", "y", "z"], "b": [["x", "y", "z"], ["x" 17 + Error: alias expansion exceeded depth limit (2 levels) 18 18 19 19 $ yamlcat --max-depth 10 --json depth_bomb.yml | head -c 50 20 20 {"a": ["x", "y", "z"], "b": [["x", "y", "z"], ["x"