CVE-2026-74940
Overview
Background
- `gfxFontEntry`
- Gecko’s per-face font object that caches shared resources like the unscaled font used to build scaled
gfxFontinstances. - `UnscaledFont`
- A reference-counted (
RefPtr) backend font object (UnscaledFontMac,UnscaledFontDWrite,UnscaledFontFreeType) shared across scaled instances of the same face. - `CreateFontInstance`
- The per-backend method that lazily creates (or reuses) the cached
mUnscaledFontand returns a scaledgfxFont. - `AutoReadLock` / `AutoWriteLock`
- RAII wrappers over an
RWLock(mLock) that serialize reader and writer access to shared member state.
Root Cause Analysis
Each backend’s CreateFontInstance read the cached member mUnscaledFont into a local RefPtr and, when empty, constructed a new UnscaledFont and stored it back into mUnscaledFont, all without holding any lock. Because font-instance creation can run concurrently on multiple threads (main and off-main-thread text/graphics work), this is an unsynchronized read/write race on a RefPtr-typed member: the invariant that a RefPtr’s refcount adjustment and pointer store are consistent under concurrency was violated. A racing store could tear the read or drop the last reference, so one thread could observe a pointer whose backing object was concurrently freed, producing a use-after-free.
The fix takes an AutoReadLock on mLock to snapshot mUnscaledFont into a local RefPtr, and takes an AutoWriteLock before constructing and assigning the new instance. This serializes the load-or-create sequence so the cached pointer is always read and written under mutual exclusion, closing the race.
mUnscaledFont cache without synchronization, allowing a concurrent write to free the object mid-use; the fix guards the snapshot with AutoReadLock and the create/store with AutoWriteLock on mLock.Attack Path
- Trigger concurrent font instantiation
Content loads styles/text that cause the same
gfxFontEntryto be turned into scaled instances from more than one thread at nearly the same time. - Race the cache read
Two threads enter
CreateFontInstanceand both read the uncachedmUnscaledFontmember without a lock. - Race the cache store
Both proceed to construct a new
UnscaledFontand assign it tomUnscaledFont, with the unsynchronizedRefPtrassignment corrupting the refcount. - Free while in use
A dropped or torn reference frees an
UnscaledFontwhile another thread still holds and dereferences it. - Use-after-free Subsequent use of the freed backend font object corrupts memory in the graphics text path.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgfx/thebes/CoreTextFontList.cpp |
modified | |
ifgfx/thebes/gfxDWriteFontList.cpp |
modified | |
ifgfx/thebes/gfxFT2FontList.cpp |
modified |
Files Changed
gfx/thebes/CoreTextFontList.cppgfx/thebes/gfxDWriteFontList.cppgfx/thebes/gfxFT2FontList.cpp
Audit Directions
- Unsynchronized lazy cachesLook for other
RefPtr/ThreadSafeWeakPtrmembers populated via check-then-create in font/graphics code that can run on multiple threads without holdingmLock. - Partial lock coverageVerify both the read snapshot and the write-back of shared members are guarded; a lock only around the store still races with unlocked readers.
- `gfxFontEntry` subclass parityAudit every backend
CreateFontInstanceand sibling methods for consistentAutoReadLock/AutoWriteLockusage, since the same pattern was duplicated across CoreText, DirectWrite, and FreeType.
Patch
diff --git a/gfx/thebes/CoreTextFontList.cpp b/gfx/thebes/CoreTextFontList.cpp
index 196f90ef01d..459abdb76f3 100644
--- a/gfx/thebes/CoreTextFontList.cpp
+++ b/gfx/thebes/CoreTextFontList.cpp
@@ -374,12 +374,17 @@ nsresult CTFontEntry::ReadCMAP(FontInfoData* aFontInfoData) {
}
gfxFont* CTFontEntry::CreateFontInstance(const gfxFontStyle* aFontStyle) {
- RefPtr<UnscaledFontMac> unscaledFont(mUnscaledFont);
+ RefPtr<UnscaledFontMac> unscaledFont;
+ {
+ AutoReadLock lock(mLock);
+ unscaledFont = RefPtr<UnscaledFontMac>(mUnscaledFont);
+ }
if (!unscaledFont) {
CGFontRef baseFont = GetFontRef();
if (!baseFont) {
return nullptr;
}
+ AutoWriteLock lock(mLock);
unscaledFont = new UnscaledFontMac(baseFont, mIsDataUserFont);
mUnscaledFont = unscaledFont;
}
diff --git a/gfx/thebes/gfxDWriteFontList.cpp b/gfx/thebes/gfxDWriteFontList.cpp
index e303b149635..6a3cbfd64ee 100644
--- a/gfx/thebes/gfxDWriteFontList.cpp
+++ b/gfx/thebes/gfxDWriteFontList.cpp
@@ -706,7 +706,11 @@ gfxFont* gfxDWriteFontEntry::CreateFontInstance(
useBoldSim ? DWRITE_FONT_SIMULATIONS_BOLD : DWRITE_FONT_SIMULATIONS_NONE;
ThreadSafeWeakPtr<UnscaledFontDWrite>& unscaledFontPtr =
useBoldSim ? mUnscaledFontBold : mUnscaledFont;
- RefPtr<UnscaledFontDWrite> unscaledFont(unscaledFontPtr);
+ RefPtr<UnscaledFontDWrite> unscaledFont;
+ {
+ AutoReadLock lock(mLock);
+ unscaledFont = RefPtr<UnscaledFontDWrite>((unscaledFontPtr);
+ }
if (!unscaledFont) {
RefPtr<IDWriteFontFace> fontFace;
nsresult rv =
@@ -714,6 +718,7 @@ gfxFont* gfxDWriteFontEntry::CreateFontInstance(
if (NS_FAILED(rv)) {
return nullptr;
}
+ AutoWriteLock lock(mLock);
// Only pass in the underlying IDWriteFont if the unscaled font doesn't
// reflect a data font. This signals whether or not we can safely query
// a descriptor to represent the font for various transport use-cases.
diff --git a/gfx/thebes/gfxFT2FontList.cpp b/gfx/thebes/gfxFT2FontList.cpp
index ff0f22222be..bdbfa7f2cb5 100644
--- a/gfx/thebes/gfxFT2FontList.cpp
+++ b/gfx/thebes/gfxFT2FontList.cpp
@@ -226,8 +226,13 @@ gfxFont* FT2FontEntry::CreateFontInstance(const gfxFontStyle* aStyle) {
loadFlags &= ~FT_LOAD_NO_AUTOHINT;
}
- RefPtr<UnscaledFontFreeType> unscaledFont(mUnscaledFont);
+ RefPtr<UnscaledFontFreeType> unscaledFont;
+ {
+ AutoReadLock lock(mLock);
+ unscaledFont = RefPtr<UnscaledFontFreeType>(mUnscaledFont);
+ }
if (!unscaledFont) {
+ AutoWriteLock lock(mLock);
RefPtr<SharedFTFace> origFace(mFTFace);
unscaledFont = !mFilename.IsEmpty() && mFilename[0] == '/'
? new UnscaledFontFreeType(mFilename.get(), mFTFontIndex,