Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in Skia
DescriptionHeap buffer overflow in Skia
ComponentSkia
Bug ClassOOB
Tracker520113415
Fix commita145861ad1c2 (skia) +100/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
src/gpu/ganesh/ops/AtlasTextOp.cpp
modified
if
tests/SlugTest.cpp
modified

Files Changed

  • src/gpu/ganesh/ops/AtlasTextOp.cpp
  • src/text/gpu/SubRunContainer.cpp
  • tests/SlugTest.cpp
From a145861ad1c2650c58dd3ff0dbd92ae6f41f1e1b Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <kjlubick@google.com>
Date: Fri, 05 Jun 2026 20:51:04 +0000
Subject: [PATCH] Reject Slugs that have creationMatrix with perspective

This shouldn't happen during normal use [1] but if the data
is corrupted, there are some assumptions that can can cause
issues, like the ones linked in the bug.

This rejects those and turns one assert into an actual runtime
check to provide defense in depth.

[1] https://github.com/google/skia/blob/9eecbdc30f7da675edab96974b23174a9d521e0c/src/text/gpu/SubRunContainer.cpp#L1578-L1581

Bug: 520113415
Fixed: 520113415
Change-Id: I6ec23df9a23ea588fa89f7a62dc1f197fe3905fd
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1256016
Commit-Queue: Kaylee Lubick <kjlubick@google.com>
Reviewed-by: Thomas Smith <thomsmit@google.com>
---

diff --git a/src/gpu/ganesh/ops/AtlasTextOp.cpp b/src/gpu/ganesh/ops/AtlasTextOp.cpp
index 27e919e..51ae890 100644
--- a/src/gpu/ganesh/ops/AtlasTextOp.cpp
+++ b/src/gpu/ganesh/ops/AtlasTextOp.cpp
@@ -565,11 +565,16 @@
 
         auto& glyphData = subRun.glyphVector().accessBackendData<GlyphData>();
 
-        SkDEBUGCODE(int strideCheck = SkToInt(glyphData.vertexStride(subRun.maskFormat(),
-                                                                     geo->fDrawMatrix)));
-        SkASSERTF(strideCheck == vertexStride,
-                   "subRun stride: %d vertex buffer stride: %d\n",
-                   strideCheck, vertexStride);
+        int strideCheck = SkToInt(glyphData.vertexStride(subRun.maskFormat(), geo->fDrawMatrix));
+        if (strideCheck != vertexStride) {
+            // We (unexpectedly) have buffers of different sizes between CPU and GPU. Bail out.
+            SKIA_LOG_D(
+                    "Warning: stride mismatch detected (subrun stride: %d vertex buffer stride: "
+                    "%d). Aborting draw.\n",
+                    strideCheck,
+                    vertexStride);
+            return;
+        }
 
         const int subRunEnd = subRun.glyphCount();
 
diff --git a/src/text/gpu/SubRunContainer.cpp b/src/text/gpu/SubRunContainer.cpp
index bd5f827..6d2a7e3 100644
--- a/src/text/gpu/SubRunContainer.cpp
+++ b/src/text/gpu/SubRunContainer.cpp
@@ -169,6 +169,11 @@
 
     SkMatrix creationMatrix;
     buffer.readMatrix(&creationMatrix);
+    // The only valid creationMatrices do not have perspective and many rendering parts assume
+    // they are non-affine, so reject any malformed matrices here.
+    if (!buffer.validate(!creationMatrix.hasPerspective())) {
+        return std::nullopt;
+    }
 
     SkSpan<SkPoint> leftTop = make_points_from_buffer(buffer, alloc);
     if (leftTop.empty()) {
diff --git a/tests/SlugTest.cpp b/tests/SlugTest.cpp
index cc1ef79..1c81925 100644
--- a/tests/SlugTest.cpp
+++ b/tests/SlugTest.cpp
@@ -5,6 +5,8 @@
  * found in the LICENSE file.
  */
 
+#include "include/core/SkBitmap.h"
+#include "include/core/SkCanvas.h"
 #include "include/core/SkFont.h"
 #include "include/core/SkFontStyle.h"
 #include "include/core/SkFontTypes.h"
@@ -20,6 +22,8 @@
 #include "include/gpu/ganesh/SkSurfaceGanesh.h"
 #include "include/private/base/SkTDArray.h"
 #include "include/private/chromium/Slug.h"
+#include "src/text/gpu/SlugImpl.h"
+#include "src/utils/SkFloatUtils.h"
 #include "tests/CtsEnforcement.h"
 #include "tests/Test.h"
 #include "tools/ToolUtils.h"
@@ -65,3 +69,84 @@
     sk_sp<sktext::gpu::Slug> slug = sktext::gpu::Slug::ConvertBlob(canvas, *blob, {10, 10}, p);
     REPORTER_ASSERT(reporter, slug == nullptr);
 }
+
+static void set_sdf_options(GrContextOptions* options) {
+    options->fMinDistanceFieldFontSize = 4;
+    options->fGlyphsAsPathsFontSize = 256;
+    options->fSupportBilerpFromGlyphAtlas = true;
+}
+
+DEF_GANESH_TEST_FOR_CONTEXTS(Slug_b520113415,
+                             skgpu::IsRenderingContext,
+                             reporter,
+                             ctxInfo,
+                             set_sdf_options,
+                             CtsEnforcement::kNextRelease) {
+    auto dContext = ctxInfo.directContext();
+
+    SkImageInfo info = SkImageInfo::MakeN32Premul(256, 256);
+    auto surface = SkSurfaces::RenderTarget(dContext, skgpu::Budgeted::kNo, info);
+    REPORTER_ASSERT(reporter, surface);
+    auto canvas = surface->getCanvas();
+
+    // This matrix is big enough such that with the font below...
+    SkMatrix canvasMatrix = SkMatrix::Scale(1.2345f, 6.7890f);
+
+    canvas->save();
+    canvas->setMatrix(canvasMatrix);
+
+    //  ... it will force SDFT (6.789 x 24 = ~162.9).
+    auto typeface = ToolUtils::CreatePortableTypeface("serif", SkFontStyle());
+    SkFont font(typeface);
+    font.setSubpixel(true);
+    font.setSize(24);
+
+    static const char* kText = "A";
+    int glyphCount = font.countText(kText, 1, SkTextEncoding::kUTF8);
+    SkTDArray<SkGlyphID> glyphs;
+    glyphs.append(glyphCount);
+    font.textToGlyphs(kText, 1, SkTextEncoding::kUTF8, glyphs);
+
+    SkTextBlobBuilder builder;
+    const SkTextBlobBuilder::RunBuffer& buf = builder.allocRun(font, glyphs.size(), 0, 0);
+    memcpy(buf.glyphs, glyphs.begin(), glyphs.size() * sizeof(SkGlyphID));
+    auto blob = builder.make();
+
+    SkPaint paint;
+    paint.setAntiAlias(true);
+
+    sk_sp<sktext::gpu::Slug> slug = sktext::gpu::Slug::ConvertBlob(canvas, *blob, {0, 0}, paint);
+    canvas->restore();
+
+    if (!slug) {
+        return;
+    }
+
+    sk_sp<SkData> data = slug->serialize();
+    if (!data || data->size() == 0) {
+        return;
+    }
+
+    // Copy data to a writable buffer so we can corrupt it
+    size_t size = data->size();
+    std::unique_ptr<uint8_t[]> writableData(new uint8_t[size]);
+    memcpy(writableData.get(), data->data(), size);
+
+
+    // The creationMatrix of VertexFiller inside SDFTSubRun is located 216 bytes in.
+    // We can overwrite it to have a matrix with perspective.
+    if (size < 216 + 9 * sizeof(float)) {
+        ERRORF(reporter, "Serialized Slug is too small to contain creationMatrix!");
+        return;
+    }
+    float* f = reinterpret_cast<float*>(writableData.get() + 216);
+    f[0] = 1.0f; f[1] = 0.0f; f[2] = 0.0f;
+    f[3] = 0.0f; f[4] = 1.0f; f[5] = 0.0f;
+    f[6] = 0.0078125f; f[7] = 0.0f; f[8] = 1.0f;
+
+    // Deserialize the forged slug. This should return nullptr. Previously, when we went to draw it,
+    // the perspective would cause issues.
+    sk_sp<sktext::gpu::Slug> forgedSlug =
+            sktext::gpu::Slug::Deserialize(writableData.get(), size, nullptr);
+    REPORTER_ASSERT(reporter, forgedSlug == nullptr);
+}
Loading diff…

Original Bug Report

reported by vm...@google.com

Out-of-bounds heap write in Ganesh SDF text rendering path via mismatched predicates

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 heap out-of-bounds write exists in Ganesh’s signed distance field (SDF) text rendering pipeline. A compromised renderer can decouple the perspective predicates used for vertex buffer allocation and CPU-side vertex filling by forging a serialized Slug. This mismatch can cause the GPU process to allocate a vertex buffer with a 16-byte stride but fill it using a 20-byte stride, writing up to 8KB past the buffer boundary.

