CVE-2026-13820
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
FloatStorageManagersrc/gpu/graphite/PipelineData.h |
modified | |
forsrc/gpu/graphite/PipelineData.h |
modified | |
ifsrc/gpu/graphite/PipelineData.h |
modified | |
SkWriteBuffersrc/shaders/SkShaderBase.cpp |
modified | |
SkShaderBasesrc/shaders/SkShaderBase.cpp |
modified |
Files Changed
src/gpu/graphite/PipelineData.hsrc/shaders/SkShaderBase.cppsrc/shaders/SkShaderBase.h
Patch
From 2e4a568f6f0d854b27dfbb2c8c8723a196071d65 Mon Sep 17 00:00:00 2001 From: Michael Ludwig <[email protected]> Date: Fri, 22 May 2026 16:44:59 -0400 Subject: [PATCH] [graphite] Ref count large gradient shaders in FloatStorageManager This also removes the unique ID from SkShaderBase as the FSM was the only system that relied on it. Fixed: 512986879 Bug: 512986879 Change-Id: Icd43c13a75a1cdd212eea2b9db03033d13be47f4 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1243937 Reviewed-by: Thomas Smith <[email protected]> Commit-Queue: Michael Ludwig <[email protected]> --- diff --git a/src/gpu/graphite/PipelineData.h b/src/gpu/graphite/PipelineData.h index bc25542..d528c86 100644 --- a/src/gpu/graphite/PipelineData.h +++ b/src/gpu/graphite/PipelineData.h @@ -480,9 +480,13 @@ static_assert(std::numeric_limits<uint32_t>::max() / sizeof(float) <= (uint32_t) std::numeric_limits<int>::max()); public: - FloatStorageManager() = default; + FloatStorageManager() { this->reset(); } void reset() { + // Remove the manually added refs on the keys in fGradientOffsetCache + for (auto [k, _] : fGradientOffsetCache) { + k->unref(); // k might be deleted at this point but we won't use it anymore + } fGradientStorage.clear(); fGradientOffsetCache.reset(); } @@ -499,7 +503,7 @@ return {nullptr, -1}; } - int* existingOffset = fGradientOffsetCache.find(shader->uniqueID()); + int* existingOffset = fGradientOffsetCache.find(shader); if (existingOffset) { return {nullptr, *existingOffset}; } @@ -508,7 +512,10 @@ // Only cache the storage if it was allocated successfully. if (ptr) { SkASSERT(offset >= 0); - fGradientOffsetCache.set(shader->uniqueID(), offset); + // Since FloatStorageManager is single threaded, adding a ref and then storing in the + // map should be fine. + shader->ref(); + fGradientOffsetCache.set(shader, offset); } return {ptr, offset}; @@ -555,8 +562,18 @@ // storage buffer can be bound once and accessed at random. SkTDArray<float> fGradientStorage; - // We use the shader's unique ID as a key to de-duplicate gradient data. - skia_private::THashMap<uint32_t, int> fGradientOffsetCache; + // We use the shader's address as a key to de-duplicate gradient data. Each key has a ref added + // when it's first put in the map. The map does not key off of sk_sp<SkGradientBaseShader> to as + // that is not compatible with SkGoodHash. These refs are dropped in reset(). While this extends + // the lifetime of the SkShaders, it only applies to large gradients. Hopefully clients are + // trying to reuse such shaders across frames already. + // + // If we didn't keep the shaders alive, we'd have to worry about cache collisions from + // re-allocations using the same address. Using a unique ID can wrap, leading to potential + // mismatches. Adding sufficient data to eliminate this risk (e.g. keying off the unique ID, the + // address, the number of color stops, AND a hash of the color data) is likely more expensive + // than taking a ref. + skia_private::THashMap<const SkGradientBaseShader*, int> fGradientOffsetCache; std::optional<BindBufferInfo> fBufferInfo = std::nullopt; }; diff --git a/src/shaders/SkShaderBase.cpp b/src/shaders/SkShaderBase.cpp index 5d20ba4..dfbc3e2 100644 --- a/src/shaders/SkShaderBase.cpp +++ b/src/shaders/SkShaderBase.cpp @@ -20,22 +20,6 @@ class SkWriteBuffer; -namespace { - -// The 32-bit shader ID counter is not expected to wrap. In the unlikely event that it does, we -// assume that the associated shaders will be sufficiently temporally separated such that shaders -// with the same recycled IDs are no longer in use. -uint32_t next_unique_id() { - static std::atomic<uint32_t> gNextUniqueID{SK_InvalidUniqueID + 1}; - uint32_t id = SK_InvalidUniqueID; - do { - id = gNextUniqueID.fetch_add(1, std::memory_order_relaxed); - } while (id == SK_InvalidUniqueID); - return id; -} - -} // anonymous namespace - namespace SkShaders { MatrixRec::MatrixRec(const SkMatrix& ctm) : fCTM(ctm) {} @@ -92,7 +76,7 @@ /////////////////////////////////////////////////////////////////////////////////////// -SkShaderBase::SkShaderBase() : fUniqueID(next_unique_id()) {} +SkShaderBase::SkShaderBase() {} SkShaderBase::~SkShaderBase() = default; diff --git a/src/shaders/SkShaderBase.h b/src/shaders/SkShaderBase.h index 16b80c4..ecd25e2 100644 --- a/src/shaders/SkShaderBase.h +++ b/src/shaders/SkShaderBase.h @@ -186,8 +186,6 @@ public: ~SkShaderBase() override; - uint32_t uniqueID() const { return fUniqueID; } - sk_sp<SkShader> makeInvertAlpha() const; sk_sp<SkShader> makeWithCTM(const SkMatrix&) const; // owns its own ctm @@ -409,8 +407,6 @@ } private: - const uint32_t fUniqueID; - friend class SkShaders::MatrixRec; }; inline SkShaderBase* as_SB(SkShader* shader) {
Original Bug Report
Potential GPU OOB storage buffer read in Graphite due to uniqueID wrap and missing cache size check
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic error in Skia’s Graphite backend allows for a potential cross-origin information leak via a GPU out-of-bounds read. This is caused by de-duplicating gradient data using a 32-bit uniqueID that can wrap, without verifying the size of the cached allocation. On macOS with Graphite/Metal, where GPU robustness is disabled, a compromised renderer could potentially read sensitive data from shared GPU buffers.
Affected files:
third_party/skia/src/gpu/graphite/PipelineData.hthird_party/skia/src/shaders/SkShaderBase.cppthird_party/skia/src/gpu/graphite/KeyHelpers.cppthird_party/skia/src/sksl/sksl_graphite_frag.skslgpu/command_buffer/service/dawn_context_provider.cc
Estimated timestamp from git blame: 2025-08-15
Summary
A potential vulnerability in the gradient data management of Skia’s Graphite backend can lead to an out-of-bounds (OOB) memory read on the GPU. This issue stems from a combination of a 32-bit identifier wrap and a lack of size validation in the FloatStorageManager cache. This can be leveraged by a compromised renderer process to leak sensitive information across origins from the GPU process.
Root Cause Analysis
The vulnerability exists in FloatStorageManager::allocateGradientData within third_party/skia/src/gpu/graphite/PipelineData.h. This class manages a cache of gradient allocations (fGradientOffsetCache) keyed by the shader’s uniqueID to de-duplicate data within a single Graphite Recording session.
std::pair<float*, int> allocateGradientData(int numStops, const SkGradientBaseShader* shader) {
SkASSERT(!this->isFinalized());
int* existingOffset = fGradientOffsetCache.find(shader->uniqueID());
if (existingOffset) {
// VULNERABILITY: Returns cached offset without verifying numStops
return {nullptr, *existingOffset};
}
auto [ptr, offset] = this->allocateFloatData(numStops * 5);
fGradientOffsetCache.set(shader->uniqueID(), offset);
return {ptr, offset};
}
When a cache hit occurs, the manager returns the existing buffer offset. However, it does not verify that the previously allocated space at that offset is large enough to accommodate the numStops of the current shader request.
The uniqueID is generated by a 32-bit global atomic counter in third_party/skia/src/shaders/SkShaderBase.cpp. This counter will wrap around after 2^32 allocations, allowing two distinct shader objects to share the same ID. If both shaders are used within the same Graphite Recording, the second shader will reuse the first shader’s allocation regardless of size.
Potential Exploitation Path
(Note: These are potential steps as a functional PoC has not yet been executed.)
- Shader Pinning: A compromised renderer triggers the creation of a gradient shader with a small number of stops (e.g., 2). This shader is assigned a
uniqueID(e.g.,K). The attacker ensures this shader is kept alive in the GPU process (e.g., via the Shader Transfer Cache). - ID Wrap: The renderer performs approximately 2^32 shader constructions to force the global atomic counter to wrap back to
K. This typically takes 1-2 hours of sustained IPC traffic. - Triggering Collision: Within a single drawing recording, the renderer first draws using the pinned small shader (filling the
FloatStorageManagercache with IDKat offsetO). It then triggers the creation of a new gradient shader with a large number of stops (e.g., 1024), which receives the collidinguniqueID = K. It then draws using this large shader. - OOB Memory Read: Due to the cache hit on ID
K, the large gradient is assigned the small allocation’s offsetO. The fragment shader ($colorize_grad_bufinthird_party/skia/src/sksl/sksl_graphite_frag.sksl) uses the largenumStopsvalue (1024) to index into the storage buffer starting atO. Since the underlying allocation was only sized for 2 stops, the shader performs an OOB read.
Impact and Environment
This issue primarily affects Chrome on macOS where the Graphite backend (running on Dawn/Metal) is enabled. In this configuration, Chrome explicitly disables GPU robustness features (via the disable_robustness toggle in gpu/command_buffer/service/dawn_context_provider.cc). Consequently, the OOB read is not clamped by the driver, allowing the shader to read raw data from adjacent GPU buffers which may contain sensitive cross-origin information.
Suggested Fix
The FloatStorageManager should verify that a cached allocation is large enough for the current request, or include the numStops in the cache key.
// Suggested modification in PipelineData.h
int* existingOffset = fGradientOffsetCache.find(shader->uniqueID());
if (existingOffset) {
// Check if the current request is compatible with the previous allocation.
// This may require tracking the size of allocations in fGradientOffsetCache.
if (is_size_compatible) {
return {nullptr, *existingOffset};
}
}
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.