Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in Skia
DescriptionHeap buffer overflow in Skia
ComponentSkia
Bug ClassOOB
Tracker495700484
Fix commit0566b2f5f0d1 (skia) +127/-109
CISA KEVNot listed
Credited86ac1f1587b71893ed2ad792cd7dde32
Disclosed2026-04-15

Changed Functions

FunctionChangeNotes
ResourceKey
src/gpu/ResourceKey.h
modified
ScratchKey
src/gpu/ResourceKey.h
modified
Builder
src/gpu/ResourceKey.h
modified
UniqueKey
src/gpu/ResourceKey.h
modified

Files Changed

  • src/gpu/ResourceKey.h
  • src/gpu/ganesh/GrStyle.cpp
  • src/gpu/ganesh/GrStyle.h
From 0566b2f5f0d1f218d9990eba838af16826d2e7e3 Mon Sep 17 00:00:00 2001
From: Michael Ludwig <[email protected]>
Date: Wed, 01 Apr 2026 09:48:48 -0400
Subject: [PATCH] Use 16-bit size for ResourceKeys

Internally, ResourceKey required the size to fit into a uint16_t so this
makes that explicit in the public API. It also changes how the size is
stored to instead record the num32DataCount directly and then convert to
bytes as needed, whereas previously it was requiring that the actual
byte count fit into a uint16_t. This gives a bit more head room.

Call sites to the ResourceKey builders are updated to now have the
responsibility of checking that their size can fit into a uint16_t. For
the most part, these were fixed or trivially small variable key sizes.
The two exceptions were Ganesh's style key (with dashes) and its
inherited key system for shapes with applied styles and path effects.
They now have reasonable limits to prevent the keys from growing bigger
than about 1kb.

Bug: b/495700484
Change-Id: I6ac4f17628b9a2e1a777c473b74e6d1f5c68b27d
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1199497
Reviewed-by: Robert Phillips <[email protected]>
Commit-Queue: Michael Ludwig <[email protected]>
---

diff --git a/src/gpu/ResourceKey.h b/src/gpu/ResourceKey.h
index f8dee79..19851a6 100644
--- a/src/gpu/ResourceKey.h
+++ b/src/gpu/ResourceKey.h
@@ -19,6 +19,7 @@
 
 #include <cstdint>
 #include <cstring>
+#include <limits>
 #include <new>
 #include <utility>
 
@@ -77,14 +78,10 @@
         }
 
     protected:
-        Builder(ResourceKey* key, uint32_t domain, int data32Count) : fKey(key) {
-            size_t count = SkToSizeT(data32Count);
+        Builder(ResourceKey* key, uint16_t domain, uint16_t data32Count) : fKey(key) {
             SkASSERT(domain != kInvalidDomain);
-            key->fKey.reset(kMetaDataCnt + count);
-            size_t size = (count + kMetaDataCnt) * sizeof(uint32_t);
-            SkASSERT(SkToU16(size) == size);
-            SkASSERT(SkToU16(domain) == domain);
-            key->fKey[kDomainAndSize_MetaDataIdx] = SkToU32(domain | (size << 16));
+            key->fKey.reset(kMetaDataCnt + data32Count);
+            key->fKey[kDomainAndSize_MetaDataIdx] = domain | (data32Count << 16);
         }
 
     private:
@@ -92,7 +89,7 @@
     };
 
 protected:
-    static const uint32_t kInvalidDomain = 0;
+    static const uint16_t kInvalidDomain = 0;
 
     ResourceKey() { this->reset(); }
 
@@ -118,10 +115,10 @@
         return *this;
     }
 
-    uint32_t domain() const { return fKey[kDomainAndSize_MetaDataIdx] & 0xffff; }
+    uint16_t domain() const { return fKey[kDomainAndSize_MetaDataIdx] & 0xffff; }
 
     /** size of the key data, excluding meta-data (hash, domain, etc).  */
