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 <[email protected]> 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 <[email protected]> Commit-Queue: Andres Ricardo Perez Rojas <[email protected]> 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