CVE-2026-117979
Overview
Background
- SWGL (`gfx/wr/swgl`)
- WebRender’s software GL rasterizer.
swgl_ext.hcontains 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 fromsampler->bufvia per-row pointers. - `sampler->width` / `computeRow` / `row0[ixn.x]`
- the sampler describes the source texture;
row0/row1are pointers to two adjacent texel rows, and the loop indexes them byixn.x(the x texel index), loading packed texels withunaligned_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.
width - swgl_StepSize and diverts out-of-range cases to the bounds-safe fallback.Attack Path
- 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.) - 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 accumulateduv_steprounding across the span. - 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. - 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.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgfx/wr/swgl/src/swgl_ext.h |
modified | |
forgfx/wr/swgl/src/swgl_ext.h |
modified |
Files Changed
gfx/wr/swgl/src/swgl_ext.h
Audit Directions
- Direct `row[index]` / `unaligned_load` in SWGLaudit 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 awidth - swgl_StepSizebound. - Accumulated-UV roundingreview loops that advance
uv.x += uv_step.xand recompute indices; floating-point accumulation can drift past bounds even when the initial index is valid. - Fallback coverageverify every fast path has a correctly-parameterized
*Fallbackescape 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 checksthe patch bounds the x index; confirm the y/row selection (
computeRow,computeNextRowOffset) is independently guaranteed in range.
Patch
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]),