Repository navigation
fix(markdown): show a text with unclosed spans or many lists as plain - #472
Merged
Merged
Conversation
The parser takes time quadratic in two more constructs that the cost guard did not count. A backtick run, `<!` or `<?` that never closes, and a link title that never closes, is read to the end of its paragraph, so a paragraph of many such openers, escaped code spans, unclosed inline HTML comments, processing instructions, declarations and link titles among them, is slow to read. So is a text that opens many separate lists, each ended by a paragraph, a thematic break or another marker, at the top level or in one quote. A text within MAX_CHARS could hold enough of either to hold up the main thread each time it was first read, on a card, a course page or question, and in the Studio's release check, and a release can hold many such texts. The guard now counts both. MAX_LOOKAHEAD, 500,000, caps the characters the parser may read ahead for a span's or title's end, added up over the text: each opener counts to its closer, a backtick run to the next run of its length, `<!--` to `-->`, a link's title to its quote or paren, or to the next blank line when none comes. So closed spans, as in a long list or table of commands, count about once over, and only openers that never close add up to the cap. The count reads the text only as far as it counts and stops past the cap. MAX_LISTS, 200, caps the lists a text opens: a list's next items and the lines its items hold open none. An item that starts empty ends at a blank line right after it, as an item can start with at most one blank line, so the line after leaves its list. A marker that cannot interrupt a paragraph, an empty item or an ordered one that starts other than at 1, opens none while a paragraph is surely open. A marker counts wherever it is, in fenced code or an HTML block too, and a lazy line leaves the list, so the count errs high, and a fence holding a long diff can pass it. A text past either is shown as plain text, as past the guard's other limits; the pathological tests pin both shapes, at the cap and past it. No text in the library's decks or courses comes near either limit, and none is newly shown as plain. parseDocument lifts MAX_LOOKAHEAD with MAX_CHARS, as the count grows with a page's length, and refuses a page past MAX_LISTS. The Markdown docs list both limits. Refs: GHSA-25hm-hjrf-w8fh
ixuz
enabled auto-merge
November 1, 2026 04:29
CodeQL's js/bad-tag-filter flagged the `-->` pattern the cost guard reads inline HTML comments with, as it does not take `--!>`. The guard is no HTML filter: it counts how far the parser reads ahead, and the parser closes a comment at `-->` only, as CommonMark does, so a count that stopped at `--!>` would count low and let the quadratic input back in. The closer is now found as a `--` before a `>`, which counts the same characters in the same linear time, and a comment says why `--!>` does not count. Refs: #472
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Nov 1, 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.
What and why
The Markdown parser takes time quadratic in two more constructs that the cost guard did not count. The first is an opener that never closes: a backtick run,
<!,<?or a link title. The parser reads such an opener to the end of its paragraph, so a paragraph with many of them is slow to read. The second is a text that opens many separate lists. A text withinMAX_CHARScould hold enough of either to hold up the main thread each time it was first read: on a card, on a course page or question, and in the Studio's release check. A release can hold many such texts.The guard in
packages/markdown/src/parse.tsnow counts both:MAX_LOOKAHEAD(500,000 characters). It caps how far the parser may read ahead for the end of a span or title, added up over the text. Each opener counts only to its closer: a backtick run to the next run of its length,<!--to-->,<?to?>, a link's title to its closing quote or paren, or to the next blank line when none comes. Closed spans, as in a long list or table of commands, therefore count about once, and only openers that never close add up to the cap. The count itself takes time linear in the text and stops once past the cap.MAX_LISTS(200). It caps the lists a text opens. A list's next items, and the lines its items hold, open none. An item that starts empty ends at a blank line right after it, because an item can start with at most one blank line. A marker that cannot interrupt a paragraph (an empty item, or an ordered item that does not start at 1) opens no list while a paragraph is open.parseDocumentliftsMAX_LOOKAHEADtogether withMAX_CHARS, because the count grows with a page's length. It refuses a page pastMAX_LISTS.One existing test changed:
"a\n- b\n".repeat(MAX_UNDERLINES + 1)now goes pastMAX_LISTSfirst, so theMAX_UNDERLINEStest uses"a\n- b\n- c\n"instead.docs/markdown.md lists both limits and says how they are counted.
Refs: GHSA-25hm-hjrf-w8fh
Checklist
Refs:, notCloses:. Leave this unchecked otherwise.npm run checkpasses (lint, Markdown and spelling included),npm run lint:reusetoo if a file was added, andnpm run crosschecktoo ifns/ordecks/changed. (lint:reuseandcrosscheckn/a: no file was added, andns/anddecks/are unchanged)docs/is listed in docs/docs.ttl. (no new page)packages/ui/src/i18n/). (n/a: no message added or changed)ns/was agreed in an issue first; no publishedns/shapes/*/vN.ttlordecks/*/vN.ttlwas edited; generated files were regenerated, not edited. (n/a:ns/,decks/and generated files are unchanged)!and aBREAKING CHANGE:footer).npm run check:breakingsees a line ofapi/that goes, but not a feature removed, nor a reader that reads less than before. (check:breakingpasses, and every deck in the library still passeslibrary:check)npm run lint:reuse), or a new deck its line inREUSE.toml; third-party content is credited. (n/a: no file, deck or third-party content added)npm run dev) and tried the change, and considered the user journeys.type(scope): subject,!and aBREAKING CHANGE:footer if it breaks something), with an imperative subject and a body that explains why (CONTRIBUTING.md).