patch 9.2.0815: deeply nested regexp patterns may cause stack overflow
Commit:
https://github.com/vim/vim/commit/a79cd6bfc3e85e1d9f1ec48eda76c7385b44bf69
Author: lipengyu <
lipe...@kylinos.cn>
Date: Mon Jul 20 16:55:55 2026 +0000
patch 9.2.0815: deeply nested regexp patterns may cause stack overflow
Problem: Deeply nested regexp groups can cause uncontrolled recursion
in the regexp compiler and exhaust the C stack.
Solution: Limit recursive regexp parsing depth in both the backtracking
and NFA compilers (lipengyu)
closes: #20731
Signed-off-by: lipengyu <
lipe...@kylinos.cn>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/regexp.c b/src/regexp.c
index 54fadc899..7f52f9a95 100644
--- a/src/regexp.c
+++ b/src/regexp.c
@@ -408,6 +408,9 @@ static int nextchr; // used for ungetchr()
#define REG_ZPAREN 2 // \z(\)
#define REG_NPAREN 3 // \%(\)
+// Limit recursive parsing of nested regexp atoms to avoid using up the C stack.
+#define REG_MAX_PAREN_DEPTH 1000
+
typedef struct
{
char_u *regparse;
diff --git a/src/regexp_bt.c b/src/regexp_bt.c
index 1fa80f5c7..1dd6b066d 100644
--- a/src/regexp_bt.c
+++ b/src/regexp_bt.c
@@ -245,6 +245,7 @@ static int num_complex_braces; // Complex \{...} count
static char_u *regcode; // Code-emit pointer, or JUST_CALC_SIZE
static long regsize; // Code size.
static int reg_toolong; // TRUE when offset out of range
+static int bt_reg_parse_depth; // nesting depth in reg()
static char_u had_endbrace[NSUBEXP]; // flags, TRUE if end of () found
static long brace_min[10]; // Minimums for complex brace repeats
static long brace_max[10]; // Maximums for complex brace repeats
@@ -477,6 +478,7 @@ regcomp_start(
#endif
regsize = 0L;
reg_toolong = FALSE;
+ bt_reg_parse_depth = 0;
regflags = 0;
#if defined(FEAT_SYN_HL)
had_eol = FALSE;
@@ -2383,10 +2385,17 @@ reg(
else
ret = NULL;
+ if (bt_reg_parse_depth >= REG_MAX_PAREN_DEPTH)
+ EMSG_RET_NULL(_(e_command_too_complex));
+ ++bt_reg_parse_depth;
+
// Pick up the branches, linking them together.
br = regbranch(&flags);
if (br == NULL)
- return NULL;
+ {
+ ret = NULL;
+ goto theend;
+ }
if (ret != NULL)
regtail(ret, br); // [MZ]OPEN -> first.
else
@@ -2402,7 +2411,10 @@ reg(
skipchr();
br = regbranch(&flags);
if (br == NULL || reg_toolong)
- return NULL;
+ {
+ ret = NULL;
+ goto theend;
+ }
regtail(ret, br); // BRANCH -> BRANCH.
if (!(flags & HASWIDTH))
*flagp &= ~HASWIDTH;
@@ -2427,26 +2439,56 @@ reg(
{
#ifdef FEAT_SYN_HL
if (paren == REG_ZPAREN)
- EMSG_RET_NULL(_(e_unmatched_z));
+ {
+ emsg(_(e_unmatched_z));
+ rc_did_emsg = TRUE;
+ ret = NULL;
+ goto theend;
+ }
else
#endif
- if (paren == REG_NPAREN)
- EMSG2_RET_NULL(_(e_unmatched_str_percent_open), reg_magic == MAGIC_ALL);
+ if (paren == REG_NPAREN)
+ {
+ semsg(_(e_unmatched_str_percent_open),
+ reg_magic == MAGIC_ALL ? "" : "\");
+ rc_did_emsg = TRUE;
+ ret = NULL;
+ goto theend;
+ }
else
- EMSG2_RET_NULL(_(e_unmatched_str_open), reg_magic == MAGIC_ALL);
+ {
+ semsg(_(e_unmatched_str_open),
+ reg_magic == MAGIC_ALL ? "" : "\");
+ rc_did_emsg = TRUE;
+ ret = NULL;
+ goto theend;
+ }
}
else if (paren == REG_NOPAREN && peekchr() != NUL)
{
if (curchr == Magic(')'))
- EMSG2_RET_NULL(_(e_unmatched_str_close), reg_magic == MAGIC_ALL);
+ {
+ semsg(_(e_unmatched_str_close),
+ reg_magic == MAGIC_ALL ? "" : "\");
+ rc_did_emsg = TRUE;
+ ret = NULL;
+ goto theend;
+ }
else
- EMSG_RET_NULL(_(e_trailing_characters)); // "Can't happen".
- // NOTREACHED
+ {
+ emsg(_(e_trailing_characters)); // "Can't happen".
+ rc_did_emsg = TRUE;
+ ret = NULL;
+ goto theend;
+ }
}
// Here we set the flag allowing back references to this set of
// parentheses.
if (paren == REG_PAREN)
had_endbrace[parno] = TRUE; // have seen the close paren
+
+theend:
+ --bt_reg_parse_depth;
return ret;
}
diff --git a/src/regexp_nfa.c b/src/regexp_nfa.c
index d52ea4034..f47d0c834 100644
--- a/src/regexp_nfa.c
+++ b/src/regexp_nfa.c
@@ -248,6 +248,7 @@ static int nfa_re_flags; // re_flags passed to nfa_regcomp()
static int *post_start; // holds the postfix form of r.e.
static int *post_end;
static int *post_ptr;
+static int nfa_reg_parse_depth; // nesting depth in nfa_reg()
// Set when the pattern should use the NFA engine.
// E.g. [[:upper:]] only allows 8bit characters for BT engine,
@@ -310,6 +311,7 @@ nfa_regcomp_start(
wants_nfa = FALSE;
rex.nfa_has_zend = FALSE;
rex.nfa_has_backref = FALSE;
+ nfa_reg_parse_depth = 0;
// shared with BT engine
regcomp_start(expr, re_flags);
@@ -2516,6 +2518,7 @@ nfa_reg(
int paren) // REG_NOPAREN, REG_PAREN, REG_NPAREN or REG_ZPAREN
{
int parno = 0;
+ int status = FAIL;
if (paren == REG_PAREN)
{
@@ -2533,14 +2536,18 @@ nfa_reg(
}
#endif
+ if (nfa_reg_parse_depth >= REG_MAX_PAREN_DEPTH)
+ EMSG_RET_FAIL(_(e_command_too_complex));
+ ++nfa_reg_parse_depth;
+
if (nfa_regbranch() == FAIL)
- return FAIL; // cascaded error
+ goto theend; // cascaded error
while (peekchr() == Magic('|'))
{
skipchr();
if (nfa_regbranch() == FAIL)
- return FAIL; // cascaded error
+ goto theend; // cascaded error
EMIT(NFA_OR);
}
@@ -2548,17 +2555,23 @@ nfa_reg(
if (paren != REG_NOPAREN && getchr() != Magic(')'))
{
if (paren == REG_NPAREN)
- EMSG2_RET_FAIL(_(e_unmatched_str_percent_open),
- reg_magic == MAGIC_ALL);
+ semsg(_(e_unmatched_str_percent_open),
+ reg_magic == MAGIC_ALL ? "" : "\");
else
- EMSG2_RET_FAIL(_(e_unmatched_str_open), reg_magic == MAGIC_ALL);
+ semsg(_(e_unmatched_str_open),
+ reg_magic == MAGIC_ALL ? "" : "\");
+ rc_did_emsg = TRUE;
+ goto theend;
}
else if (paren == REG_NOPAREN && peekchr() != NUL)
{
if (peekchr() == Magic(')'))
- EMSG2_RET_FAIL(_(e_unmatched_str_close), reg_magic == MAGIC_ALL);
+ semsg(_(e_unmatched_str_close),
+ reg_magic == MAGIC_ALL ? "" : "\");
else
- EMSG_RET_FAIL(_(e_nfa_regexp_proper_termination_error));
+ emsg(_(e_nfa_regexp_proper_termination_error));
+ rc_did_emsg = TRUE;
+ goto theend;
}
/*
* Here we set the flag allowing back references to this set of
@@ -2574,7 +2587,11 @@ nfa_reg(
EMIT(NFA_ZOPEN + parno);
#endif
- return OK;
+ status = OK;
+
+theend:
+ --nfa_reg_parse_depth;
+ return status;
}
#ifdef DEBUG
diff --git a/src/testdir/test_regexp_latin.vim b/src/testdir/test_regexp_latin.vim
index 573cf139b..c169525fb 100644
--- a/src/testdir/test_regexp_latin.vim
+++ b/src/testdir/test_regexp_latin.vim
@@ -981,6 +981,12 @@ func Test_regexp_error()
call assert_equal('', matchstr('abcd', '\%o181\%o142'))
endfunc
+func Test_regexp_recursion_limit()
+ let nested = repeat('\%(', 20000) .. 'x' .. repeat('\)', 20000)
+ call assert_fails("call matchstr('x', '\%#=2' .. nested)", 'E74:')
+ call assert_fails("call matchstr('x', '\%#=1' .. nested)", 'E74:')
+endfunc
+
" Test for using the last substitute string pattern (~)
func Test_regexp_last_subst_string()
new
diff --git a/src/version.c b/src/version.c
index 90b5ad6dc..28726e373 100644
--- a/src/version.c
+++ b/src/version.c
@@ -759,6 +759,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 815,
/**/
814,
/**/