Medium CVSS 7.7 firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
7.7
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionWhen segmenting specially crafted text, segmentation would corrupt memory leading to a potentially exploitable crash.
ComponentSpiderMonkey
Bug ClassLogic Error
Tracker1933023
Fix commit6a1bdb80cb57 (firefox) +15/-5
CWECWE-401
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:L
CISA KEVNot listed
CreditedNils Bars
Disclosed2025-01-07

Background

`Intl.Segmenter`
a JavaScript builtin implemented in SpiderMonkey that splits text into graphemes, words, or sentences according to Unicode segmentation rules.
Latin1 vs. TwoByte strings
SpiderMonkey stores string characters either as 8-bit JS::Latin1Char or 16-bit char16_t, and the character width selects which break-iterator specialization is used.
`SegmentsObject` / `SegmentIteratorObject`
NativeObject subclasses that cache the segmented string’s characters in a fixed slot (STRING_CHARS_SLOT) and hold the underlying ICU4X break iterator.
`STRING_CHARS_SLOT`
the reserved slot storing a tagged pointer to the string’s raw character buffer, whose tag records whether the buffer is Latin1 or TwoByte.

Root Cause Analysis

The segmentation code determined the character width by calling segments->getString()->hasLatin1Chars(), querying the live JSString object rather than the character buffer that was actually cached in the object’s STRING_CHARS_SLOT when segmentation began. Because a JSString’s representation can change out from under the segmenter (for example a rope being flattened, or a string being linearized/depended upon), the width reported by the string could diverge from the width of the buffer stored in the slot. When that happened, code such as DestroyBreakIterator, GraphemeBoundaries, WordBoundaries, SentenceBoundaries, and EnsureBreakIterator would pick the wrong BreakIterator specialization (e.g. BreakIteratorLatin1 for a two-byte buffer or vice versa), causing the iterator to read the cached buffer with the wrong stride and bounds and corrupting memory.

The fix adds hasLatin1StringChars(), which reads the width directly from the cached getStringChars() tag, so the width used to select the specialization always matches the buffer actually being indexed, restoring the invariant that the buffer’s stored type and its interpreted type agree.

Key insight
The core mistake was deriving the character-width decision from the mutable JSString (getString()->hasLatin1Chars()) instead of from the immutable, already-captured character buffer stored in STRING_CHARS_SLOT; the fix routes every width check through hasLatin1StringChars(), which inspects getStringChars() and therefore can never disagree with the buffer being read.

Attack Path

  1. Create a segmenter over a mutable string Script constructs an Intl.Segmenter and calls segment() on a string whose backing representation (e.g. a rope or dependent string) can later be changed to a different character width.
  2. Trigger cached-buffer capture Iterating or calling boundary methods stores the current character buffer and its width tag into STRING_CHARS_SLOT on the SegmentsObject/SegmentIteratorObject.
  3. Cause a representation change Further JavaScript operations flatten or otherwise convert the underlying JSString so that getString()->hasLatin1Chars() no longer matches the width of the buffer already cached in the slot.
  4. Force a mismatched break iterator A subsequent GraphemeBoundaries/WordBoundaries/SentenceBoundaries or EnsureBreakIterator call reads the stale width from the string and selects the wrong BreakIterator specialization for the cached buffer.
  5. Corrupt memory The mis-specialized iterator indexes the buffer with the wrong element size and bounds, reading or writing out of range and producing a potentially exploitable crash.

Impact Assessment

An attacker who can run JavaScript in the content process gains an out-of-bounds memory access driven by a controllable character-width mismatch during text segmentation, which Mozilla rates moderate and describes as a potentially exploitable crash. Exploitation requires only that the page use Intl.Segmenter on specially crafted text whose backing string representation changes after the segmenter caches its buffer, with no elevated privileges needed.

Files Changed

  • js/src/builtin/intl/Segmenter.cpp
  • js/src/builtin/intl/Segmenter.h

Audit Directions

  • Cached-buffer vs. live-object divergence
    Look for code that snapshots a buffer or width into a slot but later re-queries the mutable source object (getString(), rope/dependent strings) for a property that must stay consistent with the snapshot.
  • Width-selected specializations
    Audit every site that picks a Latin1/TwoByte or element-size-dependent code path (hasLatin1Chars, template specialization on JS::Latin1Char vs. char16_t) to confirm the width source is the same buffer that is subsequently indexed.
  • ICU4X/`Intl` iterator lifetimes
    Review Intl.Segmenter and related Intl iterator objects for other places where a break iterator or cached pointer can outlive or diverge from the string representation it was built against.
