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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
Claude
2026-09-24 09:16:04 -07:00
committed by Kyle Isom
parent 812cde090f
commit 9f3da72765
8 changed files with 136 additions and 23 deletions
+2
View File
@@ -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
+8
View File
@@ -11,6 +11,7 @@
#include <string_view>
#include "Command.h"
#include "RegexGuard.h"
#include "syntax/HighlighterRegistry.h"
#include "syntax/NullHighlighter.h"
#include "Editor.h"
@@ -797,6 +798,8 @@ search_compute_matches_regex(const Buffer &buf, const std::string &pattern, std:
try {
const std::regex rx(pattern);
const auto &rows = buf.Rows();
// 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<std::string>(rows[y]);
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
@@ -807,6 +810,7 @@ search_compute_matches_regex(const Buffer &buf, const std::string &pattern, std:
});
}
}
});
} catch (const std::regex_error &e) {
err_out = e.what();
// Return empty results on error
@@ -3212,6 +3216,9 @@ cmd_newline(CommandContext &ctx)
std::size_t nrows = buf->Nrows();
if (nrows > 1 && buf->GetLineString(nrows - 1).empty())
--nrows;
// 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);
@@ -3224,6 +3231,7 @@ cmd_newline(CommandContext &ctx)
++changed;
}
}
});
clamp_cursor_to_buffer(*buf);
buf->SetDirty(true);
ctx.editor.SetStatus("Regex replaced in " + std::to_string(changed) + " line(s)");
+4 -1
View File
@@ -10,6 +10,7 @@
#include <regex>
#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) {
+4 -1
View File
@@ -1,4 +1,5 @@
#include "QtFrontend.h"
#include "RegexGuard.h"
#include <QApplication>
#include <QWidget>
@@ -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);
+51
View File
@@ -0,0 +1,51 @@
#include "RegexGuard.h"
#include <exception>
#include <pthread.h>
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<void()> *fn;
std::exception_ptr error;
};
void *
run_job(void *arg)
{
auto *job = static_cast<Job *>(arg);
try {
(*job->fn)();
} catch (...) {
job->error = std::current_exception();
}
return nullptr;
}
} // namespace
void
RunWithLargeStack(const std::function<void()> &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
+22
View File
@@ -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 <cstddef>
#include <functional>
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<void()> &fn);
} // namespace kte
+4 -1
View File
@@ -10,6 +10,7 @@
#include <string>
#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) {
+21
View File
@@ -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"));
}