CVE-2026-79144
Overview
Background
- `SmallPathAtlasMgr`
- Ganesh GPU backend manager that caches rendered small-path masks as
SmallPathShapeDataentries keyed bySmallPathShapeDataKeyinfShapeCache. - `SmallPathShapeDataKey`
- the cache key for a path entry, whose
Hash(viaSkChecksum::Hash32) for small, complex geometry effectively depends only on the path’s generation ID, so distinct paths sharing a reused gen ID collide. - `SkIDChangeListener`
- a Skia callback interface whose
changed()fires when the associated path’s contents or generation ID are modified or discarded, letting caches invalidate stale entries. - `GrStyledShape`
- the styled-path object passed into
findOrCreate; viaaddGenIDChangeListenerit can register a listener that is notified when the underlying path changes.
Root Cause Analysis
In the vulnerable code, SmallPathAtlasMgr::findOrCreate(const SmallPathShapeDataKey&) looked up fShapeCache purely by key and returned any matching SmallPathShapeData without validating that the cached entry still corresponded to the caller’s live path. Because the SmallPathShapeDataKey::Hash of small, complex paths relies essentially on the path’s generation ID, an attacker who created and discarded many such paths could get a gen ID recycled onto a new path, producing a key that collides with a stale entry left in the cache. The invariant that “a cache hit describes the requested path” was therefore violated: a lookup could return a previously cached victim path’s rendered data instead of the attacker’s, i.e. duplicate/aliased entries.
The fix attaches a SmallPathIDChangeListener to every SmallPathShapeData and registers it on the shape via shape.addGenIDChangeListener, so when the original path is modified or discarded the listener’s changed() sets fHasChanged. findOrCreate now checks shapeData->fIDChangeListener->hasChanged() on a hit and calls deleteCacheEntry to purge the stale entry before recreating it, guaranteeing a returned entry belongs to the current path and eliminating the aliasing.
Attack Path
- Force gen-ID-only keys
Create small paths with complex geometry so that
SmallPathShapeDataKey::Hashdepends only on the generation ID, making key collisions cheap to engineer. - Churn the cache
Repeatedly create and discard such paths to overwhelm
SmallPathAtlasMgrand cause a discarded path’s generation ID to be recycled onto a new path. - Trigger the aliased hit
Submit a render request whose recycled gen ID produces a
SmallPathShapeDataKeymatching a staleSmallPathShapeDatainfShapeCache, sofindOrCreatereturns the previous (victim) entry instead of building a fresh one. - Observe leaked path data
Receive the victim’s cached path/mask rather than the requested one, disclosing information belonging to another path/context
(inferred)as to the exact rendered bytes exposed, since the diff shows the aliasing but not a concrete read sink.
Impact Assessment
SmallPathAtlasMgr to return a different (victim) path’s cached rendering, yielding an information leak in Skia’s Ganesh GPU rendering path as described by the advisory. Preconditions are the ability to create and discard many small, complex paths to recycle a generation ID and force a colliding SmallPathShapeDataKey. The advisory classifies this as a LogicError information leak and states no exploited-in-the-wild status; any escalation beyond disclosure of aliased cached path data is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/ops/SmallPathAtlasMgr.cpp |
modified | |
SmallPathIDChangeListenersrc/gpu/ganesh/ops/SmallPathShapeData.h |
modified | |
SmallPathShapeDataKeysrc/gpu/ganesh/ops/SmallPathShapeData.h |
modified | |
SmallPathShapeDatasrc/gpu/ganesh/ops/SmallPathShapeData.h |
modified |
Files Changed
src/gpu/ganesh/ops/SmallPathAtlasMgr.hsrc/gpu/ganesh/ops/SmallPathShapeData.hsrc/gpu/ganesh/ops/SmallPathAtlasMgr.cpp
Audit Directions
- Key-only cache lookups over recyclable IDsAudit other Ganesh/Skia caches keyed by hashes that collapse to a generation ID (as
SmallPathShapeDataKey::Hashdoes) and confirm each cache hit is validated against the live object, not just matched by key. - Missing change-listener registrationLook for caches storing rendered path/shape data without an
SkIDChangeListener, mirroring the pattern already used inSoftwarePathRenderer.cpp, since absence of invalidation permits stale-entry aliasing. - Listener lifetime and deregistrationReview entries that own an
SkIDChangeListenerto ensure they callmarkShouldDeregisteron destruction and correctly reset state, so listeners are neither leaked nor left signaling freed cache entries.
Patch
From 013ad6e7097238654847bfa5a5fb2c6c3823ee41 Mon Sep 17 00:00:00 2001
From: Alexis Cruz-Ayala <alexisdavidc@google.com>
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 <fmalita@google.com>
Commit-Queue: Alexis Cruz-Ayala <alexisdavidc@google.com>
---
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
Original Bug Report
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.cppthird_party/skia/src/core/SkPathData.cppthird_party/skia/src/core/SkPath.cppthird_party/skia/src/gpu/ganesh/geometry/GrStyledShape.cppthird_party/skia/src/gpu/ganesh/ops/SmallPathShapeData.cppthird_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
- 30-bit Generation ID Wraparound: In
third_party/skia/src/core/SkPathData.cpp, thenext_pathdata_unique_id()function generates unique IDs forSkPathDataobjects. It masks the ID to 30 bits (id <<= 2; id >>= 2;) to reserve space for fill type information. This causes the globalgenIDcounter to wrap around after approximately 1.07 billion allocations. - Cache Key Truncation: When rendering small paths,
SmallPathRenderergenerates a cache key viaGrStyledShape::writeUnstyledKey. If a path is complex (has more than 10 verbs, perkMaxKeyFromDataVerbCnt), the function drops the raw geometry (points and verbs) from the key. The key relies entirely on the path’s 30-bitgenIDand state (fill type, winding). - Shared GPU Cache without Eviction: The
SmallPathAtlasMgrcaches these rendered paths into aGrDrawOpAtlas. Because it is owned by aGrDirectContextshared viaSharedContextStatein the GPU process, this cache is globally accessible across all renderer processes. Furthermore, unlike other renderers (e.g.,SoftwarePathRenderer),SmallPathAtlasMgrdoes not register anSkIDChangeListener. 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.
- 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. - An attacker origin executes a JavaScript loop to repeatedly draw and discard paths to a
<canvas>context. By relying on Blink’sClientPaintCachebudget 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. - After approximately 1.07 billion path creations, the GPU process’s atomic 30-bit
genIDcounter wraps around. - 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
genIDwith the victim’s cachedgenID. - The cache key collides in
SmallPathAtlasMgr::findOrCreate. Instead of rasterizing the attacker’s path,SmallPathOp::onPrepareDrawsbinds the victim’s UV coordinates from the shared atlas. - 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
- Register an
SkIDChangeListenerinSmallPathAtlasMgrorSmallPathShapeData(similar to the pattern used inSoftwarePathRendererandTriangulatingPathRenderer) so that cached atlas entries are explicitly invalidated when the underlyingSkPathis modified or destroyed. - 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.