diff --git a/js/src/builtin/intl/Segmenter.cpp b/js/src/builtin/intl/Segmenter.cpp
index 437a2f1a5d6..ab1f09a87be 100644
--- a/js/src/builtin/intl/Segmenter.cpp
+++ b/js/src/builtin/intl/Segmenter.cpp
@@ -574,7 +574,7 @@ static void DestroyBreakIterator(const T* segments) {
   void* brk = segments->getBreakIterator();
   MOZ_ASSERT(brk);
 
-  bool isLatin1 = segments->getString()->hasLatin1Chars();
+  bool isLatin1 = segments->hasLatin1StringChars();
 
   switch (segments->getGranularity()) {
     case SegmenterGranularity::Grapheme: {
@@ -660,7 +660,7 @@ static Boundaries FindBoundaryFrom(Handle<T*> segments, int32_t index) {
 template <typename T>
 static Boundaries GraphemeBoundaries(Handle<T*> segments, int32_t index) {
 #if defined(MOZ_ICU4X)
-  if (segments->getString()->hasLatin1Chars()) {
+  if (segments->hasLatin1StringChars()) {
     return FindBoundaryFrom<GraphemeClusterSegmenter::BreakIteratorLatin1>(
         segments, index);
   }
@@ -674,7 +674,7 @@ static Boundaries GraphemeBoundaries(Handle<T*> segments, int32_t index) {
 template <typename T>
 static Boundaries WordBoundaries(Handle<T*> segments, int32_t index) {
 #if defined(MOZ_ICU4X)
-  if (segments->getString()->hasLatin1Chars()) {
+  if (segments->hasLatin1StringChars()) {
     return FindBoundaryFrom<WordSegmenter::BreakIteratorLatin1>(segments,
                                                                 index);
   }
@@ -687,7 +687,7 @@ static Boundaries WordBoundaries(Handle<T*> segments, int32_t index) {
 template <typename T>
 static Boundaries SentenceBoundaries(Handle<T*> segments, int32_t index) {
 #if defined(MOZ_ICU4X)
-  if (segments->getString()->hasLatin1Chars()) {
+  if (segments->hasLatin1StringChars()) {
     return FindBoundaryFrom<SentenceSegmenter::BreakIteratorLatin1>(segments,
                                                                     index);
   }
@@ -782,7 +782,7 @@ static bool EnsureBreakIterator(JSContext* cx, Handle<T*> segments,
   }
 
 #if defined(MOZ_ICU4X)
-  bool isLatin1 = segments->getString()->hasLatin1Chars();
+  bool isLatin1 = segments->hasLatin1StringChars();
 
   // Create a new break iterator based on the granularity and character type.
   void* brk;
diff --git a/js/src/builtin/intl/Segmenter.h b/js/src/builtin/intl/Segmenter.h
index 3b204578b45..2b75b864882 100644
--- a/js/src/builtin/intl/Segmenter.h
+++ b/js/src/builtin/intl/Segmenter.h
@@ -198,6 +198,11 @@ class SegmentsObject : public NativeObject {
     setFixedSlot(STRING_CHARS_SLOT, PrivateValue(chars.tagged()));
   }
 
+  bool hasLatin1StringChars() const {
+    MOZ_ASSERT(hasStringChars());
+    return getStringChars().has<JS::Latin1Char>();
+  }
+
   int32_t getIndex() const {
     const auto& slot = getFixedSlot(INDEX_SLOT);
     if (slot.isUndefined()) {
@@ -295,6 +300,11 @@ class SegmentIteratorObject : public NativeObject {
     setFixedSlot(STRING_CHARS_SLOT, PrivateValue(chars.tagged()));
   }
 
+  bool hasLatin1StringChars() const {
+    MOZ_ASSERT(hasStringChars());
+    return getStringChars().has<JS::Latin1Char>();
+  }
+
   int32_t getIndex() const {
     const auto& slot = getFixedSlot(INDEX_SLOT);
     if (slot.isUndefined()) {
Loading diff…