Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Skia
DescriptionInformation leak in Skia
ComponentSkia
Bug ClassLogic Error
Tracker514078656
Fix commit2c88cbf6fba3 (skia) +377/-242
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-01

Background

Skia
The 2D graphics library Chrome uses to rasterize text, shapes, and images, including GPU-accelerated glyph rendering.
Glyph atlas
A shared GPU texture that caches rasterized glyph masks so repeated text draws can be composited from cached entries instead of re-rasterizing.
`SkPackedGlyphID`
A compact identifier that packs a glyph’s SkGlyphID together with its sub-pixel X/Y position, used as the cache key for atlas-backed glyphs.
`MaskFormat`
The pixel format and padding of a cached glyph mask (for example coverage A8, 565/RGBA8, or a signed-distance-field value), which governs how many bytes each atlas texel occupies and how it is sampled.

Root Cause Analysis

Before the fix, each backend’s TextStrike keyed its GlyphEntries by SkPackedGlyphID alone, so the atlas cache key encoded only the glyph and its sub-pixel offset but not the mask format, padding amount, or whether the stored data was a coverage or distance value. This violated the invariant that an atlas cache hit must return data whose pixel layout matches what the subrun being drawn actually requests: two draws of the same glyph with different MaskFormat, padding, or coverage/SDF semantics would collide on the same key and reuse an entry rasterized under the wrong configuration.

The fix introduces PackedGPUGlyphID, which wraps SkPackedGlyphID and packs the mask format, padding, and data-type into the previously free bits, and switches the strikes to key GlyphEntries on this richer identifier. Because the format and padding are now resolved against the atlas manager’s configuration before the key is formed (passed through initBackendData and GlyphData instead of being supplied late to regenerateAtlas), a cache hit can only occur when every layout-affecting property is consistent. This closes the mismatch so glyph vertices are always generated against atlas data of the format and stride they assume, eliminating the disclosure of adjacent or wrongly-interpreted atlas memory.

Key insight
The single core mistake was using a cache key (SkPackedGlyphID) that omitted the layout-defining properties (MaskFormat, padding, coverage-versus-distance), letting glyph draws alias atlas entries of an incompatible format; the fix folds those properties into a new PackedGPUGlyphID key so mismatched entries can never produce a cache hit.

Attack Path

  1. Render text needing one format A page draws GPU-accelerated text that populates the glyph atlas with an entry keyed by SkPackedGlyphID under one MaskFormat/padding/data-type.
  2. Redraw the same glyph differently The page draws the same glyph in a configuration requiring a different mask format, padding, or coverage/SDF semantics, colliding on the identical key.
  3. Force a stale cache hit The atlas returns the previously cached entry despite the layout mismatch, since the key does not distinguish the differing properties.
  4. Sample under the wrong layout Vertex generation reads the atlas region using the requested format’s stride and interpretation, pulling in bytes belonging to adjacent glyphs or uninitialized atlas memory.
  5. Recover the leaked pixels The mismatched output is composited into the rendered surface and can be read back, disclosing memory the page should not observe.

Impact Assessment

An attacker-controlled page can cause the GPU text renderer to sample glyph-atlas memory under a mismatched format, leaking adjacent or uninitialized atlas pixel data into rendered output that the page can read back, yielding an information disclosure. This occurs in the process performing Skia GPU rasterization within Chrome’s rendering path. The precondition is GPU-accelerated text drawing where the same glyph is requested under differing mask-format, padding, or coverage/SDF configurations so their entries alias on the old key.

Files Changed

  • bench/GlyphQuadFillBench.cpp
  • gn/gpu.gni
  • src/core/SkGlyph.h
  • src/gpu/ganesh/ops/AtlasTextOp.cpp
  • src/gpu/ganesh/ops/AtlasTextOp.h

