Released July 29, 2025

CVE-2025-43265

CVSS 4 severity medium CWE-125 vector CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

Overview

Impact:

Description: Processing maliciously crafted web content may disclose internal states of the app

Researchers: HexRabbit (@h3xr4bb1t) from DEVCORE Research Team

Key insight. The YARR-JIT surrogate-pair index advance used a fragile sentinel-conditional to set the extra read size, which could be left wrong and push the match index out of bounds, leaking memory; using a fixed advance of 1 removes the desync.

Patch Details

diff --git a/Source/JavaScriptCore/yarr/YarrJIT.cpp b/Source/JavaScriptCore/yarr/YarrJIT.cpp
index 2ec28e58f53c..4dafe9f18ac2 100644
--- a/Source/JavaScriptCore/yarr/YarrJIT.cpp
+++ b/Source/JavaScriptCore/yarr/YarrJIT.cpp
@@ -92,15 +92,6 @@ static_assert(areCanonicallyEquivalentCharArgReg == GPRInfo::returnValueGPR);
 #endif
 #endif
 
-#if ENABLE(YARR_JIT_UNICODE_EXPRESSIONS) && ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
-// This enhancement allows us to advance the index by 2 when we read a non-BMP surrogate pair, but fail to match.
-// The way it works is that we initialize the firstCharacterAdditionalReadSize register to an initial sentinal value.
-// When reading a possible surrogate pair, we change firstCharacterAdditionalReadSize from the sentinal to 0 if we read
-// a BMP (16-bit) character or 1 if the read value is a non-BMP. Once changed from the sentinel value, we don't change
-// again during the next read. We add firstCharacterAdditionalReadSize to index for the next iteration on a failed
-// match and when setting the the possible new match start location.
-constexpr static int32_t additionalReadSizeSentinel = 0x4;
-#endif
 
 WTF_MAKE_TZONE_ALLOCATED_IMPL(BoyerMooreBitmap);
 WTF_MAKE_TZONE_ALLOCATED_IMPL(BoyerMooreFastCandidates);
@@ -364,23 +355,14 @@ void tryReadUnicodeCharImpl(VM& vm, CCallHelpers& jit, MacroAssembler::RegisterI
 #endif
 
 #if ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
-    if constexpr (useNonBMPOptimization == TryReadUnicodeCharGenFirstNonBMPOptimization::UseOptimization) {
-        // If this is the first read of the alternation, set additional read size to 1 because we got a non-BMP code point.
-        jit.moveConditionallyTest32(MacroAssembler::NonZero, regs.firstCharacterAdditionalReadSize, MacroAssembler::TrustedImm32(additionalReadSizeSentinel), ARM64Registers::zr, regs.firstCharacterAdditionalReadSize);
-        jit.addOneConditionally32(MacroAssembler::NonZero, regs.firstCharacterAdditionalReadSize, regs.firstCharacterAdditionalReadSize);
-    }
+    if (useNonBMPOptimization == TryReadUnicodeCharGenFirstNonBMPOptimization::UseOptimization)
+        jit.move(MacroAssembler::TrustedImm32(1), regs.firstCharacterAdditionalReadSize);
 #endif
     done.append(jit.jump());
 
     isBMP.link(jit);
     jit.and32(MacroAssembler::TrustedImm32(0xffff), resultReg);
 
-#if ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
-    if constexpr (useNonBMPOptimization == TryReadUnicodeCharGenFirstNonBMPOptimization::UseOptimization) {
-        // If this is the first read of the alternation, set additional read size to 0.
-        jit.moveConditionallyTest32(MacroAssembler::NonZero, regs.firstCharacterAdditionalReadSize, MacroAssembler::TrustedImm32(additionalReadSizeSentinel), ARM64Registers::zr, regs.firstCharacterAdditionalReadSize);
-    }
-#endif
     done.append(jit.jump());
 
     slowCases.link(&jit);
@@ -455,13 +437,6 @@ void tryReadUnicodeCharSlowImpl(CCallHelpers& jit)
     isBMP.link(jit);
     jit.and32(MacroAssembler::TrustedImm32(0xffff), resultReg);
 
-#if ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
-    if constexpr (useNonBMPOptimization == TryReadUnicodeCharGenFirstNonBMPOptimization::UseOptimization) {
-        // If this is the first read of the alternation, set additional read size to 0.
-        jit.moveConditionallyTest32(MacroAssembler::NonZero, regs.firstCharacterAdditionalReadSize, MacroAssembler::TrustedImm32(additionalReadSizeSentinel), ARM64Registers::zr, regs.firstCharacterAdditionalReadSize);
-    }
-#endif
-
     jit.ret();
 
     checkForDanglingSurrogates.link(&jit);
@@ -495,13 +470,6 @@ void tryReadUnicodeCharSlowImpl(CCallHelpers& jit)
 
     bmpDone.link(&jit);
 
-#if ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
-    if constexpr (useNonBMPOptimization == TryReadUnicodeCharGenFirstNonBMPOptimization::UseOptimization) {
-        // If this is the first read of the alternation, set additional read size to 0.
-        jit.moveConditionallyTest32(MacroAssembler::NonZero, regs.firstCharacterAdditionalReadSize, MacroAssembler::TrustedImm32(additionalReadSizeSentinel), ARM64Registers::zr, regs.firstCharacterAdditionalReadSize);
-    }
-#endif
-
     haveResult.link(&jit);
 }
 
