WebKit · JSC Runtime
CVE-2026-65340
OOB in JSC Runtime
Overview
Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
Background
- YARR JIT
- JavaScriptCore’s regular-expression engine; the JIT compiles regexes to machine code, including backtracking for nested alternatives.
- Non-BMP / surrogate pairs
- Characters above U+FFFF (e.g. \u{10000}) occupy two UTF-16 code units; matching them adjusts the input index by an extra read size.
- Matches array
- createRegExpMatchesArray builds the result from each subpattern’s start/end; if end < start the computed length is negative/huge, causing an OOB.
Root Cause Analysis
This fixes an index over/underflow in the YARR JIT backtracking for non-BMP characters that could produce a match whose end is before its start (or past the subject), leading to an out-of-bounds match array. In YarrGenerator’s backtracking for a nested alternative, when the first-non-BMP-character optimization is active (m_useFirstNonBMPCharacterOptimization), the fix adds the firstCharacterAdditionalReadSize to m_regs.index and, instead of an unconditional jump to the alternative reentry, links through checkInput() so the input-position bounds are re-validated (advancedIndexForNonBMP path). It also adds RELEASE_ASSERT(result.end >= result.start) in createRegExpMatchesArray. The violated invariant is that a successful match’s end index is >= its start and within the subject length; without re-checking input after adjusting the index for surrogate-pair (non-BMP) matches during backtracking, the computed index could underflow/overflow so subpatternResults yielded end < start, which the matches-array construction then used to compute a negative/huge length — an out-of-bounds access. The two regression tests use /u regexes mixing BMP and non-BMP alternatives (\u{10000}) against crafted subjects to drive the overflow and underflow. Established by the diff; the label is LogicError but the effect is an OOB from a bad index.
Key insight
During backtracking with the first-non-BMP-character optimization, the input index was adjusted for a surrogate pair without re-validating input bounds, so a match’s end could fall before its start or past the subject. The fix re-checks bounds through checkInput() after the adjustment and asserts end >= start when building the matches array.
Attack Path
- Craft a Unicode regex with mixed BMP/non-BMP alternatives The page builds a /u RegExp whose alternatives mix ordinary and astral (\u{10000}) characters so the YARR JIT uses the first-non-BMP optimization.
- Execute against a crafted subject exec() runs the regex on a subject that forces backtracking through the non-BMP alternative.
- Corrupt the match index During backtracking the index is adjusted for the surrogate pair without re-validating input, so the resulting match end is less than start (or past subject length).
- Out-of-bounds match array createRegExpMatchesArray uses end<start to size/populate the results, reading or writing out of bounds and crashing (now caught by RELEASE_ASSERT / prevented by the checkInput reentry).
Impact Assessment
An out-of-bounds access from a crafted /u regular expression, driven by index over/underflow during backtracking on non-BMP characters. Because the corrupted index feeds match-array construction, the attacker can force an OOB read/length miscomputation in the WebContent process — a memory-safety issue despite the LogicError label.
Changed Functions
| Function | Change | Notes |
|---|---|---|
YarrGenerator backtracking (nested alternative)Source/JavaScriptCore/yarr/YarrJIT.cpp |
modified | For the non-BMP first-character optimization, adds firstCharacterAdditionalReadSize to index and links to the alternative reentry via checkInput() (bounds re-check) instead of an unconditional jump. |
createRegExpMatchesArraySource/JavaScriptCore/runtime/RegExpMatchesArray.h |
modified | Adds RELEASE_ASSERT(result.end >= result.start) to enforce the match-bounds invariant when building the matches array. |
Files Changed
JSTests/stress/yarr-jit-non-bmp-backtrack-index-overflow.jsJSTests/stress/yarr-jit-non-bmp-backtrack-index-underflow.jsSource/JavaScriptCore/runtime/RegExpMatchesArray.hSource/JavaScriptCore/yarr/YarrJIT.cpp
Audit Directions
- Index adjustments without checkInput()Audit YARR JIT backtracking for other places that add/subtract read sizes for surrogate pairs or optimizations and then jump to reentry without re-validating input position.
- Match-bounds invariantsAssert end >= start and within-subject at every matches-array construction and subpatternResults consumer, not only the path patched here.
Patch
diff --git a/JSTests/stress/yarr-jit-non-bmp-backtrack-index-overflow.js b/JSTests/stress/yarr-jit-non-bmp-backtrack-index-overflow.js
new file mode 100644
index 000000000000..8657a02fb82c
--- /dev/null
+++ b/JSTests/stress/yarr-jit-non-bmp-backtrack-index-overflow.js
@@ -0,0 +1,12 @@
+const re = /d\0e?|\u{10000}c/u;
+const subj = "\u{10000}d";
+const m = re.exec(subj);
+
+if (m !== null) {
+ throw new Error(
+ "expected null, got match=" + JSON.stringify(m[0]) +
+ " at index=" + m.index +
+ " (m.index + m[0].length = " + (m.index + m[0].length) +
+ " > subj.length = " + subj.length + ")"
+ );
+}
diff --git a/JSTests/stress/yarr-jit-non-bmp-backtrack-index-underflow.js b/JSTests/stress/yarr-jit-non-bmp-backtrack-index-underflow.js
new file mode 100644
index 000000000000..52fb0bbf4021
--- /dev/null
+++ b/JSTests/stress/yarr-jit-non-bmp-backtrack-index-underflow.js
@@ -0,0 +1,5 @@
+const re = /(?!\u{10000})a*|\u{10000}b/u;
+const subj = "\u{10000}cc";
+const m = re.exec(subj);
+if (m[0].length > subj.length)
+ throw new Error("m[0].length (" + m[0].length + ") > subj.length (" + subj.length + ")");
diff --git a/Source/JavaScriptCore/runtime/RegExpMatchesArray.h b/Source/JavaScriptCore/runtime/RegExpMatchesArray.h
index 4d5d2304e1d1..4d46a7f017f8 100644
--- a/Source/JavaScriptCore/runtime/RegExpMatchesArray.h
+++ b/Source/JavaScriptCore/runtime/RegExpMatchesArray.h
@@ -76,7 +76,8 @@ ALWAYS_INLINE JSArray* createRegExpMatchesArray(
result.start = position;
result.end = subpatternResults[1];
-
+ RELEASE_ASSERT(result.end >= result.start);
+
JSArray* array;
JSArray* indicesArray = nullptr;
diff --git a/Source/JavaScriptCore/yarr/YarrJIT.cpp b/Source/JavaScriptCore/yarr/YarrJIT.cpp
index d5dc7005cc4d..90ead5391420 100644
--- a/Source/JavaScriptCore/yarr/YarrJIT.cpp
+++ b/Source/JavaScriptCore/yarr/YarrJIT.cpp
@@ -3925,9 +3925,19 @@ class YarrGenerator final : public YarrJITInfo {
// already correctly incremented, if more than one then decrement as appropriate.
unsigned delta = alternative->m_minimumSize - beginOp->m_alternative->m_minimumSize;
ASSERT(delta);
+ bool advancedIndexForNonBMP = false;
+#if ENABLE(YARR_JIT_UNICODE_EXPRESSIONS) && ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
+ if (m_useFirstNonBMPCharacterOptimization) {
+ m_jit.add32(m_regs.firstCharacterAdditionalReadSize, m_regs.index);
+ advancedIndexForNonBMP = true;
+ }
+#endif
if (delta != 1)
m_jit.sub32(MacroAssembler::Imm32(delta - 1), m_regs.index);
- m_jit.jump(beginOp->m_reentry);
+ if (advancedIndexForNonBMP)
+ checkInput().linkTo(beginOp->m_reentry, &m_jit);
+ else
+ m_jit.jump(beginOp->m_reentry);
} else {
// If the first alternative has minimum size 0xFFFFFFFFu, then there cannot
// be sufficent input available to handle this, so just fall through.
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker.
References
On This Page