Skip to content

Limit how deep the parser nests rules - #258

Merged
FlorianRappl merged 1 commit into
AngleSharp:develfrom
michaelthyregod:feature/rule-nesting-limit
Nov 1, 2026
Merged

FlorianRappl merged 1 commit into
AngleSharp:develfrom
michaelthyregod:feature/rule-nesting-limit

Conversation

@michaelthyregod

Copy link
Copy Markdown
Contributor

Types of Changes

Prerequisites

Please make sure you can check the following two boxes:

  • I have read the CONTRIBUTING document
  • My code follows the code style of this project

Contribution Type

What types of changes does your code introduce? Put an x in all the boxes that apply:

  • Bug fix (non-breaking change which fixes an issue, please reference the issue id)
  • New feature (non-breaking change which adds functionality, make sure to open an associated issue first)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • My change requires a change to the documentation
  • I have updated the documentation accordingly
  • I have added tests to cover my changes
  • All new and existing tests passed

Description

The style sheet counterpart of AngleSharp #1402 (selector nesting) and AngleSharp.Xml #42 (element nesting). There is no separate issue.

The CSS parser recurses once per level of nested blocks: @media, @supports, @layer, @container, @scope, @starting-style, @document and nested style rules. A deeply nested style sheet overflows the stack, and a StackOverflowException cannot be caught, so the whole process ends. One hostile style sheet is enough.

With no limit, the deepest nesting that still parses (a fresh process per case, so before the JIT optimizes):

net8.0, 1 MB stack net8.0, 256 KB stack net472, 1 MB stack net472, 256 KB stack
nested @media ~580 ~125 ~1,690 ~325
nested style rules (& b { … }) ~1,530 ~335 ~2,170 ~420

The change

  • CssParserOptions.MaxNestingDepth, default 64 (CssParserOptions.DefaultMaxNestingDepth); 0 uses the default. It counts the blocks a rule is nested in: grouping rules and style rules holding nested style rules.
  • Beyond the limit, the rule is dropped like an invalid rule, with the new CssParseError.NestingTooDeep, and parsing continues after it.
  • Why 64: a .NET 8 thread with a 256 KB stack overflows at about 125 nested @media, so 128 would not hold there (the tests show it). 64 leaves a 2x margin on such a thread and about 9x on a 1 MB stack. Real style sheets nest a few levels.
  • Tests (Parsing/DeepNesting.cs) parse on a thread with a fixed 256 KB stack, as #1402 does: 10,000 levels for each grouping rule and for nested style rules, the limit itself (64 kept, 65 dropped with the error), custom limits, and declarations continuing after a dropped nested rule. Against the unlimited parser the test host crashes, and with a default of 128 it crashes too.
  • Benchmark DeepStyleSheetBenchmarks: nested @media and nested style rules at 10 and 60 levels. Not run yet; I'll post paired results.

Mitigation, not a fix: the bug is still present

The limit only stops the input before it gets deep enough to crash. The parser still recurses once per level, so the crash is still there:

  • A raised limit brings it back. Anyone who sets MaxNestingDepth higher, or parses on a thread with a smaller stack, takes the risk back:

    var parser = new CssParser(new CssParserOptions { MaxNestingDepth = Int32.MaxValue });
    var css = String.Concat(Enumerable.Repeat("@media all{", 1000)) + "a{color:red}";
    parser.ParseStyleSheet(css);   // still "Stack overflow." with this PR (net8.0, 1 MB stack)

    The XML docs of the option say so, with the numbers above.

  • Trees built through the DOM are not covered. The HTML parser caps documents at 512 levels, but the DOM API does not. On a deeper tree, rendering (IWindow.Render), GetInnerText, RenderNodeExtensions.Find and DownloadResources still recurse once per element and overflow the stack at a few thousand levels. Your document-level depth limit from AngleSharp.Xml System.FormatException thown parsing CSS #39 would cover them; I also have explicit-stack versions of those walks if you want them, with benchmarks.

  • Values and conditions (parentheses in calc() and in @media/@supports conditions) recurse too; that is a separate, smaller PR after this one.

Suggested CHANGELOG line

I left CHANGELOG.md out so you can word it; suggestion:

  • Improved the parser with a nesting limit for rules (MaxNestingDepth) (#PR) @michaelthyregod

🤖 Generated with Claude Code

A style sheet nesting blocks deeply ended the process with a stack overflow,
which no caller can catch: the parser recurses once per level of @media,
@supports, @layer, @container, @scope and nested style rules. On a 256 KB
thread stack, about 125 nested @media rules were enough (.NET 8, before the
JIT optimizes); on a 1 MB stack about 580.

- Add CssParserOptions.MaxNestingDepth (default 64). A rule nested in more
  blocks is dropped like an invalid rule, raising the new
  CssParseError.NestingTooDeep, and parsing continues after it
- Add tests at depths that crashed before and at the limit, parsing on a
  256 KB thread stack; with a default of 128 the test host crashes
- Add a benchmark parsing nested @media and nested style rules

This is a mitigation, not a fix: parsing still recurses once per level, and
only the limit stops it. Raising the limit brings the crash back.
@CLAassistant

CLAassistant commented Nov 1, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@FlorianRappl FlorianRappl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@FlorianRappl FlorianRappl added this to the v1.3.1 milestone Nov 1, 2026
@FlorianRappl
FlorianRappl merged commit 0d0c908 into AngleSharp:devel Nov 1, 2026
5 checks passed
@michaelthyregod
michaelthyregod deleted the feature/rule-nesting-limit branch November 2, 2026 05:55
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.

3 participants