Overview

Critical
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in Skia
DescriptionHeap buffer overflow in Skia
ComponentSkia
Bug ClassOOB
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: 16-bit size-wrap via debug-only assert is supported. But primary Builder allocates full count, so 'allocated using wrapped size' is partly inaccurate (under-sizing is in size-field/inner-key consumers). 'Strong RCE primitive' overstates impact vs CVSS 4.3.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker495700484
Fix commit0566b2f5f0d1 (skia) +127/-109
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
Credited86ac1f1587b71893ed2ad792cd7dde32
Reported2026-03-24 (per vendor advisory)
Fix landed2026-04-02 (public git)
Disclosed2026-04-15
Patch gap13 days — fix public in source before the advisory

Background

`ResourceKey`
Skia GPU resource identifier that packs a domain and the key’s byte size into a single uint32_t, with the size stored in the high 16 bits.
`ResourceKey::Builder`
helper that allocates and populates a key; it computed size = (count + kMetaDataCnt) * sizeof(uint32_t) and packed it into the size field.
`SkASSERT`
Skia’s debug-only assertion (SkToU16(size) == size), elided in release builds, so an overflowing size was never rejected at runtime.
Ganesh style / shape keys
Ganesh dashed-style keys and inherited shape/path-effect keys whose serialized data could exceed 65535 bytes.

Root Cause Analysis

ResourceKey stored the key’s total byte size in the high 16 bits of a uint32_t, but the only guard against overflow was a release-elided SkASSERT(SkToU16(size) == size). Some call sites, notably Ganesh style keys containing dash intervals and inherited shape/path-effect keys, could construct keys whose byte count exceeded 65535. When that happened the size field silently wrapped, so internalSize()/dataSize() reported a truncated small value while the actual key payload was much larger. The key was then allocated, compared, and copied using the wrapped small size, so subsequent reads and writes of the full key data ran past the undersized allocation, producing a heap buffer overflow in the GPU process.

The fix stores the 32-bit-word count (which fits in 16 bits far more comfortably) instead of the byte size, promotes the public API types to uint16_t, and adds explicit size limits at the offending call sites so an over-large key is rejected rather than silently wrapping.

Key insight
A release-elided SkASSERT was the only thing standing between a legitimately-large key and a silent 16-bit size wraparound, so the invariant that “size fits in uint16_t” was never actually enforced in shipping builds. Storing the word count instead of the byte size raises the effective ceiling ~4x and makes the truncation explicit and checkable at every call site.

Attack Path

  1. Attacker crafts content (e.g. an SVG/canvas path with a very long dash-interval array or a deeply inherited shape/path-effect) that drives a Ganesh style or shape key over 65535 bytes.
  2. ResourceKey::Builder computes size in bytes, wraps it into the high-16-bit size field, and the debug-only SkASSERT does not fire in a release build.
  3. The key is allocated and its data buffer sized using the wrapped (truncated) size, while the true payload written by the builder is far larger.
  4. Populating or later comparing/copying the key reads and writes the full-length data against the undersized allocation, corrupting adjacent heap memory in the GPU process.
  5. With controlled key contents and layout, the attacker shapes the overflow toward exploitable corruption (metadata/pointer overwrite) for memory disclosure or code execution.

Impact Assessment

Heap buffer overflow in the Skia GPU ResourceKey code path, reachable from web content that controls path/style/shape parameters, corrupting heap memory in the GPU process. Because both the overflow length and the written bytes are attacker-influenced, this is a strong primitive that can escalate from memory corruption toward information disclosure or remote code execution. Exploitation requires no special privileges beyond rendering hostile content.

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

Audit Directions

  • Release-elided size assertions
    flag any SkASSERT/assert that is the sole check a value fits a narrower width before packing/truncation; treat these as unenforced in shipping builds and require an explicit release-time bound.
  • Bit-packed size/length fields
    audit structures that pack a size or count into a sub-word bitfield of a larger integer; verify every producing call site clamps or rejects out-of-range inputs so the field cannot wrap relative to the real allocation.
  • Untrusted-controlled key/serialization sizes
    trace user-content-driven parameters (dash arrays, inherited shape/path-effect data) into cache-key and serialization builders and confirm the resulting length is validated against the allocation size actually used downstream.
From 0566b2f5f0d1f218d9990eba838af16826d2e7e3 Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
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 <robertphillips@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
---

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 se...@gmail.com

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 <bsalomon@google.com>
  • 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