Moderate firefox OOB 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionIncorrect boundary conditions in the Layout: Text and Fonts component
ComponentLayout
Bug ClassOOB
Tracker2068458
Fix commit63aaea311e6c (firefox) +12/-4
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

`nsTextFrame`
The layout frame that renders a run of text for a DOM text node (layout/generic/nsTextFrame.cpp). A single text node can be split across several continuation frames, each covering a [GetContentOffset(), GetContentEnd()) sub-range of the node’s character data.
`CharacterDataBuffer`
The backing character storage for the text node; GetLength() is its length. 1-byte or 2-byte storage is selected via Is2b(), and the text pointer is computed from start.
`BuildTextRunsScanner::FindBoundaries`
Walks frames to find text-run break boundaries; it reads the first/last characters of each frame’s slice of the buffer, using start and length to index into the buffer.
`SetFirstLetterLength` / continuations
::first-letter styling splits a text frame; the first-letter frame covers aLength characters and the remainder flows into a continuation whose mContentOffset must be kept consistent with the parent’s offset and length. mContentLengthHint caches the expected length; SetLength() properly re-derives offsets/lengths.

Root Cause Analysis

FindBoundaries computed uint32_t start = textFrame->GetContentOffset() and uint32_t length = textFrame->GetContentLength() and then indexed the CharacterDataBuffer by start/length to read boundary characters. These offsets are frame state that can become inconsistent with the buffer: if a frame’s content range is stale or malformed (e.g. start past end, or start+length beyond GetLength()), the unsigned arithmetic silently produced an out-of-range index and the scanner read outside the character-data buffer — an out-of-bounds read of text memory. The advisory’s “incorrect boundary conditions” is exactly this: the code trusted the frame’s offset/length without validating them against the actual buffer length.

The patch recomputes the range as signed int32_t start = GetContentOffset(), int32_t end = GetContentEnd(), int32_t bufferLength = GetLength(), and inserts a guard: if (start < 0 || end < start || end > bufferLength) { MOZ_ASSERT_UNREACHABLE(...); return FB_CONTINUE; }, deriving length = end - start only after the range is known valid. The companion fix is in SetFirstLetterLength: the old code assigned mContentLengthHint = aLength unconditionally and then set the continuation’s offset by hand with next->mContentOffset = GetContentOffset() + aLength. The patch instead only sets mContentLengthHint = 0 in the early-return !aLength && !next case and replaces the manual offset assignment with SetLength(aLength, nullptr, 0), which recomputes offsets/lengths consistently. This removes the path that could leave a continuation frame with an offset/length inconsistent with its buffer — the source of the invalid ranges FindBoundaries then tripped over.

Key insight
Text-frame content offset/length were taken as trusted indices into the character-data buffer and used without bounds-checking against the buffer length; a first-letter continuation could be left with an inconsistent offset, so the text-run scanner read out of bounds.

Attack Path

  1. Craft content Attacker serves a page with text styled so that ::first-letter splitting and continuation creation exercise SetFirstLetterLength (inferred: reachable from ordinary styled text layout).
  2. Create inconsistent range The manual mContentOffset/mContentLengthHint assignment leaves a continuation frame with an offset/length that no longer matches its CharacterDataBuffer.
  3. Trigger scan Layout runs BuildTextRunsScanner::FindBoundaries over the frame, which reads boundary characters using the stale start/length.
  4. OOB read Unsigned indexing reads 1- or 2-byte characters outside the buffer bounds.
  5. Primitive An out-of-bounds read of heap text memory in the renderer, which (inferred) can disclose adjacent heap contents or crash the content process depending on layout.

Impact Assessment

Renderer-process out-of-bounds read in layout, reachable from web content with no special privilege. The directly substantiated primitive is an OOB read of the character-data buffer (potential info disclosure of adjacent heap or a crash); the diff does not show a write, so this is not demonstrably a write primitive or RCE — escalation beyond disclosure/crash is inferred. The MOZ_ASSERT_UNREACHABLE in the guard indicates the invalid state is not expected to occur normally, consistent with the second hunk removing the way it was produced; severity is rated “moderate”.

Changed Functions

FunctionChangeNotes
if
layout/generic/nsTextFrame.cpp
modified

Files Changed

  • layout/generic/nsTextFrame.cpp

Audit Directions

  • Other `GetContentOffset`/`GetContentLength` buffer indexing
    Audit other nsTextFrame consumers that index CharacterDataBuffer by raw offset/length without validating against GetLength().
  • Manual `mContentOffset` writes
    Search for other direct assignments to mContentOffset/mContentLengthHint that bypass SetLength, which can leave frames with inconsistent ranges.
  • Continuation invariants
    Review first-letter, first-line, and bidi continuation creation for places that split text but don’t re-derive child offsets, and consider whether the new signed guard should be applied at other text-run entry points.
diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp
index f1835f10d40..093b6f23678 100644
--- a/layout/generic/nsTextFrame.cpp
+++ b/layout/generic/nsTextFrame.cpp
@@ -1467,8 +1467,16 @@ BuildTextRunsScanner::FindBoundaryResult BuildTextRunsScanner::FindBoundaries(
     }
     const CharacterDataBuffer& characterDataBuffer =
         textFrame->CharacterDataBuffer();
-    uint32_t start = textFrame->GetContentOffset();
-    uint32_t length = textFrame->GetContentLength();
+    const int32_t start = textFrame->GetContentOffset();
+    const int32_t end = textFrame->GetContentEnd();
+    const int32_t bufferLength = characterDataBuffer.GetLength();
+
+    if (MOZ_UNLIKELY(start < 0 || end < start || end > bufferLength)) {
+      MOZ_ASSERT_UNREACHABLE("Invalid text-frame content range");
+      return FB_CONTINUE;
+    }
+
+    int32_t length = end - start;
     const void* text;
     const nsAtom* language = textFrame->StyleFont()->GetLangAtom();
     if (characterDataBuffer.Is2b()) {
@@ -10732,9 +10740,9 @@ void nsTextFrame::SetFirstLetterLength(int32_t aLength) {
     return;
   }
 
-  mContentLengthHint = aLength;
   nsTextFrame* next = static_cast<nsTextFrame*>(GetNextInFlow());
   if (!aLength && !next) {
+    mContentLengthHint = 0;
     return;
   }
 
@@ -10755,7 +10763,7 @@ void nsTextFrame::SetFirstLetterLength(int32_t aLength) {
     next = letterFrame->CreateContinuationForFramesAfter(this);
   }
 
-  next->mContentOffset = GetContentOffset() + aLength;
+  SetLength(aLength, nullptr, 0);
 
   ClearTextRuns();
 }
Loading diff…