Skip to content

feat!: bound untrusted-input cost and parse inlines on a delimiter stack - #4

Merged
plimeor merged 4 commits into
mainfrom
fix/bounded-cost-untrusted-input
Oct 5, 2026
Merged

plimeor merged 4 commits into
mainfrom
fix/bounded-cost-untrusted-input

Conversation

@plimeor

@plimeor plimeor commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Summary

Bounds the cost of parsing untrusted Markdown and moves the inline parser onto CommonMark's delimiter-stack algorithm. SemVer-breaking: parse output changes for crossing marks, BOM/NUL input, bracket precedence, and input nested past the limits.

Commits

  • fix: bound parse and serialize cost on untrusted input: removes the quadratic, cubic, and exponential paths. Raw-content scans (code spans, autolinks, raw HTML, MDX, wikilinks, directive labels) go through per-input memo tables. MDX JSX tags are matched through a tag index. Nesting limits keep stack use bounded.
  • docs: adopt spec-flow layout and plan inline delimiter stack: moves the docs to the overview / specs / plans / archive / decisions layout and adds the plan.
  • feat!: parse inline marks and brackets on a delimiter stack:
    • All emphasis-like marks (*, _, ~~, ~, ^, ++, ==, ||, underline __) now pair on one delimiter stack. Links, images, and footnotes resolve on a bracket stack when ] arrives. The closer-search budget, the link-label verdict memo, and link recursion are gone.
    • All inline containers share one 32-level nesting limit.
    • A leading U+FEFF is skipped and U+0000 is read as U+FFFD, as CommonMark requires; spans stay in original-input coordinates.
  • fix: correct emphasis spans, reference fallback, and table spoiler splitting:
    • Emphasis/Strong spans now cover exactly their own delimiters and content.
    • [foo][bar falls back to a shortcut reference.
    • Validation accepts a hard line break that ends link text, alt text, an inline footnote, or a text directive label.
    • One table-row scanner, shared by the parser and the serializer, replaces three diverging spoiler predictions.
    • The plan is archived and its spec changes are merged into docs/specs/.

Behavior changes reviewers should know

  • Crossing marks resolve in closing order. **a ==b** c== now gives Strong(a ==b) + c==. Marks that don't cross parse as before.
  • BOM / NUL handling.
    • "\u{feff}# t" is now a heading.
    • NUL is written as U+FFFD in every node value.
  • Inputs past the nesting limits. Inner levels form and the outer delimiters stay literal.
  • Autolinks inside brackets follow GitHub (micromark), not comrak. The two conformance suites contradict each other on [https://…].
  • Table spoiler prediction is lexical. A spoiler that a link, a crossing mark, inline math, raw HTML, or an autolink breaks inside a cell leaves its bars as text (see docs/specs/block-syntax.md).

Design and trade-offs:

  • docs/decisions/0006-bounded-cost-on-untrusted-input.md
  • docs/archive/2026-10-05-inline-delimiter-stack/plan.md

Validation

  • Tests and builds: cargo test passes with 225 tests, and with --features html 244 pass. cargo fmt --check, RUSTDOCFLAGS='-D warnings' cargo doc --no-deps, the wasm32 build, and cargo +1.82 build all pass. Clippy has no new warning kinds.
  • Conformance: 2233/2236. The remaining 3 are the comrak cases for autolinks inside brackets, which conflict with micromark.
  • Linear time: adversarial growth sweeps across 6 dialects, plus serialize/render/validate, show no superlinear growth. A table-row growth check is also linear.
  • Stack: 98 worst-case inputs × operations stay within 615 KiB (debug) and 199 KiB (release), under a 2 MiB thread stack.
  • Diff against the previous commit (fixture corpus, CommonMark examples, and 4k generated inputs): only the intended changes. No input newly fails to serialize or to round-trip stably.
  • Independent review: the two problems it found (escaped pipes merging table cells; escaped backticks read as code-span delimiters) are fixed and covered by tests.
  • Generated table rows: the spoilers the scanner predicts match the spoilers the inline parser forms in each cell.

Known gaps (not addressed here)

  • Indented lines: inline spans on lines that carry leading whitespace inside their block are offset by that whitespace. This predates the change and is listed under Next in docs/overview.md.
  • Table speed: rows where every cell holds a code span and a spoiler parse about 12–20% slower and serialize about 15% slower. Plain tables and ordinary documents are unchanged.

🤖 Generated with Claude Code

plimeor and others added 4 commits October 4, 2026 13:10
Adversarial Markdown could make parsing quadratic, cubic, or exponential
(unclosed brackets, nested links, unclosed MDX JSX tags and expressions)
and could abort the process by overflowing the stack on deep nesting.

- Memoize forward closer scans per input (src/memo.rs): position tables,
  path memos, and bracket memos share step functions with the unmemoized
  walks, so answers are identical.
- Index MDX JSX tags once per text and match closing tags over same-name
  chains through a persistent per-name map (n log n); block JSX and
  expression blocks reuse the same walks over joined lines.
- Cap nesting at 32 block / 32 inline / 16 emphasis levels; content past a
  limit stays literal text. Make block-quote paragraph continuation
  iterative.
- Budget the start-dependent `++` / `==` / underline closer search; past
  the budget further openers in the span stay literal.
- Make serializer escaping and line-length tracking linear, and check
  `html_inline` before copying raw HTML.

Output is byte-identical to the previous parser on all non-adversarial
inputs checked (fixture corpus plus seeded random inputs, all dialects).
README documents the cost contract and limits; decision 006 records the
rationale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add docs/overview.md and behavior specs under docs/specs/ (public API,
  block and inline syntax, serialization, validation, HTML rendering,
  untrusted-input cost), moving the behavior contract out of README.md.
- Renumber decisions to 0001-0006 in the spec-flow record format and
  rewrite 0001 for the published package boundary.
- Archive the two completed plans and drop docs/index.md.
- Replace the AGENTS.md docs section with the spec-flow conventions.
- Add the inline-delimiter-stack plan: CommonMark BOM/NUL handling and
  moving emphasis-like marks and brackets onto a delimiter stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Implement docs/plans/inline-delimiter-stack:

- Ignore one leading U+FEFF and read every U+0000 as U+FFFD when
  recognizing structure and in node values; spans stay in original
  input coordinates.
- Pair `*`, `_`, `~~`, `~`, `^`, `++`, `==`, `||`, and underline `__` on
  one delimiter stack in closing order. Properly nested marks parse as
  before; crossing marks now resolve in closing order.
- Resolve links, images, inline footnotes, and footnote references with
  a bracket stack when `]` arrives; a formed link deactivates earlier
  `[` openers. Remove the link-label verdict memo, link recursion, and
  the `++` / `==` / underline closer-search budget.
- Share the 32-level inline nesting limit across every inline container
  (emphasis keeps its 16-level cap), so the deepest inline tree fits a
  2 MiB stack.
- Rewrite decision 0006 for the resulting design.

Conformance: 2233/2236 (was 2229). The three remaining failures are
comrak's "no literal autolinks inside brackets" cases, which conflict
with five micromark (GitHub) cases; GitHub's behavior is kept.

BREAKING CHANGE: parse output changes for crossing emphasis-like marks,
for input with a leading BOM or embedded NUL, for bracket precedence
where the previous forward scan chose an outer bracket, and for nesting
past the limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…litting

- Cut every emphasis-like span from its consumed delimiters' text nodes, so
  Emphasis and Strong spans cover exactly their own delimiters and content
  (`***a***` was giving a Strong span outside its Emphasis).
- Fall back to a shortcut reference when the `[` after a link label opens no
  reference label (`[foo][bar` with `[foo]: /u`), as CommonMark specifies.
- Accept a hard line break at the end of link text, image alt text, inline
  footnotes, and text directive labels in validation; a `]` closes after a
  line break, and the parser already produced that shape.
- Replace the three diverging table-row spoiler predictions (row splitting,
  the block-quote row check, and the serializer's cell check) with one
  linear scanner that pairs `||` runs as the inline parser pairs them in a
  cell, reads code spans and escaped backticks as the inline parser does,
  and never lets an escaped pipe merge two cells.
- State the `^[` caret rule as the lexical decision it is.

Merge the inline-delimiter-stack spec changes into docs/specs and archive
the plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@plimeor
plimeor merged commit bd00a0f into main Oct 5, 2026
3 checks passed
@plimeor
plimeor deleted the fix/bounded-cost-untrusted-input branch October 5, 2026 11:24
@github-actions github-actions Bot mentioned this pull request Oct 5, 2026
plimeor pushed a commit that referenced this pull request Oct 7, 2026
…nto the branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ShSm97dW5me9qB345EiTs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant