From: lipengyu Date: Mon, 20 Jul 2026 16:55:55 +0000 (+0000) Subject: patch 9.2.0815: deeply nested regexp patterns may cause stack overflow X-Git-Tag: v9.2.0815^0 X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=a79cd6bfc3e85e1d9f1ec48eda76c7385b44bf69;p=thirdparty%2Fvim.git 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 Signed-off-by: Christian Brabandt --- diff --git a/src/regexp.c b/src/regexp.c index 54fadc899e..7f52f9a955 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 1fa80f5c72..1dd6b066d6 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 d52ea4034d..f47d0c8344 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 573cf139ba..c169525fb9 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 90b5ad6dc0..28726e3739 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, /**/