Cap nesting depth in Gem::SafeMarshal::Reader - #9870
Closed
davutselcuk wants to merge 1 commit into
Closed
Conversation
`Reader#read_element` recurses once per nested element and counts nothing,
so a marshal stream of nested arrays exhausts the stack before any other
limit applies. Marshal spends two bytes per level, so ~4.8 KB is enough.
`SystemStackError` descends from `Exception`, not `StandardError`, so it
walks past callers that already guard this parse. `Gem::Source#fetch_spec`
is written to tolerate a bad spec and carry on:
spec = begin
Gem::SafeMarshal.safe_load(spec)
rescue StandardError
nil
end
That rescue cannot see a `SystemStackError`, so the intended "ignore the
bad spec" path never runs.
This adds a depth cap symmetric with the one `yaml_serializer.rb` already
applies to the other parser in this tree:
MAX_NESTING_DEPTH = 1_000
Past the cap the reader raises `Reader::TooDeeplyNestedError`, which is a
`Reader::Error` and therefore a `StandardError`, so existing rescues
behave as written.
Depth is counted per `read_element`, so width is unaffected: an array of
5,000 siblings still parses, and so does a 998-level nest.
Tests cover the cap, a nest just under it, and the wide-but-shallow case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Opened at your request, from the closing comment on HackerOne #4013093:
What this does
Reader#read_elementrecurses once per nested element and counts nothing, so a stream of nested arrays runs the stack down before any other limit applies. Marshal spends two bytes per level.This adds the symmetric cap:
Past it the reader raises
Reader::TooDeeplyNestedError, which is aReader::Errorand therefore aStandardError. That matters for callers already written to tolerate a bad spec, such asGem::Source#fetch_spec:A
SystemStackErrordescends fromException, so that rescue never sees it and the intended "skip the bad spec" path does not run. With the cap, it does.Depth, not width
The counter lives in
read_elementand unwinds in anensure, so sibling count is unaffected:TooDeeplyNestedErrorcaseis untouched; the diff is 12 lines of library code.Tests
Three added, covering the cap, a nest just under it, and the wide-but-shallow case.
Also checked a real
rack-3.1.8gemspec fetched from rubygems.org still parses.🤖 Generated with Claude Code