Audit Directions

  • Incomplete cache keys
    Audit any atlas, texture, or resource cache whose key omits format, padding, stride, or interpretation properties that affect how the cached bytes are later read, since such omissions let mismatched draws alias.
  • Late-resolved layout parameters
    Flag paths where mask format or padding is resolved or applied after the cache lookup rather than before key formation, as the lookup can then return an entry built under a different configuration.
  • Coverage-versus-distance conflation
    Review code that shares glyph or mask storage between coverage masks and signed-distance-field data, ensuring the data type is part of the identity so the two are never sampled with each other’s semantics.
From 2c88cbf6fba335c230bab575b3f76a030f30cc41 Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
Date: Tue, 04 Aug 2026 07:05:57 -0700
Subject: [PATCH] Reland "[text] Introduce PackedGPUGlyphID to add more metadata to SkPackedGlyphID"

This reverts commit 48b58ee222f14b2b14a08c2e8574fae8256b26e0.

Reason for revert: Fixing GlyphData max size

Original change's description:
> Revert "[text] Introduce PackedGPUGlyphID to add more metadata to SkPackedGlyphID"
>
> This reverts commit f82e81ea65ed4cd9549f9416e2cc4e983eada195.
>
> Reason for revert: Breaking tree, and Michael (CL author) is not here to discuss path forward
>
> Failure Link: https://ci.chromium.org/raw/build/logs.chromium.org/skia/79e459c81677dc11/+/annotations
> Original change's description:
> > [text] Introduce PackedGPUGlyphID to add more metadata to SkPackedGlyphID
> >
> > Both Ganesh and Graphite share a lot of common structure to their glyph
> > handling code, so the changes outlined below are applied pretty
> > similarly to both codebases.
> >
> > 1. Adds a new shared type PackedGPUGlyphID that wraps SkPackedGlyphID
> > and packs into the free bits the rest of the information that
> > atlas-backed glyphs require, which is the mask format, the amount of
> > padding, and whether or not the data is a coverage or distance value.
> >
> > 2. Pulls back the presence of MaskFormat from the GlyphVector concepts
> > and functions as it's now handled internally by each backend's GlyphData
> > classes and embedded into the packed GPU IDs that they make.
> >
> > 3. Each backend's TextStrike implementation stores PackedGPUGlyphIDs as
> > the keys to GlyphEntries instead of SkPackedGlyphIDs. This ensures that
> > an atlas will only have a cache hit if all of the mask/padding/data-type
> > properties are consistent with what was in the atlas and what is
> > requested by the subrun being drawn.
> >
> > 4. Each backend's GlyphData implementation takes in these additional
> > properties in its constructor (propagating to initBackendData() calls).
> > It then resolves the mask format and padding with the configuration of
> > the backend's atlas manager so that all subsequent PackedGPUGlyphIDs
> > that it makes represent the final configuration.
> >
> > 5. The atlas manager's now require the format and padding to be
> > pre-resolved in many of their functions, such that 565 has been lifted
> > to RGBA8 and 1px of padding is added for direct masks if the caps
> > require all direct masks to have padding (this automatically makes
> > transformed mask subrun glyphs and direct mask subrun glyphs key the
> > same when fSupportBilerpAtlas is true).
> >
> > Backend-specific changes for Graphite:
> >
> > The properties that GlyphData requires for filling out the
> > PackedGPUGlyphID are basically the `sktext::gpu::RendererData` that was
> > being stored in the SubRunData object. I added the srcPadding to it and
> > then made it accessible from GlyphData, so it could be removed from
> > SubRunData. This keeps the number of arguments to the various functions
> > fairly concise.
> >
> > As part of this, RendererData moved from SubRunContainer.h to
> > GlyphVector.h
> >
> > Backend-specific changes for Ganesh:
> >
> > Ganesh didn't use RendererData, so its GlyphData just takes the
> > parameters in directly and from the AtlasTextOp. Its GlyphData class
> > also had an unused declared constructor that I removed, and
> > GrAtlasManager::addToAtlas() could be made private (which made enforcing
> > resolvedMaskFormat easier).
> >
> > Bug: 514078656
> > Change-Id: I07977910f64d84e15fe6e4c687d8635afb356b7c
> > Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1292556
> > Reviewed-by: Alexis Cruz-Ayala <alexisdavidc@google.com>
> > Reviewed-by: Robert Phillips <robertphillips@google.com>
> > Commit-Queue: Michael Ludwig <michaelludwig@google.com>
>
> Bug: 514078656
> No-Presubmit: true
> No-Tree-Checks: true
> No-Try: true
> Change-Id: Iec81ee8afd60d7fd9864bdc9dff8d4cca2c95d51
> Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1314716
> Bot-Commit: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
> Commit-Queue: Michael Ludwig <michaelludwig@google.com>

