From 9f3da727651d57fc1c5b1d0ef36253a2e454a293 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 04:20:52 +0000 Subject: [PATCH] Regex: don't overflow the stack on long lines libstdc++'s std::regex matcher recurses about once per matched character, so '.*', 'a+' or '(a|b)*' on a line of a few tens of thousands of characters (minified JS/JSON) overflowed the 8 MiB stack and crashed the editor from the regex search prompt or regex replace. Regex search and regex replace now run their matching on a worker thread with a 1 GiB reserved stack (RunWithLargeStack; the caller waits, so nothing is shared concurrently). Renderers skip regex match highlighting on lines over 10,000 bytes. Catastrophic backtracking (e.g. (a*)*b) is still possible: std::regex has no time limit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB --- CMakeLists.txt | 2 ++ Command.cc | 48 ++++++++++++++++++------------- ImGuiRenderer.cc | 5 +++- QtFrontend.cc | 5 +++- RegexGuard.cc | 51 +++++++++++++++++++++++++++++++++ RegexGuard.h | 22 ++++++++++++++ TerminalRenderer.cc | 5 +++- tests/test_audit_regressions.cc | 21 ++++++++++++++ 8 files changed, 136 insertions(+), 23 deletions(-) create mode 100644 RegexGuard.cc create mode 100644 RegexGuard.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 84cc805..bb78587 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,7 @@ set(COMMON_SOURCES ErrorHandler.cc SyscallWrappers.cc ErrorRecovery.cc + RegexGuard.cc TerminalInputHandler.cc TerminalRenderer.cc TerminalFrontend.cc @@ -384,6 +385,7 @@ if (BUILD_TESTS) ErrorHandler.cc SyscallWrappers.cc ErrorRecovery.cc + RegexGuard.cc KKeymap.cc SwapRecorder.h OptimizedSearch.cc diff --git a/Command.cc b/Command.cc index 7d69937..170a69e 100644 --- a/Command.cc +++ b/Command.cc @@ -11,6 +11,7 @@ #include #include "Command.h" +#include "RegexGuard.h" #include "syntax/HighlighterRegistry.h" #include "syntax/NullHighlighter.h" #include "Editor.h" @@ -797,16 +798,19 @@ search_compute_matches_regex(const Buffer &buf, const std::string &pattern, std: try { const std::regex rx(pattern); const auto &rows = buf.Rows(); - for (std::size_t y = 0; y < rows.size(); ++y) { - std::string line = static_cast(rows[y]); - for (auto it = std::sregex_iterator(line.begin(), line.end(), rx); - it != std::sregex_iterator(); ++it) { - const auto &m = *it; - out.push_back(RegexMatch{ - y, static_cast(m.position()), static_cast(m.length()) - }); + // std::regex recurses per matched character; see RegexGuard.h. + kte::RunWithLargeStack([&] { + for (std::size_t y = 0; y < rows.size(); ++y) { + std::string line = static_cast(rows[y]); + for (auto it = std::sregex_iterator(line.begin(), line.end(), rx); + it != std::sregex_iterator(); ++it) { + const auto &m = *it; + out.push_back(RegexMatch{ + y, static_cast(m.position()), static_cast(m.length()) + }); + } } - } + }); } catch (const std::regex_error &e) { err_out = e.what(); // Return empty results on error @@ -3212,18 +3216,22 @@ cmd_newline(CommandContext &ctx) std::size_t nrows = buf->Nrows(); if (nrows > 1 && buf->GetLineString(nrows - 1).empty()) --nrows; - for (std::size_t y = 0; y < nrows; ++y) { - const std::string before = buf->GetLineString(y); - const std::string after = std::regex_replace(before, rx, repl); - if (after != before) { - replace_rows_text(*buf, y, before, after, ru); - // A replacement containing newlines adds rows; skip past them. - const auto added = static_cast(std::count(after.begin(), after.end(), '\n')); - y += added; - nrows += added; - ++changed; + // std::regex recurses per matched character; see RegexGuard.h. The + // caller waits for the worker, so the buffer is never shared. + kte::RunWithLargeStack([&] { + for (std::size_t y = 0; y < nrows; ++y) { + const std::string before = buf->GetLineString(y); + const std::string after = std::regex_replace(before, rx, repl); + if (after != before) { + replace_rows_text(*buf, y, before, after, ru); + // A replacement containing newlines adds rows; skip past them. + const auto added = static_cast(std::count(after.begin(), after.end(), '\n')); + y += added; + nrows += added; + ++changed; + } } - } + }); clamp_cursor_to_buffer(*buf); buf->SetDirty(true); ctx.editor.SetStatus("Regex replaced in " + std::to_string(changed) + " line(s)"); diff --git a/ImGuiRenderer.cc b/ImGuiRenderer.cc index ed406ab..67f378f 100644 --- a/ImGuiRenderer.cc +++ b/ImGuiRenderer.cc @@ -10,6 +10,7 @@ #include #include "ImGuiRenderer.h" +#include "RegexGuard.h" #include "Highlight.h" #include "GUITheme.h" #include "Buffer.h" @@ -359,7 +360,9 @@ ImGuiRenderer::Draw(Editor &ed) if (search_mode) { // In regex mode, reuse the compiled regex hoisted above the loop. if (regex_mode) { - if (search_rx_valid) { + // Long lines are not highlighted: std::regex recursion + // could overflow the stack (RegexGuard.h). + if (search_rx_valid && line.size() <= kte::kRegexRenderLineLimit) { try { for (auto it = std::sregex_iterator(line.begin(), line.end(), search_rx); it != std::sregex_iterator(); ++it) { diff --git a/QtFrontend.cc b/QtFrontend.cc index b0b77ee..16b9e98 100644 --- a/QtFrontend.cc +++ b/QtFrontend.cc @@ -1,4 +1,5 @@ #include "QtFrontend.h" +#include "RegexGuard.h" #include #include @@ -165,7 +166,9 @@ protected: (ed_->CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed_->CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) { - try { + // Long lines are not highlighted: std::regex recursion + // could overflow the stack (RegexGuard.h). + if (line.size() <= kte::kRegexRenderLineLimit) try { std::regex rx(ed_->SearchQuery()); for (auto it = std::sregex_iterator( line.begin(), line.end(), rx); diff --git a/RegexGuard.cc b/RegexGuard.cc new file mode 100644 index 0000000..31dd133 --- /dev/null +++ b/RegexGuard.cc @@ -0,0 +1,51 @@ +#include "RegexGuard.h" + +#include +#include + +namespace kte { +namespace { +// 1 GiB supports matches spanning several million characters. +constexpr std::size_t kLargeStackBytes = std::size_t{1} << 30; + +struct Job { + const std::function *fn; + std::exception_ptr error; +}; + + +void * +run_job(void *arg) +{ + auto *job = static_cast(arg); + try { + (*job->fn)(); + } catch (...) { + job->error = std::current_exception(); + } + return nullptr; +} +} // namespace + + +void +RunWithLargeStack(const std::function &fn) +{ + Job job{&fn, nullptr}; + pthread_attr_t attr; + bool started = false; + pthread_t tid{}; + if (pthread_attr_init(&attr) == 0) { + if (pthread_attr_setstacksize(&attr, kLargeStackBytes) == 0) + started = pthread_create(&tid, &attr, run_job, &job) == 0; + pthread_attr_destroy(&attr); + } + if (!started) { + fn(); + return; + } + pthread_join(tid, nullptr); + if (job.error) + std::rethrow_exception(job.error); +} +} // namespace kte diff --git a/RegexGuard.h b/RegexGuard.h new file mode 100644 index 0000000..6d0ebac --- /dev/null +++ b/RegexGuard.h @@ -0,0 +1,22 @@ +// RegexGuard.h - keep std::regex from overflowing the stack +// +// libstdc++'s std::regex matcher recurses roughly once per character of a +// match, so a pattern such as ".*" or "a+" on a line of a few tens of +// thousands of characters (minified JS/JSON) overflows a default 8 MiB +// stack and crashes the editor. +#pragma once + +#include +#include + +namespace kte { +// Renderers highlight regex matches only on lines up to this many bytes; +// longer lines are drawn without match highlighting. +constexpr std::size_t kRegexRenderLineLimit = 10000; + +// Run fn to completion on a thread with a large stack (address space is +// reserved, pages are committed only as used), so regex work over long lines +// cannot overflow. If such a thread cannot be created, fn runs on the calling +// thread. Exceptions thrown by fn are rethrown to the caller. +void RunWithLargeStack(const std::function &fn); +} // namespace kte diff --git a/TerminalRenderer.cc b/TerminalRenderer.cc index 465c806..ebca618 100644 --- a/TerminalRenderer.cc +++ b/TerminalRenderer.cc @@ -10,6 +10,7 @@ #include #include "TerminalRenderer.h" +#include "RegexGuard.h" #include "Buffer.h" #include "Editor.h" #include "Highlight.h" @@ -70,7 +71,9 @@ TerminalRenderer::Draw(Editor &ed) if (ed.PromptActive() && ( ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed. CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) { - try { + // Long lines are not highlighted: std::regex recursion + // could overflow the stack (RegexGuard.h). + if (sline.size() <= kte::kRegexRenderLineLimit) try { std::regex rx(ed.SearchQuery()); for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx); it != std::sregex_iterator(); ++it) { diff --git a/tests/test_audit_regressions.cc b/tests/test_audit_regressions.cc index d515e83..2b88e3c 100644 --- a/tests/test_audit_regressions.cc +++ b/tests/test_audit_regressions.cc @@ -695,3 +695,24 @@ TEST(Audit_UArg_DoesNotOverflow) ed.UArgStart(); ASSERT_EQ(ed.UArgGet(), 1000000); } + + +// std::regex recurses per matched character. Regex search and replace on a +// very long line (minified JS/JSON) must not overflow the stack. +TEST(Audit_Regex_LongLine_NoStackOverflow) +{ + TestHarness h; + Editor &ed = h.EditorRef(); + Buffer &b = h.Buf(); + b.insert_text(0, 0, std::string(200000, 'a') + "\nshort\n"); + b.SetCursor(0, 1); + + ASSERT_TRUE(h.Exec(CommandId::RegexFindStart)); + ASSERT_TRUE(h.Exec(CommandId::InsertText, "(a|b)*")); + ASSERT_TRUE(h.Exec(CommandId::Refresh)); + ASSERT_TRUE(!ed.PromptActive()); + + regex_replace_all(h, "a+", "b"); + ASSERT_EQ(h.Line(0), std::string("b")); + ASSERT_EQ(h.Line(1), std::string("short")); +}