CVE-2026-3909
Overview
Background
- Ganesh
- Skia’s GPU backend that renders glyphs by caching their rasterized masks in a texture atlas.
- `MaskFormat`
- an enum describing a glyph mask’s pixel layout (
kA81 byte,kA5652 bytes,kARGB4 bytes per pixel), which determines the atlas it belongs to and its byte stride. - `SkPackedGlyphID`
- a compact identifier encoding a glyph index plus sub-pixel position, historically used as the sole atlas cache key.
- `GrAtlasManager`
- the component that allocates atlas space and copies a glyph’s raster image into the correct format-specific atlas texture.
Root Cause Analysis
Skia’s Ganesh text atlas identified cached glyphs by SkPackedGlyphID alone: GlyphEntry stored only fPackedID, and both TextStrike::getGlyph and GlyphData::makeGlyphFromID ignored the glyph’s MaskFormat. Because a single glyph can be requested from different AtlasSubRuns under different mask formats, the first lookup would cache a GlyphEntry bound to one format’s atlas location, and a later lookup with a different maskFormat would retrieve that same entry rather than allocating a new one. In GrAtlasManager::addGlyphToAtlas, the destination atlas and its bytesPerPixel stride were derived independently from skGlyph.maskFormat(), so a stale entry whose atlas location and byte layout were sized for one format could be written using the byte stride and dimensions of another, violating the invariant that a glyph’s cache entry, its atlas location, and the pixel stride used to fill it all share one MaskFormat. When the resolved bytesPerPixel exceeds what the target atlas region was allocated for, the image copy runs past the allocated bounds, producing the out-of-bounds write.
The fix makes MaskFormat part of the cache key via a new GlyphEntryKey, so distinct formats can no longer collide on the same entry, and addGlyphToAtlas now resolves the format from the entry’s stored fFormat so allocation and copy stay consistent.
MaskFormat from the glyph atlas cache key, letting glyphs of one pixel format alias entries allocated for another; the fix folds the format into a composite GlyphEntryKey and threads it through lookup, allocation, and the byte-per-pixel calculation so the entry, its atlas slot, and its copy stride always agree.Attack Path
- Render mixed-format text
A page draws glyphs (e.g. via a font mixing bitmap-color and alpha coverage masks) such that the same
SkPackedGlyphIDis emitted acrossAtlasSubRuns carrying differentMaskFormatvalues. - Prime the cache
The first subrun calls
TextStrike::getGlyphand creates aGlyphEntryandGrAtlasLocatorsized for itsMaskFormat. - Alias with a second format
A second subrun with a different
maskFormatlooks up the sameSkPackedGlyphID, retrieves the stale entry, and reuses its atlas location under the mismatched format. - Mismatched copy
GrAtlasManager::addGlyphToAtlascomputesbytesPerPixelfrom the wrongMaskFormat, so the glyph image is copied into the atlas with a stride that overruns the allocated region. - Out-of-bounds write The overrun corrupts memory adjacent to the atlas backing store, giving a controllable heap write during text rendering.
Impact Assessment
MaskFormat, which is reachable from ordinary web-controlled fonts and text.Changed Functions
| Function | Change | Notes |
|---|---|---|
forsrc/gpu/ganesh/text/GlyphData.cpp |
modified | |
TextStrikesrc/gpu/ganesh/text/GlyphData.h |
modified | |
ifsrc/gpu/ganesh/text/TextStrike.cpp |
modified |
Files Changed
bench/GlyphQuadFillBench.cppgn/tests.gnisrc/gpu/ganesh/ops/AtlasTextOp.cppsrc/gpu/ganesh/text/GlyphData.cppsrc/gpu/ganesh/text/GlyphData.hsrc/gpu/ganesh/text/GrAtlasManager.cppsrc/gpu/ganesh/text/TextStrike.cppsrc/gpu/ganesh/text/TextStrike.h
Audit Directions
- Composite cache keysAudit every cache or hash map keyed by a glyph or resource ID for other attributes (format, size, color type) that change memory layout but were omitted from the key, since such omissions let differently-shaped objects alias.
- Format re-derivationFlag places that recompute a
MaskFormatorbytesPerPixelfrom a source object (skGlyph.maskFormat()) rather than from the authoritative allocation record, because independent derivations can diverge and desynchronize stride from buffer size. - Stride-versus-allocationReview atlas and texture copy paths where the fill stride and the allocated region’s dimensions come from different code paths, and confirm both are governed by one shared
MaskFormatvalue.
Patch
From 0cab3e4ee34b3bca6ba7df676639d73ffe4b2135 Mon Sep 17 00:00:00 2001 From: Greg Daniel <[email protected]> Date: Tue, 10 Mar 2026 16:22:54 -0400 Subject: [PATCH] Make sure we are getting the correct atlas for glyph mask format. Bug: b/491421267 Change-Id: I4eacd46599eca2df8c10a3fc894b9ce890fae1e2 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1184076 Commit-Queue: Greg Daniel <[email protected]> Reviewed-by: Michael Ludwig <[email protected]> --- diff --git a/bench/GlyphQuadFillBench.cpp b/bench/GlyphQuadFillBench.cpp index c478602..b926c6f 100644 --- a/bench/GlyphQuadFillBench.cpp +++ b/bench/GlyphQuadFillBench.cpp @@ -73,7 +73,7 @@ sktext::gpu::TextBlobTools::FirstSubRun(fBlob.get()); SkASSERT_RELEASE(subRun); if (!subRun->glyphVector().hasBackendData()) { - subRun->glyphVector().initBackendData<GlyphData>(&fCache); + subRun->glyphVector().initBackendData<GlyphData>(&fCache, subRun->maskFormat()); } const auto& glyphData = subRun->glyphVector().accessBackendData<GlyphData>(); fVertices.reset(new char[glyphData.vertexStride(subRun->maskFormat(), drawMatrix) * diff --git a/gn/tests.gni b/gn/tests.gni index 902f44c..ba21a70 100644 --- a/gn/tests.gni +++ b/gn/tests.gni @@ -437,6 +437,7 @@ ganesh_tests_sources = [ "$_tests/AdvancedBlendTest.cpp", "$_tests/ApplyGammaTest.cpp", + "$_tests/AtlasOobTest.cpp", "$_tests/BackendAllocationTest.cpp", "$_tests/BackendSurfaceMutableStateTest.cpp", "$_tests/BlendTest.cpp", diff --git a/src/gpu/ganesh/ops/AtlasTextOp.cpp b/src/gpu/ganesh/ops/AtlasTextOp.cpp index eee7f52..26d10a4 100644 --- a/src/gpu/ganesh/ops/AtlasTextOp.cpp +++ b/src/gpu/ganesh/ops/AtlasTextOp.cpp @@ -538,7 +538,7 @@ const sktext::gpu::AtlasSubRun& subRun = geo->fSubRun; if (!subRun.glyphVector().hasBackendData()) { - subRun.glyphVector().initBackendData<GlyphData>(target->strikeCache()); + subRun.glyphVector().initBackendData<GlyphData>(target->strikeCache(), maskFormat); } auto& glyphData = subRun.glyphVector().accessBackendData<GlyphData>(); diff --git a/src/gpu/ganesh/text/GlyphData.cpp b/src/gpu/ganesh/text/GlyphData.cpp index c1416af..503ac353 100644 --- a/src/gpu/ganesh/text/GlyphData.cpp +++ b/src/gpu/ganesh/text/GlyphData.cpp @@ -171,7 +171,9 @@ GlyphData::~GlyphData() = default; -Glyph GlyphData::makeGlyphFromID(SkPackedGlyphID id) { return Glyph{fTextStrike->getGlyph(id)}; } +Glyph GlyphData::makeGlyphFromID(SkPackedGlyphID id, MaskFormat format) { + return Glyph{fTextStrike->getGlyph(id, format)}; +} std::tuple<bool, int> GlyphData::regenerateAtlas(int begin, int end, @@ -199,7 +201,7 @@ bool success = true; for (int i = begin; i < end; i++) { const Glyph& glyph = glyphSpan[i]; - + SkASSERT(glyph.entry().fGlyphEntryKey.fFormat == maskFormat); if (!atlasManager->hasGlyph(maskFormat, glyph.entry())) { const SkGlyph& skGlyph = *metricsAndImages.glyph(glyph.packedID()); auto code = atlasManager->addGlyphToAtlas(skGlyph, diff --git a/src/gpu/ganesh/text/GlyphData.h b/src/gpu/ganesh/text/GlyphData.h index 7715492..7c57d42 100644 --- a/src/gpu/ganesh/text/GlyphData.h +++ b/src/gpu/ganesh/text/GlyphData.h @@ -34,13 +34,31 @@ namespace skgpu::ganesh { class TextStrike; +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; + } + bool operator!=(const GlyphEntryKey& that) const { + return !(*this == that); + } + + uint32_t hash() const { + return fPackedID.hash(); + } +}; + /** * Ganesh-specific glyph type with atlas location information. */ struct GlyphEntry { - explicit GlyphEntry(SkPackedGlyphID id) : fPackedID(id) {} + explicit GlyphEntry(SkPackedGlyphID id, MaskFormat format) : fGlyphEntryKey(id, format) {} - const SkPackedGlyphID fPackedID; + const GlyphEntryKey fGlyphEntryKey; GrAtlasLocator fAtlasLocator; }; @@ -56,7 +74,7 @@ public: explicit Glyph(GlyphEntry* entry) : fEntry{entry} { SkASSERT(entry); } - SkPackedGlyphID packedID() const { return fEntry->fPackedID; } + SkPackedGlyphID packedID() const { return fEntry->fGlyphEntryKey.fPackedID; } GlyphEntry& entry() const { return *fEntry; } }; @@ -74,7 +92,7 @@ ~GlyphData(); - Glyph makeGlyphFromID(SkPackedGlyphID); + Glyph makeGlyphFromID(SkPackedGlyphID, MaskFormat); // Regenerate atlas entries for glyphs in range [begin, end). // Returns {success, glyphs_placed_in_atlas}. diff --git a/src/gpu/ganesh/text/GrAtlasManager.cpp b/src/gpu/ganesh/text/GrAtlasManager.cpp index f5dc77b..f1a2bd3 100644 --- a/src/gpu/ganesh/text/GrAtlasManager.cpp +++ b/src/gpu/ganesh/text/GrAtlasManager.cpp @@ -177,8 +177,7 @@ } SkASSERT(glyph != nullptr); - MaskFormat glyphFormat = sktext::gpu::FormatFromSkGlyph(skGlyph.maskFormat()); - MaskFormat expectedMaskFormat = this->resolveMaskFormat(glyphFormat); + MaskFormat expectedMaskFormat = this->resolveMaskFormat(glyph->fGlyphEntryKey.fFormat); int bytesPerPixel = MaskFormatBytesPerPixel(expectedMaskFormat); int padding; diff --git a/src/gpu/ganesh/text/TextStrike.cpp b/src/gpu/ganesh/text/TextStrike.cpp index 8771fca..94b3521 100644 --- a/src/gpu/ganesh/text/TextStrike.cpp +++ b/src/gpu/ganesh/text/TextStrike.cpp @@ -28,20 +28,21 @@ return newStrike; } -GlyphEntry* TextStrike::getGlyph(SkPackedGlyphID packedGlyphID) { - GlyphEntry* glyph = fCache.findOrNull(packedGlyphID); +GlyphEntry* TextStrike::getGlyph(SkPackedGlyphID packedGlyphID, MaskFormat format) { + GlyphEntryKey localKey(packedGlyphID, format); + GlyphEntry* glyph = fCache.findOrNull(localKey); if (glyph == nullptr) { - glyph = fAlloc.make<GlyphEntry>(packedGlyphID); + glyph = fAlloc.make<GlyphEntry>(packedGlyphID, format); fCache.set(glyph); this->addMemoryUsed(sizeof(GlyphEntry)); } return glyph; } -const SkPackedGlyphID& TextStrike::HashTraits::GetKey(const GlyphEntry* glyph) { - return glyph->fPackedID; +const GlyphEntryKey& TextStrike::HashTraits::GetKey(const GlyphEntry* glyph) { + return glyph->fGlyphEntryKey; } -uint32_t TextStrike::HashTraits::Hash(SkPackedGlyphID key) { return key.hash(); } +uint32_t TextStrike::HashTraits::Hash(GlyphEntryKey key) { return key.hash(); } } // namespace skgpu::ganesh diff --git a/src/gpu/ganesh/text/TextStrike.h b/src/gpu/ganesh/text/TextStrike.h index 8222eff..de4f421 100644 --- a/src/gpu/ganesh/text/TextStrike.h +++ b/src/gpu/ganesh/text/TextStrike.h @@ -10,6 +10,7 @@ #include "include/core/SkRefCnt.h" #include "src/core/SkTHash.h" +#include "src/gpu/MaskFormat.h" #include "src/text/gpu/StrikeCache.h" struct SkPackedGlyphID; @@ -18,6 +19,7 @@ namespace skgpu::ganesh { struct GlyphEntry; +struct GlyphEntryKey; /** * Ganesh-specific text strike cache entry. @@ -34,14 +36,15 @@ static sk_sp<TextStrike> GetOrCreate(sktext::gpu::StrikeCache* strikeCache, const SkStrikeSpec& strikeSpec);