CVE-2026-113106
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
SmallPathShapeDataKeysrc/gpu/ganesh/ops/SmallPathShapeData.cpp |
modified | |
SmallPathShapeDataKeysrc/gpu/ganesh/ops/SmallPathShapeData.h |
modified | |
SmallPathShapeDataKeysrc/gpu/ganesh/ops/SmallPathShapeData.h |
modified | |
SmallPathShapeDatasrc/gpu/ganesh/ops/SmallPathShapeData.h |
modified | |
DEF_TESTtests/SmallPathRendererTest.cpp |
modified |
Files Changed
tests/SmallPathRendererTest.cppsrc/gpu/ganesh/ops/SmallPathShapeData.hsrc/gpu/ganesh/ops/SmallPathShapeData.cpp
Patch
From f9247c57c7d1eb1fcf40a4c846e6a2065c249189 Mon Sep 17 00:00:00 2001
From: Thomas Smith <thomsmit@google.com>
Date: Wed, 23 Sep 2026 10:24:26 -0400
Subject: [PATCH] [ganesh] Prevent SmallPathAtlas SDF and Bitmap collision
* Very clever draws can cause a collision between SDFs and Bitmaps in
Ganesh's SmallPathAtlas, leading to a potential data leak.
* This patch adds a data tag to the SmallPath entries, which precludes
collisions.
Bug: b/553126587
Change-Id: I2fcca20ff9d61a4b63f54ee66ce53896b9ef5736
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1369157
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Thomas Smith <thomsmit@google.com>
---
diff --git a/src/gpu/ganesh/ops/SmallPathShapeData.cpp b/src/gpu/ganesh/ops/SmallPathShapeData.cpp
index 28e767d..e6257ad 100644
--- a/src/gpu/ganesh/ops/SmallPathShapeData.cpp
+++ b/src/gpu/ganesh/ops/SmallPathShapeData.cpp
@@ -24,9 +24,10 @@
SkASSERT(shape.style().isSimpleFill());
SkASSERT(shape.hasUnstyledKey());
int shapeKeySize = shape.unstyledKeySize();
- fKey.reset(1 + shapeKeySize);
- fKey[0] = dim;
- shape.writeUnstyledKey(&fKey[1]);
+ fKey.reset(2 + shapeKeySize);
+ fKey[0] = static_cast<uint32_t>(Type::kSDF);
+ fKey[1] = dim;
+ shape.writeUnstyledKey(&fKey[2]);
}
SmallPathShapeDataKey::SmallPathShapeDataKey(const GrStyledShape& shape, const SkMatrix& ctm) {
@@ -47,13 +48,14 @@
SkFixed fracX = SkScalarToFixed(tx) & 0x0000FF00;
SkFixed fracY = SkScalarToFixed(ty) & 0x0000FF00;
int shapeKeySize = shape.unstyledKeySize();
- fKey.reset(5 + shapeKeySize);
- fKey[0] = SkFloat2Bits(sx);
- fKey[1] = SkFloat2Bits(sy);
- fKey[2] = SkFloat2Bits(kx);
- fKey[3] = SkFloat2Bits(ky);
- fKey[4] = fracX | (fracY >> 8);
- shape.writeUnstyledKey(&fKey[5]);
+ fKey.reset(6 + shapeKeySize);
+ fKey[0] = static_cast<uint32_t>(Type::kBitmap);
+ fKey[1] = SkFloat2Bits(sx);
+ fKey[2] = SkFloat2Bits(sy);
+ fKey[3] = SkFloat2Bits(kx);
+ fKey[4] = SkFloat2Bits(ky);
+ fKey[5] = fracX | (fracY >> 8);
+ shape.writeUnstyledKey(&fKey[6]);
}
} // namespace skgpu::ganesh
diff --git a/src/gpu/ganesh/ops/SmallPathShapeData.h b/src/gpu/ganesh/ops/SmallPathShapeData.h
index 908c5ad..ad5f235 100644
--- a/src/gpu/ganesh/ops/SmallPathShapeData.h
+++ b/src/gpu/ganesh/ops/SmallPathShapeData.h
@@ -40,6 +40,11 @@
class SmallPathShapeDataKey {
public:
+ enum class Type : uint32_t {
+ kSDF = 0,
+ kBitmap = 1,
+ };
+
// TODO: add a move variant
SmallPathShapeDataKey(const SmallPathShapeDataKey& that) {
fKey.reset(that.fKey.count());
@@ -63,10 +68,10 @@
const uint32_t* data() const { return fKey.get(); }
private:
- // The key is composed of the GrStyledShape's key, and either the dimensions of the DF
- // generated for the path (32x32 max, 64x64 max, 128x128 max) if an SDF image or
- // the matrix for the path with only fractional translation.
- skia_private::AutoSTArray<24, uint32_t> fKey;
+ // The key is composed of the domain tag (kSDF or KBitmap), GrStyledShape's key, and either the
+ // dimensions of the DF generated for the path (32x32 max, 64x64 max, 128x128 max) if an SDF
+ // image or the matrix for the path with only fractional translation.
+ skia_private::AutoSTArray<25, uint32_t> fKey;
};
class SmallPathShapeData {
diff --git a/tests/SmallPathRendererTest.cpp b/tests/SmallPathRendererTest.cpp
index 60d392e..5e74703 100644
--- a/tests/SmallPathRendererTest.cpp
+++ b/tests/SmallPathRendererTest.cpp
@@ -48,3 +48,94 @@
sdc->drawPath(nullptr, GrPaint(), GrAA::kYes, m, smolpath, style);
ctx->flushAndSubmit();
}
+
+#if !defined(SK_ENABLE_OPTIMIZE_SIZE)
+
+#include "src/core/SkFloatBits.h"
+#include "src/gpu/ganesh/geometry/GrStyledShape.h"
+#include "src/gpu/ganesh/ops/SmallPathShapeData.h"
+
+DEF_TEST(SmallPathShapeDataKey_DomainSeparation, reporter) {
+ SkPath path = SkPath::Polygon({{-1, -1}, {1, -1}, {1, 1}, {-1, 1}}, true);
+ GrStyledShape shape(path);
+ skgpu::ganesh::SmallPathShapeDataKey sdfKey(shape, 32);
+ skgpu::ganesh::SmallPathShapeDataKey bmKey(shape, SkMatrix::I());
+
+ REPORTER_ASSERT(reporter, !(sdfKey == bmKey));
+ REPORTER_ASSERT(reporter, sdfKey.data()[0] ==
+ static_cast<uint32_t>(skgpu::ganesh::SmallPathShapeDataKey::Type::kSDF));
+ REPORTER_ASSERT(reporter, bmKey.data()[0] ==
+ static_cast<uint32_t>(skgpu::ganesh::SmallPathShapeDataKey::Type::kBitmap));
+}
+
+DEF_TEST(SmallPathShapeDataKey_CollisionReproduction, reporter) {
+ // 1. Construct Shape B (Bitmap side)
+ const float p0_x = SkBits2Float(0x28);
+ const float p0_y = 0.45f;
+ const float p1_x = 0.2f; // "free"
+ const float p1_y = 0.45f;
+ const float p2_x = 0.49f;
+ const float p2_y = SkBits2Float(0x700000);
+ const float p3_x = 65.0f;
+ const float p3_y = SkBits2Float(0x6);
+ const float p4_x = -1.0f;
+ const float p4_y = 1.5f;
+
+ SkPath pathB = SkPathBuilder()
+ .moveTo(p0_x, p0_y)
+ .lineTo(p1_x, p1_y)
+ .lineTo(p2_x, p2_y)
+ .quadTo(p3_x, p3_y, p4_x, p4_y)
+ .detach();
+ pathB.setFillType(SkPathFillType::kWinding);
+
+ SkMatrix ctmB = SkMatrix::MakeAll(
+ SkBits2Float(162), -1.0f, 0.0f,
+ -1.0f, SkBits2Float(0x10C), 0.0f,
+ 0.0f, 0.0f, 1.0f
+ );
+
+ GrStyledShape shapeB(pathB);
+ skgpu::ganesh::SmallPathShapeDataKey keyB(shapeB, ctmB);
+
+ // 2. Construct Shape A (DF side)
+ SkRect rectA = SkRect::MakeLTRB(-1.0f, -1.0f, 0.0f, SkBits2Float(0x110));
+ SkVector radiiA[4] = {
+ { SkBits2Float(4), SkBits2Float(0x02010100) }, // UL
+ { SkBits2Float(0x28), 0.45f }, // UR
+ { 0.2f, 0.45f }, // LR
+ { 0.49f, SkBits2Float(0x700000) } // LL
+ };
+ SkRRect rrectA;
+ rrectA.setRectRadii(rectA, radiiA);
+
+ SkPaint strokePaint;
+ strokePaint.setStyle(SkPaint::kStroke_Style);
+ strokePaint.setStrokeWidth(1.5f);
+ strokePaint.setStrokeJoin(SkPaint::kRound_Join);
+ strokePaint.setStrokeCap(SkPaint::kButt_Cap);
+ GrStyle styleA(strokePaint);
+
+ GrStyledShape parentShapeA(rrectA, styleA);
+ GrStyledShape shapeA = parentShapeA.applyStyle(GrStyle::Apply::kPathEffectAndStrokeRec, 65.0f);
+ skgpu::ganesh::SmallPathShapeDataKey keyA(shapeA, 162);
+
+ // Both keys have identical lengths (19 words: 1 word tag + 18 payload words)
+ REPORTER_ASSERT(reporter, keyA.count32() == 19);
+ REPORTER_ASSERT(reporter, keyB.count32() == 19);
+
+ // Verify the 18 payload words match byte-for-byte!
+ // Without the domain tag, this would have been an exact collision.
+ int payloadDiff = memcmp(&keyA.data()[1], &keyB.data()[1], 18 * sizeof(uint32_t));
+ REPORTER_ASSERT(reporter, payloadDiff == 0);
+
+ // With the domain tag, keyA != keyB:
+ REPORTER_ASSERT(reporter, !(keyA == keyB));
+ REPORTER_ASSERT(reporter, keyA.data()[0] ==
+ static_cast<uint32_t>(skgpu::ganesh::SmallPathShapeDataKey::Type::kSDF));
+ REPORTER_ASSERT(reporter, keyB.data()[0] ==
+ static_cast<uint32_t>(skgpu::ganesh::SmallPathShapeDataKey::Type::kBitmap));
+}
+
+#endif // !defined(SK_ENABLE_OPTIMIZE_SIZE)
+
Original Bug Report
Cross-origin pixel leak via SmallPathShapeDataKey SDF/bitmap collision in Ganesh
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: In Skia Ganesh’s SmallPathAtlasMgr, distance-field (SDF) and bitmap shape cache keys share a single cache without a domain or type discriminator tag, allowing crafted keys to collide. A collision allows an SDF path lookup to hit a bitmap entry, bypassing distance-field atlas padding insets (insetSrc(SK_DistanceFieldPad)). When rendered with linear filtering, the unpadded atlas cell can bleed texels from neighboring cells in the GPU-shared SmallPathAtlas, potentially leaking cross-origin vector shape masks.
Affected files:
third_party/skia/src/gpu/ganesh/ops/SmallPathShapeData.cppthird_party/skia/src/gpu/ganesh/ops/SmallPathAtlasMgr.cppthird_party/skia/src/gpu/ganesh/ops/SmallPathRenderer.cppthird_party/skia/src/gpu/ganesh/ops/SmallPathShapeData.h
Estimated timestamp from git blame: 2017-02-28
Summary
In Skia Ganesh, SmallPathAtlasMgr manages a shared texture atlas (SmallPathAtlas) and cache (fShapeCache) used to store rasterized small path masks and distance fields. SmallPathShapeDataKey has two constructors with different key structures but no entry-type or domain tag:
- SDF Entries (
third_party/skia/src/gpu/ganesh/ops/SmallPathShapeData.cpp:21):fKey = [dim, unstyledShapeKey...](length: $1 + S$ words) - Bitmap Entries (
third_party/skia/src/gpu/ganesh/ops/SmallPathShapeData.cpp:32):fKey = [sx, sy, kx, ky, fracX|(fracY>>8), unstyledShapeKey...](length: $5 + S’$ words)
Key equality is evaluated via raw word count and memcmp (SmallPathShapeData.h:57-60), and both entry types are stored in the same hash table fShapeCache (SmallPathAtlasMgr.h:100). If an attacker crafts an SDF draw and a bitmap draw such that $1 + S = 5 + S’$ and the word contents match identically, an SDF lookup hits the bitmap cache entry.
Impact
When a cache hit occurs on reuse, SmallPathRenderer::onExecute assumes the entry was already added and skips addDFPathToAtlas (SmallPathRenderer.cpp:327-340). Consequently, the call to shapeData->fAtlasLocator.insetSrc(SK_DistanceFieldPad) (which applies a 4px inset to prevent bilinear filter bleed) is bypassed (SmallPathRenderer.cpp:393, 489). Bitmap entries are added with srcInset = 0 (SmallPathRenderer.cpp:557), leaving the UV coordinates flush with the cell bounds.
When the SDF path is rendered with GrDistanceFieldPathGeoProc using GrSamplerState::Filter::kLinear (SmallPathRenderer.cpp:242-245), the unpadded cell boundaries sample outside the cell via bilinear filtering. Because SmallPathAtlasMgr is a GrDirectContext singleton shared across all origins in Chromium’s GPU process, the neighboring atlas cells contain small vector paths, icons, or glyph silhouettes from other web origins, potentially resulting in cross-origin visual information disclosure.
Potential Trigger Steps
Note: These are suggested/potential steps based on source code analysis; our tooling agent does not have execution capabilities to run dynamic PoCs.
- Bitmap Entry Insertion: An attacker from a compromised renderer issues a draw call for an
SkPathwith 4 verbs and 5 points under an affine CTM chosen so that the 5 bitmap header words (sx, sy, kx, ky, frac) and 13 unstyled path key words match the 18-word layout of target shape $A$.SmallPathRenderertakes the bitmap branch, inserts the entry intofShapeCache, and uploads an A8 cell withsrcInset = 0. - Interleaved Cross-Origin Draws: Legitimate pages render small vector paths (e.g., SVG icons or checkbox glyphs) on the shared GPU context, packing masks into adjacent atlas cells.
- Colliding SDF Lookup: The attacker draws a stroked
SkRRect$A$ under uniform scaling ($65.0\text{f}$). The stroke is baked into a 17-word inherited key, and the transformed dimensions exceed 162px, selecting the SDF path withdim = 162. The 18-word SDF key[162, inheritedKey...]matches the 18-word bitmap key infShapeCache. - Bilinear Bleed Readout:
SmallPathRendererreuses the existing 0-inset entry and skipsinsetSrc(SK_DistanceFieldPad). The SDF shader bilerps adjacent atlas texels at quad boundaries, allowing the attacker to reconstruct neighbor shape alpha masks via standard render target pixel readback.
Suggested Fix
To prevent cross-family cache collisions:
- Add an explicit domain/entry-type tag to
SmallPathShapeDataKey(or incorporate anisSDF/paddingbit into the key header, similar tosktext::gpu::PackedGPUGlyphID), OR - Maintain two separate caches in
SmallPathAtlasMgr(e.g.,fSDFShapeCacheandfBitmapShapeCache).
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
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.