From 324ff67ee0450e9b953bf36a0a83d5aa1ac70aed Mon Sep 17 00:00:00 2001 From: fszontagh Date: Thu, 27 Aug 2026 12:20:21 +0200 Subject: [PATCH 1/4] fix: bound plain-text runs in parse_prompt_attention regex --- ggml | 2 +- src/core/util.cpp | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ggml b/ggml index 8e800cef2..032b6997d 160000 --- a/ggml +++ b/ggml @@ -1 +1 @@ -Subproject commit 8e800cef2948046cc47f9db6090491c6128ca42c +Subproject commit 032b6997db4c9c75dc85d8d2bb2beec77b1231b0 diff --git a/src/core/util.cpp b/src/core/util.cpp index ff53c65fc..ce97546c9 100644 --- a/src/core/util.cpp +++ b/src/core/util.cpp @@ -801,7 +801,11 @@ std::vector> parse_prompt_attention(const std::str float round_bracket_multiplier = 1.1f; float square_bracket_multiplier = 1 / 1.1f; - std::regex re_attention(R"(\\\(|\\\)|\\\[|\\\]|\\\\|\\|\(|\[|:([+-]?[.\d]+)\)|\)|\]|\bBREAK\b|[^\\()\[\]:B]+|:|\bB)"); + // libstdc++ std::regex recurses per matched character; unbounded runs overflow + // the stack. Split runs are merged back by the equal-weight pass below. + const int max_plain_text_run = 1024; + std::regex re_attention(R"(\\\(|\\\)|\\\[|\\\]|\\\\|\\|\(|\[|:([+-]?[.\d]+)\)|\)|\]|\bBREAK\b|[^\\()\[\]:B]{1,)" + + std::to_string(max_plain_text_run) + R"(}|:|\bB)"); std::regex re_break(R"(\s*\bBREAK\b\s*)"); auto multiply_range = [&](int start_position, float multiplier) { From d3cb32d17f060f10a392eceeb13f3b28fe455b4f Mon Sep 17 00:00:00 2001 From: fszontagh Date: Wed, 2 Sep 2026 09:39:38 +0200 Subject: [PATCH 2/4] fix: bound the weight run and parse it without exceptions --- src/core/util.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/util.cpp b/src/core/util.cpp index ce97546c9..201f9b46f 100644 --- a/src/core/util.cpp +++ b/src/core/util.cpp @@ -804,7 +804,9 @@ std::vector> parse_prompt_attention(const std::str // libstdc++ std::regex recurses per matched character; unbounded runs overflow // the stack. Split runs are merged back by the equal-weight pass below. const int max_plain_text_run = 1024; - std::regex re_attention(R"(\\\(|\\\)|\\\[|\\\]|\\\\|\\|\(|\[|:([+-]?[.\d]+)\)|\)|\]|\bBREAK\b|[^\\()\[\]:B]{1,)" + + const int max_weight_chars = 32; + std::regex re_attention(R"(\\\(|\\\)|\\\[|\\\]|\\\\|\\|\(|\[|:([+-]?[.\d]{1,)" + + std::to_string(max_weight_chars) + R"(})\)|\)|\]|\bBREAK\b|[^\\()\[\]:B]{1,)" + std::to_string(max_plain_text_run) + R"(}|:|\bB)"); std::regex re_break(R"(\s*\bBREAK\b\s*)"); @@ -827,7 +829,13 @@ std::vector> parse_prompt_attention(const std::str square_brackets.push_back((int)res.size()); } else if (!weight.empty()) { if (!round_brackets.empty()) { - multiply_range(round_brackets.back(), std::stof(weight)); + // strtof does not throw, and a non-finite weight would poison every + // multiplier that follows. + float weight_value = std::strtof(weight.c_str(), nullptr); + if (!std::isfinite(weight_value)) { + weight_value = 1.0f; + } + multiply_range(round_brackets.back(), weight_value); round_brackets.pop_back(); } } else if (text == ")" && !round_brackets.empty()) { From d7b6ecfb68dd87471ca9f044785c27417eb0a8a1 Mon Sep 17 00:00:00 2001 From: fszontagh Date: Fri, 11 Sep 2026 18:22:54 +0200 Subject: [PATCH 3/4] fix: lex prompt attention weights outside the regex to drop the length cap --- src/core/util.cpp | 59 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/src/core/util.cpp b/src/core/util.cpp index 201f9b46f..202932897 100644 --- a/src/core/util.cpp +++ b/src/core/util.cpp @@ -804,9 +804,7 @@ std::vector> parse_prompt_attention(const std::str // libstdc++ std::regex recurses per matched character; unbounded runs overflow // the stack. Split runs are merged back by the equal-weight pass below. const int max_plain_text_run = 1024; - const int max_weight_chars = 32; - std::regex re_attention(R"(\\\(|\\\)|\\\[|\\\]|\\\\|\\|\(|\[|:([+-]?[.\d]{1,)" + - std::to_string(max_weight_chars) + R"(})\)|\)|\]|\bBREAK\b|[^\\()\[\]:B]{1,)" + + std::regex re_attention(R"(\\\(|\\\)|\\\[|\\\]|\\\\|\\|\(|\[|\)|\]|\bBREAK\b|[^\\()\[\]:B]{1,)" + std::to_string(max_plain_text_run) + R"(}|:|\bB)"); std::regex re_break(R"(\s*\bBREAK\b\s*)"); @@ -816,28 +814,57 @@ std::vector> parse_prompt_attention(const std::str } }; + // Lexed here rather than in the regex: a bounded repetition would reject long + // but valid weights, and an unbounded one is what overflows the stack. + // Returns the length of ":)" past the colon, or 0 if it is not a weight. + auto lex_weight = [](const std::string& s, float& value) -> size_t { + size_t end = 0; + if (end < s.size() && (s[end] == '+' || s[end] == '-')) { + ++end; + } + while (end < s.size() && (std::isdigit((unsigned char)s[end]) || s[end] == '.')) { + ++end; + } + if (end >= s.size() || s[end] != ')') { + return 0; + } + std::string number = s.substr(0, end); + char* number_end = nullptr; + float parsed = std::strtof(number.c_str(), &number_end); + const char* expected = number.c_str() + number.size(); + // A partial parse means the text is not a number at all (".", "+.", "1.2.3"); + // a non-finite value would poison every multiplier that follows. + if (number.empty() || number_end != expected || !std::isfinite(parsed)) { + return 0; + } + value = parsed; + return end + 1; + }; + std::smatch m, m2; std::string remaining_text = text; while (std::regex_search(remaining_text, m, re_attention)) { std::string text = m[0]; - std::string weight = m[1]; + std::string suffix = m.suffix(); + + if (text == ":") { + float weight_value = 1.0f; + size_t weight_length = lex_weight(suffix, weight_value); + if (weight_length > 0) { + if (!round_brackets.empty()) { + multiply_range(round_brackets.back(), weight_value); + round_brackets.pop_back(); + } + remaining_text = suffix.substr(weight_length); + continue; + } + } if (text == "(") { round_brackets.push_back((int)res.size()); } else if (text == "[") { square_brackets.push_back((int)res.size()); - } else if (!weight.empty()) { - if (!round_brackets.empty()) { - // strtof does not throw, and a non-finite weight would poison every - // multiplier that follows. - float weight_value = std::strtof(weight.c_str(), nullptr); - if (!std::isfinite(weight_value)) { - weight_value = 1.0f; - } - multiply_range(round_brackets.back(), weight_value); - round_brackets.pop_back(); - } } else if (text == ")" && !round_brackets.empty()) { multiply_range(round_brackets.back(), round_bracket_multiplier); round_brackets.pop_back(); @@ -852,7 +879,7 @@ std::vector> parse_prompt_attention(const std::str res.push_back({text, 1.0f}); } - remaining_text = m.suffix(); + remaining_text = suffix; } for (int pos : round_brackets) { From 4fab75bdd9501f6722e270b464a2285048d29221 Mon Sep 17 00:00:00 2001 From: fszontagh Date: Sat, 12 Sep 2026 12:52:23 +0200 Subject: [PATCH 4/4] fix: trim comments in parse_prompt_attention --- src/core/util.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/core/util.cpp b/src/core/util.cpp index a1f328504..2a233e695 100644 --- a/src/core/util.cpp +++ b/src/core/util.cpp @@ -821,8 +821,8 @@ std::vector> parse_prompt_attention(const std::str float round_bracket_multiplier = 1.1f; float square_bracket_multiplier = 1 / 1.1f; - // libstdc++ std::regex recurses per matched character; unbounded runs overflow - // the stack. Split runs are merged back by the equal-weight pass below. + // libstdc++ std::regex recurses per matched character, so unbounded runs + // overflow the stack. Split runs are merged back below. const int max_plain_text_run = 1024; std::regex re_attention(R"(\\\(|\\\)|\\\[|\\\]|\\\\|\\|\(|\[|\)|\]|\bBREAK\b|[^\\()\[\]:B]{1,)" + std::to_string(max_plain_text_run) + R"(}|:|\bB)"); @@ -834,9 +834,8 @@ std::vector> parse_prompt_attention(const std::str } }; - // Lexed here rather than in the regex: a bounded repetition would reject long - // but valid weights, and an unbounded one is what overflows the stack. - // Returns the length of ":)" past the colon, or 0 if it is not a weight. + // Kept out of the regex: bounding the repetition rejects valid long weights, + // leaving it unbounded overflows the stack. auto lex_weight = [](const std::string& s, float& value) -> size_t { size_t end = 0; if (end < s.size() && (s[end] == '+' || s[end] == '-')) { @@ -852,8 +851,7 @@ std::vector> parse_prompt_attention(const std::str char* number_end = nullptr; float parsed = std::strtof(number.c_str(), &number_end); const char* expected = number.c_str() + number.size(); - // A partial parse means the text is not a number at all (".", "+.", "1.2.3"); - // a non-finite value would poison every multiplier that follows. + // Without this ".", "+." and "1.2.3" would silently become weights. if (number.empty() || number_end != expected || !std::isfinite(parsed)) { return 0; }