CVE-2025-43265
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