CVE-2026-117978
Overview
Background
- `DrawTargetCairo`
- the Cairo-backed
DrawTargetingfx/2d. It must be fed Cairo-compatible patterns; gradient patterns carry gradient stops that are backend-specific. - `GradientStops` / `mStops`
- a
LinearGradientPattern/RadialGradientPattern/ConicGradientPatternholdsmStops, aGradientStopsobject whoseGetBackendType()must match the target backend (hereBackendType::CAIRO). Using stops created for a different backend with Cairo is a type/backend mismatch. - `PatternIsCompatible`
- predicate that decides whether a pattern can be used with Cairo; for gradients it inspected
pattern.mStops->GetBackendType(). - `GfxPatternToCairoPattern`
- converts a gfx
Patterninto acairo_pattern_t*; it was documented as “Never returns nullptr” and assumed its input was already Cairo-compatible. - `gfxPattern::GetPattern`
- higher-level helper that lazily builds
mStopsfrom a storedmStopsListfor a givenDrawTarget.
Root Cause Analysis
Two linked defects are fixed. First, gfxPattern::GetPattern created gradient stops only when mStops was null: if (!mStops && !mStopsList.IsEmpty()). If mStops already existed but had been created for a different backend than the current aTarget (e.g. reused across draw targets), it was not recreated, so a GradientStops of the wrong backend was handed downstream to a Cairo target — a backend/type mismatch. Second, GfxPatternToCairoPattern assumed its pattern was always Cairo-compatible (“Never returns nullptr. As such, you must always pass in Cairo-compatible patterns”), and the compatibility guard PatternIsCompatible itself dereferenced pattern.mStops->GetBackendType() without checking mStops for null. So a gradient pattern with a null or wrong-backend mStops could either null-deref in PatternIsCompatible or, worse, be converted by GfxPatternToCairoPattern into a Cairo pattern using stops of an incompatible backend.
The patch hardens all three. PatternIsCompatible now short-circuits with pattern.mStops && pattern.mStops->GetBackendType() == BackendType::CAIRO for each gradient type (null-safe, and treats null stops as incompatible). GfxPatternToCairoPattern now calls if (!PatternIsCompatible(aPattern)) return nullptr; up front and is documented to return nullptr for incompatible patterns, so the conversion refuses mismatched stops instead of using them; the redundant per-call PatternIsCompatible early-returns in DrawTargetCairo::DrawPattern and MaskSurface are removed in favor of this centralized gate. Finally, gfxPattern::GetPattern recreates stops when the backend does not match: if (!mStopsList.IsEmpty() && (!mStops || mStops->GetBackendType() != aTarget->GetBackendType())), ensuring mStops always matches the target backend. The violated invariant was “gradient stops passed to the Cairo backend are non-null and Cairo-typed”; the advisory’s generic “incorrect boundary conditions” maps, per the diff, to a backend type-confusion plus a null-pointer gap.
mStops cached for one backend could be reused with a Cairo DrawTarget (or be null), and the Cairo conversion path assumed its input was always compatible — so it would consume stops of the wrong backend. The fix makes the compatibility check null-safe, forces GfxPatternToCairoPattern to reject incompatible patterns (return nullptr), and regenerates mStops whenever the backend type differs from the target.Attack Path
- Create a gradient
Content uses canvas/CSS gradients that produce a gfx gradient
Patternwith stops built frommStopsList. (web-reachability inferred; the diff is backend graphics code.) - Reuse across backends
Cause the same
gfxPatternto be drawn to a Cairo target after itsmStopswere created for a different backend (or leavemStopsnull), so the cached stops no longer match the target. - Mismatched conversion
The old
GetPatternkeeps the stalemStops;GfxPatternToCairoPattern(which “never returns nullptr”) builds acairo_pattern_t*using the wrong-backend/nullstops. - Invalid use in Cairo
Cairo draws with gradient-stop data of an incompatible type, dereferencing/interpreting memory under false type assumptions (or null-dereferencing in
PatternIsCompatible). - Primitive Backend type-confusion over gradient-stop objects in the graphics code (with a null-deref variant), leading to memory misinterpretation/corruption. (escalation inferred)
Impact Assessment
mStops to be null or of a non-Cairo backend when drawn to a Cairo target — a content-influenceable condition (cross-backend reuse / lazy-stop ordering), though the exact triggering sequence is not spelled out by the diff. The diff substantiates a type/backend confusion of gradient-stop objects passed to Cairo, plus a null-pointer dereference gap in PatternIsCompatible; the advisory’s “incorrect boundary conditions” is a vaguer label than what the code shows. Memory corruption is plausible; promotion to code execution is inferred, not demonstrated.Files Changed
gfx/2d/DrawTargetCairo.cppgfx/thebes/gfxPattern.cpp
Audit Directions
- Backend-typed objects cached across DrawTargetsaudit other gfx objects (
SourceSurface,FilterNode,GradientStops) reused acrossDrawTargets for missingGetBackendType()revalidation when the target changes. - "Never returns nullptr" contractsreview helpers documented as always succeeding that dereference optional members (
mStops, etc.) without null checks; several such assumptions proved unsafe here. - Other backends' pattern conversioncheck the Skia/D2D/WebRender equivalents of
GfxPatternToCairoPatternfor the same “assume compatible” shortcut and null-stop handling. - Lazy member initialization keyed only on nullgrep for
if (!mX && ...)patterns that rebuild cached state only when null, missing the “exists but wrong parameters” case.
Patch
diff --git a/gfx/2d/DrawTargetCairo.cpp b/gfx/2d/DrawTargetCairo.cpp
index e419e6a37e1..121b0f0f6ef 100644
--- a/gfx/2d/DrawTargetCairo.cpp
+++ b/gfx/2d/DrawTargetCairo.cpp
@@ -167,17 +167,20 @@ static bool PatternIsCompatible(const Pattern& aPattern) {
case PatternType::LINEAR_GRADIENT: {
const LinearGradientPattern& pattern =
static_cast<const LinearGradientPattern&>(aPattern);
- return pattern.mStops->GetBackendType() == BackendType::CAIRO;
+ return pattern.mStops &&
+ pattern.mStops->GetBackendType() == BackendType::CAIRO;
}
case PatternType::RADIAL_GRADIENT: {
const RadialGradientPattern& pattern =
static_cast<const RadialGradientPattern&>(aPattern);
- return pattern.mStops->GetBackendType() == BackendType::CAIRO;
+ return pattern.mStops &&
+ pattern.mStops->GetBackendType() == BackendType::CAIRO;
}
case PatternType::CONIC_GRADIENT: {
const ConicGradientPattern& pattern =
static_cast<const ConicGradientPattern&>(aPattern);
- return pattern.mStops->GetBackendType() == BackendType::CAIRO;
+ return pattern.mStops &&
+ pattern.mStops->GetBackendType() == BackendType::CAIRO;
}
default:
return true;
@@ -445,8 +448,7 @@ static inline void CairoPatternAddGradientStop(cairo_pattern_t* aPattern,
aStop.color.a);
}
-// Never returns nullptr. As such, you must always pass in Cairo-compatible
-// patterns, most notably gradients with a GradientStopCairo.
+// Returns nullptr for patterns with incompatible Cairo gradient stops.
// The pattern returned must have cairo_pattern_destroy() called on it by the
// caller.
// As the cairo_pattern_t returned may depend on the Pattern passed in, the
@@ -455,6 +457,10 @@ static inline void CairoPatternAddGradientStop(cairo_pattern_t* aPattern,
static cairo_pattern_t* GfxPatternToCairoPattern(const Pattern& aPattern,
Float aAlpha,
const Matrix& aTransform) {
+ if (!PatternIsCompatible(aPattern)) {
+ return nullptr;
+ }
+
cairo_pattern_t* pat;
const Matrix* matrix = nullptr;
@@ -1046,10 +1052,6 @@ void DrawTargetCairo::DrawPattern(const Pattern& aPattern,
return;
}
- if (!PatternIsCompatible(aPattern)) {
- return;
- }
-
AutoClearDeviceOffset clear(aPattern);
cairo_pattern_t* pat =
@@ -1592,10 +1594,6 @@ void DrawTargetCairo::MaskSurface(const Pattern& aSource, SourceSurface* aMask,
AutoClearDeviceOffset clearSource(aSource);
AutoClearDeviceOffset clearMask(aMask);
- if (!PatternIsCompatible(aSource)) {
- return;
- }
-
cairo_set_antialias(mContext,
GfxAntialiasToCairoAntialias(aOptions.mAntialiasMode));
diff --git a/gfx/thebes/gfxPattern.cpp b/gfx/thebes/gfxPattern.cpp
index 20201ab780c..1359828caa5 100644
--- a/gfx/thebes/gfxPattern.cpp
+++ b/gfx/thebes/gfxPattern.cpp
@@ -113,7 +113,9 @@ Pattern* gfxPattern::GetPattern(const DrawTarget* aTarget,
}
patternToUser.NudgeToIntegers();
- if (!mStops && !mStopsList.IsEmpty()) {
+ // Ensure stops are compatible with the DT.
+ if (!mStopsList.IsEmpty() &&
+ (!mStops || mStops->GetBackendType() != aTarget->GetBackendType())) {
mStops = aTarget->CreateGradientStops(mStopsList.Elements(),
mStopsList.Length(), mExtend);
}