-    size_t dataSize() const { return this->size() - 4 * kMetaDataCnt; }
+    size_t dataSize() const { return (fKey[kDomainAndSize_MetaDataIdx] >> 16) * sizeof(uint32_t); }
 
     /** ptr to the key data, excluding meta-data (hash, domain, etc).  */
     const uint32_t* data() const {
@@ -149,14 +146,17 @@
 private:
     enum MetaDataIdx {
         kHash_MetaDataIdx,
-        // The key domain and size are packed into a single uint32_t.
+        // The key domain and size are packed into a single uint32_t. The stored size is in units
+        // of uint32_t and does not include the metadata, i.e. it stores the data32Count provided
+        // to the original key builder.
         kDomainAndSize_MetaDataIdx,
 
         kLastMetaDataIdx = kDomainAndSize_MetaDataIdx
     };
     static const uint32_t kMetaDataCnt = kLastMetaDataIdx + 1;
 
-    size_t internalSize() const { return fKey[kDomainAndSize_MetaDataIdx] >> 16; }
+    // Total size in bytes, including metadata
+    size_t internalSize() const { return this->dataSize() + sizeof(uint32_t) * kMetaDataCnt; }
 
     void validate() const {
         SkASSERT(this->isValid());
@@ -197,7 +197,7 @@
 class ScratchKey : public ResourceKey {
 public:
     /** Uniquely identifies the type of resource that is cached as scratch. */
-    typedef uint32_t ResourceType;
+    typedef uint16_t ResourceType;
 
     /** Generate a unique ResourceType. */
     static ResourceType GenerateResourceType();
@@ -219,7 +219,7 @@
 
     class Builder : public ResourceKey::Builder {
     public:
-        Builder(ScratchKey* key, ResourceType type, int data32Count)
+        Builder(ScratchKey* key, ResourceType type, uint16_t data32Count)
                 : ResourceKey::Builder(key, type, data32Count) {}
     };
 };
@@ -240,7 +240,7 @@
  */
 class UniqueKey : public ResourceKey {
 public:
-    typedef uint32_t Domain;
+    typedef uint16_t Domain;
     /** Generate a Domain for unique keys. */
     static Domain GenerateDomain();
 
@@ -279,17 +279,17 @@
 
     class Builder : public ResourceKey::Builder {
     public:
-        Builder(UniqueKey* key, Domain type, int data32Count, const char* tag = nullptr)
+        Builder(UniqueKey* key, Domain type, uint16_t data32Count, const char* tag = nullptr)
                 : ResourceKey::Builder(key, type, data32Count) {
             key->fTag = tag;
         }
 
         /** Used to build a key that wraps another key and adds additional data. */
-        Builder(UniqueKey* key, const UniqueKey& innerKey, Domain domain, int extraData32Cnt,
+        Builder(UniqueKey* key, const UniqueKey& innerKey, Domain domain, uint16_t extraData32Cnt,
                 const char* tag = nullptr)
                 : ResourceKey::Builder(key,
                                        domain,
-                                       Data32CntForInnerKey(innerKey) + extraData32Cnt) {
+                                       Data32CntForInnerKey(innerKey, extraData32Cnt)) {
             SkASSERT(&innerKey != key);
             // add the inner key to the end of the key so that op[] can be indexed normally.
             uint32_t* innerKeyData = &this->operator[](extraData32Cnt);
@@ -300,9 +300,15 @@
         }
 
     private:
-        static int Data32CntForInnerKey(const UniqueKey& innerKey) {
-            // key data + domain
-            return SkToInt((innerKey.dataSize() >> 2) + 1);
+        static uint16_t Data32CntForInnerKey(const UniqueKey& innerKey, uint16_t extraData32Cnt) {
+            // key data + domain + extraData32Cnt needs to fit into a uint16_t. This key builder is
+            // only used in Ganesh for wrapping textures
+            uint16_t innerData32Cnt = innerKey.dataSize() >> 2;
+            // The Builder API doesn't have a way to return a failure, so if this is somehow
+            // exceeded, then we have no way to recover.
+            SkASSERT_RELEASE((uint32_t) extraData32Cnt + (uint32_t) innerData32Cnt + 1 <=
+                             (uint32_t) std::numeric_limits<uint16_t>::max());
+            return innerData32Cnt + extraData32Cnt + 1;
         }
     };
 
diff --git a/src/gpu/ganesh/GrStyle.cpp b/src/gpu/ganesh/GrStyle.cpp
index 5d7bc9c..d1bdcf5 100644
--- a/src/gpu/ganesh/GrStyle.cpp
+++ b/src/gpu/ganesh/GrStyle.cpp
@@ -18,8 +18,18 @@
 
 int GrStyle::KeySize(const GrStyle &style, Apply apply, uint32_t flags) {
     static_assert(sizeof(uint32_t) == sizeof(SkScalar));
+
+    // We embed the dash interval pattern into the key, and the key size must fit within 16-bits.
+    // However, we put a more conservative upper limit on the dashes because we don't want to keep
+    // key memory locked up in caches during pathological cases.
+    static constexpr int kDashIntervalKeyLimit = 512;
+
     int size = 0;
     if (style.isDashed()) {
+        if (style.dashIntervalCnt() > kDashIntervalKeyLimit) {
+            return -1; // Disable caching for pathologically large dash patterns
+        }
+
         // One scalar for scale, one for dash phase, and one for each dash value.
         size += 2 + style.dashIntervalCnt();
     } else if (style.pathEffect()) {
diff --git a/src/gpu/ganesh/GrStyle.h b/src/gpu/ganesh/GrStyle.h
index 41b0ce9..252b975 100644
--- a/src/gpu/ganesh/GrStyle.h
+++ b/src/gpu/ganesh/GrStyle.h
@@ -74,6 +74,8 @@
      * into a key. This occurs when there is a path effect that is not a dash. The key can
      * either reflect just the path effect (if one) or the path effect and the strokerec. Note
      * that a simple fill has a zero sized key.
+     *
+     * If a positive value is returned, it will fit in a uint16_t.
      */
     static int KeySize(const GrStyle&, Apply, uint32_t flags = 0);
Loading diff…

Original Bug Report

reported by [email protected]

Security issue update: Heap-Buffer-Overflow in ResourceKey::Builder::finish via Canvas2D Dash Pattern Size Packing Truncation

Note

This issue is a resubmission of the previous one: https://issues.chromium.org/issues/494644478. There was an error in describing the affected scope, which led to unsuccessful reproduction: This vulnerability affects desktop platforms using the Ganesh backend. Chromium on macOS defaults to Graphite. I have completed testing on Linux. Please reproduce using Linux Chromium.

Summary

A heap-buffer-overflow read occurs in the Chromium GPU process when a web page draws a Canvas2D stroked shape with a shadow and a dash pattern containing exactly 16364 intervals. The skgpu::ResourceKey::Builder packs the key’s byte size into the upper 16 bits of a uint32_t, but only an SkASSERT guards against the size exceeding 65535 bytes. When the total key size reaches exactly 65536, the packed size truncates to zero. The subsequent call to ResourceKeyHash in Builder::finish computes a hash length of 0 - 4, which underflows as size_t to an enormous value, causing the hash function to read far past the end of the allocated key buffer. This vulnerability affects desktop platforms that use the Ganesh backend., with no special GPU hardware requirements.

Bisect

Introducing Commit: 24db3b1c35fb935660229da164fc5ad31977387f

  • Date: 2015-01-23
  • Author: bsalomon <[email protected]>
  • Review: https://codereview.chromium.org/858123002

Root Cause

The skgpu::ResourceKey::Builder constructor packs two fields into a single uint32_t metadata slot: the key’s domain in the lower 16 bits and the key’s total byte size in the upper 16 bits.

// third_party/skia/src/gpu/ResourceKey.h:80-87
Builder(ResourceKey* key, uint32_t domain, int data32Count) : fKey(key) {
    size_t count = SkToSizeT(data32Count);
    key->fKey.reset(kMetaDataCnt + count);
    size_t size = (count + kMetaDataCnt) * sizeof(uint32_t);
    SkASSERT(SkToU16(size) == size);  // release: removed
    key->fKey[kDomainAndSize_MetaDataIdx] = SkToU32(domain | (size << 16));
}

The expression size << 16 is computed in size_t (64-bit), but the result is then truncated to uint32_t by SkToU32. When size equals 65536 (0x10000), the shift produces 0x100000000, which truncates to 0. The SkASSERT that would catch this is stripped in release builds.

Later, internalSize() extracts the packed size by shifting right:

// third_party/skia/src/gpu/ResourceKey.h:159
size_t internalSize() const { return fKey[kDomainAndSize_MetaDataIdx] >> 16; }

This returns 0. When finish() calls ResourceKeyHash, it passes internalSize() - sizeof(uint32_t) as the byte count. Since internalSize() is 0 and the subtraction operates on size_t, the result underflows to 0xFFFFFFFFFFFFFFFC on 64-bit systems, causing the hash function (wyhash) to attempt reading approximately 18 exabytes of data starting from the key buffer.

// third_party/skia/src/gpu/ResourceKey.h:62-67
void finish() {
    if (nullptr == fKey) { return; }
    uint32_t* hash = &fKey->fKey[kHash_MetaDataIdx];
    *hash = ResourceKeyHash(hash + 1, fKey->internalSize() - sizeof(uint32_t));
    // ...
}

The trigger path from web content uses Canvas2D’s setLineDash() API, which accepts an array of arbitrary length (only validating that values are finite and non-negative). When combined with shadowBlur, the rendering path enters GrBlurUtils::DrawShapeWithMaskFilter, which applies the dash path effect to the shape and then constructs a cache key. The GrStyle::KeySize function returns 2 + dashIntervalCnt for the path effect portion, plus 4 for the stroke record. Combined with the geometric key (5 uint32s for a rect) and the fixed overhead in compute_key_and_clip_bounds (7 uint32s), the total data32Count passed to the Builder is dashIntervalCnt + 18. Adding the 2-element metadata prefix and multiplying by 4 bytes gives the total size as (dashIntervalCnt + 20) * 4. Setting dashIntervalCnt = 16364 yields 16384 * 4 = 65536 bytes, the exact overflow boundary.

Reproduce

Tested at commit e6831951cd5fd2d7db105507e6f5e06ba600e073 on Ubuntu 22.04.

Configure an ASAN build with the following args.gn in out/asan:

is_asan = true
is_debug = false
dcheck_always_on = false

Build Chrome with autoninja -C out/asan chrome. No source modifications are required; the PoC is a self-contained HTML file.

Or download the newest asan-chromium:

wget https://www.googleapis.com/download/storage/v1/b/chromium-browser-asan/o/linux-release%2Fasan-linux-release-1604232.zip\?generation\=1774375934171278\&alt\=media

Launch Chrome as follows:

out/asan/chrome --user-data-dir=./userdata poc.html

The GPU process will crash within seconds with an AddressSanitizer heap-buffer-overflow report originating from wyhash in SkChecksum.cpp, called through ResourceKey::Builder::finish in ResourceKey.h. The crash occurs because a 65536-byte ResourceKey buffer is read past its end when the size_t length argument underflows to a massive value. The ASAN summary line reads heap-buffer-overflow ... in wyhash(void const*, unsigned long, unsigned long, unsigned long const*) and the access is located 3 bytes after a 65536-byte heap region.

ASAN output:

=================================================================
==191842==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7a9b28cf4803 at pc 0x5e7ce9ad9692 bp 0x7ffd713380b0 sp 0x7ffd713380a8
READ of size 8 at 0x7a9b28cf4803 thread T0 (chrome)
    #0 0x5e7ce9ad9691 in wyhash(void const*, unsigned long, unsigned long, unsigned long const*) third_party/skia/src/core/SkChecksum.cpp:45:5
    #1 0x5e7d02ee0448 in skgpu::ganesh::SoftwarePathRenderer::onDrawPath(skgpu::ganesh::PathRenderer::DrawPathArgs const&) third_party/skia/src/gpu/ResourceKey.h:67:21
    #2 0x5e7d02cf7ad2 in skgpu::ganesh::SurfaceDrawContext::drawShapeUsingPathRenderer(GrClip const*, GrPaint&&, GrAA, SkMatrix const&, GrStyledShape&&, bool) third_party/skia/src/gpu/ganesh/SurfaceDrawContext.cpp:1897:9
    #3 0x5e7d02d005c7 in skgpu::ganesh::SurfaceDrawContext::drawShape(GrClip const*, GrPaint&&, GrAA, SkMatrix const&, GrStyledShape&&) third_party/skia/src/gpu/ganesh/SurfaceDrawContext.cpp:1561:11
    #4 0x5e7d02c003ec in GrBlurUtils::draw_shape_with_mask_filter(GrRecordingContext*, skgpu::ganesh::SurfaceDrawContext*, GrClip const*, GrPaint&&, SkMatrix const&, SkMaskFilterBase const*, GrStyledShape const&) third_party/skia/src/gpu/ganesh/GrBlurUtils.cpp:303:10
......

The complete ASAN log is attached as asan.txt.

References

Credit

Please use 86ac1f1587b71893ed2ad792cd7dde32 as the credit for this vulnerability. Thank you.

View on issue tracker