CVE-2026-16357
Overview
Background
- COLRv1
- a color-font table format that describes glyphs as trees of
painttables (gradients, transforms, composites) stored inside the font binary. - `PaintState`
- the per-render context in
COLRFonts.cppthat holds the parsed COLR header, the total table length (mCOLRLength), and helpers such asCOLRv1BaseAddr()for resolving offsets into the table. - Item Variation Store
- the COLRv1 sub-structure of delta sets that
ApplyVariationreads 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.
Attack Path
- Craft a malicious font
An attacker builds a font with a COLRv1
ItemVariationStorewhoseinnerIndex/count fields point a delta set outside the actual table extent. - Deliver via web content
The font is served to a victim through a web page (
@font-face) or other document that triggers COLRv1 rendering. - Reach `ApplyVariation`
Rendering a variable color glyph invokes
ApplyVariation, which computesdeltaDatabeyond the table bounds. - Out-of-bounds read
The unguarded loop dereferences
deltaDataacrossregionIndexCountiterations, reading memory past the COLR table. - Leak or misbehavior The read values feed back into glyph variation, potentially disclosing adjacent heap contents or destabilizing rendering.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forgfx/thebes/COLRFonts.cpp |
modified |
Files Changed
gfx/thebes/COLRFonts.cpp
Audit Directions
- Unchecked scaled pointer arithmeticFlag any
base + size * indexcomputed from font/file fields that is dereferenced before comparing against both the lower (base) and upper (base + length) bounds. - Signed byte-pointer mathLook for
const char*used for length and offset arithmetic on binary tables, where signedness can mask overflow or negative offsets; preferconst uint8_t*. - Variation-store indicesAudit other COLRv1/OpenType parsers where
innerIndex,regionIndexCount, or delta-set sizes come from the font and drive loop bounds or pointer walks.
Patch
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: \