CVE-2026-74945
Overview
Background
- OTS (OpenType Sanitizer)
- Firefox’s library that validates and rewrites downloadable font data into a sanitized copy before it reaches the font backend.
- `gfxOTSExpandingMemoryStream`
- an
ots::OTSStreamsubclass that accumulates the sanitizer’s rewritten font bytes in a heap buffer that grows on demand. - `gfxOTSAllocator::Grow`
- the allocator helper that
malloc/realloc-style expands the stream’s backing buffer to a new length without zeroing the added region. - Uninitialized memory disclosure
- a bug class where bytes never written by the program are exposed to an attacker, leaking prior heap contents.
Root Cause Analysis
The gfxOTSExpandingMemoryStream allocated its initial buffer via mAlloc.Grow(nullptr, mLength) and grew it via mAlloc.Grow(mPtr, newLength) in the write path, but neither call zeroed the freshly obtained bytes. Because an ots::OTSStream supports seeking (Seek) and writing at arbitrary offsets, a sanitized font can legitimately leave gaps between written table regions, and the buffer’s logical length can exceed the sum of bytes actually written. The violated invariant is that every byte of the stream buffer that becomes part of the emitted font must have a defined value; instead, unwritten gaps and any tail slack retained stale heap contents left over from previous allocations.
The fix restores the invariant by std::memset-ing the newly allocated region to zero — the whole initial block in the constructor and only the newly added [mLength, newLength) span on each grow — so any byte not subsequently overwritten by real font data reads back as zero. This closes the leak while avoiding redundant re-zeroing of already-initialized bytes on each growth.
Attack Path
- Craft a sparse font An attacker builds a downloadable web font whose sanitized layout leaves gaps or trailing slack in the output stream that OTS never writes over.
- Trigger sanitization
A victim page loads the font via
@font-face, driving OTS to serialize the rewritten font into agfxOTSExpandingMemoryStream. - Capture stale heap bytes The gap and tail regions retain uninitialized heap memory from the process, which becomes embedded in the emitted sanitized font blob.
- Observe the leaked bytes The attacker recovers those bytes through an observable channel derived from the font data (for example rendered glyph metrics or shaping behavior driven by the leaked table content).
Impact Assessment
Files Changed
gfx/thebes/gfxOTSUtils.h
Audit Directions
- Grow-without-zero allocatorsAudit every use of
mAlloc.Grow,realloc, and custom bump/expand allocators feeding serialized output for the assumption that new capacity is zeroed. - Seekable output streamsReview
OTSStream/ots::OTSStreamsubclasses and other seekable writers where logical length can outrun written bytes, leaving unwritten gaps in exported buffers. - Sanitizer output boundariesCheck that any buffer whose contents cross a trust boundary (font, image, or media data returned to renderers) is fully initialized before exposure, not merely up to the last write.
Patch
diff --git a/gfx/thebes/gfxOTSUtils.h b/gfx/thebes/gfxOTSUtils.h
index b0bb74339b0..b5ebb8db482 100644
--- a/gfx/thebes/gfxOTSUtils.h
+++ b/gfx/thebes/gfxOTSUtils.h
@@ -29,7 +29,8 @@ class gfxOTSExpandingMemoryStream : public ots::OTSStream {
explicit gfxOTSExpandingMemoryStream(size_t initial,
size_t limit = DEFAULT_LIMIT)
: mLength(initial), mLimit(limit), mOff(0) {
- mPtr = mAlloc.Grow(nullptr, mLength);
+ mPtr = mAlloc.Grow(nullptr, initial);
+ std::memset(mPtr, 0, initial);
}
~gfxOTSExpandingMemoryStream() { mAlloc.Free(mPtr); }
@@ -59,6 +60,7 @@ class gfxOTSExpandingMemoryStream : public ots::OTSStream {
newLength = mLimit;
}
mPtr = mAlloc.Grow(mPtr, newLength);
+ std::memset(static_cast<char*>(mPtr) + mLength, 0, newLength - mLength);
mLength = newLength;
return WriteRaw(data, length);
}