[libc-commits] [libc] ad46922 - [libc][regex] Add alternation, grouping, and subexpression counting (#235943)

via libc-commits libc-commits at lists.llvm.org
Wed Oct 28 02:06:55 PDT 2026


Author: Jeff Bailey
Date: 2026-10-28T09:06:50Z
New Revision: ad46922aac460ed74e4572622131c0021e983fc7

URL: https://github.com/llvm/llvm-project/commit/ad46922aac460ed74e4572622131c0021e983fc7
DIFF: https://github.com/llvm/llvm-project/commit/ad46922aac460ed74e4572622131c0021e983fc7.diff

LOG: [libc][regex] Add alternation, grouping, and subexpression counting (#235943)

In POSIX Extended Regular Expressions (ERE), alternation '|' has the lowest precedence and separates alternative branches, while grouping '()' overrides precedence and delimits subexpressions. POSIX specifies that unmatched ')'
at top level is treated as an ordinary character, empty subexpressions '()' are valid, and unclosed '(' or empty alternation branches must be rejected. Uncontrolled recursive parsing of deeply nested parentheses also presents a stack exhaustion risk that standard libraries must defend against.

Implement alternation and parenthesized subexpressions in regex_parser via recursive descent with an explicit nesting cap of 128 levels (MAX_GROUP_DEPTH) returning REG_ESPACE upon overflow. Share a single ParsedAtomStack across recursive frames and fold both concatenation and alternation branches right-to-left so canonical right-associated AST construction remains O(1) per branch without per-frame buffers or orphaned intermediate nodes.  Treat unmatched ')' at nesting depth 0 as a literal character per POSIX.1-2024
9.4.3, enforce REG_EPAREN on unclosed parentheses, reject empty alternation branches with REG_BADPAT, and populate preg->re_nsub with the count of parenthesized subexpressions upon successful compilation.

External compliance suites: Henry Spencer suite passes 168/456 (+42) boolean/error cases and 65/456 (+10) full-position cases; AT&T testregex passes 252/400 (+178) ERE boolean/error cases and 254/473 (+178) overall
boolean/error cases with 0 regressions.

Assisted-by: Automated tooling, human reviewed.

Added: 
    

Modified: 
    libc/src/__support/regex/regex_parser.cpp
    libc/src/__support/regex/regex_parser.h
    libc/src/regex/regcomp.cpp
    libc/test/src/__support/regex/matcher_test.cpp
    libc/test/src/regex/regex_basic_test.cpp

Removed: 
    


################################################################################
diff  --git a/libc/src/__support/regex/regex_parser.cpp b/libc/src/__support/regex/regex_parser.cpp
index c178f5621432db..bbbb249c2ab211 100644
--- a/libc/src/__support/regex/regex_parser.cpp
+++ b/libc/src/__support/regex/regex_parser.cpp
@@ -162,9 +162,19 @@ ErrorOr<ExprId> parse_bracket_expr(cpp::string_view &rem, int cflags,
   return Error(REG_EBRACK);
 }
 
+// Maximum parenthesized group nesting depth to prevent parser stack overflow.
+constexpr size_t MAX_GROUP_DEPTH = 128;
+
+struct ParsedAtomStack;
+
+ErrorOr<ExprId> parse_alt(cpp::string_view &rem, int cflags, ExprPool &pool,
+                          size_t depth, size_t &re_nsub,
+                          ParsedAtomStack &stack);
+
 // Parses a single ERE atom and an optional trailing '*' quantifier.
 ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
-                                      ExprPool &pool) {
+                                      ExprPool &pool, size_t depth,
+                                      size_t &re_nsub, ParsedAtomStack &stack) {
   char c = rem.front();
   rem.remove_prefix(1);
   ErrorOr<ExprId> atom = INVALID_EXPR_ID;
@@ -239,12 +249,25 @@ ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
   case '[':
     atom = parse_bracket_expr(rem, cflags, pool);
     break;
-  // TODO: Wire ERE metacharacters as each operator is implemented in
-  // subsequent patches.
-  case '(':
+  case '(': {
+    if (depth >= MAX_GROUP_DEPTH)
+      return Error(REG_ESPACE);
+    ++re_nsub;
+    ErrorOr<ExprId> inner =
+        parse_alt(rem, cflags, pool, depth + 1, re_nsub, stack);
+    if (!inner.has_value())
+      return inner;
+    if (!rem.starts_with(')'))
+      return Error(REG_EPAREN);
+    rem.remove_prefix(1);
+    atom = inner;
+    break;
+  }
   case ')':
-  case '|':
-    return Error(REG_BADPAT);
+    // At depth 0, unmatched ')' is treated as a literal character per
+    // POSIX.1-2024 9.4.3.  At depth > 0, parse_concat stops before ')'.
+    atom = pool.make_lit(')');
+    break;
   default:
     atom = pool.make_lit(c);
     break;
@@ -261,32 +284,27 @@ ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
   return atom;
 }
 
-} // namespace
-
-ErrorOr<ExprId> parse_ere(cpp::string_view pattern, int cflags,
-                          ExprPool &pool) {
-  // Collect quantified atoms in a 64-atom (256-byte) stack buffer, spilling to
-  // the heap only for longer chains, then fold right-to-left so
-  // right-associative make_concat stays O(1) without orphaned prefixes.
-  // TODO: Replace inline_atoms + heap_atoms with an AllocChecker-backed
-  // SmallVector / unique_ptr once available in src/__support/CPP/.
-  constexpr size_t INLINE_ATOM_COUNT = 64;
+// Shared stack of parsed ExprId nodes across recursive parse_alt and
+// parse_concat frames.  Keeps a 64-element (256-byte) inline buffer on the
+// top-level parse_ere frame, spilling to the heap only for larger patterns.
+// TODO: Replace ParsedAtomStack with an AllocChecker-backed SmallVector /
+// unique_ptr once available in src/__support/CPP/.
+struct ParsedAtomStack {
+  static constexpr size_t INLINE_ATOM_COUNT = 64;
   cpp::array<ExprId, INLINE_ATOM_COUNT> inline_atoms{};
   ExprId *heap_atoms = nullptr;
-  cpp::scope_exit cleanup([&heap_atoms] { delete[] heap_atoms; });
-  cpp::span<ExprId> atoms(inline_atoms);
-  size_t atom_count = 0;
+  cpp::span<ExprId> atoms{inline_atoms};
+  size_t count = 0;
 
-  cpp::string_view rem = pattern;
-  while (!rem.empty()) {
-    if (atom_count >= ExprPool::MAX_NODE_LIMIT)
-      return Error(REG_ESPACE);
-    if (atom_count == atoms.size()) {
+  bool push(ExprId id) {
+    if (count >= ExprPool::MAX_NODE_LIMIT)
+      return false;
+    if (count == atoms.size()) {
       size_t new_cap = atoms.size() * 2;
       AllocChecker ac;
       ExprId *new_atoms = new (ac) ExprId[new_cap];
       if (!ac)
-        return Error(REG_ESPACE);
+        return false;
       ExprId *dst = new_atoms;
       for (ExprId existing : atoms) {
         *dst = existing;
@@ -296,23 +314,107 @@ ErrorOr<ExprId> parse_ere(cpp::string_view pattern, int cflags,
       heap_atoms = new_atoms;
       atoms = cpp::span<ExprId>(heap_atoms, new_cap);
     }
-    ErrorOr<ExprId> atom = parse_quantified_atom(rem, cflags, pool);
+    atoms[count] = id;
+    ++count;
+    return true;
+  }
+
+  ExprId pop() {
+    --count;
+    return atoms[count];
+  }
+};
+
+// Parses a single concatenation branch (sequence of quantified atoms).
+ErrorOr<ExprId> parse_concat(cpp::string_view &rem, int cflags, ExprPool &pool,
+                             size_t depth, size_t &re_nsub,
+                             ParsedAtomStack &stack) {
+  // Collect quantified atoms for this branch on the shared stack, then fold
+  // right-to-left so right-associative make_concat stays O(1) without orphaned
+  // prefixes.
+  size_t atom_count = 0;
+  while (!rem.empty() && !rem.starts_with('|') &&
+         !(depth > 0 && rem.starts_with(')'))) {
+    ErrorOr<ExprId> atom =
+        parse_quantified_atom(rem, cflags, pool, depth, re_nsub, stack);
     if (!atom.has_value())
       return atom;
-    atoms[atom_count] = *atom;
+    if (!stack.push(*atom))
+      return Error(REG_ESPACE);
     ++atom_count;
   }
 
   ErrorOr<ExprId> acc = pool.empty_str();
   if (!acc.has_value())
     return acc;
-  for (size_t i = atom_count; i > 0; --i) {
-    acc = pool.make_concat(atoms[i - 1], *acc);
+  for (; atom_count > 0; --atom_count) {
+    acc = pool.make_concat(stack.pop(), *acc);
+    if (!acc.has_value())
+      return acc;
+  }
+  return acc;
+}
+
+// Parses an alternation expression (branches separated by '|').
+ErrorOr<ExprId> parse_alt(cpp::string_view &rem, int cflags, ExprPool &pool,
+                          size_t depth, size_t &re_nsub,
+                          ParsedAtomStack &stack) {
+  if (rem.starts_with('|'))
+    return Error(REG_BADPAT);
+
+  ErrorOr<ExprId> first =
+      parse_concat(rem, cflags, pool, depth, re_nsub, stack);
+  if (!first.has_value() || !rem.starts_with('|'))
+    return first;
+  if (!stack.push(*first))
+    return Error(REG_ESPACE);
+  size_t branch_count = 1;
+
+  while (rem.starts_with('|')) {
+    rem.remove_prefix(1);
+    if (rem.empty() || rem.starts_with('|') ||
+        (depth > 0 && rem.starts_with(')')))
+      return Error(REG_BADPAT);
+
+    ErrorOr<ExprId> branch =
+        parse_concat(rem, cflags, pool, depth, re_nsub, stack);
+    if (!branch.has_value())
+      return branch;
+    if (!stack.push(*branch))
+      return Error(REG_ESPACE);
+    ++branch_count;
+  }
+
+  // Fold alternation branches right-to-left so right-associative sorted
+  // make_alt stays O(1) per branch without orphaned spine nodes.
+  ErrorOr<ExprId> acc = EMPTY_SET_ID;
+  for (; branch_count > 0; --branch_count) {
+    acc = pool.make_alt(stack.pop(), *acc);
     if (!acc.has_value())
       return acc;
   }
   return acc;
 }
 
+} // namespace
+
+// Top-level entry point: parses an ERE pattern starting from alternation at
+// depth 0 and optionally returns the total count of parenthesized
+// subexpressions.
+ErrorOr<ExprId> parse_ere(cpp::string_view pattern, int cflags, ExprPool &pool,
+                          size_t *out_re_nsub) {
+  cpp::string_view rem = pattern;
+  size_t re_nsub = 0;
+  ParsedAtomStack stack;
+  cpp::scope_exit cleanup([&stack] { delete[] stack.heap_atoms; });
+  ErrorOr<ExprId> res =
+      parse_alt(rem, cflags, pool, /*depth=*/0, re_nsub, stack);
+  if (!res.has_value())
+    return res;
+  if (out_re_nsub != nullptr)
+    *out_re_nsub = re_nsub;
+  return res;
+}
+
 } // namespace regex
 } // namespace LIBC_NAMESPACE_DECL

diff  --git a/libc/src/__support/regex/regex_parser.h b/libc/src/__support/regex/regex_parser.h
index 2bf42cfdf2d8a6..8e9594689d5216 100644
--- a/libc/src/__support/regex/regex_parser.h
+++ b/libc/src/__support/regex/regex_parser.h
@@ -24,7 +24,10 @@ namespace LIBC_NAMESPACE_DECL {
 namespace regex {
 
 // Parses an Extended Regular Expression (ERE) into the given ExprPool.
-ErrorOr<ExprId> parse_ere(cpp::string_view pattern, int cflags, ExprPool &pool);
+// Optionally populates out_re_nsub with the count of parenthesized
+// subexpressions.
+ErrorOr<ExprId> parse_ere(cpp::string_view pattern, int cflags, ExprPool &pool,
+                          size_t *out_re_nsub = nullptr);
 
 } // namespace regex
 } // namespace LIBC_NAMESPACE_DECL

diff  --git a/libc/src/regex/regcomp.cpp b/libc/src/regex/regcomp.cpp
index 7bb362a592ea3a..2b4b3134595fcf 100644
--- a/libc/src/regex/regcomp.cpp
+++ b/libc/src/regex/regcomp.cpp
@@ -57,8 +57,9 @@ LLVM_LIBC_FUNCTION(int, regcomp,
     return REG_ESPACE;
   cpp::scope_exit internal_guard([internal] { delete internal; });
 
-  ErrorOr<regex::ExprId> root =
-      regex::parse_ere(cpp::string_view(pattern), cflags, internal->pool);
+  size_t re_nsub = 0;
+  ErrorOr<regex::ExprId> root = regex::parse_ere(
+      cpp::string_view(pattern), cflags, internal->pool, &re_nsub);
   if (!root.has_value())
     return root.error();
 
@@ -66,6 +67,7 @@ LLVM_LIBC_FUNCTION(int, regcomp,
   internal->root = *root;
   internal->cflags = cflags;
   preg->__internal = internal;
+  preg->re_nsub = re_nsub;
   return 0;
 }
 

diff  --git a/libc/test/src/__support/regex/matcher_test.cpp b/libc/test/src/__support/regex/matcher_test.cpp
index 8c363ccf7cb596..d535d57b4966c6 100644
--- a/libc/test/src/__support/regex/matcher_test.cpp
+++ b/libc/test/src/__support/regex/matcher_test.cpp
@@ -294,15 +294,33 @@ TEST(LlvmLibcRegexMatcherTest, ParseEreEscapesAndErrors) {
   }
 
   const string_view bad_patterns[] = {
-      "a+",      "a?",    "a{1}",  "{",     "(",     ")",     "[[:<:]]",
-      "[[:>:]]", "|",     R"(\1)", R"(\2)", R"(\3)", R"(\4)", R"(\5)",
-      R"(\6)",   R"(\7)", R"(\8)", R"(\9)", R"(\b)", R"(\B)", R"(\<)",
-      R"(\>)",   R"(\A)", R"(\Z)", R"(\`)", R"(\')"};
+      // Unimplemented quantifiers.
+      "a+", "a?", "a{1}", "{",
+      // Unimplemented word boundaries.
+      "[[:<:]]", "[[:>:]]",
+      // Unimplemented backreferences.
+      R"(\1)", R"(\2)", R"(\3)", R"(\4)", R"(\5)", R"(\6)", R"(\7)", R"(\8)",
+      R"(\9)",
+      // Unimplemented boundary assertions.
+      R"(\b)", R"(\B)", R"(\<)", R"(\>)", R"(\A)", R"(\Z)", R"(\`)", R"(\')"};
   for (string_view bad : bad_patterns) {
     ErrorOr<ExprId> err = parse_ere(bad, REG_EXTENDED, pool);
     ASSERT_FALSE(err.has_value());
     EXPECT_EQ(err.error(), REG_BADPAT);
   }
+
+  ErrorOr<ExprId> empty_pipe = parse_ere("|", REG_EXTENDED, pool);
+  ASSERT_FALSE(empty_pipe.has_value());
+  EXPECT_EQ(empty_pipe.error(), REG_BADPAT);
+
+  ErrorOr<ExprId> unclosed_paren = parse_ere("(", REG_EXTENDED, pool);
+  ASSERT_FALSE(unclosed_paren.has_value());
+  EXPECT_EQ(unclosed_paren.error(), REG_EPAREN);
+
+  ErrorOr<ExprId> literal_close = parse_ere(")", REG_EXTENDED, pool);
+  ASSERT_TRUE(literal_close.has_value());
+  EXPECT_EQ(match(pool, *literal_close, ")"), MatchResult::Match);
+  EXPECT_EQ(match(pool, *literal_close, "x"), MatchResult::NoMatch);
 }
 
 TEST(LlvmLibcRegexMatcherTest, BracketMatchingAndRanges) {
@@ -461,3 +479,42 @@ TEST(LlvmLibcRegexMatcherTest, PoolExhaustionPropagation) {
   ASSERT_FALSE(concat_exhausted.has_value());
   EXPECT_EQ(concat_exhausted.error(), REG_ESPACE);
 }
+
+TEST(LlvmLibcRegexMatcherTest, AlternationAndGroupingStabilization) {
+  ExprPool pool;
+  ErrorOr<ExprId> ere =
+      parse_ere("(wee|week)(knights|night)", REG_EXTENDED, pool);
+  ASSERT_TRUE(ere.has_value());
+  EXPECT_EQ(match(pool, *ere, "weeknights"), MatchResult::Match);
+  EXPECT_EQ(match(pool, *ere, "weenight"), MatchResult::Match);
+  EXPECT_EQ(match(pool, *ere, "weeknight"), MatchResult::Match);
+  EXPECT_EQ(match(pool, *ere, "weeknightsxyz"), MatchResult::Match);
+  EXPECT_EQ(match(pool, *ere, "weeks"), MatchResult::NoMatch);
+
+  // Derivative state pool stabilizes after warming up on matches.
+  size_t warm_nodes = pool.size();
+  EXPECT_EQ(match(pool, *ere, "weeknights"), MatchResult::Match);
+  EXPECT_EQ(match(pool, *ere, "weenight"), MatchResult::Match);
+  EXPECT_EQ(pool.size(), warm_nodes);
+
+  // Parentheses control precedence without adding Group wrapper nodes,
+  // preserving hash-consing deduplication.
+  ErrorOr<ExprId> lit_a = parse_ere("a", REG_EXTENDED, pool);
+  ErrorOr<ExprId> grouped_a = parse_ere("((a))", REG_EXTENDED, pool);
+  ASSERT_TRUE(lit_a.has_value());
+  ASSERT_TRUE(grouped_a.has_value());
+  EXPECT_EQ(*grouped_a, *lit_a);
+
+  ErrorOr<ExprId> ab = parse_ere("a|b", REG_EXTENDED, pool);
+  ErrorOr<ExprId> grouped_ab = parse_ere("(a|b)|(b|a)", REG_EXTENDED, pool);
+  ASSERT_TRUE(ab.has_value());
+  ASSERT_TRUE(grouped_ab.has_value());
+  EXPECT_EQ(*grouped_ab, *ab);
+
+  // Right-to-left alternation folding interns only the 2 base nodes, 4 literal
+  // nodes, and 3 Alt spine nodes (9 total) with zero orphaned Alt nodes.
+  ExprPool alt_pool;
+  ErrorOr<ExprId> multi_alt = parse_ere("a|b|c|d", REG_EXTENDED, alt_pool);
+  ASSERT_TRUE(multi_alt.has_value());
+  EXPECT_EQ(alt_pool.size(), size_t(9));
+}

diff  --git a/libc/test/src/regex/regex_basic_test.cpp b/libc/test/src/regex/regex_basic_test.cpp
index 76eacc993421b2..b36cbe9e965680 100644
--- a/libc/test/src/regex/regex_basic_test.cpp
+++ b/libc/test/src/regex/regex_basic_test.cpp
@@ -334,8 +334,8 @@ TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
   LIBC_NAMESPACE::regfree(&preg);
 
   // Unimplemented ERE metacharacters and special escapes return REG_BADPAT.
-  const char *const bad_patterns[] = {
-      "a+", "a?", "(a)", "a{1}", "a|b", R"(\1)", R"(\b)", "[[:<:]]", "[[:>:]]"};
+  const char *const bad_patterns[] = {"a+",    "a?",      "a{1}",   R"(\1)",
+                                      R"(\b)", "[[:<:]]", "[[:>:]]"};
   for (const char *pat : bad_patterns) {
     EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_BADPAT);
     LIBC_NAMESPACE::regfree(&preg);
@@ -427,6 +427,132 @@ TEST(LlvmLibcRegexTest, PosixCharacterClassesAndShorthands) {
   check("^[^[:digit:][:space:]]*$", "abc1", REG_NOMATCH);
 }
 
+TEST(LlvmLibcRegexTest, AlternationAndGrouping) {
+  auto check = [](const char *pat, const char *str, int expected = 0) {
+    regex_t preg{};
+    ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED | REG_NOSUB), 0);
+    scope_exit free_guard([&preg] { LIBC_NAMESPACE::regfree(&preg); });
+    EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, str, 0, nullptr, 0), expected);
+  };
+
+  auto check_err = [](const char *pat, int expected_err) {
+    regex_t preg{};
+    EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), expected_err);
+    LIBC_NAMESPACE::regfree(&preg);
+  };
+
+  // Basic alternation branches.
+  check("cat|dog|bird", "cat");
+  check("cat|dog|bird", "dog");
+  check("cat|dog|bird", "bird");
+  check("cat|dog|bird", "fish", REG_NOMATCH);
+
+  // Grouping, operator precedence, and POSIX 9.4.9 subexpression anchoring.
+  check("^ab|cd$", "abXYZ");
+  check("^ab|cd$", "XYZcd");
+  check("^ab|cd$", "XYZab", REG_NOMATCH);
+  check("^(ab|cd)$", "ab");
+  check("^(ab|cd)$", "cd");
+  check("^(ab|cd)$", "abXYZ", REG_NOMATCH);
+  check("^(ab|cd)$", "XYZcd", REG_NOMATCH);
+  check("^(ab|cd)$", "abcd", REG_NOMATCH);
+  check("(^ab)", "abcdef");
+  check("(^ab)", "cdefab", REG_NOMATCH);
+  check("(ef$)", "abcdef");
+  check("(ef$)", "cdefab", REG_NOMATCH);
+
+  // Near-miss negative checks.
+  check("^x(cat|dog)y$", "xcaty");
+  check("^x(cat|dog)y$", "xdogy");
+  check("^x(cat|dog)y$", "xcatdogy", REG_NOMATCH);
+  check("^x(cat|dog)y$", "xcay", REG_NOMATCH);
+  check("^x(cat|dog)y$", "xy", REG_NOMATCH);
+
+  // Nullable branch anchoring rule.
+  check("^(a|b*)$", "a");
+  check("^(a|b*)$", "bbb");
+  check("^(a|b*)$", "");
+  check("^(a|b*)$", "ac", REG_NOMATCH);
+  check("^(a|b*)$", "bc", REG_NOMATCH);
+
+  // Nested grouping and repetition under Kleene star.
+  check("^((a)|(b))*$", "ababba");
+  check("^((a)|(b))*$", "ababbc", REG_NOMATCH);
+  check("(wee|week)(knights|night)", "weeknights");
+
+  // Depth-0 unmatched closing parenthesis treated as a literal character.
+  check("a)", "a)");
+  check(")", ")");
+  check("a)b", "a)b");
+  check("a)b", "ab", REG_NOMATCH);
+  check("a|)", "a");
+  check("a|)", ")");
+  check("a|)", "b", REG_NOMATCH);
+
+  // Empty subexpression matches empty string.
+  check("()", "");
+  check("()", "abc");
+  check("a()b", "ab");
+
+  // Error condition rejections.
+  check_err("|", REG_BADPAT);
+  check_err("|a", REG_BADPAT);
+  check_err("a|", REG_BADPAT);
+  check_err("a||b", REG_BADPAT);
+  check_err("(|a)", REG_BADPAT);
+  check_err("(a|)", REG_BADPAT);
+  check_err("a(", REG_EPAREN);
+  check_err("(a", REG_EPAREN);
+  check_err("((a)", REG_EPAREN);
+
+  // Subexpression count (re_nsub).
+  auto check_nsub = [](const char *pat, size_t expected_nsub) {
+    regex_t preg{};
+    ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), 0);
+    scope_exit free_guard([&preg] { LIBC_NAMESPACE::regfree(&preg); });
+    EXPECT_EQ(preg.re_nsub, expected_nsub);
+  };
+  check_nsub("abc", 0);
+  check_nsub("a|b", 0);
+  check_nsub("a)b", 0);
+  check_nsub("a(b)c", 1);
+  check_nsub("()", 1);
+  check_nsub("(a|b)", 1);
+  check_nsub("(a)(b)", 2);
+  check_nsub("((a))", 2);
+  check_nsub("((a|b)|c)", 2);
+  check_nsub("(((a|b)|c))", 3);
+  check_nsub("(wee|week)(knights|night)", 2);
+
+  // 128 nested parentheses must succeed within MAX_GROUP_DEPTH.
+  char deep_128[300];
+  for (size_t i = 0; i < 128; ++i)
+    deep_128[i] = '(';
+  deep_128[128] = 'x';
+  for (size_t i = 129; i < 257; ++i)
+    deep_128[i] = ')';
+  deep_128[257] = '\0';
+  check(deep_128, "x");
+
+  // 129 nested parentheses must return REG_ESPACE.
+  char deep_129[300];
+  for (size_t i = 0; i < 129; ++i)
+    deep_129[i] = '(';
+  deep_129[129] = 'x';
+  for (size_t i = 130; i < 259; ++i)
+    deep_129[i] = ')';
+  deep_129[259] = '\0';
+  check_err(deep_129, REG_ESPACE);
+
+  // 150 nested unclosed parentheses must return REG_ESPACE without stack
+  // overflow.
+  char deep_150[200];
+  for (size_t i = 0; i < 150; ++i)
+    deep_150[i] = '(';
+  deep_150[150] = '\0';
+  check_err(deep_150, REG_ESPACE);
+}
+
 #if defined(LIBC_ADD_NULL_CHECKS)
 TEST(LlvmLibcRegexTest, NullPointerCrash) {
   regex_t preg{};


        


More information about the libc-commits mailing list