Medium CVSS 7.5 webkit Integer Overflow 🔧 Commit mapped

Overview

Medium
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentJSC
Sub-componentJSC YARR
Bug ClassInteger Overflow
Tracker309601
Fix commit7663d811d06c (WebKit/WebKit) +347/-29
CWECWE-119, CWE-120 (Buffer bounds error, Buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedLuka Rački
Disclosed2026-05-11

Background

YARR
JavaScriptCore’s regular-expression engine that parses a pattern and compiles it into matching bytecode or JIT code.
Call frame size
the amount of backtracking-state stack space (currentCallFrameSize) YARR reserves per alternative, accumulated term by term in setupAlternativeOffsets.
`CheckedUint32`
a WTF wrapper (Checked<unsigned>) that records arithmetic overflow so callers can test hasOverflowed() instead of silently wrapping.
`AssemblerBuffer`
the growable byte buffer that JSC’s assembler writes emitted machine code into, tracked by m_index and m_storage.capacity().

Root Cause Analysis

When YARR lays out backtracking state in setupAlternativeOffsets and setupDisjunctionOffsets, it walked every term and accumulated currentCallFrameSize into a plain unsigned, adding fixed amounts such as YarrStackSpaceForBackTrackInfoParentheses for each capturing group and assertion. A maliciously crafted pattern with an enormous number of captures or nested alternatives could drive this running total past UINT32_MAX, wrapping it around to a small value and violating the invariant that the reported frame size actually bounds the state the matcher will use at runtime. Downstream code then allocated an undersized frame while the generated matcher indexed far beyond it, corrupting the call frame / assembler buffer.

The fix threads the accumulator through CheckedUint32, calls hasOverflowed() after every += and returns the new ErrorCode::FrameTooLarge, and it caps the total number of captures via countCaptures/maxCapturesCount (yielding ErrorCode::TooManyCaptures) so the overflow can no longer be reached; the parallel AssemblerBuffer changes wrap grow, isAvailable and putIntegral in Checked<unsigned>/uint64_t math so the emit path cannot silently overflow either.

Key insight
The single mistake was accumulating an attacker-influenced per-term size into an unchecked 32-bit unsigned, so a huge pattern could wrap the total to a tiny number that no longer bounds the real state; the fix makes the accumulator a CheckedUint32, checks hasOverflowed() at each step, and adds an explicit capture-count ceiling to reject such patterns before compilation.

Attack Path

  1. Craft an oversized pattern Build a RegExp string with a very large number of capturing groups or nested alternations, e.g. '(a|b)*'.repeat(1000), that survives the maxPatternSize byte limit.
  2. Trigger frame layout Constructing the RegExp runs YARR’s setupOffsets, which recurses through setupDisjunctionOffsets and setupAlternativeOffsets summing per-term stack space.
  3. Overflow the accumulator The unsigned currentCallFrameSize running total exceeds UINT32_MAX and silently wraps to a small value, so the reported frame size understates the real requirement.
  4. Corrupt the matcher state Compilation/execution allocates the undersized frame while emitted code writes state at the true (larger) offsets, corrupting the call frame or AssemblerBuffer.
  5. Crash the process The resulting out-of-bounds access produces an unexpected process crash in the JavaScript engine.

Impact Assessment

An attacker who gets a victim to process a crafted regular expression gains an out-of-bounds write / state corruption in the JavaScriptCore YARR engine, running in the renderer/content process that evaluates the script, leading to an unexpected process crash. The only precondition is that the target evaluate attacker-supplied web content containing the malicious RegExp; no special privileges or user interaction beyond loading the page are required.

Changed Functions

FunctionChangeNotes
if
Source/JavaScriptCore/yarr/YarrParser.h
modified
if
Source/JavaScriptCore/yarr/YarrPattern.cpp
modified
for
JSTests/stress/regexp-alternative-heavy.js
modified

Files Changed

  • JSTests/stress/regexp-alternative-heavy.js
  • JSTests/stress/regexp-bol-optimize-out-of-stack.js
  • JSTests/stress/regexp-combined-large.js
  • JSTests/stress/regexp-deep-nested.js
  • JSTests/stress/regexp-heavy-mixed.js
  • JSTests/stress/regexp-lookahead-heavy.js
  • JSTests/stress/stack-overflow-regexp.js
  • LayoutTests/js/script-tests/stack-overflow-regexp.js
  • LayoutTests/js/stack-overflow-regexp-expected.txt
  • Source/JavaScriptCore/assembler/AssemblerBuffer.h
  • Source/JavaScriptCore/yarr/YarrErrorCode.cpp
  • Source/JavaScriptCore/yarr/YarrErrorCode.h
  • Source/JavaScriptCore/yarr/YarrParser.h
  • Source/JavaScriptCore/yarr/YarrPattern.cpp
  • Source/JavaScriptCore/yarr/YarrSyntaxChecker.cpp

Audit Directions

  • Unchecked size accumulation
    Audit any loop or recursion that sums attacker-influenced per-element sizes into a plain unsigned/int; require Checked<>/CheckedUint32 with hasOverflowed() at each accumulation.
  • Additive bounds checks
    Flag index + size <= capacity style comparisons on 32-bit types where the addend is attacker-controlled, and widen to uint64_t or use checked arithmetic.
  • Missing input ceilings
    Look for parser-facing resources (captures, groups, nesting depth) that lack an explicit hard cap like maxCapturesCount, since such caps are what keep later size math within range.
diff --git a/Source/JavaScriptCore/assembler/AssemblerBuffer.h b/Source/JavaScriptCore/assembler/AssemblerBuffer.h
index cd42b3061b13..49e1344d0369 100644
--- a/Source/JavaScriptCore/assembler/AssemblerBuffer.h
+++ b/Source/JavaScriptCore/assembler/AssemblerBuffer.h
@@ -211,7 +211,8 @@ namespace JSC {
 
         void grow(unsigned extraCapacity = 0)
         {
-            m_capacity = m_capacity + m_capacity / 2 + extraCapacity;
+            auto capacity = Checked<unsigned>(m_capacity) + m_capacity / 2 + extraCapacity;
+            m_capacity = capacity.value();
             if (isInlineBuffer()) {
                 m_buffer = static_cast<char*>(AssemblerDataMalloc::malloc(m_capacity));
                 memcpy(m_buffer, m_inlineBuffer, InlineCapacity);
@@ -325,7 +326,6 @@ namespace JSC {
     public:
         AssemblerBuffer()
             : m_storage()
-            , m_index(0)
 #if ENABLE(JIT_SIGN_ASSEMBLER_BUFFER)
             , m_hash()
             , m_hashes()
@@ -345,7 +345,7 @@ namespace JSC {
 
         bool isAvailable(unsigned space)
         {
-            return m_index + space <= m_storage.capacity();
+            return static_cast<uint64_t>(m_index) + space <= m_storage.capacity();
         }
 
         void ensureSpace(unsigned space)
@@ -444,7 +444,7 @@ namespace JSC {
             void putIntegralUnchecked(IntegralType value)
             {
 WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
-                ASSERT(m_index + sizeof(IntegralType) <= m_buffer.m_storage.capacity());
+                ASSERT(static_cast<uint64_t>(m_index) + sizeof(IntegralType) <= m_buffer.m_storage.capacity());
                 WTF::unalignedStore<IntegralType>(m_storageBuffer + m_index, value);
                 m_index += sizeof(IntegralType);
 WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
@@ -471,8 +471,8 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
         template<typename IntegralType>
         void putIntegral(IntegralType value)
         {
-            unsigned nextIndex = m_index + sizeof(IntegralType);
-            if (nextIndex > m_storage.capacity()) [[unlikely]]
+            auto nextIndex = Checked<unsigned>(m_index) + sizeof(IntegralType);
+            if (nextIndex.value() > m_storage.capacity()) [[unlikely]]
                 outOfLineGrow();
             putIntegralUnchecked<IntegralType>(value);
         }
@@ -517,7 +517,7 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
         friend LinkBuffer;
 
         AssemblerData m_storage;
-        unsigned m_index;
+        unsigned m_index { 0 };
 #if ENABLE(JIT_SIGN_ASSEMBLER_BUFFER)
         ARM64EHash<ShouldSign::Yes> m_hash;
         AssemblerHashes m_hashes;
diff --git a/Source/JavaScriptCore/yarr/YarrErrorCode.cpp b/Source/JavaScriptCore/yarr/YarrErrorCode.cpp
index 5444aeeee22f..7c4ab02e8ed0 100644
--- a/Source/JavaScriptCore/yarr/YarrErrorCode.cpp
+++ b/Source/JavaScriptCore/yarr/YarrErrorCode.cpp
@@ -73,6 +73,8 @@ ASCIILiteral errorMessage(ErrorCode error)
         REGEXP_ERROR_PREFIX "negated class set may contain strings"_s,                // NegatedClassSetMayContainStrings
         REGEXP_ERROR_PREFIX "invalid class set character"_s,                          // InvalidClassSetCharacter
         REGEXP_ERROR_PREFIX "invalid regular expression modifier"_s,                  // InvalidRegularExpressionModifier
+        REGEXP_ERROR_PREFIX "too many captures"_s,                                    // TooManyCaptures
+        REGEXP_ERROR_PREFIX "too many frame slots for state"_s,                       // FrameTooLarge
 
         // The following are NOT hard errors.
         REGEXP_ERROR_PREFIX "too many nested disjunctions"_s,                         // TooManyDisjunctions
@@ -118,6 +120,8 @@ JSObject* errorToThrow(JSGlobalObject* globalObject, ErrorCode error)
     case ErrorCode::NegatedClassSetMayContainStrings:
     case ErrorCode::InvalidClassSetCharacter:
     case ErrorCode::InvalidRegularExpressionModifier:
+    case ErrorCode::TooManyCaptures:
+    case ErrorCode::FrameTooLarge:
         return createSyntaxError(globalObject, errorMessage(error));
     case ErrorCode::TooManyDisjunctions:
         return createOutOfMemoryError(globalObject, errorMessage(error));
diff --git a/Source/JavaScriptCore/yarr/YarrErrorCode.h b/Source/JavaScriptCore/yarr/YarrErrorCode.h
index 0f6df8adce24..061bfd12fce5 100644
--- a/Source/JavaScriptCore/yarr/YarrErrorCode.h
+++ b/Source/JavaScriptCore/yarr/YarrErrorCode.h
@@ -77,6 +77,8 @@ enum class ErrorCode : uint8_t {
     NegatedClassSetMayContainStrings,
     InvalidClassSetCharacter,
     InvalidRegularExpressionModifier,
+    TooManyCaptures,
+    FrameTooLarge,
 
     // The following are NOT hard errors.
     TooManyDisjunctions, // we ran out stack compiling.
diff --git a/Source/JavaScriptCore/yarr/YarrParser.h b/Source/JavaScriptCore/yarr/YarrParser.h
index 6322e9d12e28..28f092d9423c 100644
--- a/Source/JavaScriptCore/yarr/YarrParser.h
+++ b/Source/JavaScriptCore/yarr/YarrParser.h
@@ -65,7 +65,6 @@ template <class T> concept YarrSyntaxCheckable = requires (T& checker, Vector<Ve
     { checker.atomCharacterClassPushNested(bool { }) } -> std::same_as<void>;
     { checker.atomCharacterClassPopNested(bool { }) } -> std::same_as<void>;
     { checker.atomCharacterClassEnd() } -> std::same_as<void>;
-    { checker.atomParenthesesSubpatternBegin() } -> std::same_as<void>;
     { checker.atomParenthesesSubpatternBegin(bool { }) } -> std::same_as<void>;
     { checker.atomParenthesesSubpatternBegin(bool { }, std::optional<String> { }) } -> std::same_as<void>;
     { checker.atomParentheticalAssertionBegin(bool { }, MatchDirection { }) } -> std::same_as<void>;
@@ -102,7 +101,7 @@ public:
      */
     ErrorCode parse()
     {
-        if (m_size > MAX_PATTERN_SIZE)
+        if (m_size > maxPatternSize)
             return ErrorCode::PatternTooLarge;
 
         parseTokens();
@@ -1557,9 +1556,10 @@ private:
                     }
 
                     auto setAddResult = m_namedCaptureGroups.add(groupName.value());
-                    if (setAddResult.isNewEntry)
+                    if (setAddResult.isNewEntry) {
                         m_delegate.atomParenthesesSubpatternBegin(true, groupName);
-                    else
+                        countCaptures();
+                    } else
                         m_errorCode = ErrorCode::DuplicateGroupName;
                 } else {
                     if (tryConsume('=')) {
@@ -1644,8 +1644,10 @@ private:
             default:
                 m_errorCode = ErrorCode::ParenthesesTypeInvalid;
             }
-        } else
-            m_delegate.atomParenthesesSubpatternBegin();
+        } else {
+            m_delegate.atomParenthesesSubpatternBegin(true);
+            countCaptures();
+        }
 
         if (type == ParenthesesType::Subpattern && !isNonCapturingGroup)
             ++m_numSubpatterns;
@@ -2188,6 +2190,12 @@ private:
         return std::nullopt;
     }
 
+    void countCaptures()
+    {
+        if (++m_numCaptures > maxCapturesCount)
+            m_errorCode = ErrorCode::TooManyCaptures;
+    }
+
     bool isLegacyCompilation() const { return m_compileMode == CompileMode::Legacy; }
     bool isUnicodeCompilation() const { return m_compileMode == CompileMode::Unicode; }
     bool isUnicodeSetsCompilation() const { return m_compileMode == CompileMode::UnicodeSets; }
@@ -2204,14 +2212,15 @@ private:
     unsigned m_backReferenceLimit;
     unsigned m_numSubpatterns { 0 };
     unsigned m_maxSeenBackReference { 0 };
+    unsigned m_numCaptures { 0 };
     bool m_isNamedForwardReferenceAllowed;
     bool m_kIdentityEscapeSeen { false };
     Vector<ParenthesesType, 16> m_parenthesesStack;
     NamedCaptureGroups m_namedCaptureGroups;
     UncheckedKeyHashSet<String> m_forwardReferenceNames;
 
-    // Derived by empirical testing of compile time in PCRE and WREC.
-    static constexpr unsigned MAX_PATTERN_SIZE = 1024 * 1024;
+    static constexpr unsigned maxPatternSize = 1024 * 1024; // Derived by empirical testing of compile time in PCRE and WREC.
+    static constexpr unsigned maxCapturesCount = (1 << 16) / 2; // Derived from V8's configuration.
 };
 
 /*
diff --git a/Source/JavaScriptCore/yarr/YarrPattern.cpp b/Source/JavaScriptCore/yarr/YarrPattern.cpp
index c1fdd84117e9..32d36a12a30d 100644
--- a/Source/JavaScriptCore/yarr/YarrPattern.cpp
+++ b/Source/JavaScriptCore/yarr/YarrPattern.cpp
@@ -1439,7 +1439,7 @@ public:
         m_pattern.m_userCharacterClasses.append(WTF::move(newCharacterClass));
     }
 
-    void atomParenthesesSubpatternBegin(bool capture = true, std::optional<String> optGroupName = std::nullopt)
+    void atomParenthesesSubpatternBegin(bool capture, std::optional<String> optGroupName = std::nullopt)
     {
         unsigned subpatternId = m_pattern.m_numSubpatterns + 1;
         if (capture) {
@@ -1774,7 +1774,7 @@ public:
         return m_error;
     }
 
-    [[nodiscard]] ErrorCode setupAlternativeOffsets(PatternAlternative* alternative, unsigned currentCallFrameSize, unsigned initialInputPosition, unsigned& newCallFrameSize)
+    [[nodiscard]] ErrorCode setupAlternativeOffsets(PatternAlternative* alternative, CheckedUint32 currentCallFrameSize, unsigned initialInputPosition, CheckedUint32& newCallFrameSize)
     {
         if (!isSafeToRecurse()) [[unlikely]]
             return ErrorCode::TooManyDisjunctions;
@@ -1798,6 +1798,8 @@ public:
                 term.inputPosition = currentInputPosition;
                 term.frameLocation = currentCallFrameSize;
                 currentCallFrameSize += YarrStackSpaceForBackTrackInfoBackReference;
+                if (currentCallFrameSize.hasOverflowed())
+                    return ErrorCode::FrameTooLarge;
                 alternative->m_hasFixedSize = false;
                 break;
 
@@ -1810,6 +1812,8 @@ public:
                 if (term.quantityType != QuantifierType::FixedCount) {
                     term.frameLocation = currentCallFrameSize;
                     currentCallFrameSize += YarrStackSpaceForBackTrackInfoPatternCharacter;
+                    if (currentCallFrameSize.hasOverflowed())
+                        return ErrorCode::FrameTooLarge;
                     alternative->m_hasFixedSize = false;
                 } else if (m_pattern.eitherUnicode()) {
                     CheckedUint32 tempCount = term.quantityMaxCount;
@@ -1826,10 +1830,14 @@ public:
                 if (term.quantityType != QuantifierType::FixedCount) {
                     term.frameLocation = currentCallFrameSize;
                     currentCallFrameSize += YarrStackSpaceForBackTrackInfoCharacterClass;
+                    if (currentCallFrameSize.hasOverflowed())
+                        return ErrorCode::FrameTooLarge;
                     alternative->m_hasFixedSize = false;
                 } else if (m_pattern.eitherUnicode()) {
                     term.frameLocation = currentCallFrameSize;
                     currentCallFrameSize += YarrStackSpaceForBackTrackInfoCharacterClass;
+                    if (currentCallFrameSize.hasOverflowed())
+                        return ErrorCode::FrameTooLarge;
                     if (term.characterClass->hasOneCharacterSize() && !term.invert()) {
                         CheckedUint32 tempCount = term.quantityMaxCount;
                         tempCount *= term.characterClass->hasNonBMPCharacters() ? 2 : 1;
@@ -1849,6 +1857,8 @@ public:
                 term.frameLocation = currentCallFrameSize;
                 if (term.quantityMaxCount == 1 && !term.parentheses.isCopy) {
                     currentCallFrameSize += YarrStackSpaceForBackTrackInfoParenthesesOnce;
+                    if (currentCallFrameSize.hasOverflowed())
+                        return ErrorCode::FrameTooLarge;
                     error = setupDisjunctionOffsets(term.parentheses.disjunction, currentCallFrameSize, currentInputPosition, currentCallFrameSize);
                     if (hasError(error))
                         return error;
@@ -1858,6 +1868,8 @@ public:
                     term.inputPosition = currentInputPosition;
                 } else if (term.parentheses.isTerminal) {
                     currentCallFrameSize += YarrStackSpaceForBackTrackInfoParenthesesTerminal;
+                    if (currentCallFrameSize.hasOverflowed())
+                        return ErrorCode::FrameTooLarge;
                     error = setupDisjunctionOffsets(term.parentheses.disjunction, currentCallFrameSize, currentInputPosition, currentCallFrameSize);
                     if (hasError(error))
                         return error;
@@ -1865,6 +1877,8 @@ public:
                 } else {
                     term.inputPosition = currentInputPosition;
                     currentCallFrameSize += YarrStackSpaceForBackTrackInfoParentheses;
+                    if (currentCallFrameSize.hasOverflowed())
+                        return ErrorCode::FrameTooLarge;
                     error = setupDisjunctionOffsets(term.parentheses.disjunction, currentCallFrameSize, currentInputPosition, currentCallFrameSize);
                     if (hasError(error))
                         return error;
@@ -1877,7 +1891,10 @@ public:
                 unsigned disjunctionInitialInputPosition = (term.matchDirection() == Forward) ? currentInputPosition.value() : 0;
                 term.inputPosition = currentInputPosition;
                 term.frameLocation = currentCallFrameSize;
-                error = setupDisjunctionOffsets(term.parentheses.disjunction, currentCallFrameSize + YarrStackSpaceForBackTrackInfoParentheticalAssertion, disjunctionInitialInputPosition, currentCallFrameSize);
+                currentCallFrameSize += YarrStackSpaceForBackTrackInfoParentheticalAssertion;
+                if (currentCallFrameSize.hasOverflowed())
+                    return ErrorCode::FrameTooLarge;
+                error = setupDisjunctionOffsets(term.parentheses.disjunction, currentCallFrameSize, disjunctionInitialInputPosition, currentCallFrameSize);
                 if (hasError(error))
                     return error;
                 break;
@@ -1889,6 +1906,8 @@ public:
                 term.inputPosition = initialInputPosition;
                 m_pattern.m_initialStartValueFrameLocation = currentCallFrameSize;
                 currentCallFrameSize += YarrStackSpaceForDotStarEnclosure;
+                if (currentCallFrameSize.hasOverflowed())
+                    return ErrorCode::FrameTooLarge;
                 m_pattern.m_saveInitialStartValue = true;
                 break;
             }
@@ -1897,17 +1916,20 @@ public:
         }
 
         alternative->m_minimumSize = currentInputPosition - initialInputPosition;
-        newCallFrameSize = currentCallFrameSize;
+        newCallFrameSize = currentCallFrameSize.value();
         return error;
     }
 
-    ErrorCode setupDisjunctionOffsets(PatternDisjunction* disjunction, unsigned initialCallFrameSize, unsigned initialInputPosition, unsigned& callFrameSize)
+    ErrorCode setupDisjunctionOffsets(PatternDisjunction* disjunction, CheckedUint32 initialCallFrameSize, unsigned initialInputPosition, CheckedUint32& callFrameSize)
     {
         if (!isSafeToRecurse()) [[unlikely]]
             return ErrorCode::TooManyDisjunctions;
 
-        if ((disjunction != m_pattern.m_body) && (disjunction->m_alternatives.size() > 1))
+        if ((disjunction != m_pattern.m_body) && (disjunction->m_alternatives.size() > 1)) {
             initialCallFrameSize += YarrStackSpaceForBackTrackInfoAlternative;
+            if (initialCallFrameSize.hasOverflowed())
+                return ErrorCode::FrameTooLarge;
+        }
 
         bool shareOffsets = (disjunction == m_pattern.m_body);
 
@@ -1916,15 +1938,15 @@ public:
         bool hasFixedSize = true;
         ErrorCode error = ErrorCode::NoError;
 
-        unsigned perAlternativeInitial = initialCallFrameSize;
+        CheckedUint32 perAlternativeInitial = initialCallFrameSize;
         for (unsigned alt = 0; alt < disjunction->m_alternatives.size(); ++alt) {
             PatternAlternative* alternative = disjunction->m_alternatives[alt].get();
-            unsigned currentAlternativeCallFrameSize;
+            CheckedUint32 currentAlternativeCallFrameSize;
             error = setupAlternativeOffsets(alternative, perAlternativeInitial, initialInputPosition, currentAlternativeCallFrameSize);
             if (hasError(error))
                 return error;
             minimumInputSize = std::min(minimumInputSize, alternative->m_minimumSize);
-            maximumCallFrameSize = std::max(maximumCallFrameSize, currentAlternativeCallFrameSize);
+            maximumCallFrameSize = std::max(maximumCallFrameSize, currentAlternativeCallFrameSize.value());
             hasFixedSize &= alternative->m_hasFixedSize;
             if (alternative->m_minimumSize > INT_MAX)
                 m_pattern.m_containsUnsignedLengthPattern = true;
@@ -1944,7 +1966,7 @@ public:
     ErrorCode setupOffsets()
     {
         // FIXME: Yarr should not use the stack to handle subpatterns (rdar://problem/26436314).
-        unsigned ignoredCallFrameSize;
+        CheckedUint32 ignoredCallFrameSize;
         return setupDisjunctionOffsets(m_pattern.m_body, 0, 0, ignoredCallFrameSize);
     }
 
diff --git a/Source/JavaScriptCore/yarr/YarrSyntaxChecker.cpp b/Source/JavaScriptCore/yarr/YarrSyntaxChecker.cpp
index 5f960c3bd4d2..c64adcdf98e9 100644
--- a/Source/JavaScriptCore/yarr/YarrSyntaxChecker.cpp
+++ b/Source/JavaScriptCore/yarr/YarrSyntaxChecker.cpp
@@ -48,7 +48,7 @@ public:
     void NODELETE atomCharacterClassPushNested(bool) { }
     void NODELETE atomCharacterClassPopNested(bool) { }
     void NODELETE atomCharacterClassEnd() { }
-    void NODELETE atomParenthesesSubpatternBegin(bool = true, std::optional<String> = std::nullopt) { }
+    void NODELETE atomParenthesesSubpatternBegin(bool, std::optional<String> = std::nullopt) { }
     void NODELETE atomParentheticalAssertionBegin(bool, MatchDirection) { }
     void NODELETE atomParentheticalModifierBegin(OptionSet<Flags>, OptionSet<Flags>) { }
     void NODELETE atomParenthesesEnd() { }
diff --git a/JSTests/stress/regexp-alternative-heavy.js b/JSTests/stress/regexp-alternative-heavy.js
new file mode 100644
index 000000000000..67fafb77ac15
--- /dev/null
+++ b/JSTests/stress/regexp-alternative-heavy.js
@@ -0,0 +1,41 @@
+//@ runDefault
+
+function tryCompileAndRun(label, buildPattern, testInput) {
+    // print(`\n[*] ${label}`);
+    let pattern;
+    try {
+        pattern = buildPattern();
+        // print(`    Pattern length: ${pattern.length} chars`);
+    } catch(e) {
+        print(`    BUILD ERROR: ${e.message}`);
+        return;
+    }
+
+    let re;
+    try {
+        re = new RegExp(pattern, 'g');
+        // print(`    RegExp constructed OK`);
+    } catch(e) {
+        print(`    REGEXP CONSTRUCTION ERROR (likely limit hit): ${e.message}`);
+        return;
+    }
+
+    // Warm the JIT — JSC typically tiered-compiles after ~6 invocations
+    try {
+        for (let i = 0; i < 8; i++) {
+            re.lastIndex = 0;
+            re.exec(testInput);
+        }
+        // print(`    exec() completed without crash — check for SP corruption artifacts`);
+    } catch(e) {
+        print(`    RUNTIME ERROR: ${e.message}`);
+    }
+}
+
+for (const n of [1000]) {
+    tryCompileAndRun(
+        `Approach C: ${n} alternation groups (a|b)*`,
+        () => '(a|b)*'.repeat(n),
+        'ab'.repeat(50)
+    );
+}
diff --git a/JSTests/stress/regexp-bol-optimize-out-of-stack.js b/JSTests/stress/regexp-bol-optimize-out-of-stack.js
index 7b99447a6b32..91ee1390ed5b 100644
--- a/JSTests/stress/regexp-bol-optimize-out-of-stack.js
+++ b/JSTests/stress/regexp-bol-optimize-out-of-stack.js
@@ -4,7 +4,7 @@
 
 arrayLength = typeof(arrayLength) === 'undefined' ? 50000 : arrayLength;
 
-let expectedException = "SyntaxError: Invalid regular expression: regular expression too large";
+let expectedException = "SyntaxError: Invalid regular expression: too many captures";
 
 function test()
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/JSTests/stress/regexp-alternative-heavy.js b/JSTests/stress/regexp-alternative-heavy.js
new file mode 100644
index 000000000000..67fafb77ac15
--- /dev/null
+++ b/JSTests/stress/regexp-alternative-heavy.js
@@ -0,0 +1,41 @@
+//@ runDefault
+
+function tryCompileAndRun(label, buildPattern, testInput) {
+    // print(`\n[*] ${label}`);
+    let pattern;
+    try {
+        pattern = buildPattern();
+        // print(`    Pattern length: ${pattern.length} chars`);
+    } catch(e) {
+        print(`    BUILD ERROR: ${e.message}`);
+        return;
+    }
+
+    let re;
+    try {
+        re = new RegExp(pattern, 'g');
+        // print(`    RegExp constructed OK`);
+    } catch(e) {
+        print(`    REGEXP CONSTRUCTION ERROR (likely limit hit): ${e.message}`);
+        return;
+    }
+
+    // Warm the JIT — JSC typically tiered-compiles after ~6 invocations
+    try {
+        for (let i = 0; i < 8; i++) {
+            re.lastIndex = 0;
+            re.exec(testInput);
+        }
+        // print(`    exec() completed without crash — check for SP corruption artifacts`);
+    } catch(e) {
+        print(`    RUNTIME ERROR: ${e.message}`);
+    }
+}
+
+for (const n of [1000]) {
+    tryCompileAndRun(
+        `Approach C: ${n} alternation groups (a|b)*`,
+        () => '(a|b)*'.repeat(n),
+        'ab'.repeat(50)
+    );
+}
diff --git a/JSTests/stress/regexp-bol-optimize-out-of-stack.js b/JSTests/stress/regexp-bol-optimize-out-of-stack.js
index 7b99447a6b32..91ee1390ed5b 100644
--- a/JSTests/stress/regexp-bol-optimize-out-of-stack.js
+++ b/JSTests/stress/regexp-bol-optimize-out-of-stack.js
@@ -4,7 +4,7 @@
 
 arrayLength = typeof(arrayLength) === 'undefined' ? 50000 : arrayLength;
 
-let expectedException = "SyntaxError: Invalid regular expression: regular expression too large";
+let expectedException = "SyntaxError: Invalid regular expression: too many captures";
 
 function test()
 {
diff --git a/JSTests/stress/regexp-combined-large.js b/JSTests/stress/regexp-combined-large.js
new file mode 100644
index 000000000000..605736beddc1
--- /dev/null
+++ b/JSTests/stress/regexp-combined-large.js
@@ -0,0 +1,48 @@
+//@ runDefault
+
+function tryCompileAndRun(label, buildPattern, testInput) {
+    // print(`\n[*] ${label}`);
+    let pattern;
+    try {
+        pattern = buildPattern();
+        // print(`    Pattern length: ${pattern.length} chars`);
+    } catch(e) {
+        print(`    BUILD ERROR: ${e.message}`);
+        return;
+    }
+
+    let re;
+    try {
+        re = new RegExp(pattern, 'g');
+        // print(`    RegExp constructed OK`);
+    } catch(e) {
+        print(`    REGEXP CONSTRUCTION ERROR (likely limit hit): ${e.message}`);
+        return;
+    }
+
+    // Warm the JIT — JSC typically tiered-compiles after ~6 invocations
+    try {
+        for (let i = 0; i < 8; i++) {
+            re.lastIndex = 0;
+            re.exec(testInput);
+        }
+        // print(`    exec() completed without crash — check for SP corruption artifacts`);
+    } catch(e) {
+        print(`    RUNTIME ERROR: ${e.message}`);
+    }
+}
+
+const LARGE_N = 12_500_000;
+tryCompileAndRun(
+    `Approach F: ${LARGE_N} flat groups (maximum feasible)`,
+    () => {
+        // Build in chunks to avoid single large string concat
+        let parts = [];
+        const chunk = '(a)*'.repeat(10000); // 40KB per chunk
+        for (let i = 0; i < LARGE_N / 10000; i++) {
+            parts.push(chunk);
+        }
+        return parts.join('');
+    },
+    'a'
+);
diff --git a/JSTests/stress/regexp-deep-nested.js b/JSTests/stress/regexp-deep-nested.js
new file mode 100644
index 000000000000..d6a80563d9ee
--- /dev/null
+++ b/JSTests/stress/regexp-deep-nested.js
@@ -0,0 +1,41 @@
+//@ runDefault
+
+function tryCompileAndRun(label, buildPattern, testInput) {
+    // print(`\n[*] ${label}`);
+    let pattern;
+    try {
+        pattern = buildPattern();
+        // print(`    Pattern length: ${pattern.length} chars`);
+    } catch(e) {
+        print(`    BUILD ERROR: ${e.message}`);
+        return;
+    }
+
+    let re;
+    try {
+        re = new RegExp(pattern, 'g');
+        // print(`    RegExp constructed OK`);
+    } catch(e) {
+        print(`    REGEXP CONSTRUCTION ERROR (likely limit hit): ${e.message}`);
+        return;
+    }
+
+    // Warm the JIT — JSC typically tiered-compiles after ~6 invocations
+    try {
+        for (let i = 0; i < 8; i++) {
+            re.lastIndex = 0;
+            re.exec(testInput);
+        }
+        // print(`    exec() completed without crash — check for SP corruption artifacts`);
+    } catch(e) {
+        print(`    RUNTIME ERROR: ${e.message}`);
+    }
+}
+
+for (const depth of [100, 500, 1000, 5000, 10000, 50000]) {
+    tryCompileAndRun(
+        `Approach B: nested depth ${depth} — ((((a)*)*...)*)*`,
+        () => '('.repeat(depth) + 'a' + ')*'.repeat(depth),
+        'a'.repeat(20)
+    );
+}
diff --git a/JSTests/stress/regexp-heavy-mixed.js b/JSTests/stress/regexp-heavy-mixed.js
new file mode 100644
index 000000000000..71bc1776c1f6
--- /dev/null
+++ b/JSTests/stress/regexp-heavy-mixed.js
@@ -0,0 +1,46 @@
+//@ runDefault
+
+function tryCompileAndRun(label, buildPattern, testInput) {
+    // print(`\n[*] ${label}`);
+    let pattern;
+    try {
+        pattern = buildPattern();
+        // print(`    Pattern length: ${pattern.length} chars`);
+    } catch(e) {
+        print(`    BUILD ERROR: ${e.message}`);
+        return;
+    }
+
+    let re;
+    try {
+        re = new RegExp(pattern, 'g');
+        // print(`    RegExp constructed OK`);
+    } catch(e) {
+        print(`    REGEXP CONSTRUCTION ERROR (likely limit hit): ${e.message}`);
+        return;
+    }
+
+    // Warm the JIT — JSC typically tiered-compiles after ~6 invocations
+    try {
+        for (let i = 0; i < 8; i++) {
+            re.lastIndex = 0;
+            re.exec(testInput);
+        }
+        // print(`    exec() completed without crash — check for SP corruption artifacts`);
+    } catch(e) {
+        print(`    RUNTIME ERROR: ${e.message}`);
+    }
+}
+
+tryCompileAndRun(
+    `Approach E: mixed named/quantified/lookahead (10k groups)`,
+    () => {
+        // Each unit: (?<x>a)* — named capture with quantifier
+        // Duplicate named captures in alternation arms force numDuplicateNamedCaptures > 0
+        const unit = '(?<g>a(?<h>b)*)';
+        const repeated = unit.repeat(5000);
+        // Wrap in alternation to trigger duplicate-named-capture counting
+        return `(?:${repeated}|${unit.repeat(100)})+`;
+    },
+    'ab'.repeat(30)
+);
diff --git a/JSTests/stress/regexp-lookahead-heavy.js b/JSTests/stress/regexp-lookahead-heavy.js
new file mode 100644
index 000000000000..820a5e2917ff
--- /dev/null
+++ b/JSTests/stress/regexp-lookahead-heavy.js
@@ -0,0 +1,41 @@
+//@ runDefault
+
+function tryCompileAndRun(label, buildPattern, testInput) {
+    // print(`\n[*] ${label}`);
+    let pattern;
+    try {
+        pattern = buildPattern();
+        // print(`    Pattern length: ${pattern.length} chars`);
+    } catch(e) {
+        print(`    BUILD ERROR: ${e.message}`);
+        return;
+    }
+
+    let re;
+    try {
+        re = new RegExp(pattern, 'g');
+        // print(`    RegExp constructed OK`);
+    } catch(e) {
+        print(`    REGEXP CONSTRUCTION ERROR (likely limit hit): ${e.message}`);
+        return;
+    }
+
+    // Warm the JIT — JSC typically tiered-compiles after ~6 invocations
+    try {
+        for (let i = 0; i < 8; i++) {
+            re.lastIndex = 0;
+            re.exec(testInput);
+        }
+        // print(`    exec() completed without crash — check for SP corruption artifacts`);
+    } catch(e) {
+        print(`    RUNTIME ERROR: ${e.message}`);
+    }
+}
+
+for (const n of [500, 5000, 20000]) {
+    tryCompileAndRun(
+        `Approach D: ${n} lookaheads (?=(a))*`,
+        () => '(?=(a))'.repeat(n) + 'a*',
+        'a'.repeat(50)
+    );
+}
diff --git a/JSTests/stress/stack-overflow-regexp.js b/JSTests/stress/stack-overflow-regexp.js
index c8bccff65891..7ef0d3117820 100644
--- a/JSTests/stress/stack-overflow-regexp.js
+++ b/JSTests/stress/stack-overflow-regexp.js
@@ -7,9 +7,9 @@ function testDeepNesting() {
     try {
         new RegExp(Array(520000).join("(") + "a" + Array(520000).join(")"));
     } catch (e) {
-        if (e instanceof RangeError)
+        if (e instanceof SyntaxError || e instanceof RangeError)
             return;
-        throw new Error("Expected RangeError, got " + e);
+        throw new Error("Expected SyntaxError or RangeError, got " + e);
     }
     // With certain stack configurations the pattern may fit; that's OK.
 }
diff --git a/LayoutTests/js/script-tests/stack-overflow-regexp.js b/LayoutTests/js/script-tests/stack-overflow-regexp.js
new file mode 100644
index 000000000000..730e82177440
--- /dev/null
+++ b/LayoutTests/js/script-tests/stack-overflow-regexp.js
@@ -0,0 +1,31 @@
+//@ skip if $memoryLimited
+//  &&&&
+description('Test that we do not overflow the stack while handling regular expressions');
+
+// Base case.
+shouldThrow('new RegExp(Array(500000).join("(") + "a" + Array(500000).join(")"))', '"SyntaxError: Invalid regular expression: too many captures"');
+
+{ // Verify that a deep JS stack does not help avoiding the error.
+    function recursiveCall(depth) {
+        if (!(depth % 10)) {
+            debug("Creating RegExp at depth " + depth);
+            shouldThrow('new RegExp(Array(500000).join("(") + "a" + Array(500000).join(")"))', '"SyntaxError: Invalid regular expression: too many captures"');
+        }
+        if (depth < 100) {
+            recursiveCall(depth + 1);
+        }
+    }
+    recursiveCall(0);
... (truncated)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.