Repository navigation
Limit how deep values and conditions nest - #260
Merged
FlorianRappl merged 1 commit intoNov 2, 2026
Merged
Conversation
A value or condition nesting parentheses deeply ended the process with a stack overflow, which no caller can catch: the parser recurses once per level of parentheses and math functions in values (calc(), min(), max(), clamp()) and of parentheses in @media and @supports conditions, also when a value is set through the CSSOM. On a 256 KB thread stack, about 430 parentheses in calc() were enough (.NET 8, before the JIT optimizes); on a 1 MB stack about 1,900. - Add CssParserOptions.MaxValueNestingDepth (default 64). A value or condition nested deeper is invalid. The value parsers run inside the converters, which do not see the options, so a parser makes its limit current while it builds (ValueNesting); values set through the CSSOM without a parser get the default - Add tests at depths that crashed before and at the limit, parsing on a 256 KB thread stack, and for the option per parser, per browsing context and its scope - Add calc() and @supports shapes to the nesting benchmark 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.
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.
Types of Changes
Prerequisites
Please make sure you can check the following two boxes:
Contribution Type
What types of changes does your code introduce? Put an
xin all the boxes that apply:Description
The follow-up to #258, for values and conditions. There is no separate issue.
Parentheses and math functions in values (
calc(),min(),max(),clamp()) and parentheses in@mediaand@supportsconditions are parsed recursively, once per level, also when a value is set through the CSSOM (e.g.SetProperty). A deeply nested value overflows the stack, and aStackOverflowExceptioncannot be caught, so the whole process ends.With no limit, the deepest nesting that still parses (a fresh process per case, so before the JIT optimizes):
calc()@supportscondition@mediaconditionThe change
CssParserOptions.MaxValueNestingDepth, default 64 (CssParserOptions.DefaultMaxValueNestingDepth); 0 uses the default, asMaxNestingDepthdoes. A value or condition nested deeper is invalid, like any other invalid value.CssParsermakes its limit current on the thread while it builds (an internalValueNestingscope; building is synchronous). Values set through the CSSOM without a parser use the default. Making it configurable per browsing context or process-wide would need the options to reach the converters, a bigger change I left out.Parsing/DeepNesting.cs) parse on a thread with a fixed 256 KB stack, as in Limit how deep the parser nests rules #258: depths that crashed before for each case, the limit itself, a custom limit per parser and through the browsing context, and that the scope is restored afterwards.DeepStyleSheetBenchmarksgetscalc()and@supportsshapes at 10 and 60 levels.Benchmarks
Paired builds of the same benchmark code, alternating which goes first each round. BenchmarkDotNet 0.15.8, net10.0, AngleSharp 1.5.0 for both, pinned to the performance cores of an Intel Core Ultra 7 165H. Median of 3 rounds against #258 (b7b5db8):
CssParserBenchmarks, the 8 sample filescalc-10/calc-60supports-10/supports-60media-10/media-60style-10/style-60Allocations are the same.
CssCascadeBenchmarks.ParseInlineDeclarations, which parses many small declarations and so sets the scope most often, showed +3.9% in that run; a 6-round rerun put it at 408.9 μs against 408.2 μs (+0.2%), and two identical builds differ by up to ±5% in single rounds on this machine.For #258, which was merged before I posted its numbers: against
devel(cf08f08), the sample files were within −3.0% to +3.0% with sign changes between rounds, and the four nesting shapes within ±3% over 6 rounds, with the same allocations.Mitigation, not a fix: the bug is still present
As in #258, the limit only stops the input before it gets deep enough to crash. The parsers still recurse once per level:
A raised limit brings it back. Anyone who sets
MaxValueNestingDepthhigher, or parses on a thread with a smaller stack, takes the risk back:The XML docs of the option say so.
Trees built through the DOM (rendering,
GetInnerText,Find,DownloadResources) are still not covered, as noted in Limit how deep the parser nests rules #258.Suggested CHANGELOG line
I left
CHANGELOG.mdout so you can word it; suggestion:MaxValueNestingDepth) (#PR) @michaelthyregod🤖 Generated with Claude Code