Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Skia
DescriptionInsufficient validation of untrusted input in Skia
ComponentSkia
Bug ClassLogic Error
Tracker520571816
Fix commiteac15152bf4b (skia) +66/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
tests/SlugTest.cpp
modified

Files Changed

  • src/text/gpu/SubRunContainer.cpp
  • tests/SlugTest.cpp
From eac15152bf4bea57c0720d8e9c16702393a2b24b Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <kjlubick@google.com>
Date: Mon, 08 Jun 2026 18:12:57 +0000
Subject: [PATCH] Avoid improper mask formats for SDFT runs

SDFTSubRun has a hard assumption of the kA8 mask format and
if the vertex filler differs, there will be a memory mismatch.

This catches it when deserializing the Slug and changes the
debug-only assert to be runtime to make sure we don't miss other
places.

Bug: 520571816
Fixed: 520571816
Change-Id: I9ae3e509397e23d38d621cb7eb8bb5b955a43d7d
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1257397
Commit-Queue: Kaylee Lubick <kjlubick@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Thomas Smith <thomsmit@google.com>
---

diff --git a/src/text/gpu/SubRunContainer.cpp b/src/text/gpu/SubRunContainer.cpp
index 6d2a7e3..6465c50 100644
--- a/src/text/gpu/SubRunContainer.cpp
+++ b/src/text/gpu/SubRunContainer.cpp
@@ -892,7 +892,7 @@
             , fUseLCDText{useLCDText}
             , fAntiAliased{antiAliased}
             , fMatrixRange{matrixRange} {
-        SkASSERT(fVertexFiller.maskFormat() == MaskFormat::kA8);
+        SkASSERT_RELEASE(fVertexFiller.maskFormat() == MaskFormat::kA8);
     }
 
     static SubRunOwner Make(SkZip<const SkPackedGlyphID, const SkPoint> accepted,
@@ -943,6 +943,8 @@
                                                            skglyph::kSDFT, kGlyphInsetting);
         if (!buffer.validate(vertexFiller.has_value())) { return nullptr; }
 
+        if (!buffer.validate(vertexFiller->maskFormat() == MaskFormat::kA8)) { return nullptr; }
+
         return alloc->makeUnique<SDFTSubRun>(useLCD,
                                              isAntiAliased,
                                              matrixRange,
diff --git a/tests/SlugTest.cpp b/tests/SlugTest.cpp
index 1c81925..9e14972 100644
--- a/tests/SlugTest.cpp
+++ b/tests/SlugTest.cpp
@@ -22,6 +22,7 @@
 #include "include/gpu/ganesh/SkSurfaceGanesh.h"
 #include "include/private/base/SkTDArray.h"
 #include "include/private/chromium/Slug.h"
+#include "src/gpu/MaskFormat.h"
 #include "src/text/gpu/SlugImpl.h"
 #include "src/utils/SkFloatUtils.h"
 #include "tests/CtsEnforcement.h"
@@ -150,3 +151,65 @@
             sktext::gpu::Slug::Deserialize(writableData.get(), size, nullptr);
     REPORTER_ASSERT(reporter, forgedSlug == nullptr);
 }
