fix: a recipe is refused for its depth whichever style it nests in - #64
Merged
Conversation
The limit that stops a small recipe from exhausting the machine counted flow collections - the ones written with brackets and braces - because that is how the bomb it was written for was written. The same nesting in block style walks past it: "- - - - x" is four nested sequences in eight bytes and carries no bracket at all. Measured against the previous build with tfg validate: "- " x20 000 40 kB 0.40 s refused, by a limit inside the parser "- " x100 000 200 kB 7.41 s refused "- " x250 000 500 kB 88.2 s fatal error: out of memory, exit 2 "- " x500 000 1 MB 70.7 s exit 1, and not one word of output The third printed 41 kB of Go runtime stack and left with the exit code the frozen table gives a mistyped flag. The machine has 31.9 GB. The cost is the nesting rather than the size, and that is measured: 500 kB of flat mapping is refused cleanly in 0.62 s. An alias bomb does not amplify at all, because the schema refuses it before anything expands. Block sequences are now counted from the column each dash sits in, added to the flow depth that was already counted, and the limit stays at 32. An ordinary recipe reaches one and an archive declaring its contents reaches two. The same file is now refused in 0.76 s. The whole defence rests on the pre-scan being cheap, so that was measured rather than assumed - tools/probes/yamldepth: lexer.Tokenize on the bomb is 42 ms and 28.9 MB at 200 kB and 422 ms and 145 MB at 1 MB, against the parser's 70 seconds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
donislawdev
force-pushed
the
security/recipe-depth
branch
from
September 6, 2026 13:15
316236f to
42ee699
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The limit that stops a small recipe from exhausting the machine counted flow collections - the ones written with brackets and braces - because that is how the bomb it was written for was written. The same nesting in block style walks past it:
- - - - xis four nested sequences in eight bytes and carries no bracket at all.Measured against the previous build, with
tfg validate-x20 000-x100 000-x250 000fatal error: out of memory, 41 kB of Go runtime stack on stderr, exit 2-x500 000MaxBytesallowsExit 2 is what the frozen table gives a mistyped flag. The machine has 31.9 GB.
The cost is the nesting rather than the size, and that is measured. 500 kB of flat mapping - a hundred thousand lines of
a: 1- is refused cleanly in 0.62 s. An alias bomb does not amplify at all: the schema refuses it in 0.2 s before anything expands. So neither a limit on bytes nor one on token count could have answered this.The fix
Block sequences are counted from the column each dash sits in - further right opens a sequence inside the last one, the same column is the next entry, further left closes - and that is added to the flow depth already counted. The limit stays at 32. An ordinary recipe reaches one and an archive declaring its contents reaches two. The 1 MB file is now refused in 0.76 s.
The whole defence rests on the pre-scan being cheap, since it has to run before the parser. That was measured rather than assumed, with
tools/probes/yamldepth:lexer.Tokenizeon the bomb is 7 ms at 40 kB, 42 ms at 200 kB and 422 ms at 1 MB, linear, against the parser's 70 to 88 seconds.Guards
Three new cases in
TestAHostileRecipeCannotHangTheReader: the block bomb refused, and two the fix could have broken - 200 targets in one list, and a list inside a list. Four mutations, all caught, including one that had gone stale whenlimits.gowas rewritten and was repaired rather than counted.What this counter over-counts is written beside it: two sibling sequences under different keys, the second indented further, read as nested. Reaching 32 that way needs 32 keys each indented further than the last.
🤖 Generated with Claude Code