High chrome Memory Corruption 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Canvas
DescriptionMemory corruption in Canvas
ComponentCanvas
Sub-componentCanvas2d
Bug ClassMemory Corruption
Tracker569127861
Fix commit15cfaf3d3261 (chromium/src) +95/-20
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc
modified
if
third_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.cc
  • third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc
  • third_party/blink/web_tests/fast/canvas/canvas-drawText-style-recalc-crash.html
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.