From 2d0bab0c83c892f0fe59ccd861fcab11b09bec27 Mon Sep 17 00:00:00 2001 From: Daichi Sakai Date: Fri, 13 Nov 2026 15:46:11 +0900 Subject: [PATCH] fix: avoid stack overflow on table separator lines with many columns The header separator pattern repeated a column group greedily ("(?:COL\|)+COL"). java.util.regex matches a greedy group repetition recursively, so the stack depth grew with the number of columns and a separator line with several hundred columns threw StackOverflowError. Repeat the column group possessively so it is matched in a loop, and order the column alternatives longest first so that alignment markers are still recognised without backtracking. Co-Authored-By: Claude Fable 5.2 --- .../internal/TableParagraphPreProcessor.java | 9 +- .../TableSeparatorStackOverflowTest.java | 99 +++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 flexmark-ext-tables/src/test/java/com/vladsch/flexmark/ext/tables/TableSeparatorStackOverflowTest.java diff --git a/flexmark-ext-tables/src/main/java/com/vladsch/flexmark/ext/tables/internal/TableParagraphPreProcessor.java b/flexmark-ext-tables/src/main/java/com/vladsch/flexmark/ext/tables/internal/TableParagraphPreProcessor.java index b1647c3d6f..7a93ea732f 100644 --- a/flexmark-ext-tables/src/main/java/com/vladsch/flexmark/ext/tables/internal/TableParagraphPreProcessor.java +++ b/flexmark-ext-tables/src/main/java/com/vladsch/flexmark/ext/tables/internal/TableParagraphPreProcessor.java @@ -121,7 +121,9 @@ public static Pattern getTableHeaderSeparator(int minColumnDashes, String intell int minColDash = minColumnDashes >= 2 ? minColumnDashes - 1 : 1; int minColDashes = minColumnDashes >= 3 ? minColumnDashes - 2 : 1; // to prevent conversion to arabic numbers, using string - String COL = String.format(Locale.US, "(?:" + "\\s*-{%d,}\\s*|\\s*:-{%d,}\\s*|\\s*-{%d,}:\\s*|\\s*:-{%d,}:\\s*" + ")", minCol, minColDash, minColDash, minColDashes); + // The column repetition below is possessive, so a column never backtracks into another alternative: + // the alternatives must stay ordered longest first (":-:" before ":-", "-:" before "-"). + String COL = String.format(Locale.US, "(?:" + "\\s*+:-{%d,}+:\\s*+|\\s*+:-{%d,}+\\s*+|\\s*+-{%d,}+:\\s*+|\\s*+-{%d,}+\\s*+" + ")", minColDashes, minColDash, minColDash, minCol); boolean noIntelliJ = intellijDummyIdentifier.isEmpty(); String add = noIntelliJ ? "" : TableFormatOptions.INTELLIJ_DUMMY_IDENTIFIER; @@ -132,7 +134,10 @@ public static Pattern getTableHeaderSeparator(int minColumnDashes, String intell String regex = "\\|" + COL + "\\|?\\s*" + "|" + COL + "\\|\\s*" + "|" + - "\\|?" + "(?:" + COL + "\\|)+" + COL + "\\|?\\s*"; + // Not "(?:COL\\|)+COL": java.util.regex matches a greedy group repetition recursively, one set of stack + // frames per column, and a separator line with several hundred columns overflowed the stack. + // A possessive repetition is matched in a loop. + "\\|?" + COL + "(?:\\|" + COL + ")++" + "\\|?\\s*"; String withIntelliJ = regex.replace("\\s", sp).replace("\\|", pipe).replace("-", ds); diff --git a/flexmark-ext-tables/src/test/java/com/vladsch/flexmark/ext/tables/TableSeparatorStackOverflowTest.java b/flexmark-ext-tables/src/test/java/com/vladsch/flexmark/ext/tables/TableSeparatorStackOverflowTest.java new file mode 100644 index 0000000000..d688de95ff --- /dev/null +++ b/flexmark-ext-tables/src/test/java/com/vladsch/flexmark/ext/tables/TableSeparatorStackOverflowTest.java @@ -0,0 +1,99 @@ +package com.vladsch.flexmark.ext.tables; + +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.data.MutableDataSet; +import org.junit.Test; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class TableSeparatorStackOverflowTest { + // The default thread stack size differs per platform, so the tests pin one to stay deterministic. + private static final long STACK_SIZE_BYTES = 512 * 1024; + private static final int COLUMNS = 5000; + + private static String renderOnSmallStack(String markdown, int minSeparatorDashes) throws InterruptedException { + DataHolder options = new MutableDataSet() + .set(TablesExtension.MIN_SEPARATOR_DASHES, minSeparatorDashes) + .set(Parser.EXTENSIONS, Arrays.asList(TablesExtension.create())) + .toImmutable(); + Parser parser = Parser.builder(options).build(); + HtmlRenderer renderer = HtmlRenderer.builder(options).build(); + + AtomicReference html = new AtomicReference<>(); + AtomicReference failure = new AtomicReference<>(); + Thread thread = new Thread(null, () -> { + try { + html.set(renderer.render(parser.parse(markdown))); + } catch (Throwable t) { + failure.set(t); + } + }, "table-separator-stack-overflow-test", STACK_SIZE_BYTES); + thread.start(); + thread.join(); + + assertNull("rendering must not fail: " + failure.get(), failure.get()); + return html.get(); + } + + private static String wideTable(String separatorCell) { + StringBuilder header = new StringBuilder("|"); + StringBuilder separator = new StringBuilder("|"); + StringBuilder body = new StringBuilder("|"); + for (int i = 0; i < COLUMNS; i++) { + header.append("h|"); + separator.append(separatorCell).append('|'); + body.append("v|"); + } + return header + "\n" + separator + "\n" + body + "\n"; + } + + private static int count(String text, String needle) { + int count = 0; + for (int i = text.indexOf(needle); i >= 0; i = text.indexOf(needle, i + needle.length())) count++; + return count; + } + + @Test + public void wideTable_plainSeparator() throws InterruptedException { + String html = renderOnSmallStack(wideTable("---"), 3); + assertEquals(COLUMNS, count(html, "")); + assertEquals(COLUMNS, count(html, "")); + } + + @Test + public void wideTable_leftAlignedSeparator() throws InterruptedException { + String html = renderOnSmallStack(wideTable(":-"), 1); + assertEquals(COLUMNS, count(html, "")); + } + + @Test + public void wideTable_rightAlignedSeparator() throws InterruptedException { + String html = renderOnSmallStack(wideTable("-:"), 1); + assertEquals(COLUMNS, count(html, "")); + } + + @Test + public void wideTable_centerAlignedSeparator() throws InterruptedException { + String html = renderOnSmallStack(wideTable(" :-: "), 1); + assertEquals(COLUMNS, count(html, "")); + } + + @Test + public void mixedAlignment_isKept() throws InterruptedException { + String html = renderOnSmallStack("" + + "a|b|c|d\n" + + ":--|:-:|--:|---\n" + + "1|2|3|4\n", 3); + assertTrue(html, html.contains("a")); + assertTrue(html, html.contains("b")); + assertTrue(html, html.contains("c")); + assertTrue(html, html.contains("d")); + } +}