CVE-2026-117961
Overview
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 viaIs2b(), and the text pointer is computed fromstart. - `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
startandlengthto index into the buffer. - `SetFirstLetterLength` / continuations
::first-letterstyling splits a text frame; the first-letter frame coversaLengthcharacters and the remainder flows into a continuation whosemContentOffsetmust be kept consistent with the parent’s offset and length.mContentLengthHintcaches 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.
Attack Path
- Craft content
Attacker serves a page with text styled so that
::first-lettersplitting and continuation creation exerciseSetFirstLetterLength(inferred: reachable from ordinary styled text layout). - Create inconsistent range
The manual
mContentOffset/mContentLengthHintassignment leaves a continuation frame with an offset/length that no longer matches itsCharacterDataBuffer. - Trigger scan
Layout runs
BuildTextRunsScanner::FindBoundariesover the frame, which reads boundary characters using the stalestart/length. - OOB read Unsigned indexing reads 1- or 2-byte characters outside the buffer bounds.
- 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
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
| Function | Change | Notes |
|---|---|---|
iflayout/generic/nsTextFrame.cpp |
modified |
Files Changed
layout/generic/nsTextFrame.cpp
Audit Directions
- Other `GetContentOffset`/`GetContentLength` buffer indexingAudit other
nsTextFrameconsumers that indexCharacterDataBufferby raw offset/length without validating againstGetLength(). - Manual `mContentOffset` writesSearch for other direct assignments to
mContentOffset/mContentLengthHintthat bypassSetLength, which can leave frames with inconsistent ranges. - Continuation invariantsReview 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.
Patch
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();
}