From 38d30a9666b60b000420a627419a548aaeda2f43 Mon Sep 17 00:00:00 2001 From: Tomi Virtanen Date: Mon, 22 Jun 2026 11:07:47 +0300 Subject: [PATCH] fix: prevent StackOverflowError converting large mSync values MSYNC_INPUT_VALUE_PATTERN matched the JSON value with the group ((?:[^"\]|\.)*). On OpenJDK this quantified alternation recurses once per repetition, so a large mSync value (e.g. a big text-area payload in a recorded UIDL POST) exhausts the call stack and the HAR-to-k6 conversion dies with a StackOverflowError. Replace the group with ((?:[^"\]++|\.)*+), using a possessive quantifier on the character class and a possessive outer star. The non-quote/ non-backslash runs are now consumed in a single pass with no backtracking, eliminating the per-character recursion. The match and capture semantics are unchanged: it still captures any run of non-"/non-\ characters and \-escapes up to the closing quote. Add HarToK6ConverterTest#largeMsyncValueDoesNotOverflowStack. Fixes: #2266 --- .../loadtest/util/HarToK6Converter.java | 9 ++- .../loadtest/util/HarToK6ConverterTest.java | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java index 1dce24103..db79bcb1c 100644 --- a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java +++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java @@ -57,9 +57,14 @@ public class HarToK6Converter { private static final Pattern ENTITY_KEY_ARG_PATTERN = Pattern.compile( "\"templateEventMethodName\":\"(select|setDetailsVisible)\",\"templateEventMethodArgs\":\\[\"(\\d+)\"\\]"); - /** Matches mSync input values (user-entered form data) in UIDL bodies. */ + /** + * Matches mSync input values (user-entered form data) in UIDL bodies. The + * value group uses a possessive quantifier and atomic alternation + * ({@code ++}/{@code *+}) so the JSON string content is consumed in a + * single pass. + */ private static final Pattern MSYNC_INPUT_VALUE_PATTERN = Pattern.compile( - "\"type\":\"mSync\",\"node\":\\d+,\"feature\":\\d+,\"property\":\"value\",\"value\":\"((?:[^\"\\\\]|\\\\.)*)\""); + "\"type\":\"mSync\",\"node\":\\d+,\"feature\":\\d+,\"property\":\"value\",\"value\":\"((?:[^\"\\\\]++|\\\\.)*+)\""); /** * Matches positive {@code "node":N} occurrences in UIDL request bodies. diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarToK6ConverterTest.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarToK6ConverterTest.java index 9f75892b4..af813f5c5 100644 --- a/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarToK6ConverterTest.java +++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/HarToK6ConverterTest.java @@ -13,12 +13,14 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class HarToK6ConverterTest { @@ -132,6 +134,64 @@ void csvRoundTripPreservesMultipleSpecialValues() throws IOException { "Second field round-trip should preserve escaped quotes"); } + @Test + void largeMsyncValueDoesNotOverflowStack() throws IOException { + // Reproduces issue #2266: MSYNC_INPUT_VALUE_PATTERN used a quantified + // alternation ((?:[^"\\]|\\.)*) which, on OpenJDK, recurses once per + // repetition. A large mSync value therefore blows the call stack with + // a StackOverflowError before this is fixed. + // + // Run the matcher on its own thread with a small stack so the test + // fails fast and deterministically across platforms, rather than + // depending on the (large, JVM-default) main-thread stack size. + String value = "x".repeat(200_000); + String initEntry = entry("GET", "http://localhost:8080/?v-r=init"); + String msyncBody = "{\"csrfToken\":\"abc-123\"," + + "\"rpc\":[{\"type\":\"mSync\",\"node\":42,\"feature\":1," + + "\"property\":\"value\"," + "\"value\":\"" + value + "\"}]," + + "\"syncId\":1,\"clientId\":1}"; + String postEntry = entryWithBody("POST", + "http://localhost:8080/?v-r=uidl&v-uiId=0", msyncBody); + + Path harFile = tempDir.resolve("msync-large.har"); + Path outputFile = tempDir.resolve("msync-large.js"); + Files.writeString(harFile, createHar(initEntry, postEntry)); + + AtomicReference failure = new AtomicReference<>(); + Thread worker = new Thread(null, () -> { + try { + new HarToK6Converter().convert(harFile, outputFile); + } catch (Throwable t) { + failure.set(t); + } + }, "converter", 256 * 1024); + worker.start(); + try { + worker.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError("Interrupted while waiting for converter", + e); + } + + Throwable thrown = failure.get(); + if (thrown instanceof StackOverflowError) { + throw new AssertionError( + "Converting a large mSync value overflowed the stack (issue #2266)", + thrown); + } + assertNull(thrown, + "Conversion should complete without error, but threw: " + + thrown); + + // The value is captured into the CSV, confirming the matcher ran. + Path csvFile = tempDir.resolve("msync-large-data.csv"); + assertTrue(Files.exists(csvFile), "CSV data file should be created"); + List> records = parseCsvRecords(Files.readString(csvFile)); + assertEquals(value, records.get(1).get(0), + "Large mSync value should round-trip into the CSV"); + } + /** * Helper: converts a single mSync value through the full HAR→k6 pipeline * and returns the generated CSV file path.