High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionIncorrect boundary conditions in the Graphics component
ComponentGraphics
Bug ClassLogic Error
Tracker2053326
Fix commit5ac3207a2cc1 (firefox) +15/-11
CISA KEVNot listed
Credited5up3rh3i
Disclosed2026-07-21

Background

COLRv1
a color-font table format that describes glyphs as trees of paint tables (gradients, transforms, composites) stored inside the font binary.
`PaintState`
the per-render context in COLRFonts.cpp that holds the parsed COLR header, the total table length (mCOLRLength), and helpers such as COLRv1BaseAddr() for resolving offsets into the table.
Item Variation Store
the COLRv1 sub-structure of delta sets that ApplyVariation reads to adjust numeric values for the active variation-axis coordinates.
Delta set
a packed row of per-region deltas whose location is computed as deltaSets + deltaSetSize * innerIndex, indexed by attacker-controllable values from the font.

Root Cause Analysis

The vulnerable path is ApplyVariation in gfx/thebes/COLRFonts.cpp, which computes deltaData = deltaSets + deltaSetSize * innerIndex and then loops regionIndexCount times dereferencing deltaData without ever confirming the pointer stays within the COLR table. Because innerIndex, regionIndexCount, and wordDeltaCount derive from font-controlled fields, a malformed font can drive deltaData past the end (or, via overflow, before the start) of the mapped table, producing an out-of-bounds read. The invariant that every pointer walked through the variation store must remain inside [COLRv1BaseAddr(), COLRv1BaseAddr() + mCOLRLength) was never enforced at this site.

The fix adds an explicit bounds check that returns the unmodified aValue when deltaData falls below deltaSets or beyond COLRv1BaseAddr() + mCOLRLength - deltaSetSize, guaranteeing the subsequent read loop stays in bounds.

The patch additionally normalizes pointer arithmetic from const char* to const uint8_t* throughout so length math and comparisons are unsigned and consistent.

Key insight
The single mistake was performing attacker-influenced pointer arithmetic into the variation delta store and dereferencing it without validating the result against the table’s real bounds; the fix reinstates that missing boundary check before the read loop begins.

Attack Path

  1. Craft a malicious font An attacker builds a font with a COLRv1 ItemVariationStore whose innerIndex/count fields point a delta set outside the actual table extent.
  2. Deliver via web content The font is served to a victim through a web page (@font-face) or other document that triggers COLRv1 rendering.
  3. Reach `ApplyVariation` Rendering a variable color glyph invokes ApplyVariation, which computes deltaData beyond the table bounds.
  4. Out-of-bounds read The unguarded loop dereferences deltaData across regionIndexCount iterations, reading memory past the COLR table.
  5. Leak or misbehavior The read values feed back into glyph variation, potentially disclosing adjacent heap contents or destabilizing rendering.

Impact Assessment

An attacker gains an out-of-bounds read against memory adjacent to the COLR table, driven entirely by data in an untrusted font. This occurs in the process performing font rendering when a victim loads attacker-controlled font data (for example via a web page), requiring only that the target parse and render a crafted COLRv1 font. Severity is rated high per the metadata, consistent with a boundary-condition read reachable from web content.

Changed Functions

FunctionChangeNotes
for
gfx/thebes/COLRFonts.cpp
modified

Files Changed

  • gfx/thebes/COLRFonts.cpp

Audit Directions

  • Unchecked scaled pointer arithmetic
    Flag any base + size * index computed from font/file fields that is dereferenced before comparing against both the lower (base) and upper (base + length) bounds.
  • Signed byte-pointer math
    Look for const char* used for length and offset arithmetic on binary tables, where signedness can mask overflow or negative offsets; prefer const uint8_t*.
  • Variation-store indices
    Audit other COLRv1/OpenType parsers where innerIndex, regionIndexCount, or delta-set sizes come from the font and drive loop bounds or pointer walks.