@@ -3233,6 +3201,12 @@ class YarrGenerator final : public YarrJITInfo {
 
                 termMatchTargets.append(MatchTargets());
 
+#if ENABLE(YARR_JIT_UNICODE_EXPRESSIONS) && ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
+                // Always set to zero.
+                if (m_useFirstNonBMPCharacterOptimization)
+                    m_jit.move(MacroAssembler::TrustedImm32(0), m_regs.firstCharacterAdditionalReadSize);
+#endif
+
                 // Upon entry at the head of the set of alternatives, check if input is available
                 // to run the first alternative. (This progresses the input position).
                 op.m_jumps.append(jumpIfNoAvailableInput(alternative->m_minimumSize));
@@ -3241,11 +3215,6 @@ class YarrGenerator final : public YarrJITInfo {
                 // set as appropriate to this alternative.
                 op.m_reentry = m_jit.label();
 
-#if ENABLE(YARR_JIT_UNICODE_EXPRESSIONS) && ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
-                // Clear first character read size so it can be set on the first read.
-                if (m_useFirstNonBMPCharacterOptimization)
-                    m_jit.move(MacroAssembler::TrustedImm32(additionalReadSizeSentinel), m_regs.firstCharacterAdditionalReadSize);
-#endif
 
                 // Emit fast skip path with stride if we have BoyerMooreInfo.
                 if (op.m_bmInfo) {

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-43265
Bugzilla 294182
Component JSC
Bug Class OOB
Severity medium
Commit d96ab2fa64c62763…
Advisory Apple Advisory

Terminology

  • YARR JIT — JavaScriptCore’s JIT-compiled regular-expression matcher.
  • BMP / non-BMP / surrogate pair — Non-BMP code points are encoded as two UTF-16 units (a surrogate pair); reading one advances the index by 2, not 1.
  • firstCharacterAdditionalReadSize — A register holding how much extra to advance the index after reading a possible surrogate pair on a failed match.
  • Sentinel latching — The removed scheme used a sentinel value and conditional moves to set the advance once; its state could desynchronize.

Root Cause Analysis

YarrJIT’s Unicode fast path has an optimization (YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP) that, after reading a non-BMP code point encoded as a surrogate pair, advances the subject index by 2 instead of 1 on a failed match. It implemented this with a register, firstCharacterAdditionalReadSize, initialized to a sentinel (0x4) and then conditionally changed to 0 (BMP) or 1 (non-BMP) using moveConditionallyTest32/addOneConditionally against the sentinel, intended to latch the value on the first read and not change it again. That sentinel-conditional scheme was buggy: under the right sequence the additional-read-size could be left at a wrong value, so the matcher advanced the index by the wrong amount and computed an out-of-bounds subject position, reading characters past the end of the string. Since regexp match results and internal offsets derive from that read, the bug discloses adjacent internal memory (the advisory: ‘may disclose internal states of the app’). The fix removes the sentinel dance entirely and unconditionally sets firstCharacterAdditionalReadSize to 1 for the non-BMP-optimization case (jit.move(TrustedImm32(1), ...)), deleting the fragile conditional latching in both the fast (tryReadUnicodeCharImpl) and slow (tryReadUnicodeCharSlowImpl) generators. The restored invariant is that the surrogate-pair index advance is a fixed, correct amount rather than a stateful conditional that could desynchronize.

Attack Path

1. Craft a Unicode regexp

Build a /u regexp whose JIT path uses the non-BMP first-character read optimization.

2. Feed surrogate-pair input

Match against a subject with non-BMP characters positioned so the sentinel-conditional additional-read-size is left wrong.

3. Advance index out of bounds

The matcher adds the wrong additional read size and computes a subject position past the string end.

4. Disclose memory

Out-of-bounds character reads leak adjacent internal memory into match results in the WebContent process.

Impact Assessment

An out-of-bounds read in the YARR JIT’s non-BMP index-advance logic, reachable from a crafted Unicode regexp in the WebContent process. Per the advisory it discloses internal application memory (an info-leak primitive useful for defeating ASLR), rather than a crash. Confined to WebContent; rated medium.

Audit Directions

Other conditional index advances

grep YarrJIT for moveConditionallyTest/addOneConditionally on index/read-size registers where a stateful sentinel could desync.

Non-BMP / surrogate handling

Audit surrogate-pair read paths (backreferences, character classes) for correct fixed index advances and bounds checks.

OOB read surfaces

Review how match offsets derived from JIT reads are bounds-checked against the subject length before use.

Changed Functions

Function File Change Note
tryReadUnicodeCharImpl (YarrJIT) Source/JavaScriptCore/yarr/YarrJIT.cpp modified Replaces the sentinel-based conditional latching of firstCharacterAdditionalReadSize with an unconditional move of 1 for the non-BMP optimization, removing the buggy stateful advance.
tryReadUnicodeCharSlowImpl (YarrJIT) Source/JavaScriptCore/yarr/YarrJIT.cpp modified Removes the matching sentinel-conditional code in the slow path and the additionalReadSizeSentinel constant.

Files Changed

Source Files

  • Source/JavaScriptCore/yarr/YarrJIT.cpp