Skip to content

[LOW] Prevent parser stack and cache exhaustion - #109

Closed
OskarEichler wants to merge 1 commit into
ammar:masterfrom
OskarEichler:codex/prevent-parser-resource-exhaustion
Closed

OskarEichler wants to merge 1 commit into
ammar:masterfrom
OskarEichler:codex/prevent-parser-resource-exhaustion

Conversation

@OskarEichler

Copy link
Copy Markdown

Summary

  • replaces recursive AST level assignment and traversal with iterative walks
  • maintains the total capture count incrementally instead of repeatedly summing all nesting levels
  • removes an unnecessary process-global cache of caller-supplied syntax-version strings

Security impact

Severity: LOW. Applications that parse attacker-influenced regular-expression text can be forced to raise SystemStackError with roughly 4,000 nested groups. Unlike ordinary parser errors, SystemStackError bypasses common rescue StandardError boundaries and can terminate the worker handling the input.

Applications that also expose the optional syntax-version argument can be made to retain every unique valid version alias for the process lifetime. Ten thousand supplied aliases create 10,001 cache entries; the cache has no eviction or bound.

The impact is availability only and requires an application to pass attacker-influenced patterns or syntax aliases into Regexp Parser. Songstats receives the gem through development RuboCop and does not expose either input.

Reproduction and fix

On 2.12.0 and current master, nested passive or capture groups raise SystemStackError at approximately 4,000 levels on Ruby 4.0.6. This branch parses and traverses 16,000 levels in bounded models by using explicit work stacks. Capture-heavy parsing is kept linear by tracking the already-computed total directly.

Calling Regexp::Syntax.for with 10,000 unique ruby/999.x.0 aliases retains all 10,000 caller strings on the baseline. The returned value is already a class singleton, so removing the alias cache preserves resolution while eliminating retained input.

Verification

  • Ruby 4.0.6: 2,035 existing examples, 0 failures
  • Ruby 3.2.11: 2,035 existing examples, 0 failures
  • 4,000/8,000/16,000-level parse and traversal models pass; baseline raises SystemStackError at 4,000
  • 10,000-alias model leaves no alias cache and all documented version forms still resolve
  • 64,000 nested capture groups parse in 0.30 seconds in the focused Ruby 4 model
  • changed-file RuboCop: 4 files, 0 offenses
  • Ragel regeneration and gem package build succeed
  • changed-file syntax and git diff --check pass

No repository tests were added or changed.

Compatibility and limitations

Breaking changes: none expected. AST traversal order, traversal events, child indices, nesting levels, capture numbering, syntax fallback, and returned syntax classes are unchanged. Code that introspects the undocumented Regexp::Syntax @alias_map instance variable will no longer find it.

Memory and CPU still grow with input size; this patch removes the avoidable stack failure, quadratic capture counting, and permanent alias retention rather than turning the parser into a resource sandbox. Other recursive opt-in operations on a returned AST, such as rendering very deeply nested trees, are outside this patch.

jaynetics added a commit that referenced this pull request Sep 19, 2026
- use queues or frames instead of recursion to prevent SystemStackError, closes #109
- extract static, re-usable scanner data into Scanner constants instead of writing it for every scan
- various minor performance tweaks
- improve scanner error messages by slicing only up to the offending character
- add benchmarks for traversal and comparison methods
jaynetics added a commit that referenced this pull request Sep 19, 2026
- use queues or frames instead of recursion to prevent SystemStackError, closes #109
- extract static, re-usable scanner data into Scanner constants instead of writing it for every scan
- various minor performance tweaks
- improve scanner error messages by slicing only up to the offending character
- add benchmarks for traversal and comparison methods
jaynetics added a commit that referenced this pull request Sep 19, 2026
- use queues or frames instead of recursion to prevent SystemStackError, closes #109
- extract static, re-usable scanner data into Scanner constants instead of writing it for every scan
- various minor performance tweaks
- improve scanner error messages by slicing only up to the offending character
- add benchmarks for traversal and comparison methods
jaynetics added a commit that referenced this pull request Sep 19, 2026
- use queues or frames instead of recursion to prevent SystemStackError, closes #109
- extract static, re-usable scanner data into Scanner constants instead of writing it for every scan
- various minor performance tweaks
- improve scanner error messages by slicing only up to the offending character
- add benchmarks for traversal and comparison methods
@jaynetics

Copy link
Copy Markdown
Collaborator

@OskarEichler thank you very much for pointing out this issue! I have worked around it in #110 together with some other improvements. The approach is similar to yours, but a bit more fine-tuned to the individual use cases of the various code paths, so as to avoid a negative impact on performance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants