CVE-2026-11136
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pthird_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc |
modified | |
ifthird_party/blink/renderer/platform/geometry/path_builder.cc |
modified |
Files Changed
third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.hthird_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.ccthird_party/blink/renderer/platform/geometry/path_builder.ccthird_party/blink/renderer/platform/geometry/path_builder.h
Patch
From f3b97ba13cd3890ae3369617f9dc6e4caccb22c0 Mon Sep 17 00:00:00 2001
From: Florin Malita <fmalita@chromium.org>
Date: Wed, 22 Apr 2026 11:29:57 -0700
Subject: [PATCH] [path builder] Return current path snapshot by value
Returning a reference to a std::optional-stored value is fragile, as
complex clients can end up inadvertently destroying the Path (e.g. via a
canvas 2d context loss in the linked bug) while the stale reference is
still in use.
Instead of attempting to locate and fix all vulnerable code paths,
change GetPath() to return a Path value. This is a shallow copy (the
underlying SkPathData is shared), and should not have major perf
repercussions.
Bug: 501646327
Change-Id: I7917086979b2544f3792f266811728fda8928c7f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7785233
Reviewed-by: Fredrik Söderquist <fs@opera.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1618987}
---
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.h b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.h
index 7b04ebe..117bddc 100644
--- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.h
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.h
@@ -137,7 +137,7 @@
virtual ExecutionContext* GetTopExecutionContext() const = 0;
- const Path& GetPath() const {
+ Path GetPath() const {
UpdatePathFromLineOrArcIfNecessary();
return path_builder_.CurrentPath();
}
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc
index f5c6397..e1459259 100644
--- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc
@@ -66,6 +66,7 @@
#include "third_party/blink/renderer/bindings/modules/v8/v8_typedefs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_canvasfilter_string.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_cssimagevalue_htmlcanvaselement_htmlimageelement_htmlvideoelement_imagebitmap_offscreencanvas_svgimageelement_videoframe.h"
+#include "third_party/blink/renderer/core/accessibility/ax_context.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
@@ -82,6 +83,7 @@
#include "third_party/blink/renderer/core/html/canvas/image_data.h"
#include "third_party/blink/renderer/core/html/canvas/predefined_color_space.h"
#include "third_party/blink/renderer/core/html/canvas/recording_test_utils.h"
+#include "third_party/blink/renderer/core/html/forms/html_button_element.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h"
@@ -144,6 +146,7 @@
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/core/SkSurface.h"
+#include "ui/accessibility/ax_mode.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
@@ -3612,4 +3615,24 @@
}
#endif
+TEST_P(CanvasRenderingContext2DTest, DrawFocusWithContextLost) {
+ CreateContext(kNonOpaque);
+
+ // Resize the canvas to an invalid size.
+ // This will cause a context loss downstream.
+ CanvasElement().SetSize(gfx::Size(42000, 42000));
+
+ Context2D()->rect(0, 0, 100, 100);
+
+ // Add a focused element + a11y context to trigger the problematic code path.
+ auto* button = GetDocument().CreateRawElement(html_names::kButtonTag);
+ CanvasElement().appendChild(button);
+ To<HTMLButtonElement>(button)->Focus();
+ AXContext ax_context(GetDocument(), ui::kAXModeComplete);
+
+ // DrawFocusIfNeeded() triggers a context loss internally, due to the invalid
+ // canvas size. The test passes if we don't crash.
+ Context2D()->drawFocusIfNeeded(button);
+}
+
} // namespace blink
diff --git a/third_party/blink/renderer/platform/geometry/path_builder.cc b/third_party/blink/renderer/platform/geometry/path_builder.cc
index 18f283e..398fc8ba 100644
--- a/third_party/blink/renderer/platform/geometry/path_builder.cc
+++ b/third_party/blink/renderer/platform/geometry/path_builder.cc
@@ -128,7 +128,7 @@
return current_bounds_.value();
}
-const Path& PathBuilder::CurrentPath() const {
+Path PathBuilder::CurrentPath() const {
if (!current_path_) {
current_path_.emplace(builder_.snapshot());
}
diff --git a/third_party/blink/renderer/platform/geometry/path_builder.h b/third_party/blink/renderer/platform/geometry/path_builder.h
index d329a8d..fcc7d92 100644
--- a/third_party/blink/renderer/platform/geometry/path_builder.h
+++ b/third_party/blink/renderer/platform/geometry/path_builder.h
@@ -72,7 +72,7 @@
// Avoid if possible (use Finalize instead).
// TODO(crbug.com/378688986): evaluate whether the cached path value is needed
// once the conversion is complete.
- const Path& CurrentPath() const;
+ Path CurrentPath() const;
// Gets the current point of the current path, which is conceptually the final
// point reached by the path so far. Note the Path can be empty
// (isEmpty() == true) and still have a current point.
Regression Test / PoC
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc
index f5c6397..e1459259 100644
--- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc
@@ -66,6 +66,7 @@
#include "third_party/blink/renderer/bindings/modules/v8/v8_typedefs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_canvasfilter_string.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_cssimagevalue_htmlcanvaselement_htmlimageelement_htmlvideoelement_imagebitmap_offscreencanvas_svgimageelement_videoframe.h"
+#include "third_party/blink/renderer/core/accessibility/ax_context.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
@@ -82,6 +83,7 @@
#include "third_party/blink/renderer/core/html/canvas/image_data.h"
#include "third_party/blink/renderer/core/html/canvas/predefined_color_space.h"
#include "third_party/blink/renderer/core/html/canvas/recording_test_utils.h"
+#include "third_party/blink/renderer/core/html/forms/html_button_element.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h"
@@ -144,6 +146,7 @@
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/core/SkSurface.h"
+#include "ui/accessibility/ax_mode.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
@@ -3612,4 +3615,24 @@
}
#endif
+TEST_P(CanvasRenderingContext2DTest, DrawFocusWithContextLost) {
+ CreateContext(kNonOpaque);
+
+ // Resize the canvas to an invalid size.
+ // This will cause a context loss downstream.
+ CanvasElement().SetSize(gfx::Size(42000, 42000));
+
+ Context2D()->rect(0, 0, 100, 100);
+
+ // Add a focused element + a11y context to trigger the problematic code path.
+ auto* button = GetDocument().CreateRawElement(html_names::kButtonTag);
+ CanvasElement().appendChild(button);
+ To<HTMLButtonElement>(button)->Focus();
+ AXContext ax_context(GetDocument(), ui::kAXModeComplete);
+
+ // DrawFocusIfNeeded() triggers a context loss internally, due to the invalid
+ // canvas size. The test passes if we don't crash.
+ Context2D()->drawFocusIfNeeded(button);
+}
+
} // namespace blink
Original Bug Report
CanvasRenderingContext2D::drawFocusIfNeeded dangling `Path` use-after-free
Report description
CanvasRenderingContext2D::drawFocusIfNeeded dangling Path use-after-free
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://chromium.googlesource.com/chromium/src
The problem
Please describe the technical details of the vulnerability
The problem
Summary
CanvasRenderingContext2D::drawFocusIfNeeded(Element*) passes the current canvas path into DrawFocusIfNeededInternal() by const Path&. If DrawFocusRing() loses the 2D context because the canvas size is invalid, the cached current path is destroyed before DrawFocusIfNeededInternal() returns. The same stale reference is then used by UpdateElementAccessibility(path, element).
In the tested trigger, the freed object is the SkPathData behind the cached current path. The ASAN report shows the first invalid access as WRITE of size 4 to the freed region.
Chrome Version
The crash was reproduced on a downloadable Chromium 149 ASAN build on Linux x86_64. The downloaded ASAN package was requested with --version 149.0.7779.3, and the extracted browser used for the run reported Chromium 149.0.7779.0. I also checked the corresponding Chromium 149 source tree, including Skia, to verify the code path shown below.
Root Cause
The vulnerable path starts here in third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc:
void CanvasRenderingContext2D::drawFocusIfNeeded(Element* element) {
DrawFocusIfNeededInternal(GetPath(), element);
}
void CanvasRenderingContext2D::DrawFocusIfNeededInternal(const Path& path,
Element* element) {
if (!FocusRingCallIsValid(path, element))
return;
if (element->GetDocument().FocusedElement() == element) {
ScrollPathIntoViewInternal(path);
DrawFocusRing(path, element);
}
UpdateElementAccessibility(path, element);
}
GetPath() returns a reference backed by PathBuilder::CurrentPath(). The cached object is stored in mutable std::optional<Path> current_path_.
The same call then reaches the context-loss path:
void CanvasRenderingContext2D::DrawFocusRing(const Path& path,
Element* element) {
if (!GetOrCreatePaintCanvas())
return;
...
}
CanvasResourceProvider*
CanvasRenderingContext2D::GetOrCreateResourceProvider() {
...
if (!canvas()->IsValidImageSize()) {
did_fail_to_create_resource_provider_ = true;
if (!canvas()->Size().IsEmpty()) {
LoseContext(CanvasRenderingContext::kInvalidCanvasSize);
}
return nullptr;
}
...
}
LoseContext() clears the cached path through the normal reset path:
void CanvasRenderingContext2D::LoseContext(LostContextMode lost_mode) {
if (context_lost_mode_ != kNotLostContext)
return;
context_lost_mode_ = lost_mode;
ResetInternal();
...
}
void Canvas2DRecorderContext::ResetInternal() {
...
CanvasPath::Clear();
...
}
void Clear() {
line_builder_.Clear();
arc_builder_.Clear();
path_builder_.Reset();
}
void PathBuilder::ClearCachedData() {
current_path_.reset();
current_bounds_.reset();
}
Execution then continues in the same stack frame and reuses the stale reference:
void CanvasRenderingContext2D::UpdateElementAccessibility(const Path& path,
Element* element) {
...
AXObjectCache* ax_object_cache =
element->GetDocument().ExistingAXObjectCache();
if (!ax_object_cache) {
return;
}
ax_object_cache->UpdateAXForAllDocuments();
const AffineTransform& transform = GetState().GetTransform();
const Path transformed_path =
transform.IsIdentity()
? path
: PathBuilder(path).Transform(transform).Finalize();
PhysicalRect element_rect =
PhysicalRect::EnclosingRect(transformed_path.BoundingRect());
...
}
In the attached PoC, transform.IsIdentity() is true. The dangling path is copied into transformed_path, BoundingRect() is called, and the temporary is then destroyed. The observed ASAN WRITE of size 4 matches the sk_sp<SkPathData> copy path incrementing the freed reference count.
Trigger Sequence
The trigger uses only standard web APIs.
- Create a
<canvas>and get a 2D rendering context. - Resize the canvas to
20000 x 20000. - Build a non-empty current path with
beginPath()andrect(). - Focus a descendant fallback element inside the canvas.
- Call
ctx.drawFocusIfNeeded(btn).
The trigger does not call drawing or transform APIs after the resize. Calls such as fillRect(), stroke(), isPointInPath(), scale(), rotate(), or setTransform() would reach GetOrCreatePaintCanvas() before drawFocusIfNeeded() and would lose the context too early.
Reproduction
Test Builds
The following public builds were used for this report.
- ASAN build used for the crash reproduction:
Downloaded with get_asan_chrome.py --version 149.0.7779.3
Executed browser version: Chromium 149.0.7779.0
Linux x86_64
- Release build used as a public Chrome 149 reference binary:
Google Chrome for Testing 149.0.7784.0, Linux x86_64
How the test environment was prepared
The ASAN build can be downloaded with Chromium’s helper script:
python3 chromium-149/tools/get_asan_chrome/get_asan_chrome.py \
--version 149.0.7779.3 \
--os linux \
--download_directory /home/qwerty/chrome-agent/chromium-149.0.7779.3-linux-asan-download
For the runs in this report, the extracted browser was stored at:
/home/qwerty/chrome-agent/chromium-149.0.7779.3-linux-asan/chrome
The public release build can be downloaded from Chrome for Testing:
wget https://storage.googleapis.com/chrome-for-testing-public/149.0.7784.0/linux64/chrome-linux64.zip
unzip chrome-linux64.zip
For the runs in this report, the release browser was stored at:
/home/qwerty/chrome-agent/chrome-linux64/chrome
The source tree used for code verification was a local Chromium 149 checkout. The Skia submodule was initialized with:
git -C chromium-149 submodule update --init --depth=1 third_party/skia
The trigger itself uses only the attached files. No external media files or additional test corpus are required.
How to Run
Place the attached files in one directory and start the ASAN run script with the downloaded browser path:
cd pocs/AUD-BLINK_MODULES-130ee005
./run.sh /home/qwerty/chrome-agent/chromium-149.0.7779.3-linux-asan/chrome
The --force-renderer-accessibility flag is required because UpdateElementAccessibility() returns early if ExistingAXObjectCache() is null.
The second attached trigger runs the same code with ASAN quarantine disabled:
cd pocs/AUD-BLINK_MODULES-130ee005
./run_bof.sh /home/qwerty/chrome-agent/chromium-149.0.7779.3-linux-asan/chrome
The same run.sh launcher was also used with the public release binary:
cd pocs/AUD-BLINK_MODULES-130ee005
./run.sh /home/qwerty/chrome-agent/chrome-linux64/chrome
Crash Logs
ASAN run with poc.html:
==122148==ERROR: AddressSanitizer: heap-use-after-free on address 0x7261c71a5200
WRITE of size 4 at 0x7261c71a5200 thread T35 (Chrome_InProcRe)
...
0x7261c71a5200 is located 0 bytes inside of 173-byte region [0x7261c71a5200,0x7261c71a52ad)
...
SUMMARY: AddressSanitizer: heap-use-after-free
ASAN run with poc_bof.html and quarantine_size_mb=0:
==122155==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7792f77bf060
WRITE of size 4 at 0x7792f77bf060 thread T35 (Chrome_InProcRe)
...
0x7792f77bf060 is located 72 bytes after 168-byte region [0x7792f77bef70,0x7792f77bf018)
...
SUMMARY: AddressSanitizer: heap-buffer-overflow
Public release run with poc.html:
Received signal 11 SI_KERNEL000000000000
Possibly a General Protection Fault, can be due to a non-canonical address dereference.
...
r14: badbad00badbad00
bx: badbad00badbad00
Suggested Fix
One straightforward fix is to stop passing the cached current path by reference across a call that may reset the context:
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc
index 000000000000..000000000000 100644
--- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc
@@ -930,7 +930,8 @@ cc::Layer* CanvasRenderingContext2D::CcLayer() const {
}
void CanvasRenderingContext2D::drawFocusIfNeeded(Element* element) {
- DrawFocusIfNeededInternal(GetPath(), element);
+ const Path path = GetPath();
+ DrawFocusIfNeededInternal(path, element);
}
void CanvasRenderingContext2D::drawFocusIfNeeded(Path2D* path2d,
Element* element) {
Impact analysis
A web page can trigger a use-after-free in the renderer process through the Canvas2D API. The issue is reachable from JavaScript and reproduces as renderer memory corruption without additional user interaction beyond visiting the page.
The cause
What version of Chrome have you found the security issue in?
149.0.7779.0, 148.0.6613.0
Is the security issue related to a crash?
Yes, it is related to a crash.
Choose the type of vulnerability
Memory Corruption (in a sandboxed process)
How would you like to be publicly acknowledged for your report?
Jungwoo Lee (@physicube) and Wongi Lee (@_qwerty_po)