High watch ✔︎ verified CVSS 3.1 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Skia
DescriptionInappropriate implementation in Skia
ComponentSkia
Sub-componentGanesh
Bug ClassLogic Error
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker514017326
Fix commit43f969a9bce8 (skia) +15/-4
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-05-20 (public git)
Disclosed2026-05-27
Patch gap7 days — fix public in source before the advisory

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.

Key insight
The core mistake was selecting bilinear filtering from the transform flag alone, ignoring whether the atlas glyph actually had padding to safely interpolate; the fix gates linear sampling on positive glyphSrcPadding().

Attack Path

  1. 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)
  2. Force a transform mismatch Issue a text draw whose state sets fNeedsGlyphTransform, which pre-fix selected kLinear regardless of padding (src/gpu/ganesh/ops/AtlasTextOp.cpp:494).
  3. 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)
  4. Read back Recover the blended pixels to infer neighboring texel values. (inferred: read-back path not shown in the material)

Impact Assessment

The flaw allowed bilinear sampling of an unpadded shared atlas, letting rendered output blend in adjacent glyph texels, which the advisory frames as a cross-origin pixel information leak in the GPU process. The advisory rates it high severity with no in-the-wild exploitation; how a Chrome renderer reaches this Skia path (e.g., via serialized slug draws) is not shown in the material and is (inferred).

Changed Functions

FunctionChangeNotes
if
src/gpu/ganesh/ops/AtlasTextOp.cpp
modified

Files Changed

  • src/gpu/ganesh/ops/AtlasTextOp.cpp

Audit Directions

  • Filter vs. padding coupling
    Wherever a sampler filter is chosen, confirm the sampled resource was laid out (padded) for that filter mode.
  • Op-merge invariants
    When 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 metadata
    Treat deserialized matrices/flags that steer GPU sampling as untrusted and confirm they cannot relax a security-relevant sampling constraint.
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;
     }
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cpp
  • third_party/skia/src/text/gpu/VertexFiller.cpp
  • third_party/skia/src/gpu/ganesh/text/GrAtlasManager.cpp
  • third_party/skia/src/text/gpu/SubRunContainer.cpp
  • third_party/skia/src/gpu/ganesh/text/GlyphData.cpp
  • third_party/skia/src/gpu/ganesh/effects/GrBitmapTextGeoProc.cpp
  • third_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).

  1. Unvalidated Metadata: When a renderer sends a cc::DrawSlugOp, the payload includes a Slug object. This object contains VertexFiller metadata, specifically an fCreationMatrix. The GPU process deserializes this matrix directly from the renderer-supplied buffer in VertexFiller::MakeFromBuffer (third_party/skia/src/text/gpu/VertexFiller.cpp:61) without verifying its validity against the actual drawing context.

  2. Forced Bilinear Filtering: During rasterization, AtlasTextOp decides whether to use kNearest or kLinear (bilinear) filtering based on the fNeedsGlyphTransform flag. This flag is set to true if the draw-time Canvas Transformation Matrix (CTM) does not match the stored fCreationMatrix (e.g., if there is a sub-pixel translation or scale). By forging the fCreationMatrix to mismatch the actual CTM, a compromised renderer can force fNeedsGlyphTransform to be true (third_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cpp:274).

  3. Bilinear Bleed: When fNeedsGlyphTransform is true, the GPU sampler is set to GrSamplerState::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

  1. 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).
  2. Generate a malicious Slug payload where the fCreationMatrix is intentionally forged to mismatch the target draw CTM by a sub-pixel amount (e.g., 0.5 pixels).
  3. Send a cc::DrawSlugOp containing this Slug to the GPU process.
  4. Execute a draw with the Slug using the CTM that triggers the mismatch.
  5. 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.

View on issue tracker