CVE-2026-118023
Overview
Background
- WebRender `prepare.rs`
- The frame-preparation stage of WebRender (gfx/wr/webrender), which turns primitives into GPU draw commands. Runs in the GPU/compositor process.
- YUV image primitive
- A multi-plane image (e.g. 2–3 planes for Y/Cb/Cr). Each plane is an image resource referenced by a key in
yuv_image_data.yuv_key. - `ImageBufferKind` / sampler kind
- Describes how a plane’s texture is sampled: e.g.
Texture2D(texture-cache backed: raw/blob/buffer) vs. an external-texture kind (ExternalImageType::TextureHandle(kind), e.g. a hardware/external image). The sampler kind selects whichps_quad_yuvshader variant is used. - `yuv_planes_sampler_kind`
- Helper that computes the single sampler kind for a YUV image’s planes, used to pick the shader variant and fill
sampler_kindin the quad descriptor. - Shader-variant/sampler contract
- A given
ps_quad_yuvvariant binds and samples all planes through one sampler type; the planes must therefore agree on their sampler kind.
Root Cause Analysis
yuv_planes_sampler_kind in gfx/wr/webrender/src/prepare.rs returned a single ImageBufferKind for the whole YUV image but did not verify that all planes actually shared that kind. The old loop returned the kind of the first plane whose image properties carried an ExternalImageType::TextureHandle(kind), and otherwise fell through to ImageBufferKind::Texture2D. If the planes had mismatched sampler kinds (e.g. one external TextureHandle, another texture-cache Texture2D), the function still produced one kind and prepare_prim_for_render proceeded to draw: it selected a ps_quad_yuv shader variant for that one kind and sampled every plane through that single sampler type. Sampling a plane whose real backing/texture type does not match the bound sampler is an invalid GPU operation against mismatched external-image bindings.
The patch changes yuv_planes_sampler_kind to return Option<ImageBufferKind>: it now evaluates each plane’s kind (external TextureHandle kind, else Texture2D) and, via sampler_kind.get_or_insert(kind), returns None the moment any plane disagrees. prepare_prim_for_render matches on the result and returns early (skips drawing) when it is None, so a YUV image whose planes do not all share one sampler kind is never drawn. Note the advisory labels this a “sandbox escape due to use-after-free”; the diff substantiates a sampler-kind consistency bug — planes sampled through the wrong sampler/shader variant — rather than an explicit freed-memory access. The step from “external texture-handle plane bound/sampled as the wrong type” to a memory-safety/UAF primitive in the GPU process is the inferred escalation, consistent with the advisory, but is not shown by the diff.
yuv_planes_sampler_kind assumed all planes of a YUV image shared one sampler kind and returned the first plane’s kind unchecked, so a YUV image with mismatched plane kinds was drawn through a single shader variant that samples every plane — including external texture handles — through the wrong sampler type.Attack Path
- Craft a YUV image
Web content constructs a YUV image primitive whose planes resolve to different image kinds (e.g. one plane an external
TextureHandle, another a texture-cacheTexture2D). (reachability inferred) - Reach non-composited path
Arrange for the non-composited “draw directly through the quad path” branch of
prepare_prim_for_render, which callsyuv_planes_sampler_kind. - Mis-selected variant
Pre-patch, the helper returns one kind despite the mismatch; a single
ps_quad_yuvvariant is chosen for all planes. - Wrong-sampler draw The GPU samples each plane through the one sampler type of that variant, including an external texture-handle plane bound as the wrong type.
- Primitive Invalid cross-type texture sampling in the GPU process; the advisory’s use-after-free of an external-image resource is the inferred memory-safety consequence. (inferred)
Impact Assessment
Files Changed
gfx/wr/webrender/src/prepare.rs
Audit Directions
- Per-plane property assumptionsAudit other WebRender multi-plane/multi-resource paths that derive one property (format, sampler kind, image type) from a single plane and apply it to all.
- External vs. texture-cache samplingReview shader-variant selection wherever
ExternalImageType::TextureHandleandTexture2D-backed resources can coexist, ensuring the bound sampler matches each sampled texture’s real type. - "Cannot be drawn" early returnsCheck that other preparation helpers which can legitimately fail to produce a draw signal it (via
Option/early return) rather than falling back to a default that forces an invalid draw.
Patch
diff --git a/gfx/wr/webrender/src/prepare.rs b/gfx/wr/webrender/src/prepare.rs
index 813885c68ae..a42c65235f9 100644
--- a/gfx/wr/webrender/src/prepare.rs
+++ b/gfx/wr/webrender/src/prepare.rs
@@ -236,21 +236,26 @@ fn prepare_primitives(
}
/// Returns the texture sampler kind used by a YUV image's planes, which selects
-/// the matching ps_quad_yuv shader variant. All planes are expected to share the
-/// same kind. Texture-cache backed images (raw/blob/buffer) are always Texture2D.
+/// the matching ps_quad_yuv shader variant. Texture-cache backed images
+/// (raw/blob/buffer) are always Texture2D. Returns None if the planes do not all
+/// share the same kind: every plane is sampled through the one sampler type of
+/// the selected shader variant, so such an image cannot be drawn.
fn yuv_planes_sampler_kind(
yuv_image_data: &crate::prim_store::image::YuvImageData,
resource_cache: &crate::resource_cache::ResourceCache,
-) -> ImageBufferKind {
+) -> Option<ImageBufferKind> {
let plane_count = yuv_image_data.format.get_plane_num();
+ let mut sampler_kind = None;
for key in &yuv_image_data.yuv_key[.. plane_count] {
- if let Some(ExternalImageData { image_type: ExternalImageType::TextureHandle(kind), .. }) =
- resource_cache.get_image_properties(*key).and_then(|props| props.external_image)
- {
- return kind;
+ let kind = match resource_cache.get_image_properties(*key).and_then(|props| props.external_image) {
+ Some(ExternalImageData { image_type: ExternalImageType::TextureHandle(kind), .. }) => kind,
+ _ => ImageBufferKind::Texture2D,
+ };
+ if *sampler_kind.get_or_insert(kind) != kind {
+ return None;
}
}
- ImageBufferKind::Texture2D
+ sampler_kind
}
fn prepare_prim_for_render(
@@ -679,6 +684,11 @@ fn prepare_prim_for_render(
return;
}
+ let sampler_kind = match yuv_planes_sampler_kind(yuv_image_data, frame_state.resource_cache) {
+ Some(sampler_kind) => sampler_kind,
+ None => return,
+ };
+
// Non-composited: draw the YUV image directly through the quad path.
let planes = yuv_image_data.update(
prim_info.compositor_surface_kind.is_composited(),
@@ -690,7 +700,7 @@ fn prepare_prim_for_render(
format: yuv_image_data.format,
color_space: yuv_image_data.color_space.with_range(yuv_image_data.color_range),
channel_bit_depth: yuv_image_data.color_depth.bit_depth(),
- sampler_kind: yuv_planes_sampler_kind(yuv_image_data, frame_state.resource_cache),
+ sampler_kind,
};
quad::prepare_quad(