Bug: 514078656
Fixed: 514078656
Change-Id: I98e05b49519f829b82ced452e07d5b97f8195be4
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1314717
Commit-Queue: Robert Phillips <robertphillips@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
Auto-Submit: Michael Ludwig <michaelludwig@google.com>
---

diff --git a/bench/GlyphQuadFillBench.cpp b/bench/GlyphQuadFillBench.cpp
index 6f69a71..d6414a2 100644
--- a/bench/GlyphQuadFillBench.cpp
+++ b/bench/GlyphQuadFillBench.cpp
@@ -15,7 +15,7 @@
 #include "src/core/SkStrikeCache.h"
 #include "src/core/SkUTF.h"
 #include "src/core/SkUtils.h"
-#include "src/gpu/ganesh/GrRecordingContextPriv.h"
+#include "src/gpu/ganesh/GrDirectContextPriv.h"
 #include "src/gpu/ganesh/SkGr.h"
 #include "src/gpu/ganesh/text/GlyphData.h"
 #include "src/text/GlyphRun.h"
@@ -73,7 +73,15 @@
                 sktext::gpu::TextBlobTools::FirstSubRun(fBlob.get());
         SkASSERT_RELEASE(subRun);
         if (!subRun->glyphVector().hasBackendData()) {
-            subRun->glyphVector().initBackendData<GlyphData>(&fCache, subRun->maskFormat());
+            // Since isSuitableFor() requires Ganesh and this is nanobench, we know the canvas
+            // will be backed by a direct context.
+            GrDirectContext* ctx = canvas->recordingContext()->asDirectContext();
+            GrAtlasManager* atlasMgr = ctx->priv().getAtlasManager();
+            subRun->glyphVector().initBackendData<GlyphData>(&fCache,
+                                                             atlasMgr,
+                                                             subRun->maskFormat(),
+                                                             subRun->glyphSrcPadding(),
+                                                             /*isSDF=*/false);
         }
         const auto& glyphData = subRun->glyphVector().accessBackendData<GlyphData>();
         fVertices.reset(new char[glyphData.vertexStride(subRun->maskFormat(), drawMatrix) *
diff --git a/gn/gpu.gni b/gn/gpu.gni
index 7eaa81e..9956ad7 100644
--- a/gn/gpu.gni
+++ b/gn/gpu.gni
@@ -1026,6 +1026,7 @@
   "$_src/text/gpu/GlyphUtils.h",
   "$_src/text/gpu/GlyphVector.cpp",
   "$_src/text/gpu/GlyphVector.h",
+  "$_src/text/gpu/PackedGPUGlyphID.h",
   "$_src/text/gpu/SDFMaskFilter.cpp",
   "$_src/text/gpu/SDFMaskFilter.h",
   "$_src/text/gpu/SkChromeRemoteGlyphCache.cpp",
diff --git a/src/core/SkGlyph.h b/src/core/SkGlyph.h
index a92d886..c3f92b7 100644
--- a/src/core/SkGlyph.h
+++ b/src/core/SkGlyph.h
@@ -103,19 +103,19 @@
         return this->fID < that.fID;
     }
 
-    SkGlyphID glyphID() const {
+    constexpr SkGlyphID glyphID() const {
         return (fID >> kGlyphID) & kGlyphIDMask;
     }
 
-    uint32_t value() const {
+    constexpr uint32_t value() const {
         return fID;
     }
 
-    SkFixed getSubXFixed() const {
+    constexpr SkFixed getSubXFixed() const {
         return this->subToFixed(kSubPixelX);
     }
 
-    SkFixed getSubYFixed() const {
+    constexpr SkFixed getSubYFixed() const {
         return this->subToFixed(kSubPixelY);
     }
 
diff --git a/src/gpu/ganesh/ops/AtlasTextOp.cpp b/src/gpu/ganesh/ops/AtlasTextOp.cpp
index 9f29b1f..883d98e 100644
--- a/src/gpu/ganesh/ops/AtlasTextOp.cpp
+++ b/src/gpu/ganesh/ops/AtlasTextOp.cpp
@@ -560,7 +560,11 @@
         const sktext::gpu::AtlasSubRun& subRun = geo->fSubRun;
 
         if (!subRun.glyphVector().hasBackendData()) {
-            subRun.glyphVector().initBackendData<GlyphData>(target->strikeCache(), maskFormat);
+            subRun.glyphVector().initBackendData<GlyphData>(target->strikeCache(),
+                                                            atlasManager,
+                                                            maskFormat,
+                                                            subRun.glyphSrcPadding(),
+                                                            this->usesDistanceFields());
         }
 
         auto& glyphData = subRun.glyphVector().accessBackendData<GlyphData>();
@@ -579,8 +583,6 @@
             auto [ok, glyphsRegenerated] = glyphData.regenerateAtlas(subRunCursor,
                                                                      regenEnd,
                                                                      subRun.glyphVector(),
-                                                                     maskFormat,
-                                                                     subRun.glyphSrcPadding(),
                                                                      target);
 
             // There was a problem allocating the glyph in the atlas. Bail.
diff --git a/src/gpu/ganesh/ops/AtlasTextOp.h b/src/gpu/ganesh/ops/AtlasTextOp.h
index 091b58c..3a2a265 100644
--- a/src/gpu/ganesh/ops/AtlasTextOp.h
+++ b/src/gpu/ganesh/ops/AtlasTextOp.h
@@ -251,6 +251,7 @@
                MaskType::kLCDDistanceField == this->maskType();
     }
Loading diff…

Original Bug Report

reported by vm...@google.com

Cross-origin information leak in GPU process via Skia glyph atlas cache key collision

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: Skia’s glyph atlas management incorrectly omits padding requirements from its cache key, allowing subruns with different padding needs to share the same atlas entry. This leads to bilinear filtering bleeding texels from adjacent atlas cells belonging to other origins into the rendered output.

Affected files:

  • third_party/skia/src/gpu/ganesh/text/GlyphData.h
  • third_party/skia/src/gpu/graphite/text/GlyphData.h
  • third_party/skia/src/gpu/ganesh/text/GlyphData.cpp
  • third_party/skia/src/gpu/graphite/text/GlyphData.cpp
  • third_party/skia/src/gpu/ganesh/text/TextStrike.cpp
  • third_party/skia/src/gpu/graphite/text/TextStrike.cpp
  • third_party/skia/src/gpu/ganesh/text/GrAtlasManager.cpp
  • third_party/skia/src/gpu/graphite/text/TextAtlasManager.cpp
  • third_party/skia/src/text/gpu/SubRunContainer.cpp

Estimated timestamp from git blame: 2026-03-10

Summary

A vulnerability in Skia’s glyph atlas management potentially allows for a cross-origin information leak in the GPU process. The issue stems from the GlyphEntryKey struct, used to cache glyphs within a strike, omitting the srcPadding field. This omission causes subruns with different padding requirements to incorrectly share the same atlas entry. Specifically, an entry created by a subrun that does not require a bilerp-safe border (like a DirectMaskSubRun) can be reused by a subrun that does (like a TransformedMaskSubRun). When the latter reuses the borderless entry and applies kLinear filtering, it bleeds texels from adjacent atlas cells belonging to other origins into the rendered output.

Root Cause Analysis

The root cause is located in the GlyphEntryKey definition in both Ganesh (third_party/skia/src/gpu/ganesh/text/GlyphData.h) and Graphite (third_party/skia/src/gpu/graphite/text/GlyphData.h) backends. The key only considers SkPackedGlyphID and MaskFormat:

struct GlyphEntryKey {
    explicit GlyphEntryKey(SkPackedGlyphID id, MaskFormat format) : fPackedID(id), fFormat(format) {}
    const SkPackedGlyphID fPackedID;
    MaskFormat fFormat;
    bool operator==(const GlyphEntryKey& that) const {
        return fPackedID == that.fPackedID && fFormat == that.fFormat;
    }
};

A GlyphEntry’s atlas location is populated the first time a subrun calls regenerateAtlas. Subsequent subruns for the same glyph and format will find the entry via TextStrike::getGlyph and, seeing that the glyph is already in the atlas (via atlasManager->hasGlyph), skip the addGlyphToAtlas call.

In Chromium, fSupportBilerpAtlas is typically false by default (tied to the kRawDraw feature). This leads to the following potential exploit scenario:

  1. A compromised renderer sends a malicious DrawSlugOp containing a DirectMaskSubRun followed by a TransformedMaskSubRun for the same glyph G and strike S.
  2. The DirectMaskSubRun has srcPadding=0. It populates the atlas with glyph G without any padding or border. Adjacent glyphs (potentially from other origins) are packed tightly against it by the rectanizer.
  3. The TransformedMaskSubRun has srcPadding=1. It reuses the existing GlyphEntry because the cache key matches. The logic that normally handles padding and UV insetting in addGlyphToAtlas is skipped entirely because the glyph is already marked as present in the atlas.
  4. The TransformedMaskSubRun is rendered using kLinear filtering. Because the atlas entry lacks the expected border and the UVs were not inset, bilinear sampling at the edges of the glyph quad bleeds texels from the neighboring atlas cells (which may contain data from other origins) into the coverage output.

This behavior also bypasses the SkASSERT_RELEASE(width > 2*srcPadding) hardening because that check is located within the addGlyphToAtlas function, which is skipped on reuse.

Impact

Since the glyph atlas and strike cache are shared across all RasterDecoder instances (and thus all web origins) within the GPU process, a compromised renderer can potentially leak texture data from other origins. By magnifying the rendered output and using known-plaintext subtraction, an attacker might be able to recover the coverage values of adjacent glyphs in the shared atlas. This constitutes a potential high-severity cross-origin information leak in the GPU process.

Potential Steps to Reproduce

  1. From a compromised renderer, send strike data defining a strike S with one kA8 glyph G (e.g., 6x6 pixels).
  2. Serialize a DrawSlugOp whose SubRunContainer contains, in order:
    • (a) A DirectMaskSubRun referencing G from strike S with an identity matrix.
    • (b) A TransformedMaskSubRun referencing the same G and S but with a scaling matrix (e.g., 0.25x scale for 4x magnification).
  3. Execute the slug into an attacker-controlled SharedImage tile via the RasterCHROMIUM interface.
  4. Read back the tile pixels and subtract the known coverage of G to isolate the contribution from adjacent atlas cells.

Note: These are potential steps as our tooling does not yet have the ability to run code.

Suggested Fix

The GlyphEntryKey struct should be updated to include the srcPadding field (or the resulting internal padding) to ensure that subruns with different padding requirements do not share the same atlas entry. Corresponding changes should be made to TextStrike::getGlyph and the GlyphEntry constructor.

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