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 <pdr@chromium.org>
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 <szager@chromium.org>
Auto-Submit: Philip Rogers <pdr@chromium.org>
Commit-Queue: Philip Rogers <pdr@chromium.org>
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