Affected files:

  • third_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cpp
  • third_party/skia/src/gpu/ganesh/text/GlyphData.cpp
  • third_party/skia/src/gpu/ganesh/effects/GrDistanceFieldGeoProc.cpp
  • third_party/skia/src/text/gpu/SubRunContainer.cpp
  • third_party/skia/src/text/gpu/VertexFiller.cpp
  • third_party/skia/src/gpu/ganesh/Device.cpp
  • cc/paint/paint_op_reader.cc

Estimated timestamp from git blame: 2022-10-13

Description

A potential heap out-of-bounds write vulnerability has been identified in Skia’s Ganesh rendering backend, specifically within the signed distance field (SDF) text rendering path. The vulnerability stems from an inconsistency between the perspective predicates used to compute the GeometryProcessor’s vertex-attribute layout and those used to determine the structure size during the CPU-side vertex-fill process.

Root Cause Analysis

  1. GeometryProcessor Stride Calculation: In third_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cpp:195, the perspective flag kPerspective_DistanceFieldEffectFlag is derived from viewDiffMatrix.hasPerspective():

    DFGPFlags |= viewDiffMatrix.hasPerspective() ? kPerspective_DistanceFieldEffectFlag : 0;
    

    Here, viewDiffMatrix is calculated as the difference between the drawing-time positionMatrix and the serialized creationMatrix (positionMatrix · creationMatrix⁻¹).

    If the perspective flag is not set, GrDistanceFieldA8TextGeoProc selects a 2D position attribute (kFloat2_GrVertexAttribType, 8 bytes), resulting in a vertex stride of 16 bytes (8 bytes position + 4 bytes color + 4 bytes UV).

  2. CPU-Side Vertex Fill Structure Selection: In third_party/skia/src/gpu/ganesh/text/GlyphData.cpp:299-320, the structure used to write the vertex data is chosen based directly on positionMatrix.hasPerspective(), rather than the viewDifference matrix:

    SkMatrix viewDifference = vf.viewDifference(positionMatrix);
    if (!positionMatrix.hasPerspective()) {
        using Quad = Mask2DVertex[4];   // 16B/vertex
        fill2D(quadData((Quad*)vertexBuffer), color, viewDifference);
    } else {
        using Quad = Mask3DVertex[4];   // 20B/vertex
        fill3D(quadData((Quad*)vertexBuffer), color, viewDifference);
    }
    

    If positionMatrix contains perspective, fill3D writes vertices as Mask3DVertex structures, which are 20 bytes each.

  3. De-synchronization of Predicates: A compromised renderer can manipulate both matrices across the IPC boundary. During Slug deserialization in third_party/skia/src/text/gpu/SubRunContainer.cpp:171, the creationMatrix is read raw from the shared buffer without validation to verify if it contains perspective:

    SkMatrix creationMatrix;
    buffer.readMatrix(&creationMatrix);
    

Potential Attack Scenario

Note: Our tooling does not currently have the capability to execute code, so this represents a suggested potential attack flow based on static analysis of the codebase.

  1. A compromised renderer updates the canvas CTM to an arbitrary invertible perspective matrix $P$ via SetMatrixOp(P).
  2. The renderer crafts and sends a serialized DrawSlugOp carrying a forged SDFTSubRun where creationMatrix is also set to $P$.
  3. During playback in the GPU process, the drawing context evaluates positionMatrix as $P$ (has perspective).
  4. The GPU process computes viewDiff = positionMatrix * creationMatrix^-1 = P * P^-1 = Identity (no perspective).
  5. Consequently, calculate_sdf_parameters does not apply the perspective flag, setting the GP vertex stride to 16 bytes. target->makeVertexSpace allocates $16 \times 4 \times N = 64 \times N$ bytes (e.g., 32,768 bytes for $N=512$).
  6. However, GlyphData::fillVertexData evaluates positionMatrix.hasPerspective() == true and enters the else block, writing vertices as Mask3DVertex (20-byte stride) at $80 \times N$ bytes (e.g., 40,960 bytes).
  7. This results in up to an 8,192-byte heap out-of-bounds write past the allocated buffer. The written data (coordinates mapped through viewDifference and the color) is fully specified by the attacker.

Suggested Remediation

To remediate this issue, the CPU-side vertex-filling logic should be aligned with the GP-side stride logic by keying the structure selection in GlyphData::fillVertexData on viewDifference.hasPerspective() rather than positionMatrix.hasPerspective(). Alternatively, strict matrix validation can be added during Slug deserialization to reject perspective creationMatrix inputs.

Evaluated with Chrome root at commit: 57b021e1fdae94a215627d29aeb1ccf2eb5b3e91


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