Released August 17, 2026

CVE-2026-65340

CVSS 4.3 severity medium CWE-20CWE-703 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected Safari crash

Researchers: Claudio Bozzato and Francesco Benvenuto of Cisco Talos, Josef Korbel (Citadelo)

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.

Patch Details

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.

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-65340
Bugzilla 316996
Component JSC
Bug Class OOB
Severity medium
Commit 30b9a27b47e842c4…
Advisory Apple Advisory

Terminology

  • 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.

Attack Path

1. 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.

2. Execute against a crafted subject

exec() runs the regex on a subject that forces backtracking through the non-BMP alternative.

3. 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).

4. 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.

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 invariants

Assert end >= start and within-subject at every matches-array construction and subpatternResults consumer, not only the path patched here.

Changed Functions

Function File Change Note
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.
createRegExpMatchesArray Source/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

Source Files

  • Source/JavaScriptCore/runtime/RegExpMatchesArray.h
  • Source/JavaScriptCore/yarr/YarrJIT.cpp

Test Files

  • JSTests/stress/yarr-jit-non-bmp-backtrack-index-overflow.js
  • JSTests/stress/yarr-jit-non-bmp-backtrack-index-underflow.js

Regression Test

The fix ships this proof-of-concept / regression test:

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 + ")");