Chrome · Canvas › Canvas2d
CVE-2026-113154
Memory Corruption in Canvas (Canvas2d)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc |
modified | |
ifthird_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc |
modified |
Files Changed
third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.ccthird_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.ccthird_party/blink/web_tests/fast/canvas/canvas-drawText-style-recalc-crash.html
Patch
From 15cfaf3d32610b6c376b88534fdfdf21e5bd2ae2 Mon Sep 17 00:00:00 2001
From: Andres Ricardo Perez <andresrperez@chromium.org>
Date: Thu, 08 Oct 2026 13:53:06 -0700
Subject: [PATCH] [canvas2d] Obtain PaintCanvas and state after style resolution in DrawTextInternal
BaseRenderingContext2D::DrawTextInternal() currently grabs a raw
cc::PaintCanvas* pointer via GetOrCreatePaintCanvas() before calling
AccessFont(), EnsureComputedStyle(), and ToTextDirection().
Because those style-resolving calls can invoke EnsureComputedStyle(), a
synchronous style recalculation can run scripts and resize or reset the
canvas, destroying the recorder and invalidating the active PaintCanvas*
pointer and CanvasRenderingContext2DState reference before drawing.
Move GetOrCreatePaintCanvas() and GetState() in DrawTextInternal() to
after AccessFont(), EnsureComputedStyle(), and ToTextDirection() have
completed, and re-fetch GetState() after font resolution in AccessFont()
and setFont().
Bug: 569127861
Change-Id: I2aa763543239a2e9688e099f93aca3b9e098d040
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8532586
Reviewed-by: Justin Novosad <junov@chromium.org>
Commit-Queue: Andres Ricardo Perez Rojas <andresrperez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1714906}
---
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc b/third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc
index 5765ddef..9aa4a38 100644
--- a/third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc
@@ -1195,8 +1195,8 @@
return;
}
- CanvasRenderingContext2DState& state = GetState();
- if (new_font == state.UnparsedFont() && CurrentFontResolvedAndUpToDate()) {
+ if (new_font == GetState().UnparsedFont() &&
+ CurrentFontResolvedAndUpToDate()) {
return;
}
@@ -1204,8 +1204,9 @@
return;
}
- // The parse succeeded.
- state.SetUnparsedFont(new_font);
+ // The parse succeeded. Re-fetch `GetState()` as `ResolveFont()` can execute
+ // script and reset the state stack.
+ GetState().SetUnparsedFont(new_font);
}
static inline TextDirection ToTextDirection(
@@ -1377,6 +1378,13 @@
unsigned run_end,
double* max_width,
const Font* cluster_font) {
+ if (!std::isfinite(x) || !std::isfinite(y)) {
+ return;
+ }
+ if (max_width && (!std::isfinite(*max_width) || *max_width <= 0)) {
+ return;
+ }
+
HTMLCanvasElement* canvas = HostAsHTMLCanvasElement();
if (canvas) {
// The style resolution required for fonts is not available in frame-less
@@ -1392,19 +1400,6 @@
canvas, DocumentUpdateReason::kCanvas);
}
- // Abort if we don't have a paint canvas (e.g. the context was lost).
- cc::PaintCanvas* paint_canvas = GetOrCreatePaintCanvas();
- if (!paint_canvas) {
- return;
- }
-
- if (!std::isfinite(x) || !std::isfinite(y)) {
- return;
- }
- if (max_width && (!std::isfinite(*max_width) || *max_width <= 0)) {
- return;
- }
-
const Font* font =
(cluster_font != nullptr) ? cluster_font : AccessFont(canvas);
const SimpleFontData* font_data = font->PrimaryFont();
@@ -1415,16 +1410,25 @@
// FIXME: Need to turn off font smoothing.
- const CanvasRenderingContext2DState& state = GetState();
const ComputedStyle* computed_style =
canvas ? canvas->EnsureComputedStyle() : nullptr;
CanvasRenderingContextHost* host = GetCanvasRenderingContextHost();
TextDirection direction =
- ToTextDirection(state.GetDirection(), host, computed_style);
+ ToTextDirection(GetState().GetDirection(), host, computed_style);
bool is_rtl = direction == TextDirection::kRtl;
bool bidi_override =
computed_style ? IsOverride(computed_style->GetUnicodeBidi()) : false;
+ // Obtain `paint_canvas` and `state` after `AccessFont`,
+ // `EnsureComputedStyle`, and `ToTextDirection`, which can synchronously
+ // execute script and reset the canvas recorder or state stack.
+ // Abort if we don't have a paint canvas (e.g. the context was lost).
+ cc::PaintCanvas* paint_canvas = GetOrCreatePaintCanvas();
+ if (!paint_canvas) {
+ return;
+ }
+ const CanvasRenderingContext2DState& state = GetState();
+
PlainTextPainter& text_painter = host->GetPlainTextPainter();
TextRun text_run(text, direction, bidi_override);
// Draw the item text at the correct point.
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc
index 7c8f17a..5fb52c81 100644
--- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc
@@ -2742,7 +2742,9 @@
if (canvas) {
canvas->GetDocument().GetCanvasFontCache()->WillUseCurrentFont();
}
- return state.GetFont();
+ // Re-fetch `GetState()` as `setFont()` can synchronously execute script and
+ // reset the state stack.
+ return GetState().GetFont();
}
void Canvas2DRecorderContext::SnapshotStateForFilter() {
diff --git a/third_party/blink/web_tests/fast/canvas/canvas-drawText-style-recalc-crash.html b/third_party/blink/web_tests/fast/canvas/canvas-drawText-style-recalc-crash.html
new file mode 100644
index 0000000..8adda8a
--- /dev/null
+++ b/third_party/blink/web_tests/fast/canvas/canvas-drawText-style-recalc-crash.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+<div id="outer1">
+ <object id="obj1" type="text/html" data="about:blank">
+ <svg><foreignObject><canvas></canvas></foreignObject></svg>
+ </object>
+</div>
+<div id="outer2">
+ <object id="obj2" type="text/html" data="about:blank">
+ <svg><foreignObject><canvas></canvas></foreignObject></svg>
+ </object>
+</div>
+<script>
+async_test((t) => {
+ const outer = document.getElementById('outer1');
+ const obj = document.getElementById('obj1');
+ const canvas = outer.querySelector('canvas');
+ const home = canvas.parentNode;
+ const ctx = canvas.getContext('2d');
+ const other = document.implementation.createHTMLDocument();
+
+ EventTarget.prototype.addEventListener.call(obj, 'load', t.step_func_done(() => {
+ ctx.font = '20px sans-serif';
+ obj.contentWindow.onpagehide = () => {
+ other.adoptNode(canvas);
+ canvas.width = 17;
+ home.append(canvas);
+ };
+
+ outer.style.display = 'none';
+ outer.offsetWidth;
+ getComputedStyle(canvas).color;
+ Element.prototype.setAttribute.call(obj, 'type', 'image/png');
+ Element.prototype.setAttribute.call(obj, 'data', 'data:image/png,x');
+ ctx.fillText('hello world', 0, 10, 10);
+
+ const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+ assert_true(data.some((v) => v !== 0), 'Expected fillText to draw pixels');
+ }));
+}, 'Canvas2D fillText does not crash when style recalc in DrawTextInternal resizes canvas');
+
+async_test((t) => {
+ const outer = document.getElementById('outer2');
+ const obj = document.getElementById('obj2');
+ const canvas = outer.querySelector('canvas');
+ const home = canvas.parentNode;
+ const ctx = canvas.getContext('2d');
+ const other = document.implementation.createHTMLDocument();
+
+ EventTarget.prototype.addEventListener.call(obj, 'load', t.step_func_done(() => {
+ obj.contentWindow.onpagehide = () => {
+ other.adoptNode(canvas);
+ canvas.width = 17;
+ home.append(canvas);
+ };
+
+ outer.style.display = 'none';
+ outer.offsetWidth;
+ getComputedStyle(canvas).color;
+ Element.prototype.setAttribute.call(obj, 'type', 'image/png');
+ Element.prototype.setAttribute.call(obj, 'data', 'data:image/png,x');
+ ctx.fillText('hello world', 0, 10, 10);
+
+ const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+ assert_true(data.some((v) => v !== 0), 'Expected fillText to draw pixels');
+ }));
+}, 'Canvas2D fillText does not crash and draws text when style recalc in AccessFont resizes canvas');
+</script>
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/web_tests/fast/canvas/canvas-drawText-style-recalc-crash.html b/third_party/blink/web_tests/fast/canvas/canvas-drawText-style-recalc-crash.html
new file mode 100644
index 0000000..8adda8a
--- /dev/null
+++ b/third_party/blink/web_tests/fast/canvas/canvas-drawText-style-recalc-crash.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+<div id="outer1">
+ <object id="obj1" type="text/html" data="about:blank">
+ <svg><foreignObject><canvas></canvas></foreignObject></svg>
+ </object>
+</div>
+<div id="outer2">
+ <object id="obj2" type="text/html" data="about:blank">
+ <svg><foreignObject><canvas></canvas></foreignObject></svg>
+ </object>
+</div>
+<script>
+async_test((t) => {
+ const outer = document.getElementById('outer1');
+ const obj = document.getElementById('obj1');
+ const canvas = outer.querySelector('canvas');
+ const home = canvas.parentNode;
+ const ctx = canvas.getContext('2d');
+ const other = document.implementation.createHTMLDocument();
+
+ EventTarget.prototype.addEventListener.call(obj, 'load', t.step_func_done(() => {
+ ctx.font = '20px sans-serif';
+ obj.contentWindow.onpagehide = () => {
+ other.adoptNode(canvas);
+ canvas.width = 17;
+ home.append(canvas);
+ };
+
+ outer.style.display = 'none';
+ outer.offsetWidth;
+ getComputedStyle(canvas).color;
+ Element.prototype.setAttribute.call(obj, 'type', 'image/png');
+ Element.prototype.setAttribute.call(obj, 'data', 'data:image/png,x');
+ ctx.fillText('hello world', 0, 10, 10);
+
+ const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+ assert_true(data.some((v) => v !== 0), 'Expected fillText to draw pixels');
+ }));
+}, 'Canvas2D fillText does not crash when style recalc in DrawTextInternal resizes canvas');
+
+async_test((t) => {
+ const outer = document.getElementById('outer2');
+ const obj = document.getElementById('obj2');
+ const canvas = outer.querySelector('canvas');
+ const home = canvas.parentNode;
+ const ctx = canvas.getContext('2d');
+ const other = document.implementation.createHTMLDocument();
+
+ EventTarget.prototype.addEventListener.call(obj, 'load', t.step_func_done(() => {
+ obj.contentWindow.onpagehide = () => {
+ other.adoptNode(canvas);
+ canvas.width = 17;
+ home.append(canvas);
+ };
+
+ outer.style.display = 'none';
+ outer.offsetWidth;
+ getComputedStyle(canvas).color;
+ Element.prototype.setAttribute.call(obj, 'type', 'image/png');
+ Element.prototype.setAttribute.call(obj, 'data', 'data:image/png,x');
+ ctx.fillText('hello world', 0, 10, 10);
+
+ const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+ assert_true(data.some((v) => v !== 0), 'Expected fillText to draw pixels');
+ }));
+}, 'Canvas2D fillText does not crash and draws text when style recalc in AccessFont resizes canvas');
+</script>
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