CVE-2026-10011
Overview
Background
- `AtlasTextOp`
- the Ganesh GPU op that draws text from a shared glyph atlas texture (
src/gpu/ganesh/ops/AtlasTextOp.cpp). - `fNeedsGlyphTransform`
- a per-op flag that, pre-fix, alone selected bilinear vs. nearest-neighbor sampling of the atlas (
src/gpu/ganesh/ops/AtlasTextOp.cpp:494). - `glyphSrcPadding()`
- the subrun’s border padding, in texels, reserved around each glyph in the atlas (introduced into the filter decision by the fix).
Root Cause Analysis
In AtlasTextOp::onPrepareDraws the sampler filter was chosen purely from the transform flag: auto filter = fNeedsGlyphTransform ? GrSamplerState::Filter::kLinear : GrSamplerState::Filter::kNearest; (src/gpu/ganesh/ops/AtlasTextOp.cpp:494-495), and the identical expression governed createDrawForGeneratedGlyphs when atlas pages grew (AtlasTextOp.cpp:650-651). The invariant that linear sampling may only be used when the glyph was packed with border padding was not checked, so a draw with fNeedsGlyphTransform true would bilinearly sample a zero-padded glyph and interpolate across neighboring atlas texels.
The fix tightens both sites to also require padding: it computes const bool hasGlyphPadding = fHead->fSubRun.glyphSrcPadding() > 0; and uses kLinear only when fNeedsGlyphTransform && hasGlyphPadding, else kNearest (diff @@ -491 and @@ -647). Because the chosen filter looks only at the head geometry’s padding, AtlasTextOp::onCombineIfPossible (AtlasTextOp.cpp:667) is also tightened to refuse merging ops whose glyphSrcPadding() differs (diff @@ -677), so a combined op cannot silently apply the head’s filter to a differently-padded subrun.
The patch therefore restores the “no linear sampling without padding” invariant rather than validating the renderer-supplied transform metadata.
glyphSrcPadding().Attack Path
- Prime the atlas Cause victim-origin glyphs to be packed, with zero padding, into the shared Ganesh glyph atlas. (inferred: the cross-origin priming mechanism is not in the material)
- Force a transform mismatch
Issue a text draw whose state sets
fNeedsGlyphTransform, which pre-fix selectedkLinearregardless of padding (src/gpu/ganesh/ops/AtlasTextOp.cpp:494). - Bilinear bleed Bilinear sampling of a zero-padded glyph interpolates adjacent atlas texels into the rasterized output. (inferred: that the neighbors belong to another origin)
- Read back Recover the blended pixels to infer neighboring texel values. (inferred: read-back path not shown in the material)
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/ops/AtlasTextOp.cpp |
modified |
Files Changed
src/gpu/ganesh/ops/AtlasTextOp.cpp
Audit Directions
- Filter vs. padding couplingWherever a sampler filter is chosen, confirm the sampled resource was laid out (padded) for that filter mode.
- Op-merge invariantsWhen a combined op reuses the head’s state for all geometries, verify every merge predicate covers each field that state depends on.
- Renderer-supplied transform metadataTreat deserialized matrices/flags that steer GPU sampling as untrusted and confirm they cannot relax a security-relevant sampling constraint.
Patch
From 43f969a9bce8019354d392f384fe10328655f924 Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
Date: Tue, 19 May 2026 14:53:32 -0400
Subject: [PATCH] [ganesh] Require glyph padding to use linear sampling
Bug: b/514017326
Change-Id: I9425c1f86aa8762333ba72b4ba30f5a056cdbfa9
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1239738
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
---
diff --git a/src/gpu/ganesh/ops/AtlasTextOp.cpp b/src/gpu/ganesh/ops/AtlasTextOp.cpp
index e1005ce..d335480 100644
--- a/src/gpu/ganesh/ops/AtlasTextOp.cpp
+++ b/src/gpu/ganesh/ops/AtlasTextOp.cpp
@@ -491,8 +491,12 @@
} else
#endif
{
- auto filter = fNeedsGlyphTransform ? GrSamplerState::Filter::kLinear
- : GrSamplerState::Filter::kNearest;
+ // Only use linear padding if the glyphs were also padded for it. If we somehow get a direct
+ // subrun with a corrupted transform, we should still use nearest neighbor since it was
+ // packed tightly.
+ const bool hasGlyphPadding = fHead->fSubRun.glyphSrcPadding() > 0;
+ auto filter = fNeedsGlyphTransform && hasGlyphPadding ? GrSamplerState::Filter::kLinear
+ : GrSamplerState::Filter::kNearest;
// Bitmap text uses a single color, combineIfPossible ensures all geometries have the same
// color, so we can use the first's without worry.
flushInfo.fGeometryProcessor = GrBitmapTextGeoProc::Make(
@@ -647,8 +651,9 @@
} else
#endif
{
- auto filter = fNeedsGlyphTransform ? GrSamplerState::Filter::kLinear
- : GrSamplerState::Filter::kNearest;
+ const bool hasGlyphPadding = fHead->fSubRun.glyphSrcPadding() > 0;
+ auto filter = fNeedsGlyphTransform && hasGlyphPadding
+ ? GrSamplerState::Filter::kLinear : GrSamplerState::Filter::kNearest;
reinterpret_cast<GrBitmapTextGeoProc*>(gp)->addNewViews(views, numActiveViews, filter);
}
}
@@ -677,6 +682,12 @@
return CombineResult::kCannotCombine;
}
+ // We use the same filter for every Geometry that is combined, but the filter choice only looks
+ // at the head's src padding, so we can only combine if we are consistent with that.
+ if (fHead->fSubRun.glyphSrcPadding() != that->fHead->fSubRun.glyphSrcPadding()) {
+ return CombineResult::kCannotCombine;
+ }
+
if (fProcessors != that->fProcessors) {
return CombineResult::kCannotCombine;
}
Original Bug Report
Potential cross-origin information disclosure via bilinear bleed in Skia Ganesh glyph atlas
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 logic flaw in Skia’s Ganesh backend allows a compromised renderer to force bilinear sampling on a shared GPU glyph atlas that lacks texel padding. By providing forged matrix metadata in serialized Slug objects, an attacker can bypass nearest-neighbor sampling constraints. The resulting sampling bleed enables the potential disclosure of cross-origin pixel data from the shared atlas.
Affected files:
third_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cppthird_party/skia/src/text/gpu/VertexFiller.cppthird_party/skia/src/gpu/ganesh/text/GrAtlasManager.cppthird_party/skia/src/text/gpu/SubRunContainer.cppthird_party/skia/src/gpu/ganesh/text/GlyphData.cppthird_party/skia/src/gpu/ganesh/effects/GrBitmapTextGeoProc.cppthird_party/skia/src/gpu/ganesh/Device.cpp
Estimated timestamp from git blame: Unknown (Google3 checkout)
Background
In Chrome’s GPU process, Skia’s Ganesh backend (used for Out-Of-Process Rasterization, or OOP-R) manages a shared glyph atlas. This atlas is stored in GPU textures and is shared across all renderer processes and origins to improve performance and memory efficiency. By default, Chrome disables fSupportBilerpFromGlyphAtlas, causing glyphs to be packed into the atlas with zero border padding (0 texels) to conserve space. This design assumes that glyphs in the atlas will only be sampled using nearest-neighbor (kNearest) filtering.
The Vulnerability
The vulnerability lies in how sktext::gpu::AtlasTextOp determines the sampler filter mode and how the GPU process handles serialized Slug objects (representations of text draws).
-
Unvalidated Metadata: When a renderer sends a
cc::DrawSlugOp, the payload includes aSlugobject. This object containsVertexFillermetadata, specifically anfCreationMatrix. The GPU process deserializes this matrix directly from the renderer-supplied buffer inVertexFiller::MakeFromBuffer(third_party/skia/src/text/gpu/VertexFiller.cpp:61) without verifying its validity against the actual drawing context. -
Forced Bilinear Filtering: During rasterization,
AtlasTextOpdecides whether to usekNearestorkLinear(bilinear) filtering based on thefNeedsGlyphTransformflag. This flag is set totrueif the draw-time Canvas Transformation Matrix (CTM) does not match the storedfCreationMatrix(e.g., if there is a sub-pixel translation or scale). By forging thefCreationMatrixto mismatch the actual CTM, a compromised renderer can forcefNeedsGlyphTransformto betrue(third_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cpp:274). -
Bilinear Bleed: When
fNeedsGlyphTransformis true, the GPU sampler is set toGrSamplerState::Filter::kLinear(third_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cpp:494). Since the atlas was packed with zero padding, bilinear sampling at the glyph boundaries will interpolate between the attacker’s glyph and adjacent glyphs in the shared atlas belonging to other origins.
Impact
An attacker can read back the rasterized output (for example, by drawing to an accelerated 2D canvas and using getImageData()). Because the output contains a blend of the attacker’s pixels and the victim’s cross-origin glyph texels, the attacker can mathematically reconstruct cross-origin text or color-emoji data. This constitutes a high-severity cross-origin information leak in the GPU process.
Potential Exploit Steps
- From a compromised renderer, send commands to prime the shared atlas with glyphs from a victim origin (e.g., by navigating an iframe to a target site).
- Generate a malicious
Slugpayload where thefCreationMatrixis intentionally forged to mismatch the target draw CTM by a sub-pixel amount (e.g., 0.5 pixels). - Send a
cc::DrawSlugOpcontaining this Slug to the GPU process. - Execute a draw with the Slug using the CTM that triggers the mismatch.
- Read back the resulting pixels from the canvas and solve for the neighboring texel values from the victim’s origin.
Suggested Fix
The GPU process should validate that the initialPositionMatrix (or creationMatrix) within a Slug is consistent with the drawing context. Alternatively, if fSupportBilerpFromGlyphAtlas is false, Skia should strictly enforce kNearest filtering for direct mask subruns, regardless of the transform metadata provided by the renderer. A debug-only check currently exists as a SkASSERT in Device::drawSlug (third_party/skia/src/gpu/ganesh/Device.cpp:1440); this invariant should be enforced at runtime in release builds.
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.
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/ganesh/Device.cpp;l=1440
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cpp;l=274
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cpp;l=494
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/text/gpu/VertexFiller.cpp;l=61