diff --git a/gfx/thebes/COLRFonts.cpp b/gfx/thebes/COLRFonts.cpp
index 4421ee44313..2e8797a5da6 100644
--- a/gfx/thebes/COLRFonts.cpp
+++ b/gfx/thebes/COLRFonts.cpp
@@ -158,8 +158,8 @@ struct PaintState {
   uint16_t mCoordCount;
   nsTArray<uint32_t>* mVisited;
 
-  const char* COLRv1BaseAddr() const {
-    return reinterpret_cast<const char*>(mHeader.v1);
+  const uint8_t* COLRv1BaseAddr() const {
+    return reinterpret_cast<const uint8_t*>(mHeader.v1);
   }
 
   DeviceColor GetColor(uint16_t aPaletteIndex, float aAlpha) const;
@@ -405,6 +405,10 @@ static int32_t ApplyVariation(const PaintState& aState, int32_t aValue,
   uint32_t deltaSetSize = (regionIndexCount + wordDeltaCount) << longWords;
   const uint8_t* deltaData =
       reinterpret_cast<const uint8_t*>(deltaSets) + deltaSetSize * innerIndex;
+  if (deltaData < reinterpret_cast<const uint8_t*>(deltaSets) ||
+      deltaData > aState.COLRv1BaseAddr() + aState.mCOLRLength - deltaSetSize) {
+    return aValue;
+  }
   uint16_t deltaSize = longWords ? 4 : 2;
   int32_t result = aValue;
   for (uint16_t i = 0; i < regionIndexCount; ++i, deltaData += deltaSize) {
@@ -643,7 +647,7 @@ struct ColorLineT {
       return;
     }
     const auto* stop = colorStops();
-    if (reinterpret_cast<const char*>(stop) + count * sizeof(T) >
+    if (reinterpret_cast<const uint8_t*>(stop) + count * sizeof(T) >
         aState.COLRv1BaseAddr() + aState.mCOLRLength) {
       return;
     }
@@ -1960,9 +1964,9 @@ static bool DispatchPaint(const PaintState& aState, uint32_t aDepth,
     return false;
   }
 
-  const char* paint = aState.COLRv1BaseAddr() + aOffset;
+  const uint8_t* paint = aState.COLRv1BaseAddr() + aOffset;
   // All paint table formats start with an 8-bit 'format' field.
-  uint8_t format = uint8_t(*paint);
+  uint8_t format = *paint;
 
 #define DO_CASE(T)                                                            \
   case T::kFormat:                                                            \
@@ -2007,9 +2011,9 @@ static UniquePtr<Pattern> DispatchMakePattern(const PaintState& aState,
     return nullptr;
   }
 
-  const char* paint = aState.COLRv1BaseAddr() + aOffset;
+  const uint8_t* paint = aState.COLRv1BaseAddr() + aOffset;
   // All paint table formats start with an 8-bit 'format' field.
-  uint8_t format = uint8_t(*paint);
+  uint8_t format = *paint;
 
 #define DO_CASE(T)                                                       \
   case T::kFormat:                                                       \
@@ -2037,9 +2041,9 @@ static Matrix DispatchGetMatrix(const PaintState& aState, uint32_t aOffset) {
     return Matrix();
   }
 
-  const char* paint = aState.COLRv1BaseAddr() + aOffset;
+  const uint8_t* paint = aState.COLRv1BaseAddr() + aOffset;
   // All paint table formats start with an 8-bit 'format' field.
-  uint8_t format = uint8_t(*paint);
+  uint8_t format = *paint;
 
 #define DO_CASE(T)                                                             \
   case T::kFormat:                                                             \
@@ -2077,9 +2081,9 @@ static Rect DispatchGetBounds(const PaintState& aState, uint32_t aDepth,
     return Rect();
   }
 
-  const char* paint = aState.COLRv1BaseAddr() + aOffset;
+  const uint8_t* paint = aState.COLRv1BaseAddr() + aOffset;
   // All paint table formats start with an 8-bit 'format' field.
-  uint8_t format = uint8_t(*paint);
+  uint8_t format = *paint;
 
 #define DO_CASE(T)                                                   \
   case T::kFormat:                                                   \
Loading diff…