CVE-2026-11663
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forbench/GlyphRunRSXformBench.cpp |
modified | |
GlyphRunRSXformCachedBenchbench/GlyphRunRSXformBench.cpp |
modified | |
GlyphRunRSXformCachedBenchbench/GlyphRunRSXformBench.cpp |
modified | |
GlyphRunBuilderinclude/core/SkCanvas.h |
modified | |
GlyphRunListinclude/core/SkCanvas.h |
modified | |
AutoLayerForImageFilterinclude/core/SkCanvas.h |
modified | |
GrRecordingContextinclude/core/SkCanvas.h |
modified | |
SkEnumBitMaskinclude/core/SkCanvas.h |
modified | |
GlyphRuninclude/core/SkCanvas.h |
modified | |
AutoUpdateQRBoundsinclude/core/SkCanvas.h |
modified |
Files Changed
bench/GlyphRunRSXformBench.cppgn/bench.gniinclude/core/SkCanvas.h
Patch
From f6a9a592efb842825da1ebacb92493858abfad02 Mon Sep 17 00:00:00 2001 From: Alexis Cruz-Ayala <[email protected]> Date: Mon, 08 Jun 2026 15:50:12 -0400 Subject: [PATCH] Created an RAII Wrapper for SkCanvas->fScratchGlyphRunBuilder There was an issue with fScratchGlyphRunBuilder that allowed a malicious drawTextBlob to use an SkCustomTypeface with a reentrant drawable that contains a drawTextBlob, which caused a UAF. The first drawTextBlob would start using fScratchGlyphRunBuilder, then the second drawTextBlob would clear and use fScratchGlyphRunBuilder before the first drawTextBlob would finish. To resolve this issue, an STArray of GlyphRunBuilders (fScratchGlyphRunBuilders) is used with 1 GlyphRunBuilder by default, with more allocated as needed. To verify the behavior is consistent and make the maintenance easier for the user, an RAII pattern was used (AutoGlyphRunBuilder). A benchmark was added to show the performance change on RSXform Glyph Runs within a text blob, and a test was added to verify the UAF bug was fixed. # Without Changes: alexisdavidc@alexisdavidc2:~/skia/skia (testingbefore)$ ninja -C out/Release/ && out/Release/nanobench --match GlyphRunRSXform ninja: Entering directory `out/Release/' ninja: no work to do. Timer overhead: 23.9ns curr/maxrss loops min median mean max stddev samples config bench 123/122 MB 1 29.2Β΅s 29.8Β΅s 29.7Β΅s 30.1Β΅s 1% ββ ββββββββ 8888 GlyphRunRSXform_cached 147/140 MB 215 22.7Β΅s 22.9Β΅s 22.9Β΅s 23.1Β΅s 1% ββββββββββ gl GlyphRunRSXform_cached alexisdavidc@alexisdavidc2:~/skia/skia (testingbefore)$ ninja -C out/Release/ && out/Release/nanobench --match GlyphRunRSXform ninja: Entering directory `out/Release/' ninja: no work to do. Timer overhead: 23.8ns curr/maxrss loops min median mean max stddev samples config bench 123/121 MB 1 29.7Β΅s 30Β΅s 29.9Β΅s 30.2Β΅s 0% ββββββ ββ ββ 8888 GlyphRunRSXform_cached 147/139 MB 219 22.8Β΅s 23.5Β΅s 23.6Β΅s 25.8Β΅s 4% ββββββββββ gl GlyphRunRSXform_cached # With Changes: alexisdavidc@alexisdavidc2:~/skia/skia (security)$ ninja -C out/Release/ && out/Release/nanobench --match GlyphRunRSXform ninja: Entering directory `out/Release/' ninja: no work to do. Timer overhead: 23.8ns curr/maxrss loops min median mean max stddev samples config bench 123/119 MB 1 29.6Β΅s 29.9Β΅s 29.9Β΅s 30.4Β΅s 1% ββββββββββ 8888 GlyphRunRSXform_cached 147/138 MB 204 22.7Β΅s 22.9Β΅s 23.4Β΅s 24.9Β΅s 4% ββββββββββ gl GlyphRunRSXform_cached alexisdavidc@alexisdavidc2:~/skia/skia (security)$ ninja -C out/Release/ && out/Release/nanobench --match GlyphRunRSXform ninja: Entering directory `out/Release/' ninja: no work to do. Timer overhead: 23.8ns curr/maxrss loops min median mean max stddev samples config bench 123/116 MB 1 29.3Β΅s 29.7Β΅s 29.7Β΅s 30.3Β΅s 1% ββββββββββ 8888 GlyphRunRSXform_cached 147/134 MB 221 22.5Β΅s 22.7Β΅s 22.7Β΅s 22.9Β΅s 0% ββββββββββ gl GlyphRunRSXform_cached Bug: b/513820666 Change-Id: If8e5e9a3dc0f656eb1fc6b03ae79fdf39e67328e Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1248477 Commit-Queue: Alexis Cruz-Ayala <[email protected]> Reviewed-by: Kaylee Lubick <[email protected]> --- diff --git a/bench/GlyphRunRSXformBench.cpp b/bench/GlyphRunRSXformBench.cpp new file mode 100644 index 0000000..4bf7bcc --- /dev/null +++ b/bench/GlyphRunRSXformBench.cpp @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "bench/Benchmark.h" +#include "include/core/SkCanvas.h" +#include "include/core/SkFont.h" +#include "include/core/SkPaint.h" +#include "include/core/SkRSXform.h" +#include "include/core/SkString.h" +#include "include/core/SkTextBlob.h" +#include "include/private/base/SkTDArray.h" +#include "src/base/SkRandom.h" +#include "tools/fonts/FontToolUtils.h" + +// Creates an RSX form blob from the the randomized fXforms +sk_sp<SkTextBlob> makeBlob() { + SkFont font; + SkTDArray<SkGlyphID> glyphs; + SkTDArray<SkRSXform> xForms; + font = ToolUtils::DefaultFont(); + font.setSubpixel(true); + font.setSize(20); + + const char* text = "Keep your sentences short, but not overly so."; + int glyphCount = font.countText(text, strlen(text), SkTextEncoding::kUTF8); + glyphs.append(glyphCount); + font.textToGlyphs( + text, strlen(text), SkTextEncoding::kUTF8, {glyphs.data(), (size_t)glyphCount}); + + xForms.append(glyphCount); + SkRandom rand; + SkScalar x = 0; + for (int i = 0; i < glyphCount; ++i) { + SkScalar s = rand.nextF() * 0.5f + 0.5f; + SkScalar a = rand.nextF() * SK_ScalarPI * 0.25f; + xForms[i] = SkRSXform::Make(s * SkScalarCos(a), s * SkScalarSin(a), x, rand.nextF() * 20); + x += 20; + } + + return SkTextBlob::MakeFromRSXform(glyphs.data(), + glyphs.size() * sizeof(SkGlyphID), + {xForms.data(), (size_t)xForms.size()}, + font, + SkTextEncoding::kGlyphID); +} + +class GlyphRunRSXformCachedBench : public Benchmark { +public: + GlyphRunRSXformCachedBench() {} + +protected: + const char* onGetName() override { return "GlyphRunRSXform_cached"; } + + void onDelayedSetup() override { fBlob = makeBlob(); } + + void onDraw(int loops, SkCanvas* canvas) override { + SkPaint paint; + for (int i = 0; i < loops; i++) { + canvas->drawTextBlob(fBlob, 0, 0, paint); + } + } + +private: + sk_sp<SkTextBlob> fBlob; +}; + +DEF_BENCH(return new GlyphRunRSXformCachedBench();) diff --git a/gn/bench.gni b/gn/bench.gni index 0f95208..0cb5cca 100644 --- a/gn/bench.gni +++ b/gn/bench.gni @@ -60,6 +60,7 @@ "$_bench/GameBench.cpp", "$_bench/GeometryBench.cpp", "$_bench/GlyphQuadFillBench.cpp", + "$_bench/GlyphRunRSXformBench.cpp", "$_bench/GrMemoryPoolBench.cpp", "$_bench/GrMipmapBench.cpp", "$_bench/GrQuadBench.cpp", diff --git a/include/core/SkCanvas.h b/include/core/SkCanvas.h index cf31ffa..24702c8 100644 --- a/include/core/SkCanvas.h +++ b/include/core/SkCanvas.h @@ -45,11 +45,6 @@ #define SK_SUPPORT_LEGACY_GETTOTALMATRIX #endif -namespace sktext { -class GlyphRunBuilder; -class GlyphRunList; -} - class AutoLayerForImageFilter; class GrRecordingContext; @@ -81,6 +76,12 @@ template<typename E> class SkEnumBitMask; +namespace sktext { +class GlyphRunBuilder; +class GlyphRun; +class GlyphRunList; +} // namespace sktext + namespace skgpu::graphite { class Recorder; } namespace sktext::gpu { class Slug; } namespace SkRecords { class Draw; } @@ -2559,6 +2560,7 @@ friend class SkRecords::Draw; template <typename Key> friend class skiatest::TestCanvas; + friend class AutoGlyphRunBuilder; protected: // For use by SkNoDrawCanvas (via SkCanvasVirtualEnforcer, which can't be a friend) @@ -2704,7 +2706,15 @@ class AutoUpdateQRBounds; void validateClip() const; - std::unique_ptr<sktext::GlyphRunBuilder> fScratchGlyphRunBuilder; + sktext::GlyphRunBuilder* obtainGlyphRunBuilder(); + + void releaseGlyphRunBuilder(); + + /** + * fRunBuilders will be reused across text drawing commands + */ + skia_private::STArray<1, std::unique_ptr<sktext::GlyphRunBuilder>> fRunBuilders; + int fRunBuildersUsed = 0; };
Original Bug Report
Potential Use-After-Free in Skia text rendering due to reentrant drawTextBlob
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential use-after-free (UAF) vulnerability exists in Skia’s text rendering pipeline when processing glyph runs with RSX forms. A reentrant call to drawTextBlob via a glyph drawable can clobber the per-canvas scratch builder storage while it is being iterated, leading to memory corruption in the GPU process.
Affected files:
third_party/skia/src/core/SkDevice.cppthird_party/skia/include/core/SkCanvas.hthird_party/skia/src/text/GlyphRun.cppthird_party/skia/src/core/SkCanvas.cppthird_party/skia/src/text/gpu/SubRunContainer.cpp
Estimated timestamp from git blame: Unknown (Google3 checkout)
Technical Description
Root Cause Analysis
In Skia, SkCanvas manages a single fScratchGlyphRunBuilder (a std::unique_ptr<sktext::GlyphRunBuilder>) used to process text blobs and other text drawing commands. When SkCanvas::onDrawTextBlob is called, it populates this builder via blobToGlyphRunList(). This method returns a GlyphRunList where the internal runs (an SkSpan<const GlyphRun>) point directly into the builder’s persistent storage, specifically the fGlyphRunListStorage vector (std::vector<GlyphRun>).
The potential vulnerability occurs in SkDevice::simplifyGlyphRunRSXFormAndRedraw (located in third_party/skia/src/core/SkDevice.cpp). This function iterates over the GlyphRunList stored in the scratch builder. Inside the iteration loop, it may call this->drawGlyphRunList(canvas, subList, paint) synchronously to render the glyphs.
If the drawing operation involves a glyph that uses a custom drawable (such as SkPictureBackedGlyphDrawable), the execution path can reentrantly call canvas->drawTextBlob(). For example, a malicious SkPicture serialized into a font strike can contain a DRAW_TEXT_BLOB command. This reentrant call on the same SkCanvas instance will use the same fScratchGlyphRunBuilder. Any call to blobToGlyphRunList() on the builder invokes prepareBuffers(), which calls fGlyphRunListStorage.clear() and may reallocate the underlying buffers.
When the reentrant call returns, the outer loop in simplifyGlyphRunRSXFormAndRedraw continues to iterate over the now-invalidated storage, leading to a Use-After-Free (UAF). An attacker who controls the contents of the reallocated memory could potentially achieve arbitrary code execution in the GPU process.
Potential Attack Path (Compromised Renderer to GPU Process)
- Strike Data Injection: A compromised renderer sends malicious font strike data via the raster command buffer. This strike data includes a glyph drawable serialized as an
SkPicturecontaining aDRAW_TEXT_BLOBrecord. - Triggering Rasterization: The renderer issues a command to draw a text blob (Blob A) using the malicious font. Blob A is designed to have multiple runs and use RSX forms to ensure the
simplifyGlyphRunRSXFormAndRedrawpath is taken. - UAF Trigger: During rasterization in the GPU process, the iteration over Blob A’s runs triggers the drawing of the malicious glyph. This reentrantly calls
drawTextBlob(Blob B), which clobbers the scratch builder’s storage. Subsequent iterations over Blob A’s runs then access the freed memory.
Impact
This is a potential critical use-after-free vulnerability in the GPU process. Because the GPU process is unsandboxed on certain platforms such as Android, this provides a direct path for a compromised renderer to achieve higher-level privileges and potentially full system compromise.
Suggested Fix
To mitigate this issue, Skia should avoid using a persistent, shared scratch builder for text operations that can trigger reentrant drawing. Potential solutions include:
- Using a stack-allocated or local
GlyphRunBuilderwithindrawTextBloband related methods. - Implementing a reentrancy guard in
SkCanvasorGlyphRunBuilderto prevent modification of the scratch storage while it is being iterated. - Ensuring that
GlyphRunListobjects used in iteration do not hold dangling references to shared builder state across synchronous drawing calls.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.