Chrome · Paint
CVE-2026-11133
Logic Error in Paint
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/paint/paint_layer_painter.cc |
modified | |
promise_testthird_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html |
modified |
Files Changed
third_party/blink/renderer/core/paint/paint_layer_painter.ccthird_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.ccthird_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.hthird_party/blink/renderer/platform/graphics/paint/raster_invalidator.hthird_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html
Patch
From 27dafe4c2314e5c9af29f3a95e8d94aec9e87eb3 Mon Sep 17 00:00:00 2001 From: Philip Rogers <[email protected]> Date: Fri, 24 Apr 2026 17:27:13 -0700 Subject: [PATCH] [html-in-canvas] Support backdrop-filter images This patch adds support for backdrop-filter images in 3 changes: 1. PaintLayerPainter::Paint creates a paint chunk for reference filter effects, similar to regular filters. 2. EffectPaintPropertyNode::HasReferenceFilter has been updated to return true for filter references and backdrop-filter references. 3. RasterInvalidator now expands bounds to cover the backdrop filter bounds to cover cases where the backdrop filter effect has changed. A followup patch will fix the cross-origin issue (see new test). Bug: 501606085 Change-Id: Ibae248a3e5a735e142200b8a63af3556a82e9b8b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7793231 Reviewed-by: Stefan Zager <[email protected]> Auto-Submit: Philip Rogers <[email protected]> Commit-Queue: Philip Rogers <[email protected]> Cr-Commit-Position: refs/heads/main@{#1620574} --- diff --git a/third_party/blink/renderer/core/paint/paint_layer_painter.cc b/third_party/blink/renderer/core/paint/paint_layer_painter.cc index 2f251d9..eb1c45b 100644 --- a/third_party/blink/renderer/core/paint/paint_layer_painter.cc +++ b/third_party/blink/renderer/core/paint/paint_layer_painter.cc @@ -438,6 +438,8 @@ auto* properties = object.FirstFragment().PaintProperties(); ensure_chunk |= properties && properties->Filter() && properties->Filter()->HasReferenceFilter(); + ensure_chunk |= properties && properties->Effect() && + properties->Effect()->HasReferenceFilter(); if (ensure_chunk) { controller.EnsureChunk(); diff --git a/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc b/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc index 83caf3ec..90955dd2 100644 --- a/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc +++ b/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc @@ -755,7 +755,7 @@ current_clip_ = input_clip; current_effect_ = &effect; - if (effect.HasReferenceFilter()) { + if (effect.HasReferenceFilter() && effect.Filter()) { // For empty chunks, or chunks with empty bounds, with a filter applied // that produces output even when there's no input this will expand the // bounds to match. diff --git a/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h b/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h index 79318633..1f58cf7 100644 --- a/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h +++ b/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h @@ -262,8 +262,10 @@ } bool HasReferenceFilter() const { - return state_.filter_info && - state_.filter_info->operations.HasReferenceFilter(); + return (state_.filter_info && + state_.filter_info->operations.HasReferenceFilter()) || + (state_.backdrop_filter_info && + state_.backdrop_filter_info->operations.HasReferenceFilter()); } bool HasFilterThatMovesPixels() const { return state_.filter_info && diff --git a/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.h b/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.h index 9172d16..a6f1b971 100644 --- a/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.h +++ b/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.h @@ -16,6 +16,7 @@ #include "third_party/blink/renderer/platform/heap/garbage_collected.h" #include "third_party/blink/renderer/platform/wtf/vector.h" #include "ui/gfx/geometry/rect.h" +#include "ui/gfx/geometry/rect_conversions.h" #include "ui/gfx/geometry/transform.h" namespace blink { @@ -81,6 +82,15 @@ mapper.MapVisualRect(chunk_it->drawable_bounds))), chunk_to_layer_clip(mapper.ClipRect()), chunk_to_layer_transform(mapper.Transform()) { + if (chunk_it->properties.Effect().Unalias().BackdropFilter()) { + gfx::RectF backdrop_rect = + gfx::SkRectToRectF(chunk_it->properties.Effect() + .Unalias() + .BackdropFilterBounds() + .getBounds()); + bounds_in_layer.Union(invalidator.ClipByLayerBounds( + mapper.MapVisualRect(gfx::ToEnclosingRect(backdrop_rect)))); + } } PaintChunkInfo(const PaintChunkInfo& old_chunk_info, diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html new file mode 100644 index 0000000..28ee93a0 --- /dev/null +++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>drawElementImage does not use cross-origin feimage content via CSS backdrop-filter</title> + <link rel="help" href="https://github.com/WICG/html-in-canvas"> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <style> + #wrapper { + width: 10px; + height: 20px; + background: blue; + } + .child { + width: 10px; + height: 10px; + background: transparent; + } + #child-same { backdrop-filter: url('#filter-same'); } + #child-cross { backdrop-filter: url('#filter-cross'); } + </style> +</head> +<body> + <svg width="0" height="0"> + <filter id="filter-same"> + <feimage href="https://{{location[host]}}/wpt_internal/html/canvas/drawElementImage/resources/green-100x100.png" /> + </filter> + <filter id="filter-cross"> + <feimage href="https://{{hosts[alt][www]}}:{{ports[h2][0]}}/wpt_internal/html/canvas/drawElementImage/resources/red-100x100.png" /> + </filter> + </svg> + + <canvas id=canvas width="10" height="20" layoutsubtree> + <div id="wrapper"> + <div id="child-same" class="child"></div> + <div id="child-cross" class="child"></div> + </div> + </canvas> + + <script> + window.onload = () => { + promise_test(async function(t) { + const preloadImage = (url) => new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => reject(new Error(`Failed to load image: ${url}`)); + img.src = url; + }); + + const sameOriginFeImageEl = document.querySelector('#filter-same feimage'); + const crossOriginFeImageEl = document.querySelector('#filter-cross feimage'); + await Promise.all([ + preloadImage(sameOriginFeImageEl.getAttribute('href')), + preloadImage(crossOriginFeImageEl.getAttribute('href')) + ]); + + await new Promise(requestAnimationFrame); + await new Promise(setTimeout); + var ctx = canvas.getContext('2d'); + ctx.drawElementImage(document.getElementById('wrapper'), 0, 0); + + // Fetch all pixel data once to avoid multiple slow readbacks. + const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + + // Helper function to extract a pixel's RGBA array at (x, y). + const getPixel = (x, y) => { + const index = (y * canvas.width + x) * 4; + return [ + imgData[index], + imgData[index + 1], + imgData[index + 2], + imgData[index + 3] + ]; + }; + + let pixel = getPixel(5, 5); + assert_array_equals(pixel, [0, 255, 0, 255], "Same origin feImage backdrop-filter should draw"); + // TODO(https://crbug.com/501606085): Enable the cross-origin test. + // pixel = getPixel(5, 15); + // assert_array_equals(pixel, [0, 0, 255, 255], "Cross origin feImage backdrop-filter should not draw"); + }); + } + </script> +</body> +</html>
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html
new file mode 100644
index 0000000..28ee93a0
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>drawElementImage does not use cross-origin feimage content via CSS backdrop-filter</title>
+ <link rel="help" href="https://github.com/WICG/html-in-canvas">
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <style>
+ #wrapper {
+ width: 10px;
+ height: 20px;
+ background: blue;
+ }
+ .child {
+ width: 10px;
+ height: 10px;
+ background: transparent;
+ }
+ #child-same { backdrop-filter: url('#filter-same'); }
+ #child-cross { backdrop-filter: url('#filter-cross'); }
+ </style>
+</head>
+<body>
+ <svg width="0" height="0">
+ <filter id="filter-same">
+ <feimage href="https://{{location[host]}}/wpt_internal/html/canvas/drawElementImage/resources/green-100x100.png" />
+ </filter>
+ <filter id="filter-cross">
+ <feimage href="https://{{hosts[alt][www]}}:{{ports[h2][0]}}/wpt_internal/html/canvas/drawElementImage/resources/red-100x100.png" />
+ </filter>
+ </svg>
+
+ <canvas id=canvas width="10" height="20" layoutsubtree>
+ <div id="wrapper">
+ <div id="child-same" class="child"></div>
+ <div id="child-cross" class="child"></div>
+ </div>
+ </canvas>
+
+ <script>
+ window.onload = () => {
+ promise_test(async function(t) {
+ const preloadImage = (url) => new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => resolve(img);
+ img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
+ img.src = url;
+ });
+
+ const sameOriginFeImageEl = document.querySelector('#filter-same feimage');
+ const crossOriginFeImageEl = document.querySelector('#filter-cross feimage');
+ await Promise.all([
+ preloadImage(sameOriginFeImageEl.getAttribute('href')),
+ preloadImage(crossOriginFeImageEl.getAttribute('href'))
+ ]);
+
+ await new Promise(requestAnimationFrame);
+ await new Promise(setTimeout);
+ var ctx = canvas.getContext('2d');
+ ctx.drawElementImage(document.getElementById('wrapper'), 0, 0);
+
+ // Fetch all pixel data once to avoid multiple slow readbacks.
+ const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+
+ // Helper function to extract a pixel's RGBA array at (x, y).
+ const getPixel = (x, y) => {
+ const index = (y * canvas.width + x) * 4;
+ return [
+ imgData[index],
+ imgData[index + 1],
+ imgData[index + 2],
+ imgData[index + 3]
+ ];
+ };
+
+ let pixel = getPixel(5, 5);
+ assert_array_equals(pixel, [0, 255, 0, 255], "Same origin feImage backdrop-filter should draw");
+ // TODO(https://crbug.com/501606085): Enable the cross-origin test.
+ // pixel = getPixel(5, 15);
+ // assert_array_equals(pixel, [0, 0, 255, 255], "Cross origin feImage backdrop-filter should not draw");
+ });
+ }
+ </script>
+</body>
+</html>
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page