CVE-2026-117993
Overview
Background
- `gfxTextRun`
- A graphics/text object holding the shaped glyphs for a run of text. Its storage is a single allocation containing a fixed-size header/base (
aSize) followed by a per-character glyph array. - `CompressedGlyph`
- The fixed-size per-character glyph record. A text run of
aLengthcharacters needsaLength * sizeof(CompressedGlyph)bytes of glyph storage. - `AllocateStorageForTextRun(size_t aSize, uint32_t aLength)`
- Allocates
aSize + aLength * sizeof(CompressedGlyph)bytes withmalloc, returningnullptron failure (rather than throwing) because “web content can create huge runs.” - `CheckedInt<T>`
- Mozilla’s overflow-checked integer type (
mozilla/CheckedInt.h); arithmetic sets an invalid flag on overflow, checked viaisValid(). - `aLength` is `uint32_t`
- Attacker-influenced (text length); multiplying it by
sizeof(CompressedGlyph)and addingaSizecan overflow thesize_tbyte count.
Root Cause Analysis
gfxTextRun::AllocateStorageForTextRun in gfx/thebes/gfxTextRun.cpp computed its allocation size as aSize + aLength * sizeof(CompressedGlyph) directly in native integer arithmetic. Because aLength is a uint32_t derived from (attacker-controllable) text length and sizeof(CompressedGlyph) is a multi-byte constant, the product aLength * sizeof(CompressedGlyph) (and its sum with aSize) can overflow size_t, wrapping to a small value. malloc then returns a buffer far smaller than the run actually requires, while the caller proceeds to populate glyph storage for all aLength characters — a classic undersized-allocation leading to heap out-of-bounds writes. The companion memset that zero-initializes the glyph region compounded this by using the same unchecked aLength * sizeof(CompressedGlyph) length.
The patch introduces CheckedInt<size_t> storageSize = CheckedInt<size_t>(aLength) * sizeof(CompressedGlyph) + aSize; and allocates only if storageSize.isValid() (otherwise malloc is skipped and nullptr is returned, matching the existing graceful-failure contract). The subsequent memset is rewritten to use storageSize.value() - aSize, so the zeroed length is derived from the same validated total rather than recomputed with the overflow-prone expression. The advisory’s “integer overflow in Graphics: Text” matches the diff exactly.
aSize + aLength * sizeof(CompressedGlyph) without overflow checking; a large aLength wraps the size_t byte count, so malloc returns an undersized buffer that the glyph-filling code then overruns.Attack Path
- Huge text run
Web content creates a text run with a very large character length, driving
aLengthtoward values whereaLength * sizeof(CompressedGlyph) (+ aSize)overflowssize_t. (reachability via layout/text APIs inferred) - Overflowed size Pre-patch, the native multiply/add wraps to a small value.
- Undersized allocation
mallocsucceeds and returns a buffer much smaller than the run needs; the non-null result passes theif (!storage)check. - Heap overflow
Code that initializes/fills
aLengthglyphs (and the originalmemsetofaLength * sizeof(CompressedGlyph)) writes past the small allocation. - Primitive Controlled heap out-of-bounds write adjacent to the undersized text-run buffer; with grooming this is a memory-corruption primitive rather than a mere crash. (exploit strength inferred)
Impact Assessment
memset) writing beyond it — a heap out-of-bounds write. Practical exploitation is gated on allocating a run large enough to wrap size_t and on heap layout. The advisory rates it moderate and labels it an integer overflow, consistent with the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgfx/thebes/gfxTextRun.cpp |
modified |
Files Changed
gfx/thebes/gfxTextRun.cpp
Audit Directions
- Unchecked size arithmetic before `malloc`Audit gfx/thebes and other text/layout allocators for
malloc/reallocsizes computed ascount * elementSize (+ header)from web-controllable counts withoutCheckedInt. - `memset`/`memcpy` lengths recomputed from the same inputsLook for buffer-fill operations whose length is recomputed with the overflow-prone expression rather than derived from the validated allocation size.
- Other `CompressedGlyph`/glyph-array consumersReview code that indexes or fills glyph storage by
aLengthfor assumptions that the backing allocation actually holdsaLengthrecords.
Patch
diff --git a/gfx/thebes/gfxTextRun.cpp b/gfx/thebes/gfxTextRun.cpp
index 39153aa9bd5..c69528870c1 100644
--- a/gfx/thebes/gfxTextRun.cpp
+++ b/gfx/thebes/gfxTextRun.cpp
@@ -14,6 +14,7 @@
#include "gfxHarfBuzzShaper.h"
#include "gfxPlatformFontList.h"
#include "gfxUserFontSet.h"
+#include "mozilla/CheckedInt.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Likely.h"
#include "mozilla/MruCache.h"
@@ -128,7 +129,9 @@ bool gfxTextRun::NeedsGlyphExtents() const {
void* gfxTextRun::AllocateStorageForTextRun(size_t aSize, uint32_t aLength) {
// Allocate the storage we need, returning nullptr on failure rather than
// throwing an exception (because web content can create huge runs).
- void* storage = malloc(aSize + aLength * sizeof(CompressedGlyph));
+ CheckedInt<size_t> storageSize =
+ CheckedInt<size_t>(aLength) * sizeof(CompressedGlyph) + aSize;
+ void* storage = storageSize.isValid() ? malloc(storageSize.value()) : nullptr;
if (!storage) {
NS_WARNING("failed to allocate storage for text run!");
return nullptr;
@@ -136,7 +139,7 @@ void* gfxTextRun::AllocateStorageForTextRun(size_t aSize, uint32_t aLength) {
// Initialize the glyph storage (beyond aSize) to zero
memset(reinterpret_cast<char*>(storage) + aSize, 0,
- aLength * sizeof(CompressedGlyph));
+ storageSize.value() - aSize);
return storage;
}