CVE-2026-28904
Overview
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 insetupAlternativeOffsets. - `CheckedUint32`
- a WTF wrapper (
Checked<unsigned>) that records arithmetic overflow so callers can testhasOverflowed()instead of silently wrapping. - `AssemblerBuffer`
- the growable byte buffer that JSC’s assembler writes emitted machine code into, tracked by
m_indexandm_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.
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
- Craft an oversized pattern
Build a
RegExpstring with a very large number of capturing groups or nested alternations, e.g.'(a|b)*'.repeat(1000), that survives themaxPatternSizebyte limit. - Trigger frame layout
Constructing the
RegExpruns YARR’ssetupOffsets, which recurses throughsetupDisjunctionOffsetsandsetupAlternativeOffsetssumming per-term stack space. - Overflow the accumulator
The
unsigned currentCallFrameSizerunning total exceedsUINT32_MAXand silently wraps to a small value, so the reported frame size understates the real requirement. - 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. - Crash the process The resulting out-of-bounds access produces an unexpected process crash in the JavaScript engine.
Impact Assessment
RegExp; no special privileges or user interaction beyond loading the page are required.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/JavaScriptCore/yarr/YarrParser.h |
modified | |
ifSource/JavaScriptCore/yarr/YarrPattern.cpp |
modified | |
forJSTests/stress/regexp-alternative-heavy.js |
modified |
Files Changed
JSTests/stress/regexp-alternative-heavy.jsJSTests/stress/regexp-bol-optimize-out-of-stack.jsJSTests/stress/regexp-combined-large.jsJSTests/stress/regexp-deep-nested.jsJSTests/stress/regexp-heavy-mixed.jsJSTests/stress/regexp-lookahead-heavy.jsJSTests/stress/stack-overflow-regexp.jsLayoutTests/js/script-tests/stack-overflow-regexp.jsLayoutTests/js/stack-overflow-regexp-expected.txtSource/JavaScriptCore/assembler/AssemblerBuffer.hSource/JavaScriptCore/yarr/YarrErrorCode.cppSource/JavaScriptCore/yarr/YarrErrorCode.hSource/JavaScriptCore/yarr/YarrParser.hSource/JavaScriptCore/yarr/YarrPattern.cppSource/JavaScriptCore/yarr/YarrSyntaxChecker.cpp
Audit Directions
- Unchecked size accumulationAudit any loop or recursion that sums attacker-influenced per-element sizes into a plain
unsigned/int; requireChecked<>/CheckedUint32withhasOverflowed()at each accumulation. - Additive bounds checksFlag
index + size <= capacitystyle comparisons on 32-bit types where the addend is attacker-controlled, and widen touint64_tor use checked arithmetic. - Missing input ceilingsLook 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.
Patch
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)
Regression Test / PoC
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)