Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Skia
DescriptionInformation leak in Skia
ComponentSkia
Bug ClassLogic Error
Tracker501759192
Fix commit013ad6e70972 (skia) +32/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
src/gpu/ganesh/ops/SmallPathAtlasMgr.cpp
modified
SmallPathIDChangeListener
src/gpu/ganesh/ops/SmallPathShapeData.h
modified
SmallPathShapeDataKey
src/gpu/ganesh/ops/SmallPathShapeData.h
modified
SmallPathShapeData
src/gpu/ganesh/ops/SmallPathShapeData.h
modified

Files Changed

  • src/gpu/ganesh/ops/SmallPathAtlasMgr.cpp
  • src/gpu/ganesh/ops/SmallPathAtlasMgr.h
  • src/gpu/ganesh/ops/SmallPathShapeData.h
From 013ad6e7097238654847bfa5a5fb2c6c3823ee41 Mon Sep 17 00:00:00 2001
From: Alexis Cruz-Ayala <[email protected]>
Date: Fri, 17 Jul 2026 12:51:34 -0400
Subject: [PATCH] [Security] Add a SmallPathIDChangeListener to SmallPathAtlasMgr

There was a vulnerability where SkPathID could have duplicate entries if an attacker created and discarded enough Small, Complex Paths to overwhelm the SmallPathAtlasMgr cache. This could lead to the attacker submitting a request to the cache for a path, and receiving a victim's path instead.

The solution involved mirroring what is done in the SoftwarePathRenderer.cpp and using an SkIDChangeListener to listen for when a path is modified or discarded and delete the entry in the cache. This way, duplicate entries are avoided.

A test was added that shows that an ID collision can be forced amongst SmallPaths with complex geometry; any hash of two small, complex paths would rely only on the genID, making it easier to hit a duplicate entry.

Bug: b/501759192
Change-Id: Id80a0167370dfca1b7dac511f1ddcfda16a72fc1
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1294996
Reviewed-by: Florin Malita <[email protected]>
Commit-Queue: Alexis Cruz-Ayala <[email protected]>
---

diff --git a/src/gpu/ganesh/ops/SmallPathAtlasMgr.cpp b/src/gpu/ganesh/ops/SmallPathAtlasMgr.cpp
index 6fa8667..81ca65f 100644
--- a/src/gpu/ganesh/ops/SmallPathAtlasMgr.cpp
+++ b/src/gpu/ganesh/ops/SmallPathAtlasMgr.cpp
@@ -11,10 +11,12 @@
 #include "include/gpu/GpuTypes.h"
 #include "include/gpu/ganesh/GrBackendSurface.h"
 #include "include/gpu/ganesh/GrTypes.h"
+#include "include/private/SkIDChangeListener.h"
 #include "include/private/SkTo.h"
 #include "include/private/gpu/ganesh/GrTypesPriv.h"
 #include "src/gpu/MaskFormat.h"
 #include "src/gpu/ganesh/GrCaps.h"
+#include "src/gpu/ganesh/geometry/GrStyledShape.h"
 #include "src/gpu/ganesh/ops/SmallPathShapeData.h"
 
 #include <cstddef>
@@ -86,11 +88,19 @@
     delete shapeData;
 }
 
