CVE-2025-43265
Overview
Background
- 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
- Craft a Unicode regexp Build a /u regexp whose JIT path uses the non-BMP first-character read optimization.
- Feed surrogate-pair input Match against a subject with non-BMP characters positioned so the sentinel-conditional additional-read-size is left wrong.
- Advance index out of bounds The matcher adds the wrong additional read size and computes a subject position past the string end.
- Disclose memory Out-of-bounds character reads leak adjacent internal memory into match results in the WebContent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/JavaScriptCore/yarr/YarrJIT.cpp |
modified |
Files Changed
Source/JavaScriptCore/yarr/YarrJIT.cpp
Audit Directions
- Other conditional index advancesgrep YarrJIT for moveConditionallyTest/addOneConditionally on index/read-size registers where a stateful sentinel could desync.
- Non-BMP / surrogate handlingAudit surrogate-pair read paths (backreferences, character classes) for correct fixed index advances and bounds checks.
- OOB read surfacesReview how match offsets derived from JIT reads are bounds-checked against the subject length before use.
Patch
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) {