From 2b04d7ba341c16ac78fe82d3076bdd5c3de87c69 Mon Sep 17 00:00:00 2001
From: Hsiaoming Yang <me@lepture.com>
Date: Sun, 21 Jun 2026 21:12:23 +0900
Subject: [PATCH] fix(block): avoid quadratic ref link scans

---
 src/mistune/block_parser.py      | 22 ++++++++++++++++++----
 tests/test_security_ref_links.py | 15 +++++++++++++++
 2 files changed, 33 insertions(+), 4 deletions(-)
 create mode 100644 tests/test_security_ref_links.py

Index: mistune-3.1.3/src/mistune/block_parser.py
===================================================================
--- mistune-3.1.3.orig/src/mistune/block_parser.py
+++ mistune-3.1.3/src/mistune/block_parser.py
@@ -1,3 +1,4 @@
+from bisect import bisect_left
 import re
 from typing import Optional, List, Tuple, Match, Pattern, Set
 import string
@@ -254,11 +255,11 @@ class BlockParser(Parser[BlockState]):
 
         assert href_pos is not None
 
-        _blank = self.BLANK_LINE.search(state.src, href_pos)
-        if _blank:
-            max_pos = _blank.start()
-        else:
+        blank_pos = _find_next_blank_line(state, href_pos, self.BLANK_LINE)
+        if blank_pos is None:
             max_pos = state.cursor_max
+        else:
+            max_pos = blank_pos
 
         title, title_pos = parse_link_title(state.src, href_pos, max_pos)
         if title_pos:
@@ -529,6 +530,19 @@ def _parse_block_quote_line(line: str) -
     return _BLOCK_QUOTE_TRIM.sub("", text)
 
 
+def _find_next_blank_line(state: BlockState, pos: int, pattern: Pattern[str]) -> Optional[int]:
+    cache = state.env.get("__blank_line_starts__")
+    if cache is None or cache[0] is not state.src:
+        cache = (state.src, [m.start() for m in pattern.finditer(state.src)])
+        state.env["__blank_line_starts__"] = cache
+
+    positions = cache[1]
+    index = bisect_left(positions, pos)
+    if index < len(positions):
+        return positions[index]
+    return None
+
+
 def _trim_partial_next_line_indent(text: str, end_pos: int) -> int:
     line_start = text.rfind("\n") + 1
     if line_start == 0:
Index: mistune-3.1.3/tests/test_security_ref_links.py
===================================================================
--- /dev/null
+++ mistune-3.1.3/tests/test_security_ref_links.py
@@ -0,0 +1,15 @@
+import time
+from unittest import TestCase
+
+from mistune import create_markdown
+
+
+class TestRefLinkSecurity(TestCase):
+    def test_many_reference_links_return_quickly(self):
+        text = "".join("[a{}]: u\n".format(i) for i in range(3000))
+
+        start = time.monotonic()
+        create_markdown()(text)
+        elapsed = time.monotonic() - start
+
+        self.assertLess(elapsed, 0.5)