+
+DEF_GANESH_TEST_FOR_CONTEXTS(Slug_b520571816,
+                             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();
+
+    // Scale enough to force SDFT.
+    SkMatrix canvasMatrix = SkMatrix::Scale(10.0f, 10.0f);
+    canvas->save();
+    canvas->setMatrix(canvasMatrix);
+
+    auto typeface = ToolUtils::CreatePortableTypeface("serif", SkFontStyle());
+    SkFont font(typeface);
+    font.setSize(24);
+
+    static const char* kText = "A";
+    SkTextBlobBuilder builder;
+    const SkTextBlobBuilder::RunBuffer& runBuf = builder.allocRun(font, 1, 0, 0);
+    font.textToGlyphs(kText, 1, SkTextEncoding::kUTF8, SkSpan(runBuf.glyphs, 1));
+    auto blob = builder.make();
+
+    SkPaint paint;
+    paint.setAntiAlias(true);
+
+    sk_sp<sktext::gpu::Slug> slug = sktext::gpu::Slug::ConvertBlob(canvas, *blob, {0, 0}, paint);
+    SkASSERT_RELEASE(slug);
+    canvas->restore();
+
+    sk_sp<SkData> data = slug->serialize();
+    SkASSERT_RELEASE(data && data->size());
+
+    size_t size = data->size();
+    std::unique_ptr<uint8_t[]> writableData(new uint8_t[size]);
+    memcpy(writableData.get(), data->data(), size);
+
+    // Corrupt the maskFormat of VertexFiller inside SDFTSubRun at offset 208 from
+    // kA8 (0) to kARGB (2).
+    if (size < 212) {
+        ERRORF(reporter, "Serialized Slug is too small to contain maskFormat!");
+        return;
+    }
+    int32_t* formatPtr = reinterpret_cast<int32_t*>(writableData.get() + 208);
+    if (*formatPtr != 0) {
+        ERRORF(reporter, "maskFormat is not kA8 at expected offset 208!");
+        return;
+    }
+    *formatPtr = static_cast<int32_t>(skgpu::MaskFormat::kARGB);
+
+    // Since our validation fix will be active, this must fail and return nullptr. Previously, when
+    // we went to draw it, the unexpected mask format 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

GPU Process Info Leak via Forged MaskFormat in Ganesh SDFTSubRun Deserialization

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 compromised renderer can potentially deserialize a forged SDFTSubRun with a mask format of kARGB, which is normally reserved for color bitmap text. This causes a stride mismatch between the geometry processor (which expects a 16-byte or 20-byte stride) and the CPU-side vertex filler (which writes 12-byte or 16-byte structs). As a result, uninitialized memory from a shared GPU-process staging buffer pool is uploaded and rendered to the GPU, potentially leaking cross-origin graphics data.

Affected files:

  • third_party/skia/src/text/gpu/SubRunContainer.cpp
  • third_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cpp
  • third_party/skia/src/gpu/ganesh/text/GlyphData.cpp

Estimated timestamp from git blame: 2022-02-23

Detailed Description

A security vulnerability potentially exists in Skia’s Ganesh text-rendering pipeline when deserializing Slug objects from a compromised renderer. Specifically, the deserialization of SDFTSubRun does not validate that the maskFormat is restricted to kA8. This leads to a mismatch between the GPU vertex stride (defined by the Geometry Processor) and the CPU-side vertex fill stride, resulting in an uninitialized memory disclosure from the GPU process.

Analysis of the Root Cause

  1. Deserialization Validation Deficit: In third_party/skia/src/text/gpu/SubRunContainer.cpp, SDFTSubRun::MakeFromBuffer is used to deserialize SDF subruns from an untrusted renderer:

    static SubRunOwner MakeFromBuffer(SkReadBuffer& buffer, ...)
    

    It reads the VertexFiller using make_vertex_filler_from_buffer:

    MaskFormat maskFormat = buffer.read32LE(MaskFormat::kLast);
    

    Since MaskFormat::kLast is MaskFormat::kARGB (value 2), a compromised renderer can supply kARGB and pass deserialization. The constructor of SDFTSubRun contains only a debug-only assertion (SkASSERT) checking that maskFormat == MaskFormat::kA8, which is compiled out of release/production builds of Chrome.

  2. GPU Allocation (16-Byte Stride): When drawing the subrun, AtlasTextOp::Make executes. Because glyphParams().isSDF is unconditionally true for an SDFTSubRun, it routes to the distance-field path. AtlasTextOp builds a GrDistanceFieldA8TextGeoProc whose vertex layout is unconditionally composed of:

    • inPosition: 8 bytes (kFloat2_GrVertexAttribType for non-perspective)
    • inColor: 4 bytes (kUByte4_norm_GrVertexAttribType)
    • inTextureCoords: 4 bytes (kUShort2_GrVertexAttribType)

    This results in a total vertex stride of exactly 16 bytes (or 20 bytes if perspective is active). The vertex buffer is then allocated from the shared pool GrVertexBufferAllocPool using this 16-byte stride.

  3. CPU Fill (12-Byte Stride): During the draw preparation, GlyphData::fillVertexData handles the vertex data population on the CPU side. It determines the struct size based on vf.maskFormat():

    if (!positionMatrix.hasPerspective()) {
        if (vf.maskFormat() == MaskFormat::kARGB) {
            using Quad = ARGB2DVertex[4];
            ...
    

    Because the attacker forged maskFormat to kARGB, the code uses ARGB2DVertex, which is only 12 bytes in size (since the GrColor field is discarded). Thus, Skia writes only 12 bytes per vertex but advances the buffer allocation index using the 16-byte stride requested by the geometry processor.

  4. Uninitialized Memory Leak: This mismatch leaves 4 bytes (or 8 bytes under perspective variations) uninitialized at the tail of each vertex. Because the vertex buffers are allocated with ZeroInit::kNo from a shared staging pool reused across multiple contexts and origins, these uninitialized bytes contain stale graphics data from prior draws. This stale data is uploaded to the GPU, rendered via the distance field shader, and can potentially be retrieved by the attacker via canvas readback (getImageData) or texture copying.


Potential Steps to Reproduce

Since our automated tooling does not currently have the capability to execute proof-of-concept code, these are theoretical steps demonstrating how an attacker could trigger the mismatch:

  1. From a compromised renderer, construct a serialized DrawSlugOp paint-op.
  2. In the SDFTSubRun payload, set the VertexFiller’s maskFormat field to 2 (MaskFormat::kARGB).
  3. Submit the paint op over the IPC channel to the GPU process to be rasterized into an OOP-R tile backing or an accelerated 2D canvas.
  4. Read back the rasterized output from the renderer side (e.g., via CanvasRenderingContext2D.getImageData) and examine the returned pixel data for artifacts corresponding to leaked cross-origin GPU memory.

Suggested Fix

Validate the maskFormat inside SDFTSubRun::MakeFromBuffer in third_party/skia/src/text/gpu/SubRunContainer.cpp. If the format is not MaskFormat::kA8, the buffer should be marked invalid and deserialization should fail:

    static SubRunOwner MakeFromBuffer(SkReadBuffer& buffer,
                                      SubRunAllocator* alloc,
                                      const SkStrikeClient* client) {
        ...
        auto vertexFiller = make_vertex_filler_from_buffer(buffer, alloc, &glyphVector.value(),
                                                           skglyph::kSDFT, kGlyphInsetting);
        if (!buffer.validate(vertexFiller.has_value())) { return nullptr; }
        
        // Ensure the deserialized mask format is kA8 for SDFT subruns
        if (!buffer.validate(vertexFiller->maskFormat() == MaskFormat::kA8)) { return nullptr; }

        return alloc->makeUnique<SDFTSubRun>(useLCD,
                                             isAntiAliased,
                                             matrixRange,
                                             std::move(*vertexFiller),
                                             std::move(*glyphVector));
    }

Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf


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