Moderate firefox OOB 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionIncorrect boundary conditions in the Graphics: WebRender component
ComponentGraphics
Bug ClassOOB
Tracker2077739
Fix commit267ed4181dc6 (firefox) +13/-0
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

SWGL (`gfx/wr/swgl`)
WebRender’s software GL rasterizer. swgl_ext.h contains the optimized texture-sampling/blending inner loops used when compositing without a GPU.
`blendTextureLinearUpscale`
a fast path that linearly filters (upscales) a texture while blending it into the output buf. It computes integer sample coordinates from a floating UV, then reads source texels directly from sampler->buf via per-row pointers.
`sampler->width` / `computeRow` / `row0[ixn.x]`
the sampler describes the source texture; row0/row1 are pointers to two adjacent texel rows, and the loop indexes them by ixn.x (the x texel index), loading packed texels with unaligned_load.
`swgl_StepSize`
the number of texels processed per iteration (SIMD width); a valid read needs index <= width - swgl_StepSize.
`blendTextureLinearFallback`
a slower, bounds-safe variant used when the fast path’s assumptions do not hold.

Root Cause Analysis

blendTextureLinearUpscale derived the integer x sample index from the (clamped) UV and then read row0[ixn.x]/row1[ixn.x] without verifying that ixn.x stayed within the source row. Two gaps existed. First, the initial index i.x.x (computed from uv via clamp then >> 7) was not checked against the row bounds before the loop set up row0/row1 and began loading; a UV that mapped outside [0, width - swgl_StepSize] led to out-of-bounds texel reads. Second, inside the per-span loop the code advances the UV each iteration (uv.x += uv_step.x) and recomputes ixn from the accumulated UV, but the accumulated floating-point rounding could push ixn.x past the end of the row — again with no check before unaligned_load(&row0[ixn.x]). The violated invariant is that the texel index used for the direct row0/row1 pointer loads must lie within [0, width - swgl_StepSize].

The patch adds both missing bounds checks and routes violations to the safe fallback. Before the loop it computes int32_t maxX = int32_t(sampler->width) - swgl_StepSize; and, if the initial i.x.x < 0 || i.x.x > maxX, calls blendTextureLinearFallback<BLEND>(...) and returns. Inside the loop, after recomputing ixn from the advanced UV, it checks if (uint32_t(ixn.x) > uint32_t(maxX)) (a single unsigned comparison catching both negative and too-large indices) and likewise defers the remaining span to blendTextureLinearFallback and returns. This guarantees the direct row0[ixn.x]/row1[ixn.x] loads only happen for in-range indices, and otherwise falls back to the clamped-safe path.

Key insight
The upscaling fast path read source texels at an x index derived from UV coordinates — both the initial index and the per-iteration accumulated index — without bounds-checking against the row width, allowing out-of-bounds reads of the source texture. The fix validates the index against width - swgl_StepSize and diverts out-of-range cases to the bounds-safe fallback.

Attack Path

  1. Compose with SWGL upscale Content drives WebRender’s software path to upscale a sampled texture (an image/canvas/video surface) with linear filtering, invoking blendTextureLinearUpscale. (web-reachability inferred; the diff is rasterizer code.)
  2. Craft UV / scale Choose source/destination geometry and scaling so the computed UV maps the x sample index outside [0, width - swgl_StepSize], either at loop entry or via accumulated uv_step rounding across the span.
  3. Out-of-bounds read The fast path loads row0[ixn.x]/row1[ixn.x] with an index past the row, reading adjacent heap memory as texel data.
  4. Observe / corrupt output The out-of-range texels are blended into the composited output, exposing adjacent memory contents through rendered pixels (info leak) and/or causing an invalid read crash.
  5. Primitive Out-of-bounds read in the compositor/GPU process, usable as a cross-origin pixel/memory info-leak and potential crash. (any write/escalation is not supported by the diff)

Impact Assessment

The code runs in the software-rendering (compositor) path, reachable from content that controls the geometry/scaling of composited surfaces. The precondition is arranging a UV mapping whose integer x index falls outside the valid row range — plausible to influence via scaling and surface sizes, though the exact parameters are not shown. The demonstrated primitive is an out-of-bounds read of source-texture rows; the data flows into rendered output, so the realistic impact is an information disclosure (and possible crash/DoS), not memory write or RCE. The advisory’s “incorrect boundary conditions” matches the diff, which is precisely a pair of missing index bounds checks.

Changed Functions

FunctionChangeNotes
if
gfx/wr/swgl/src/swgl_ext.h
modified
for
gfx/wr/swgl/src/swgl_ext.h
modified

Files Changed

  • gfx/wr/swgl/src/swgl_ext.h

Audit Directions

  • Direct `row[index]` / `unaligned_load` in SWGL
    audit the other sampling/blend fast paths in swgl_ext.h (nearest, non-upscale, gradient, compositing variants) for texel-index computations that feed pointer loads without a width - swgl_StepSize bound.
  • Accumulated-UV rounding
    review loops that advance uv.x += uv_step.x and recompute indices; floating-point accumulation can drift past bounds even when the initial index is valid.
  • Fallback coverage
    verify every fast path has a correctly-parameterized *Fallback escape and that remaining-span/pointer arguments passed to it (int(end - buf), {prevX, uv.y}) are consistent, since the fix relies on those.
  • y/row index checks
    the patch bounds the x index; confirm the y/row selection (computeRow, computeNextRowOffset) is independently guaranteed in range.
diff --git a/gfx/wr/swgl/src/swgl_ext.h b/gfx/wr/swgl/src/swgl_ext.h
index d2d00207670..6c6306460b6 100644
--- a/gfx/wr/swgl/src/swgl_ext.h
+++ b/gfx/wr/swgl/src/swgl_ext.h
@@ -213,6 +213,12 @@ static void blendTextureLinearUpscale(S sampler, vec2 uv, int span,
   ivec2 i(clamp(uv, min_uv, max_uv));
   ivec2 frac = i;
   i >>= 7;
+  int32_t maxX = int32_t(sampler->width) - swgl_StepSize;
+  if (i.x.x < 0 || i.x.x > maxX) {
+    blendTextureLinearFallback<BLEND>(sampler, uv, span, uv_step, min_uv,
+                                      max_uv, color, buf);
+    return;
+  }
   P* row0 = (P*)sampler->buf + computeRow(sampler, ivec2_scalar(0, i.y.x));
   P* row1 = row0 + computeNextRowOffset(sampler, ivec2_scalar(0, i.y.x));
   I16 fracx = computeFracX(sampler, i, frac);
@@ -227,10 +233,17 @@ static void blendTextureLinearUpscale(S sampler, vec2 uv, int span,
   // one. However, due to the complication of upscaling, we may not necessarily
   // shift in all the next set of samples.
   for (P* end = buf + span; buf < end; buf += 4) {
+    Float prevX = uv.x;
     uv.x += uv_step.x;
     I32 ixn = cast(uv.x);
     I16 fracn = computeFracNoClamp(ixn);
     ixn >>= 7;
+    // Accumulated UV rounding must not step outside the row.
+    if (uint32_t(ixn.x) > uint32_t(maxX)) {
+      blendTextureLinearFallback<BLEND>(sampler, {prevX, uv.y}, int(end - buf),
+                                        uv_step, min_uv, max_uv, color, buf);
+      return;
+    }
     auto src0n = CONVERT(unaligned_load<packed_type>(&row0[ixn.x]),
                          signed_unpacked_type);
     auto src1n = CONVERT(unaligned_load<packed_type>(&row1[ixn.x]),
Loading diff…