-SmallPathShapeData* SmallPathAtlasMgr::findOrCreate(const SmallPathShapeDataKey& key) {
+SmallPathShapeData* SmallPathAtlasMgr::findOrCreate(const SmallPathShapeDataKey& key,
+                                                    const GrStyledShape& shape) {
     auto shapeData = fShapeCache.find(key);
+
+    if (shapeData && shapeData->fIDChangeListener->hasChanged()) {
+        this->deleteCacheEntry(shapeData);
+        shapeData = nullptr;
+    }
+
     if (!shapeData) {
         // TODO: move the key into the ctor
         shapeData = new SmallPathShapeData(key);
+        shape.addGenIDChangeListener(shapeData->fIDChangeListener);
         fShapeCache.add(shapeData);
         fShapeList.addToTail(shapeData);
 #ifdef DF_PATH_TRACKING
@@ -108,7 +118,7 @@
     SmallPathShapeDataKey key(shape, desiredDimension);
 
     // TODO: move the key into 'findOrCreate'
-    return this->findOrCreate(key);
+    return this->findOrCreate(key, shape);
 }
 
 SmallPathShapeData* SmallPathAtlasMgr::findOrCreate(const GrStyledShape& shape,
@@ -116,7 +126,7 @@
     SmallPathShapeDataKey key(shape, ctm);
 
     // TODO: move the key into 'findOrCreate'
-    return this->findOrCreate(key);
+    return this->findOrCreate(key, shape);
 }
 
 GrDrawOpAtlas::ErrorCode SmallPathAtlasMgr::addToAtlas(GrResourceProvider* resourceProvider,
diff --git a/src/gpu/ganesh/ops/SmallPathAtlasMgr.h b/src/gpu/ganesh/ops/SmallPathAtlasMgr.h
index c72fd42..9e17694 100644
--- a/src/gpu/ganesh/ops/SmallPathAtlasMgr.h
+++ b/src/gpu/ganesh/ops/SmallPathAtlasMgr.h
@@ -93,7 +93,7 @@
     void deleteCacheEntry(SmallPathShapeData*);
 
 private:
-    SmallPathShapeData* findOrCreate(const SmallPathShapeDataKey&);
+    SmallPathShapeData* findOrCreate(const SmallPathShapeDataKey&, const GrStyledShape&);
 
     void evict(GrPlotLocator) override;
 
diff --git a/src/gpu/ganesh/ops/SmallPathShapeData.h b/src/gpu/ganesh/ops/SmallPathShapeData.h
index 2828b0d..908c5ad 100644
--- a/src/gpu/ganesh/ops/SmallPathShapeData.h
+++ b/src/gpu/ganesh/ops/SmallPathShapeData.h
@@ -12,6 +12,7 @@
 #if !defined(SK_ENABLE_OPTIMIZE_SIZE)
 
 #include "include/core/SkRect.h"
+#include "include/private/SkIDChangeListener.h"
 #include "include/private/SkTemplates.h"
 #include "src/core/SkChecksum.h"
 #include "src/core/SkTInternalLList.h"
@@ -25,6 +26,18 @@
 
 namespace skgpu::ganesh {
 
+class SmallPathIDChangeListener : public SkIDChangeListener {
+public:
+    SmallPathIDChangeListener() : fHasChanged(false) {}
+
+    bool hasChanged() const { return fHasChanged.load(std::memory_order_relaxed); }
+
+    void changed() override { fHasChanged.store(true, std::memory_order_relaxed); }
+
+private:
+    std::atomic<bool> fHasChanged;
+};
+
 class SmallPathShapeDataKey {
 public:
     // TODO: add a move variant
@@ -58,7 +71,9 @@
 
 class SmallPathShapeData {
 public:
-    SmallPathShapeData(const SmallPathShapeDataKey& key) : fKey(key) {}
+    SmallPathShapeData(const SmallPathShapeDataKey& key)
+            : fKey(key), fIDChangeListener(sk_make_sp<SmallPathIDChangeListener>()) {}
+    ~SmallPathShapeData() { fIDChangeListener->markShouldDeregister(); }
 
     const SmallPathShapeDataKey fKey;
     SkRect                      fBounds;
@@ -73,6 +88,8 @@
     static inline uint32_t Hash(const SmallPathShapeDataKey& key) {
         return SkChecksum::Hash32(key.data(), sizeof(uint32_t) * key.count32());
     }
+
+    sk_sp<SmallPathIDChangeListener> fIDChangeListener;
 };
 
 }  // namespace skgpu::ganesh
Loading diff…

Original Bug Report

reported by [email protected]

Potential cross-origin info leak via 30-bit SkPathData ID wrap 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 without the Chrome Security team.

Overview: A potential cross-origin information leak exists in the GPU process when using the Ganesh rendering backend. An attacker could potentially wrap the 30-bit SkPathData generation ID counter to collide with a victim’s cached path in the shared SmallPathAtlasMgr. This collision could allow the attacker to read the alpha mask of the victim’s cross-origin paths.

Affected files:

  • third_party/skia/src/gpu/ganesh/ops/SmallPathAtlasMgr.cpp
  • third_party/skia/src/core/SkPathData.cpp
  • third_party/skia/src/core/SkPath.cpp
  • third_party/skia/src/gpu/ganesh/geometry/GrStyledShape.cpp
  • third_party/skia/src/gpu/ganesh/ops/SmallPathShapeData.cpp
  • third_party/skia/src/gpu/ganesh/ops/SmallPathRenderer.cpp

Estimated timestamp from git blame: 2025-11-03

A potential cross-origin information leak exists in the Ganesh backend’s SmallPathAtlasMgr. Due to a 30-bit wraparound in path generation IDs combined with cache sharing across origins in the GPU process, an attacker could potentially read the alpha masks (silhouettes) of small, complex paths rendered by other origins.

Technical Details

  1. 30-bit Generation ID Wraparound: In third_party/skia/src/core/SkPathData.cpp, the next_pathdata_unique_id() function generates unique IDs for SkPathData objects. It masks the ID to 30 bits (id <<= 2; id >>= 2;) to reserve space for fill type information. This causes the global genID counter to wrap around after approximately 1.07 billion allocations.
  2. Cache Key Truncation: When rendering small paths, SmallPathRenderer generates a cache key via GrStyledShape::writeUnstyledKey. If a path is complex (has more than 10 verbs, per kMaxKeyFromDataVerbCnt), the function drops the raw geometry (points and verbs) from the key. The key relies entirely on the path’s 30-bit genID and state (fill type, winding).
  3. Shared GPU Cache without Eviction: The SmallPathAtlasMgr caches these rendered paths into a GrDrawOpAtlas. Because it is owned by a GrDirectContext shared via SharedContextState in the GPU process, this cache is globally accessible across all renderer processes. Furthermore, unlike other renderers (e.g., SoftwarePathRenderer), SmallPathAtlasMgr does not register an SkIDChangeListener. The cache entry is not evicted when the original path is destroyed, keeping it alive until the atlas plot is randomly recycled.

Suggested Attacker Steps

Note: These are potential steps based on code analysis. Our tooling agent does not execute code to verify a proof-of-concept.

  1. A victim origin renders a small, complex path (e.g., >10 verbs, bounds <73px), such as an SVG icon or custom font glyph. This caches the path’s alpha mask in the GPU process’s shared SmallPathAtlasMgr.
  2. An attacker origin executes a JavaScript loop to repeatedly draw and discard paths to a <canvas> context. By relying on Blink’s ClientPaintCache budget limits and purges, or using volatile paths, the attacker can deserialize millions of paths in the GPU process without causing an Out-Of-Memory (OOM) crash.
  3. After approximately 1.07 billion path creations, the GPU process’s atomic 30-bit genID counter wraps around.
  4. The attacker times or brute-forces a draw call of an exploratory complex path (>10 verbs) using matrix parameters matching the victim’s, aligning its new genID with the victim’s cached genID.
  5. The cache key collides in SmallPathAtlasMgr::findOrCreate. Instead of rasterizing the attacker’s path, SmallPathOp::onPrepareDraws binds the victim’s UV coordinates from the shared atlas.
  6. The victim’s alpha mask is drawn onto the attacker’s canvas. The attacker extracts this cross-origin pixel data using CanvasRenderingContext2D.getImageData().

Suggested Fix

  1. Register an SkIDChangeListener in SmallPathAtlasMgr or SmallPathShapeData (similar to the pattern used in SoftwarePathRenderer and TriangulatingPathRenderer) so that cached atlas entries are explicitly invalidated when the underlying SkPath is modified or destroyed.
  2. Alternatively, prevent cross-origin cache collisions in the GPU process entirely by scoping the cache keys to the renderer process or using a 64-bit generation ID.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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