CVE-2026-28904
Overview
Background
- YARR
- JavaScriptCore’s regular-expression engine that compiles patterns to bytecode/JIT with a backtracking model.
- Backtracking call frame
- Per-match stack space YARR reserves to store backtracking state; its size is computed from the pattern structure.
- CheckedUint32 / Checked<unsigned>
- WTF integer wrappers that record overflow so it can be detected instead of silently wrapping.
- FrameTooLarge (new)
- The error code the fix returns to reject a pattern whose backtracking frame size overflows 32 bits.
Root Cause Analysis
During YARR (JavaScriptCore’s regexp engine) pattern compilation, setupAlternativeOffsets/setupDisjunctionOffsets compute the size of the backtracking call frame a regexp needs, accumulating per-construct stack space (e.g. YarrStackSpaceForBackTrackInfoParentheticalAssertion and similar) as a plain unsigned. For a sufficiently large or deeply nested pattern, this running total can exceed 2^32 and wrap around, so the computed newCallFrameSize is far smaller than the space the compiled matcher will actually use. The compiler then sizes the backtracking frame from the wrapped-small value while the matcher indexes it as if it were the true (large) size, an out-of-bounds access of the backtracking stack frame — memory corruption / crash.
The fix changes the call-frame-size accumulators to CheckedUint32 (currentCallFrameSize, initialCallFrameSize, perAlternativeInitial, currentAlternativeCallFrameSize, ignoredCallFrameSize) and, at every accumulation point, returns the new ErrorCode::FrameTooLarge when hasOverflowed() is true, so an over-large pattern is rejected at compile time instead of producing an undersized frame; a TooManyCaptures error code is likewise added. Complementary hardening in AssemblerBuffer.h switches capacity growth to Checked<unsigned> and compares indices as 64-bit to avoid analogous overflow in the code buffer.
The restored invariant is that the regexp backtracking frame size is computed with overflow checking and any pattern whose frame would overflow 32 bits is rejected rather than compiled with a truncated size.
Attack Path
- Craft an over-large regexp Construct a pattern (deeply nested groups/assertions/alternations, or very many captures) whose accumulated backtracking call-frame size exceeds 2^32.
- Compile it Have web content compile the RegExp (new RegExp / literal), driving setupAlternativeOffsets/setupDisjunctionOffsets to accumulate the frame size in a plain unsigned.
- Overflow the frame size The unsigned accumulation wraps to a small value, so newCallFrameSize is far smaller than the matcher actually needs.
- Out-of-bounds backtracking Executing a match allocates the undersized frame but indexes it at the true offsets, reading/writing out of bounds of the backtracking stack — corruption or crash in WebContent.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
YarrPatternConstructor::setupAlternativeOffsetsSource/JavaScriptCore/yarr/YarrPattern.cpp |
modified | currentCallFrameSize/newCallFrameSize become CheckedUint32; every accumulation checks hasOverflowed() and returns ErrorCode::FrameTooLarge. |
YarrPatternConstructor::setupDisjunctionOffsetsSource/JavaScriptCore/yarr/YarrPattern.cpp |
modified | initialCallFrameSize/callFrameSize/perAlternativeInitial/currentAlternativeCallFrameSize become CheckedUint32 with overflow checks before use of .value(). |
errorMessage / errorToThrow (+ ErrorCode)Source/JavaScriptCore/yarr/YarrErrorCode.cpp |
modified | Adds FrameTooLarge and TooManyCaptures error codes and their thrown messages so over-large patterns fail cleanly. |
AssemblerBuffer grow/putIntegralSource/JavaScriptCore/assembler/AssemblerBuffer.h |
modified | Uses Checked<unsigned> for capacity growth and 64-bit index comparisons to prevent analogous integer overflow in the code buffer. |
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
- Other unchecked size accumulation in YARRgrep yarr/ for
unsignedrunning totals of offsets/sizes (input positions, frame slots, capture counts) added in loops without Checked/hasOverflowed(). - Consumers of call-frame sizeAudit the JIT/interpreter backends that allocate and index the backtracking frame to ensure they use the same checked size and honor FrameTooLarge.
- Assembler buffer growthReview other AssemblerBuffer/CodeBuffer growth and index arithmetic for 32-bit overflow like the ones hardened here.