Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in Skia
DescriptionHeap buffer overflow in Skia
ComponentSkia
Bug ClassOOB
Tracker501511299
Fix commit70f79346a275 (skia) +27/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
src/base/SkTDArray.cpp
modified

Files Changed

  • include/private/base/SkTDArray.h
  • src/base/SkTDArray.cpp
  • src/gpu/ganesh/geometry/GrAAConvexTessellator.cpp
From 70f79346a275f9f310ce7daab54f64ac571a4b36 Mon Sep 17 00:00:00 2001
From: Thomas Smith <[email protected]>
Date: Mon, 04 May 2026 11:14:02 -0400
Subject: [PATCH] Prevent potential overflow in SkTDArray

* With certain inputs, an attacker can exploit the typing in SkTDArray to cause a mismatch between the actual allocation of the array which is in bytes, and the allocation that the SkTDArray *thinks* it has, which is in units of T.

* Guard this scenario by promoting size_t earlier in the memory reservation call stack.

Bug: b/501511299
Change-Id: I98e851dcc6b060dd21e1fdd2b416a0a25e7c4f08
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1223036
Commit-Queue: Thomas Smith <[email protected]>
Reviewed-by: Kaylee Lubick <[email protected]>
---

diff --git a/include/private/base/SkTDArray.h b/include/private/base/SkTDArray.h
index d3d972d..c752f44 100644
--- a/include/private/base/SkTDArray.h
+++ b/include/private/base/SkTDArray.h
@@ -76,7 +76,17 @@
     }
 
 private:
-    size_t bytes(int n) const { return SkToSizeT(n * fSizeOfT); }
+    // Fast path for pointer arithmetic. Assumes 'n' has already been bounds-checked.
+    size_t bytes(int n) const { return SkToSizeT(n) * SkToSizeT(fSizeOfT); }
+
+    // Safe path for memory allocations; protects against 32-bit overflow.
+    size_t safe_bytes(int n) const {
+        size_t size = SkToSizeT(n);
+        size_t sizeOfT = SkToSizeT(fSizeOfT);
+        SkASSERT_RELEASE(size <= SIZE_MAX / sizeOfT);
+        return size * sizeOfT;
+    }
+
     void* address(int n) { return fStorage + this->bytes(n); }
 
     // Adds delta to fSize. Crash if outside [0, INT_MAX]
diff --git a/src/base/SkTDArray.cpp b/src/base/SkTDArray.cpp
index c422cac..c58f12c 100644
--- a/src/base/SkTDArray.cpp
+++ b/src/base/SkTDArray.cpp
@@ -17,15 +17,18 @@
 #include <cstring>
 #include <new>
 
-SkTDStorage::SkTDStorage(int sizeOfT) : fSizeOfT{sizeOfT} {}
+SkTDStorage::SkTDStorage(int sizeOfT) : fSizeOfT{sizeOfT} {
+    SkASSERT(sizeOfT > 0);
+}
 
 SkTDStorage::SkTDStorage(const void* src, int size, int sizeOfT)
         : fSizeOfT{sizeOfT}
         , fCapacity{size}
         , fSize{size} {
     if (size > 0) {
+        SkASSERT(sizeOfT > 0);
         SkASSERT(src != nullptr);
-        size_t storageSize = this->bytes(size);
+        size_t storageSize = this->safe_bytes(size);
         fStorage = static_cast<std::byte*>(sk_malloc_throw(storageSize));
         memcpy(fStorage, src, storageSize);
     }
@@ -118,7 +121,7 @@
         }
 
         fCapacity = expandedReserve;
-        size_t newStorageSize = this->bytes(fCapacity);
+        size_t newStorageSize = this->safe_bytes(fCapacity);
         fStorage = static_cast<std::byte*>(sk_realloc_throw(fStorage, newStorageSize));
     }
 }
@@ -129,7 +132,8 @@
         // Because calling realloc with size of 0 is implementation defined, force to a good state
         // by freeing fStorage.
         if (fCapacity > 0) {
-            fStorage = static_cast<std::byte*>(sk_realloc_throw(fStorage, this->bytes(fCapacity)));
+            fStorage =
+                static_cast<std::byte*>(sk_realloc_throw(fStorage, this->safe_bytes(fCapacity)));
         } else {
             sk_free(fStorage);
             fStorage = nullptr;
diff --git a/src/gpu/ganesh/geometry/GrAAConvexTessellator.cpp b/src/gpu/ganesh/geometry/GrAAConvexTessellator.cpp
index e1f4cdc..44b9601 100644
--- a/src/gpu/ganesh/geometry/GrAAConvexTessellator.cpp
+++ b/src/gpu/ganesh/geometry/GrAAConvexTessellator.cpp
@@ -14,6 +14,7 @@
 #include "include/private/base/SkAssert.h"
 #include "include/private/base/SkFloatingPoint.h"
 #include "include/private/base/SkTPin.h"
+#include "src/base/SkSafeMath.h"
 #include "src/core/SkPathPriv.h"
 #include "src/gpu/ganesh/geometry/GrPathUtils.h"
 
@@ -404,6 +405,12 @@
         return false;
     }
 
