Repository navigation
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
github-actions Bot
pushed a commit
that referenced
this pull request
Sep 12, 2026
## [18.0.13](v18.0.12...v18.0.13) (2026-09-12) ### Bug Fixes * allow tabs in the thematic break that ends a list item ([#4087](#4087)) ([afbb27c](afbb27c)) * avoid O(n^2) scanning in reflinkSearch ([#4090](#4090)) ([c6a25bb](c6a25bb)) * case fold reference link labels ([#4077](#4077)) ([aed9336](aed9336)) * drop the leading whitespace after a hard line break ([#4075](#4075)) ([123ce04](123ce04)) * match html block start conditions when ending a list item ([#4072](#4072)) ([c2facac](c2facac)) * respect raw tokens when closing link labels ([#4066](#4066)) ([ef394f7](ef394f7)) * strip a tab that follows spaces in an indented code block ([#4080](#4080)) ([dbb393d](dbb393d))
HanzoRazer
added a commit
to HanzoRazer/luthiers-toolbox
that referenced
this pull request
Sep 21, 2026
Both failing checks on PR #390 are the same thing: Dependabot cannot author a CBSP21 manifest, so the PR had none of its own. The gate says so precisely -- "Manifests exist that would cover these files, but they were authored for other changes" -- which is CBSP21-NOBORROW-001 doing its job, not a false positive. Lockfile audited by parsing, not by reading the diff view, because the diff view is what let the zod 4->3 revert ride along inside PR #296. Loaded both package-lock.json files and compared the full packages{} map against the merge base 576aa93: 412 packages before, 412 after ADDED: none REMOVED: none CHANGED: node_modules/marked 18.0.12 -> 18.0.13 (only entry) SAME VERSION but integrity changed: none So nothing incidental moved. License unchanged (MIT). Release content: seven fixes, all CommonMark conformance parsing corrections, plus markedjs/marked#4090 "avoid O(n^2) scanning in reflinkSearch" -- a quadratic-blowup hardening that is worth naming because this client renders server-supplied Markdown. No CVE or GHSA is attached, so this is currency that happens to include a DoS-shaped fix, not a security update, and the manifest says that rather than inventing a stronger rationale. Call-site survey: grep finds marked in four client files, but two are the English word in comments. The only import is RMOSCncJobDetailView.vue:135, used once at :182. MarkdownRenderer.vue uses v-html with a hand-rolled renderer and does not import marked at all. Recorded in architecture_scan as one low finding, NOT repaired here: RMOSCncJobDetailView.vue renders marked(md) into v-html at :115 with no sanitizer. marked v5+ removed its built-in sanitize option and documents that callers sanitize output themselves. The input is job.operator_report_md from the CNC job detail API, so the exposure is stored rather than reflected. It predates this bump. Adding a sanitizer dependency inside a Dependabot patch bump would make the bump un-reviewable, so it is raised for separate triage. Verified: CBSP21 coverage gate PASSED, architecture scan acknowledged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This branch was successfully deployed
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