CVE-2025-0238
Overview
Background
- `nsLineBreaker`
- A DOM/layout helper that accumulates text into a buffer (
mCurrentWord) and computes line-break opportunities across queued text items when a word boundary is reached. - `FlushCurrentWord`
- The method that processes the currently buffered word, distributes computed break data back to its constituent
mTextItems, and then resets the accumulator state for the next word. - `MakeScopeExit`/`ScopeExit`
- A RAII utility from
mozilla/ScopeExit.hthat runs a supplied lambda unconditionally when the enclosing scope is exited, including on earlyreturn. - Fallible allocation
- An allocation performed with the
mozilla::fallibletag that returns a failure indication instead of aborting, requiring the caller to handle out-of-memory (OOM) explicitly.
Root Cause Analysis
nsLineBreaker::FlushCurrentWord buffers text in mCurrentWord and tracks the source spans in mTextItems, and it is expected to clear both containers along with the related word-state flags once the word has been flushed. In the vulnerable code these resets (mCurrentWord.Clear(), mTextItems.Clear(), and the flag assignments) were performed only at the very end of the function, after the success path. When the early fallible allocation of breakState failed (if (!breakState.AppendElements(length, mozilla::fallible))), the function returned NS_ERROR_OUT_OF_MEMORY without ever clearing mCurrentWord or mTextItems, so the stale entries in mTextItems — which reference text-frame/sink objects that may be destroyed after the flush fails — survived into subsequent calls and were later dereferenced, yielding a use-after-free.
The fix installs a MakeScopeExit cleanup lambda at function entry that clears mCurrentWord and mTextItems and resets every word-state flag unconditionally, guaranteeing the accumulator is reset on every exit path, including the OOM early return. Because the reset now runs regardless of how the function returns, no stale, dangling text-item references can persist for a later flush to touch.
mTextItems; the fix converts the reset into a MakeScopeExit RAII cleanup that executes on all return paths.Attack Path
- Prime the accumulator
Construct DOM content whose text feeds
nsLineBreakerso thatmCurrentWordandmTextItemshold entries referencing live text objects when a flush is triggered. - Force the fallible allocation to fail
Use controlled memory pressure so that the
breakState.AppendElements(length, mozilla::fallible)call insideFlushCurrentWordreturns failure, taking the earlyreturn NS_ERROR_OUT_OF_MEMORYbranch before the tail reset. - Leave stale references behind
Because the early return skipped the clears,
mTextItemsretains pointers to text objects that are subsequently torn down as the failed operation unwinds. - Trigger reuse of the dangling state
Drive a later text/line-breaking operation that reads or flushes the still-populated
mTextItems, dereferencing the freed objects. - Achieve a controlled crash The use-after-free produces a potentially exploitable crash whose freed memory may be groomed by the attacker.
Impact Assessment
breakState allocation while attacker-influenced text is buffered in nsLineBreaker, so exploitation requires reliable heap/allocation grooming rather than a simple malformed input. The impact is rated moderate, consistent with a hard-to-control OOM-gated UAF.Files Changed
dom/base/nsLineBreaker.cpp
Audit Directions
- Early-return-skipped cleanupLook for functions that reset or free shared/member state only at the tail while having earlier
returnbranches (especially fallible-allocation failures) that bypass that cleanup. - Fallible allocation error pathsAudit uses of
mozilla::falliblewhere anAppendElements/allocation failure returns early, checking that all buffers and back-references touched before the failure are consistently invalidated. - RAII-convertible manual resetsFlag manual multi-field state resets that would be more safely expressed as
MakeScopeExit/ScopeExitguards, since manual placement is easily defeated by added early-exit paths.
Patch
diff --git a/dom/base/nsLineBreaker.cpp b/dom/base/nsLineBreaker.cpp
index 7175e9ab1eb..1edcfdd05d2 100644
--- a/dom/base/nsLineBreaker.cpp
+++ b/dom/base/nsLineBreaker.cpp
@@ -15,6 +15,7 @@
#include "mozilla/intl/LineBreaker.h" // for LineBreaker::ComputeBreakPositions
#include "mozilla/intl/Locale.h"
#include "mozilla/intl/UnicodeProperties.h"
+#include "mozilla/ScopeExit.h"
#include "mozilla/StaticPrefs_intl.h"
using mozilla::AutoRestore;
@@ -150,6 +151,15 @@ static void SetupCapitalization(const char16_t* aWord, uint32_t aLength,
}
nsresult nsLineBreaker::FlushCurrentWord() {
+ auto cleanup = MakeScopeExit([&] {
+ mCurrentWord.Clear();
+ mTextItems.Clear();
+ mCurrentWordMightBeBreakable = false;
+ mCurrentWordContainsMixedLang = false;
+ mCurrentWordLanguage = nullptr;
+ mWordContinuation = false;
+ });
+
uint32_t length = mCurrentWord.Length();
AutoTArray<uint8_t, 4000> breakState;
if (!breakState.AppendElements(length, mozilla::fallible)) {
@@ -232,12 +242,6 @@ nsresult nsLineBreaker::FlushCurrentWord() {
offset += ti->mLength;
}
- mCurrentWord.Clear();
- mTextItems.Clear();
- mCurrentWordMightBeBreakable = false;
- mCurrentWordContainsMixedLang = false;
- mCurrentWordLanguage = nullptr;
- mWordContinuation = false;
return NS_OK;
}