Medium CVSS 4.3 webkit OOB 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentJSC Runtime
Bug ClassOOB
Tracker316996
Fix commit30b9a27b47e8 (WebKit/WebKit) +30/-2
CWECWE-20, CWE-703 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L
CISA KEVNot listed
CreditedClaudio Bozzato and Francesco Benvenuto of Cisco Talos, Josef Korbel (Citadelo)
Disclosed2026-08-17

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

  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.

Changed Functions

FunctionChangeNotes
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

  • JSTests/stress/yarr-jit-non-bmp-backtrack-index-overflow.js
  • JSTests/stress/yarr-jit-non-bmp-backtrack-index-underflow.js
  • Source/JavaScriptCore/runtime/RegExpMatchesArray.h
  • Source/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 invariants
    Assert end >= start and within-subject at every matches-array construction and subpatternResults consumer, not only the path patched here.
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.