CVE-2026-84359
Overview
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
SkGlyphIDtogether 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.
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
- Render text needing one format
A page draws GPU-accelerated text that populates the glyph atlas with an entry keyed by
SkPackedGlyphIDunder oneMaskFormat/padding/data-type. - 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.
- 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.
- 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.
- 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
Files Changed
bench/GlyphQuadFillBench.cppgn/gpu.gnisrc/core/SkGlyph.hsrc/gpu/ganesh/ops/AtlasTextOp.cppsrc/gpu/ganesh/ops/AtlasTextOp.h
Audit Directions
- Incomplete cache keysAudit 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 parametersFlag 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 conflationReview 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.
Patch
From 2c88cbf6fba335c230bab575b3f76a030f30cc41 Mon Sep 17 00:00:00 2001 From: Michael Ludwig <[email protected]> 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 <[email protected]> > > Reviewed-by: Robert Phillips <[email protected]> > > Commit-Queue: Michael Ludwig <[email protected]> > > 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: [email protected] <[email protected]> > Commit-Queue: Michael Ludwig <[email protected]> Bug: 514078656 Fixed: 514078656 Change-Id: I98e05b49519f829b82ced452e07d5b97f8195be4 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1314717 Commit-Queue: Robert Phillips <[email protected]> Reviewed-by: Robert Phillips <[email protected]> Auto-Submit: Michael Ludwig <[email protected]> --- 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(); }
Original Bug Report
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.hthird_party/skia/src/gpu/graphite/text/GlyphData.hthird_party/skia/src/gpu/ganesh/text/GlyphData.cppthird_party/skia/src/gpu/graphite/text/GlyphData.cppthird_party/skia/src/gpu/ganesh/text/TextStrike.cppthird_party/skia/src/gpu/graphite/text/TextStrike.cppthird_party/skia/src/gpu/ganesh/text/GrAtlasManager.cppthird_party/skia/src/gpu/graphite/text/TextAtlasManager.cppthird_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:
- A compromised renderer sends a malicious
DrawSlugOpcontaining aDirectMaskSubRunfollowed by aTransformedMaskSubRunfor the same glyphGand strikeS. - The
DirectMaskSubRunhassrcPadding=0. It populates the atlas with glyphGwithout any padding or border. Adjacent glyphs (potentially from other origins) are packed tightly against it by the rectanizer. - The
TransformedMaskSubRunhassrcPadding=1. It reuses the existingGlyphEntrybecause the cache key matches. The logic that normally handles padding and UV insetting inaddGlyphToAtlasis skipped entirely because the glyph is already marked as present in the atlas. - The
TransformedMaskSubRunis rendered usingkLinearfiltering. 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
- From a compromised renderer, send strike data defining a strike
Swith onekA8glyphG(e.g., 6x6 pixels). - Serialize a
DrawSlugOpwhoseSubRunContainercontains, in order:- (a) A
DirectMaskSubRunreferencingGfrom strikeSwith an identity matrix. - (b) A
TransformedMaskSubRunreferencing the sameGandSbut with a scaling matrix (e.g., 0.25x scale for 4x magnification).
- (a) A
- Execute the slug into an attacker-controlled SharedImage tile via the
RasterCHROMIUMinterface. - Read back the tile pixels and subtract the known coverage of
Gto 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.