CVE-2026-11124
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/base/SkTDArray.cpp |
modified |
Files Changed
include/private/base/SkTDArray.hsrc/base/SkTDArray.cppsrc/gpu/ganesh/geometry/GrAAConvexTessellator.cpp
Patch
From 70f79346a275f9f310ce7daab54f64ac571a4b36 Mon Sep 17 00:00:00 2001
From: Thomas Smith <thomsmit@google.com>
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 <thomsmit@google.com>
Reviewed-by: Kaylee Lubick <kjlubick@google.com>
---
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;
Original Bug Report
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.cppthird_party/skia/include/private/base/SkTDArray.hthird_party/skia/src/base/SkTDArray.cppthird_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.
- Construct the Malicious Path: An attacker crafts an
SkPathcomprising roughly 171.8 million points (e.g., using ~57.3 millioncubicToverbs). This size results in anexpandedReserveof1,073,741,830. - 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
AllPointsEqreturns true andaddPtis skipped. - Append Out-of-Bounds Payload: The final few points in the path are crafted with non-degenerate coordinates containing precise float payloads.
- Deliver to GPU Process: To bypass the standard 16MB
PaintOplimit, 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. - 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.