Index: sqlparse-0.4.4/benchmarks/bench_dollar_quote_redos.py
===================================================================
--- /dev/null
+++ sqlparse-0.4.4/benchmarks/bench_dollar_quote_redos.py
@@ -0,0 +1,90 @@
+"""Delimited-literal lexer benchmark (GHSA-prg7-hcfm-mfcr).
+
+Measures parse time for SQL text containing many unique, unmatched
+opening delimiters for the two lexer constructs that used a lazy dot-all
+regex (`[\\s\\S]*?`) terminated by a backreference or a literal closing
+sequence:
+
+- Dollar-quoted literals, e.g. `$a0$x $a1$x ... $aN$x` (backreference).
+- Multiline comments, e.g. `/* unique0 ... /* unique1 ...` (literal `*/`).
+
+When no closing delimiter is present, a lazy dot-all quantifier applied at
+every text position must scan to the end of the remaining input for every
+opener, which is O(n^2) total work as the number of openers grows.
+
+This benchmark does not assert a pass/fail threshold, since absolute timings
+and scaling ratios depend on the host machine. It exists to make the
+runtime characteristics of these code paths observable and to let it be
+re-run (e.g. after a fix) to confirm that scaling has improved.
+
+Run with:  python benchmarks/bench_dollar_quote_redos.py
+"""
+
+import signal
+import time
+
+import sqlparse
+from sqlparse.engine import grouping
+
+# Disable the grouping-stage DoS guards. They fire only after lexing
+# completes and do not bound regex CPU time, so they would otherwise mask
+# the lexer's true (unbounded) timing behind a SQLParseError at larger n.
+grouping.MAX_GROUPING_DEPTH = None
+grouping.MAX_GROUPING_TOKENS = None
+
+
+def _alarm_handler(signum, frame):
+    raise TimeoutError()
+
+
+signal.signal(signal.SIGALRM, _alarm_handler)
+
+
+def measure(label, sql, fn):
+    signal.alarm(30)
+    t0 = time.perf_counter()
+    status = 'OK'
+    try:
+        fn(sql)
+    except sqlparse.exceptions.SQLParseError:
+        status = 'CAP'
+    except TimeoutError:
+        status = 'TIMEOUT'
+    finally:
+        signal.alarm(0)
+    dt = (time.perf_counter() - t0) * 1000
+    print(f'  {status:8} {dt:8.1f} ms  {label}  ({len(sql)} B)')
+    return dt
+
+
+def make_dollar_quote_payload(n):
+    # N unique, never-closed dollar-quote openers. Each is unique so the
+    # backreference regex cannot short-circuit on an earlier match.
+    return ' '.join(f'$a{i}$x' for i in range(n))
+
+
+def make_comment_payload(n):
+    # N unique, never-closed multiline comment openers. No '*/' appears
+    # anywhere, so the closing literal can never short-circuit the scan.
+    return ' '.join(f'/* unique{i} comment never closed' for i in range(n))
+
+
+def run_scaling(label, make_payload, sizes=(250, 500, 1000, 2000, 4000, 8000)):
+    print(f'{label}:')
+    timings = {}
+    for n in sizes:
+        sql = make_payload(n)
+        timings[n] = measure(f'{label} n={n}', sql, sqlparse.parse)
+
+    print()
+    print('Scaling ratios (O(n^2) implies ~4x time per 2x input):')
+    for prev, curr in zip(sizes, sizes[1:]):
+        if timings[prev] > 0:
+            ratio = timings[curr] / timings[prev]
+            print(f'  n={prev} -> n={curr} (input x{curr / prev:.1f}): '
+                  f'time ratio = {ratio:.2f}x')
+    print()
+
+
+run_scaling('Unmatched dollar-quote openers', make_dollar_quote_payload)
+run_scaling('Unclosed multiline comments', make_comment_payload)
Index: sqlparse-0.4.4/sqlparse/keywords.py
===================================================================
--- sqlparse-0.4.4.orig/sqlparse/keywords.py
+++ sqlparse-0.4.4/sqlparse/keywords.py
@@ -5,7 +5,10 @@
 # This module is part of python-sqlparse and is released under
 # the BSD License: https://opensource.org/licenses/BSD-3-Clause
 
