Overview

High
Severity
β€”
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Skia
DescriptionUse after free in Skia
ComponentSkia
Bug ClassUAF
Tracker513820666
Fix commitf6a9a592efb8 (skia) +242/-18
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
for
bench/GlyphRunRSXformBench.cpp
modified
GlyphRunRSXformCachedBench
bench/GlyphRunRSXformBench.cpp
modified
GlyphRunRSXformCachedBench
bench/GlyphRunRSXformBench.cpp
modified
GlyphRunBuilder
include/core/SkCanvas.h
modified
GlyphRunList
include/core/SkCanvas.h
modified
AutoLayerForImageFilter
include/core/SkCanvas.h
modified
GrRecordingContext
include/core/SkCanvas.h
modified
SkEnumBitMask
include/core/SkCanvas.h
modified
GlyphRun
include/core/SkCanvas.h
modified
AutoUpdateQRBounds
include/core/SkCanvas.h
modified

Files Changed

  • bench/GlyphRunRSXformBench.cpp
  • gn/bench.gni
  • include/core/SkCanvas.h
From f6a9a592efb842825da1ebacb92493858abfad02 Mon Sep 17 00:00:00 2001
From: Alexis Cruz-Ayala <alexisdavidc@google.com>
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 <alexisdavidc@google.com>
Reviewed-by: Kaylee Lubick <kjlubick@google.com>
---

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;
 };
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cpp
  • third_party/skia/include/core/SkCanvas.h
  • third_party/skia/src/text/GlyphRun.cpp
  • third_party/skia/src/core/SkCanvas.cpp
  • third_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)

  1. 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 SkPicture containing a DRAW_TEXT_BLOB record.
  2. 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 simplifyGlyphRunRSXFormAndRedraw path is taken.
  3. 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:

  1. Using a stack-allocated or local GlyphRunBuilder within drawTextBlob and related methods.
  2. Implementing a reentrancy guard in SkCanvas or GlyphRunBuilder to prevent modification of the scratch storage while it is being iterated.
  3. Ensuring that GlyphRunList objects 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.

View on issue tracker