Moderate firefox Type Confusion 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionIncorrect boundary conditions in the Graphics component
ComponentGraphics
Bug ClassType Confusion
Tracker2077730
Fix commitc877c0614d63 (firefox) +14/-14
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

`DrawTargetCairo`
the Cairo-backed DrawTarget in gfx/2d. It must be fed Cairo-compatible patterns; gradient patterns carry gradient stops that are backend-specific.
`GradientStops` / `mStops`
a LinearGradientPattern/RadialGradientPattern/ConicGradientPattern holds mStops, a GradientStops object whose GetBackendType() must match the target backend (here BackendType::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 Pattern into a cairo_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 mStops from a stored mStopsList for a given DrawTarget.

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.

Key insight
Gradient 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

  1. Create a gradient Content uses canvas/CSS gradients that produce a gfx gradient Pattern with stops built from mStopsList. (web-reachability inferred; the diff is backend graphics code.)
  2. Reuse across backends Cause the same gfxPattern to be drawn to a Cairo target after its mStops were created for a different backend (or leave mStops null), so the cached stops no longer match the target.
  3. Mismatched conversion The old GetPattern keeps the stale mStops; GfxPatternToCairoPattern (which “never returns nullptr”) builds a cairo_pattern_t* using the wrong-backend/null stops.
  4. 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).
  5. 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

The code is in the 2D graphics/Cairo rendering path, reachable by content that produces gradient patterns. The precondition is getting a gradient’s 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.cpp
  • gfx/thebes/gfxPattern.cpp

Audit Directions

  • Backend-typed objects cached across DrawTargets
    audit other gfx objects (SourceSurface, FilterNode, GradientStops) reused across DrawTargets for missing GetBackendType() revalidation when the target changes.
  • "Never returns nullptr" contracts
    review helpers documented as always succeeding that dereference optional members (mStops, etc.) without null checks; several such assumptions proved unsafe here.
  • Other backends' pattern conversion
    check the Skia/D2D/WebRender equivalents of GfxPatternToCairoPattern for the same “assume compatible” shortcut and null-stop handling.
  • Lazy member initialization keyed only on null
    grep for if (!mX && ...) patterns that rebuild cached state only when null, missing the “exists but wrong parameters” case.
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);
   }
Loading diff…