fix: avoid O(n^2) scanning in reflinkSearch - #4090
Merged
Merged
Conversation
`reflink` and `nolink` are anchored, so the tokenizer tries each of them at a single position. `reflinkSearch` drops the anchors and runs with the global flag, which makes every '[' in the source a start position. A label crosses a bracket only by escaping it, and nothing capped how often it could, so a candidate that can never match still scanned to the end of the source: 188 KB of `'[' + '\t\\['.repeat(n)` took 21s with default options. Bound the label runs used to build `reflinkSearch`. The inline label limits how many escapes, code spans and nested brackets a link text may hold and leaves runs of ordinary characters unbounded, so link text of any length is still found. The block label is limited to 999 items, which covers every label CommonMark allows. Parse time is now linear: 188 KB goes from 20.9s to 1.1s, and doubling the input doubles the time instead of quadrupling it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@spokodev is attempting to deploy a commit to the MarkedJS Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
UziTech
approved these changes
Sep 11, 2026
UziTech
left a comment
Member
There was a problem hiding this comment.
Thanks! 💯
I didn't know CommonMark limited reflink length
styfle
approved these changes
Sep 11, 2026
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.
Marked version: current
master(a43c064); reproduces on the released 18.0.12Markdown flavor: CommonMark
Description
With default options, a run of escaped brackets parses in O(n²). The input is just text — it contains no
]at all, so nothing in it can ever become a link:masterDoubling the input quadruples the time on
master. After the change it doubles it, measured out to 1.5 MB (×1.94 to ×2.02 per doubling). For scale, 190 KB of ordinary markdown parses in 50 ms.The cost is
inline.reflinkSearch.reflinkandnolinkare anchored, so the tokenizer tries each of them at a single position, butreflinkSearchdrops the anchors and runs with the global flag — every[in the source becomes a start position. A label crosses a bracket only by escaping it, and nothing capped how often it could, so a candidate that can never match still scans to the end of the source. Isolating the rule at n=32,000 gives 3.3 s for zero matches.The fix bounds the label runs that
reflinkSearchis built from, leavingreflink,nolinkandlinkuntouched:_boundedInlineLabelbounds how many escapes, code spans and nested brackets a link text may hold. Runs of ordinary characters stay unbounded, so link text of any length is still found — CommonMark puts no limit on link text, only on link labels._boundedBlockLabelbounds the label to 999 items, which covers every label the spec permits: "A link label can have at most 999 characters inside the square brackets."Verification
test:specs1813/1813,test:unit191/191,test:umd,test:cjs,test:types,test:lintall pass.To check that nothing else moved, I diffed
master's output against this branch over 247,768 document × option comparisons — 421 real-world.mdfiles, all 1,521 markdown samples intest/specs, and 60,000 seeded bracket/escape/backtick fuzz cases, each under default,gfm: false,breaksandpedantic. Zero output differences. Shrinking the bounds to 3 makes that same harness report 24 differences and 10 spec failures, so it does detect change.Output does change for one input class: a reference label longer than 999 characters.
masteraccepts it as a link, this branch does not — which is whatcommonmark.jsdoes:masterOne note on tooling:
recheckreports this rule asvulnerableboth before and after, because a bound of 999 is still a large constant. It also flagsinline.normal.linkandinline.gfm._backpedal, which measure as fast end to end, so I went by measured parse time rather than by its verdict.test/specs/redos/quadratic_reflink_search.cjssits next to the existing quadratic guards. With the fix its two cases take 0.32 s and 0.23 s; withsrc/rules.tsreverted they take 1.63 s and 1.65 s and both fail the runner'stook too longassertion.Contributor
test/specs/redos/quadratic_reflink_search.cjs.🤖 Generated with Claude Code