+import re
+
 from sqlparse import tokens
+from sqlparse.utils import _DelimiterOccurrence, resolve_paired_delimiters
 
 # object() only supports "is" and is useful as a marker
 # use this marker to specify that the given regex in SQL_REGEX
@@ -13,12 +16,64 @@ from sqlparse import tokens
 PROCESS_AS_KEYWORD = object()
 
 
+# Dollar-quoted literals (`$tag$...$tag$`) and multiline comments
+# (`/*...*/`, `/*+...*/`) used to be matched with per-position regexes
+# using a lazy dot-all quantifier (`[\s\S]*?`) terminated by a
+# backreference or a literal delimiter. Applied at every text position by
+# the lexer loop below, that shape is O(n^2) on adversarial input with
+# many unclosed openers, since each failed attempt re-scans to the end of
+# the remaining text (GHSA-prg7-hcfm-mfcr). They are resolved instead in
+# a single linear pass by find_delimited_spans().
+_DOLLAR_QUOTE_DELIM = re.compile(r'\$(?:[_A-ZÀ-Ü]\w*)?\$', re.IGNORECASE | re.UNICODE)
+_DOLLAR_QUOTE_OPENER_OK = re.compile(r'(?<![\w"$])', re.UNICODE)
+_COMMENT_HINT_OPEN = re.compile(r'/\*\+')
+_COMMENT_OPEN = re.compile(r'/\*(?!\+)')
+_COMMENT_CLOSE = re.compile(r'\*/')
+
+
+def find_delimited_spans(text):
+    """Locate dollar-quoted literals and multiline comments in `text`.
+
+    Returns a dict mapping each span's start offset to
+    (end offset, token type).
+    """
+    has_dollar = '$' in text
+    # A comment can only ever open on "/*"; without it "*/" alone can
+    # never pair with anything, so gating on "/*" alone is sufficient to
+    # skip all three comment-related regex passes below.
+    has_comment_open = '/*' in text
+    if not has_dollar and not has_comment_open:
+        return {}
+
+    occurrences = []
+    if has_dollar:
+        for m in _DOLLAR_QUOTE_DELIM.finditer(text):
+            tag = m.group()
+            can_open = _DOLLAR_QUOTE_OPENER_OK.match(text, m.start()) is not None
+            occurrences.append(
+                _DelimiterOccurrence(m.start(), m.end(), tag, can_open, True, tokens.Literal))
+    if has_comment_open:
+        for m in _COMMENT_HINT_OPEN.finditer(text):
+            occurrences.append(_DelimiterOccurrence(
+                m.start(), m.end(), 'C', True, False,
+                tokens.Comment.Multiline.Hint))
+        for m in _COMMENT_OPEN.finditer(text):
+            occurrences.append(_DelimiterOccurrence(
+                m.start(), m.end(), 'C', True, False,
+                tokens.Comment.Multiline))
+        for m in _COMMENT_CLOSE.finditer(text):
+            occurrences.append(
+                _DelimiterOccurrence(m.start(), m.end(), 'C', False, True, None))
+    occurrences.sort(key=lambda occ: occ.start)
+
+    spans = resolve_paired_delimiters(occurrences)
+    return {start: (end, ttype) for start, end, ttype in spans}
+
+
 SQL_REGEX = [
     (r'(--|# )\+.*?(\r\n|\r|\n|$)', tokens.Comment.Single.Hint),
-    (r'/\*\+[\s\S]*?\*/', tokens.Comment.Multiline.Hint),
 
     (r'(--|# ).*?(\r\n|\r|\n|$)', tokens.Comment.Single),
-    (r'/\*[\s\S]*?\*/', tokens.Comment.Multiline),
 
     (r'(\r\n|\r|\n)', tokens.Newline),
     (r'\s+?', tokens.Whitespace),
@@ -30,7 +85,6 @@ SQL_REGEX = [
 
     (r"`(``|[^`])*`", tokens.Name),
     (r"´(´´|[^´])*´", tokens.Name),
-    (r'((?<!\S)\$(?:[_A-ZÀ-Ü]\w*)?\$)[\s\S]*?\1', tokens.Literal),
 
     (r'\?', tokens.Name.Placeholder),
     (r'%(\(\w+\))?s', tokens.Name.Placeholder),
Index: sqlparse-0.4.4/sqlparse/lexer.py
===================================================================
--- sqlparse-0.4.4.orig/sqlparse/lexer.py
+++ sqlparse-0.4.4/sqlparse/lexer.py
@@ -128,8 +128,16 @@ class Lexer:
             raise TypeError("Expected text or file-like object, got {!r}".
                             format(type(text)))
 
+        delimited_spans = keywords.find_delimited_spans(text)
+
         iterable = enumerate(text)
         for pos, char in iterable:
+            if pos in delimited_spans:
+                end, ttype = delimited_spans[pos]
+                yield ttype, text[pos:end]
+                consume(iterable, end - pos - 1)
+                continue
+
             for rexmatch, action in self._SQL_REGEX:
                 m = rexmatch(text, pos)
 
Index: sqlparse-0.4.4/sqlparse/utils.py
===================================================================
--- sqlparse-0.4.4.orig/sqlparse/utils.py
+++ sqlparse-0.4.4/sqlparse/utils.py
@@ -7,7 +7,7 @@
 
 import itertools
 import re
-from collections import deque
+from collections import defaultdict, deque, namedtuple
 from contextlib import contextmanager
 
 # This regular expression replaces the home-cooked parser that was here before.
@@ -107,6 +107,50 @@ def consume(iterator, n):
     deque(itertools.islice(iterator, n), maxlen=0)
 
 
+_DelimiterOccurrence = namedtuple(
+    '_DelimiterOccurrence', 'start end tag can_open can_close payload')
+
+
+def resolve_paired_delimiters(occurrences):
+    """Pair delimiter occurrences (quote/comment open & close markers) in
+    one left-to-right pass, instead of re-scanning the remaining text for
+    every unmatched opener -- which is what a backreference- or literal-
+    terminated lazy-dot-all regex applied at every text position ends up
+    doing, and is O(n^2) on adversarial input (GHSA-prg7-hcfm-mfcr).
+
+    `occurrences` must be sorted by `start`. Each occurrence with
+    `can_open` is paired with the nearest later occurrence sharing its
+    `tag` that has `can_close`; anything in between (including other
+    openers) is left as literal content. Unpaired openers are dropped,
+    exactly as a regex that never finds its closing delimiter fails to
+    match at all.
+
+    Returns a list of (start, end, payload) for each resolved span.
+    """
+    occurrences = list(occurrences)
+    closers = defaultdict(deque)
+    for idx, occ in enumerate(occurrences):
+        if occ.can_close:
+            closers[occ.tag].append(idx)
+
+    spans = []
+    consumed_until = 0
+    for i, occ in enumerate(occurrences):
+        if occ.can_close:
+            queue = closers[occ.tag]
+            if queue and queue[0] == i:
+                queue.popleft()
+        if occ.start < consumed_until or not occ.can_open:
+            continue
+        queue = closers[occ.tag]
+        if queue:
+            close_idx = queue.popleft()
+            close_end = occurrences[close_idx].end
+            spans.append((occ.start, close_end, occ.payload))
+            consumed_until = close_end
+    return spans
+
+
 @contextmanager
 def offset(filter_, n=0):
     filter_.offset += n