+    SkSafeMath safe;
+    int indicesAllocation = safe.addInt(safe.mulInt(18, path.countPoints()), 6);
+    if (!safe.ok()) {
+        return false;
+    }
+
     // Outer ring: 3*numPts
     // Middle ring: numPts
     // Presumptive inner ring: numPts
@@ -411,7 +418,7 @@
     // Outer ring: 12*numPts
     // Middle ring: 0
     // Presumptive inner ring: 6*numPts + 6
-    fIndices.reserve(18*path.countPoints() + 6);
+    fIndices.reserve(indicesAllocation);
 
     // Reset the accumulated error for all the future lineTo() calls when iterating over the path.
     fAccumLinearError = 0.f;
Loading diff…

Original Bug Report

reported by [email protected]

Potential heap buffer overflow in Skia's GrAAConvexTessellator via integer overflow

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 integer overflow in SkTDStorage::bytes() allows GrAAConvexTessellator::extractFromPath to allocate an undersized buffer while establishing a massive capacity. By crafting an SkPath heavily populated with degenerate edges, an attacker can bypass immediate crashes and perform a precise out-of-bounds heap write. This could potentially be leveraged for remote code execution in the GPU process.

Affected files:

  • third_party/skia/src/gpu/ganesh/geometry/GrAAConvexTessellator.cpp
  • third_party/skia/include/private/base/SkTDArray.h
  • third_party/skia/src/base/SkTDArray.cpp
  • third_party/skia/src/gpu/ganesh/geometry/GrAAConvexTessellator.h

Estimated timestamp from git blame: 2022-11-01

Background

Skia’s Ganesh rendering engine uses GrAAConvexTessellator to generate anti-aliased meshes for convex paths. When processing a path, GrAAConvexTessellator::extractFromPath attempts to pre-allocate memory for the generated vertices by calling this->reservePts(5 * path.countPoints()).

Root Cause

The reservePts method invokes reserve() on internal SkTDArray instances (like fPts, which stores SkPoint). Inside SkTDStorage::reserve(int newCapacity), the required allocation size in bytes is calculated via the bytes() helper:

// third_party/skia/include/private/base/SkTDArray.h
size_t bytes(int n) const { return SkToSizeT(n * fSizeOfT); }

Because both n and fSizeOfT are signed 32-bit integers, their product can overflow before being cast to size_t. For an array of SkPoint elements (fSizeOfT == 8), if expandedReserve reaches a massive value like 1,073,741,830, the multiplication 1,073,741,830 * 8 overflows to the truncated 32-bit value 48.

Consequently, sk_realloc_throw allocates a tiny 48-byte buffer, but the array’s internal fCapacity is erroneously recorded as 1,073,741,830.

Bypassing the Immediate Crash

Typically, a massive, immediate loop writing to an undersized buffer would crash deterministically, turning this into a Denial of Service. However, extractFromPath iterates over the path using an SkPathEdgeIter and explicitly skips degenerate segments where all points are identical:

if (!SkPathPriv::AllPointsEq({e.fPts, 2})) {
    this->lineTo(m, e.fPts[1], kSharp_CurveState);
}

If the path is constructed almost entirely of degenerate edges, lineTo (and subsequently addPt) is never called. The fSize of the array remains 0. When the iteration finally encounters non-degenerate edges at the end of the path, addPt invokes fPts.append(). Because fSize < fCapacity remains true, append simply increments fSize without reallocating, returning pointers that eventually fall outside the 48-byte allocated heap buffer.

Potential Exploitation Steps

Note: Our tooling agent cannot run code, so these are suggested steps based on static code analysis.

  1. Construct the Malicious Path: An attacker crafts an SkPath comprising roughly 171.8 million points (e.g., using ~57.3 million cubicTo verbs). This size results in an expandedReserve of 1,073,741,830.
  2. Pad with Degenerate Edges: The first ~171.79 million points are structured as degenerate edges (where start, control, and end points are identical) to ensure AllPointsEq returns true and addPt is skipped.
  3. Append Out-of-Bounds Payload: The final few points in the path are crafted with non-degenerate coordinates containing precise float payloads.
  4. Deliver to GPU Process: To bypass the standard 16MB PaintOp limit, the 1.4GB serialized path is encapsulated within a Font Strike or Skottie Transfer Cache, which uses Shared Memory to transfer massive payloads directly to the GPU process.
  5. Trigger Tessellation: When the GPU process deserializes and renders the path, the capacity overflows, allocating a 48-byte buffer. The degenerate edges are skipped, and the final payload points are written exactly out-of-bounds into adjacent PartitionAlloc heap structures, allowing the attacker to corrupt metadata or vtables.

(Note: extractFromPath also calls fIndices.reserve, which overflows to a negative signed integer. In release builds, SkTDStorage::reserve ignores negative capacity requests, rendering this secondary overflow a harmless no-op).

Suggested Fix

Refactor SkTDStorage::bytes to perform the multiplication safely in 64-bit space or check for overflow to prevent returning truncated allocation sizes:

size_t bytes(int n) const {
    return SkSafeMath::Mul(SkToSizeT(n), SkToSizeT(fSizeOfT));
}

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