]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0815: deeply nested regexp patterns may cause stack overflow v9.2.0815
authorlipengyu <lipengyu@kylinos.cn>
Mon, 20 Jul 2026 16:55:55 +0000 (16:55 +0000)
committerChristian Brabandt <cb@256bit.org>
Mon, 20 Jul 2026 16:55:55 +0000 (16:55 +0000)
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 <lipengyu@kylinos.cn>
Signed-off-by: Christian Brabandt <cb@256bit.org>
src/regexp.c
src/regexp_bt.c
src/regexp_nfa.c
src/testdir/test_regexp_latin.vim
src/version.c

index 54fadc899eb1bfaafc45b914ff9438a74e61833b..7f52f9a955a07b98f27cd32bd92b1ece4055e616 100644 (file)
@@ -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;
index 1fa80f5c7206956cc44c51ce83a04bce3e4c25da..1dd6b066d6921fa64cd1988f1529895925437cc5 100644 (file)
@@ -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;
 }
 
index d52ea4034d2d0ee868baf44d96e904611da79ef5..f47d0c834467b01b14b8db7f14b9c2af4eb5b666 100644 (file)
@@ -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
index 573cf139ba6022cb83da41aacd7044975d7abeff..c169525fb923632cc8e168a87ca6a2e4d5ed223f 100644 (file)
@@ -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
index 90b5ad6dc0c9d0d8ce0d128f82818697f2296188..28726e3739f2189b5c3c6d6cf8e2d4da383b6689 100644 (file)
@@ -759,6 +759,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    815,
 /**/
     814,
 /**/