Moderate firefox OOB 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionSandbox escape due to incorrect boundary conditions in the Graphics: WebRender component
ComponentGraphics
Bug ClassOOB
Tracker2079501
Fix commit8e4d96b50cec (firefox) +7/-1
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

`wr::ImageDescriptor`
Describes an image given to WebRender: size (width/height), stride (bytes per row), and format. It is sent over IPC from the content process to the GPU/compositor process.
`ParamTraits<ImageDescriptor>` (`WebRenderMessageUtils.h`)
The IPC (de)serializer for ImageDescriptor. On read it validates the untrusted descriptor; returning {} (empty Maybe) rejects the message.
Stride/width/bpp invariant
For a given stride, a row must actually hold width pixels: stride / bpp >= width (with bpp = BytesPerPixel(format)), otherwise consumers would read past the row’s backing bytes.
`image_templates` / `set_image_visible_rect` (`resource_cache.rs`)
WebRender’s resource cache stores image templates with a validated descriptor (including size). set_image_visible_rect updates an image’s visible_rect.
Blob images
Images rendered on demand; their size legitimately follows the visible rect. Other image templates (raw/buffer) have a fixed size validated against their data and stride.

Root Cause Analysis

Two related boundary defects let an image’s effective size be trusted without matching its validated backing. First, in ParamTraits<mozilla::wr::ImageDescriptor>::Read (gfx/layers/wr/WebRenderMessageUtils.h) the stride check was if (bpp <= 0 || stride / bpp < width). When width <= 0 (e.g. a negative or zero width from a compromised/buggy content process), stride / bpp < width is false, so a non-positive width passed validation while a non-zero stride was present. The patch adds width <= 0 to the rejection condition, so a descriptor that specifies a stride but a non-positive width is now rejected.

Second, ResourceCache::set_image_visible_rect (gfx/wr/webrender/src/resource_cache.rs) unconditionally overwrote image.descriptor.size = rect.size() (and image.visible_rect) for any image template. For non-blob images the size was already validated against the image’s data and stride, so letting the visible rect redefine size desynchronizes the descriptor from the bytes that back it. The patch returns early unless image.data.is_blob(), so only blob images — which are legitimately sized by their visible rect — have their size updated; raw/buffer templates keep the size their data and stride were validated against. The advisory’s “incorrect boundary conditions” matches the diff: both changes close paths where an image’s declared geometry could exceed/diverge from its validated backing, with the out-of-bounds read/write in the GPU process being the inferred security sink.

Key insight
An image’s effective size/width could be set to a value inconsistent with the stride/data it was validated against — either by a non-positive width slipping past IPC deserialization, or by set_image_visible_rect resizing a non-blob template to its visible rect — so downstream consumers computed buffer extents against a size the backing memory does not satisfy.

Attack Path

  1. Compromised/buggy content process The attacker controls the content-process side that sends ImageDescriptors and visible-rect updates to WebRender in the GPU/compositor process. (threat model inferred)
  2. Bad descriptor Send an ImageDescriptor with a non-zero stride but width <= 0; pre-patch ParamTraits::Read accepts it, admitting an image whose geometry does not match its stride-validated backing.
  3. Or resize via visible rect For a non-blob image template, call the path that invokes set_image_visible_rect with a rect larger than the template’s validated size; pre-patch this overwrites descriptor.size.
  4. Geometry/backing mismatch WebRender now holds an image whose declared size/width exceeds or diverges from the bytes it was validated to contain.
  5. Primitive A consumer that computes read/write extents from the (now-inflated/invalid) size indexes outside the image’s backing buffer: out-of-bounds access in the GPU process. (sink inferred)

Impact Assessment

Both code paths run in the GPU/compositor process and consume data originating from the content process, so the relevant privilege is a compromised or misbehaving renderer (the advisory frames it as a sandbox escape). The diff proves two boundary-validation fixes that stop an image’s declared geometry from diverging from its validated backing; the concrete out-of-bounds read/write that this enables in a downstream consumer is the inferred sink — the diff shows the invariant restoration, not the faulting access. Rated moderate.

Changed Functions

FunctionChangeNotes
if
gfx/layers/wr/WebRenderMessageUtils.h
modified

Files Changed

  • gfx/layers/wr/WebRenderMessageUtils.h
  • gfx/wr/webrender/src/resource_cache.rs

Audit Directions

  • IPC descriptor validators
    Audit other ParamTraits readers for gfx/WebRender types that check an upper bound (stride/bpp < width) without also rejecting non-positive dimensions, which can invert comparisons.
  • Size vs. backing coherence
    Review every writer of image.descriptor.size / visible_rect and other geometry fields for assumptions that the new size still matches the validated data and stride.
  • Blob vs. non-blob branching
    Search for resource-cache operations that treat blob and non-blob templates uniformly where only blob images may be dynamically resized.
diff --git a/gfx/layers/wr/WebRenderMessageUtils.h b/gfx/layers/wr/WebRenderMessageUtils.h
index a3360dd1fea..3becfd81064 100644
--- a/gfx/layers/wr/WebRenderMessageUtils.h
+++ b/gfx/layers/wr/WebRenderMessageUtils.h
@@ -59,7 +59,8 @@ struct ParamTraits<mozilla::wr::ImageDescriptor> {
     if (stride != 0) {
       int bpp = mozilla::gfx::BytesPerPixel(
           mozilla::wr::ImageFormatToSurfaceFormat(format));
-      if (bpp <= 0 || stride / bpp < width) {
+      // If stride specified, width must be > 0.
+      if (bpp <= 0 || width <= 0 || stride / bpp < width) {
         return {};
       }
     }
diff --git a/gfx/wr/webrender/src/resource_cache.rs b/gfx/wr/webrender/src/resource_cache.rs
index 0efa7b917f8..bdbda9b3a91 100644
--- a/gfx/wr/webrender/src/resource_cache.rs
+++ b/gfx/wr/webrender/src/resource_cache.rs
@@ -1283,6 +1283,11 @@ impl ResourceCache {
 
     fn set_image_visible_rect(&mut self, key: ImageKey, rect: &DeviceIntRect) {
         if let Some(image) = self.resources.image_templates.get_mut(key) {
+            // Only blob images are sized by their visible rect; other templates must
+            // keep the size that their data and stride were validated against.
+            if !image.data.is_blob() {
+                return;
+            }
             image.visible_rect = *rect;
             image.descriptor.size = rect.size();
         }
Loading diff…