Index: sqlparse-0.4.4/benchmarks/validate_group_comments_dos.py
===================================================================
--- /dev/null
+++ sqlparse-0.4.4/benchmarks/validate_group_comments_dos.py
@@ -0,0 +1,85 @@
+"""Validate that ``group_comments`` scales linearly on comment-only input.
+
+Regression check for the quadratic O(n^2) DoS in ``group_comments``
+(sqlparse/engine/grouping.py), reported as GHSA-f2ff-p2ww-7p4p.
+
+A statement made only of single-line comments (``'-- c\\n'`` repeated n times)
+lexes in O(n) but ``group_comments`` rescans the O(n) remaining tokens for every
+comment token, giving O(n^2) total work. ``group_comments`` runs first in
+``group()``, before the ``MAX_GROUPING_TOKENS`` guard, so the token cap does not
+protect this vector. The path is reachable via ``sqlparse.parse()`` and
+``sqlparse.format(sql, strip_comments=True)``.
+
+This script measures the scaling of the vulnerable path and reports whether the
+observed growth is quadratic (vulnerable) or roughly linear (patched).
+
+Run with:  python benchmarks/validate_group_comments_dos.py
+
+Exit code 0 => behaviour looks linear (advisory mitigated).
+Exit code 1 => behaviour looks quadratic (advisory reproduced).
+"""
+
+import sys
+import time
+
+import sqlparse
+
+
+def payload(n):
+    """A comment-only statement of n single-line comments."""
+    return '-- c\n' * n
+
+
+def measure(fn, sql):
+    t0 = time.perf_counter()
+    fn(sql)
+    return (time.perf_counter() - t0) * 1000
+
+
+def run(label, fn):
+    print(f'{label}:')
+    sizes = (1000, 2000, 4000, 8000)
+    timings = []
+    for n in sizes:
+        dt = measure(fn, payload(n))
+        timings.append(dt)
+        print(f'  n={n:5d}  {dt:8.1f} ms  ({len(payload(n))} B)')
+
+    # For each doubling of the input, quadratic growth ~4x, linear ~2x.
+    ratios = [b / a for a, b in zip(timings, timings[1:]) if a > 0]
+    print(f'  doubling ratios: {", ".join(f"{r:.2f}x" for r in ratios)}')
+    return ratios
+
+
+def classify(ratios):
+    """Quadratic if the average per-doubling ratio is closer to 4x than 2x."""
+    if not ratios:
+        return 'inconclusive', 0.0
+    avg = sum(ratios) / len(ratios)
+    # Midpoint between linear (2x) and quadratic (4x) is 3x.
+    return ('quadratic' if avg >= 3.0 else 'linear'), avg
+
+
+def main():
+    print('GHSA-f2ff-p2ww-7p4p: quadratic DoS in group_comments\n')
+
+    all_ratios = []
+    all_ratios += run('sqlparse.parse', sqlparse.parse)
+    print()
+    all_ratios += run(
+        'sqlparse.format(strip_comments=True)',
+        lambda s: sqlparse.format(s, strip_comments=True),
+    )
+    print()
+
+    verdict, avg = classify(all_ratios)
+    print(f'Average doubling ratio: {avg:.2f}x  =>  {verdict}')
+    if verdict == 'quadratic':
+        print('VULNERABLE: growth is quadratic, advisory reproduced.')
+        return 1
+    print('OK: growth is roughly linear, advisory mitigated.')
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
Index: sqlparse-0.4.4/sqlparse/engine/grouping.py
===================================================================
--- sqlparse-0.4.4.orig/sqlparse/engine/grouping.py
+++ sqlparse-0.4.4/sqlparse/engine/grouping.py
@@ -319,9 +319,14 @@ def group_comments(tlist):
     while token:
         eidx, end = tlist.token_not_matching(
             lambda tk: imt(tk, t=T.Comment) or tk.is_whitespace, idx=tidx)
-        if end is not None:
-            eidx, end = tlist.token_prev(eidx, skip_ws=False)
-            tlist.group_tokens(sql.Comment, tidx, eidx)
+        if end is None:
+            # From tidx onward everything is comment/newline: there is no
+            # terminator to group against, and every later start would hit
+            # the same dead end. Stop instead of re-scanning the tail once
+            # per remaining comment token (which is O(n**2)).
+            break
+        eidx, end = tlist.token_prev(eidx, skip_ws=False)
+        tlist.group_tokens(sql.Comment, tidx, eidx)
 
         tidx, token = tlist.token_next_by(t=T.Comment, idx=tidx)
 
