Repository navigation
Conversation
PR HealthPublish ✔️
Documentation at https://github.com/dart-lang/ecosystem/wiki/Publishing-automation. This check can be disabled by tagging the PR with Changelog Entry ✔️
Changes to files need to be accounted for in their respective changelogs. This check can be disabled by tagging the PR with Breaking changes ✔️
This check can be disabled by tagging the PR with API leaks ✔️The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.
This check can be disabled by tagging the PR with Unused Dependencies ✔️
For details on how to fix these, see dependency_validator. This check can be disabled by tagging the PR with License Headers ✔️
All source files should start with a license header. Unrelated files missing license headers
This check can be disabled by tagging the PR with Coverage ✔️
This check for test coverage is informational (issues shown here will not fail the PR). This check can be disabled by tagging the PR with |
There was a problem hiding this comment.
Code Review
This pull request introduces comprehensive performance optimizations and stack overflow protections to the markdown parser. Key changes include refactoring recursive AST traversals, inline content parsing, and HTML block parsing into iterative stack-based implementations. Additionally, it addresses quadratic time complexity issues in inline parsing by utilizing a LinkedList for the delimiter stack, caching lookups (such as footnotes, HTML closers, and backtick scans), capping container nesting depth, and enforcing limits on link label lengths and parentheses nesting. A review comment suggests a performance improvement in code_syntax.dart to directly return whether a valid closing run exists when the document has already been fully scanned, avoiding redundant scans.
| if (state.scannedToEnd && (state.lastRunAt[length] ?? -1) < openEnd) { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
When state.scannedToEnd is true, the entire document has already been scanned, and state.lastRunAt contains the final positions of all backtick runs. Instead of only returning false when no matching run exists and otherwise falling through to re-scan the document, we can directly return whether a valid closing run exists at or after openEnd. This avoids redundant scanning and improves performance.
| if (state.scannedToEnd && (state.lastRunAt[length] ?? -1) < openEnd) { | |
| return false; | |
| } | |
| if (state.scannedToEnd) { | |
| return (state.lastRunAt[length] ?? -1) >= openEnd; | |
| } |
dc6993e to
8352855
Compare
ec0c6ac to
3a4fe38
Compare
3a4fe38 to
b75b70c
Compare
b75b70c to
dd7de15
Compare
98fee9c to
3dc5181
Compare
Add `pkgs/markdown/test/pathological_test.dart`, expand
`pkgs/markdown/test/regression_test.dart`, and document the DoS fixes in
`pkgs/markdown/CHANGELOG.md`.
----
Each of the preceding commits fixes a quadratic O(n^2), exponential O(2^n), or
deep-recursion (`StackOverflowError`) vulnerability in `package:markdown`.
Without automated scaling assertions, deep-nesting tests, and fuzz testing over
adversarial token combinations, regressions in regexes, delimiter processing, or
AST traversal could silently reintroduce super-linear behavior without breaking
functional CommonMark/GFM output tests.
1. **`pkgs/markdown/test/pathological_test.dart`**:
- **Linear-scaling suite (`_cases`)**: Tests 51 pathological input generators
in both CommonMark and GFM (`ExtensionSet.gitHubWeb`) modes at 8 KB and
64 KB (8x size ratio), asserting that the 64 KB render time is within a
bounded linear multiple (`3 * _scale`) of the 8 KB render time (where
linear growth is ~8x and quadratic growth is ~64x).
- **Deep-nesting suite (`_deepCases`)**: Renders 6 deep-nesting generators at
depth 32,000 (nested strong emphasis, nested strong emphasis run, nested
strong emphasis with trailing text, nested strong emphasis in image alt,
nested blockquotes, and nested lists) in both CommonMark and GFM modes to
verify that no `StackOverflowError` is thrown.
- **Custom `linkBuilder` regression test**: Verifies that a custom
`linkBuilder` that does not invoke `getChildren()` produces the expected
AST when earlier delimiters resolve into emphasis elements.
- **Token-alphabet fuzzer**: Runs 200 deterministic PRNG iterations (`Random(42)`),
each picking a random subset of 2–6 tokens from a 23-token Markdown
alphabet and generating a 2,000-token string to verify that arbitrary token
combinations render in under 2 seconds without hanging or throwing.
2. **`pkgs/markdown/test/regression_test.dart`**:
- Adds targeted timing regression tests for unclosed inline link destinations
and titles and inline delimiter runs.
3. **`pkgs/markdown/CHANGELOG.md`**:
- Documents the security and performance fixes under `7.4.1-wip`.
3dc5181 to
d057d0b
Compare
Fixes issues combines the changes from #2728, #2729, and #2730 along with fixes for additional quadratic, exponential, and deep-recursion (
StackOverflowError) vectors acrosspackage:markdown.Structured as a series of 28 atomic single-file, single-concern code commits so each commit can be reviewed independently:
Reference links & footnotes (
link_syntax.dart,footnote_ref_syntax.dart):LinkSyntax._parseReferenceLinkLabelLinkSyntax.closeuntiltryCreateReferenceLinksucceedsnormalizeLinkLabelandsanitizeLinkAttributeuntil a reference link definition orlinkResolvermatch is found^prefix and emptyfootnoteReferencesmap before scanning, and cache case-folded footnote keys and 1-based positions viaExpandoInline link destinations & titles (
link_syntax.dart):<in bracketed inline link destinations (<...>) per CommonMark §6.3parenCount < 0) and nesting limit (parenCount > 32) in bare inline link destinations(in parenthesized inline link titles ((...)) per CommonMark §6.3InlineParserdelimiter stack, tree, and open brackets (inline_parser.dart):removeRangeand deep recursion in_combineAdjacentTextInlineParser._delimiterStackas aLinkedList<_DelimiterNode>InlineParser._treeas aLinkedList<_TreeNode>[and![brackets in a linked stack (_topBracket/_topActiveLinkBracket) for[deactivationInline syntax rescanning & backtracking (
autolink_syntax.dart,line_break_syntax.dart,inline_parser.dart,inline_html_syntax.dart,code_syntax.dart,autolink_extension_syntax.dart):<insideAutolinkSyntaxLineBreakSyntax_PlainTextSyntax<!,<!--,<?,<![CDATA[) before runningInlineHtmlSyntax.patternCodeSyntaxAutolinkExtensionSyntaxDeep recursion & container block nesting (
ast.dart,document.dart,html_block_syntax.dart,block_parser.dart):Element.accept,Element.textContent,Document._parseInlineContent, andHtmlBlockSyntax.parseChildLinesiterativeBlockParserBlock patterns,
LinkParser,CodeBlockSyntax, andHtmlRenderer(patterns.dart,link_parser.dart,code_block_syntax.dart,html_renderer.dart):tablePatternheaderPattern(in parenthesized link reference titles and avoid quadratic line concatenation inLinkParserCodeBlockSyntax.parseChildLinesHtmlRenderer.uniquifyIdTests & changelog (
test/pathological_test.dart,test/regression_test.dart,CHANGELOG.md):