CVE-2026-14389
Overview
Background
- `SkSafeMath`
- A Skia helper (added via
#include "src/base/SkSafeMath.h") that performssize_tarithmetic such asmul,add, andalignUpwhile recording whether any operation overflowed, queryable throughok(). - `SurfaceContext`
- The Ganesh GPU backend class in
src/gpu/ganesh/SurfaceContext.cppthat mediates CPU/GPU pixel transfers (read, write, and transfer-buffer paths). - Row bytes
- The number of bytes in one row of a pixel buffer, computed here as
minRowBytes()orGrColorTypeBytesPerPixel(...) * width, and multiplied by height to size an allocation. - Integer overflow
- Silent wraparound of an unsigned
size_tmultiply/add that yields a value far smaller than the true byte count, leading to an undersized allocation.
Root Cause Analysis
Three pixel-buffer sizing paths in SurfaceContext.cpp computed allocation sizes with unchecked native size_t multiplication and addition. In the read path, size_t size = tmpRB * tmpInfo.height() could wrap; in the write path, the loop accumulated src[i].info().makeColorType(allowedColorType).minRowBytes() * src[i].height() into tmpSize across levels with both an unchecked multiply and an unchecked running +=; and in the transfer path, GrColorTypeBytesPerPixel(...) * rect.width(), the subsequent SkAlignTo, and rowBytes * rect.height() were all unchecked. The violated invariant is that the computed size must equal the true number of bytes the subsequent code writes into the buffer; when the products wrapped, the allocation (std::make_unique<char[]>(size), SkData::MakeUninitialized(tmpSize), or the transfer buffer sized by size) was too small for the pixel data.
The fix routes every product and sum through an SkSafeMath safe instance (safe.mul, safe.add, safe.alignUp) and bails out (return false / return {}) whenever !safe.ok(), and it additionally guards the write path against a null tmpData allocation failure before dereferencing writable_data().
size_t multiplications of attacker-influenceable dimensions (row bytes times height, plus cross-level accumulation) that silently overflow; the fix computes those sizes with overflow-tracking SkSafeMath operations and refuses to proceed once safe.ok() is false, so an undersized buffer is never allocated.Attack Path
- Supply oversized image dimensions
An attacker drives a pixel operation (read, write, or transfer) with dimensions/color type whose row-bytes-times-height product exceeds
size_trange, so the multiply wraps to a small value. - Trigger the vulnerable sizing path
Reaching the read block (
tmpRB * tmpInfo.height()), the multi-level write loop (minRowBytes * heightaccumulated intotmpSize), or the transfer path (rowBytes * rect.height()) causes the wrapped, too-small size to be used for allocation. - Undersized allocation
The wrapped value is passed to
std::make_unique<char[]>(size),SkData::MakeUninitialized(tmpSize), or the transfer buffer, yielding a buffer smaller than the pixel data that will be placed in it. - Out-of-bounds write into the short buffer
Subsequent pixel copy/conversion into the undersized buffer writes past its end, corrupting adjacent heap memory.
(inferred)— the diff shows only the sizing and allocation, not the specific copy sink. - Escalation to a stronger primitive
Controlled heap corruption is shaped toward a read/write primitive or code execution in the GPU/renderer context.
(inferred)— no exploitation primitive is present in the diff.
Impact Assessment
SurfaceContext pixel paths can force an integer overflow that undersizes a heap allocation, enabling heap buffer corruption in the process running the Ganesh GPU backend (renderer or GPU process). The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. Any progression from the demonstrated undersized allocation to a controlled read/write primitive, sandbox escape, or remote code execution is (inferred) and not established by the patch itself.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/SurfaceContext.cpp |
modified | |
forsrc/gpu/ganesh/SurfaceContext.cpp |
modified |
Files Changed
src/gpu/ganesh/SurfaceContext.cpp
Audit Directions
- Row-bytes-times-height sizingSearch other Ganesh/Skia backends for allocation sizes built from
rowBytes * height(orbytesPerPixel * width) using rawsize_tarithmetic instead ofSkSafeMath. - Accumulated multi-level sizesReview loops that sum per-level or per-mip sizes into a running total (like the
tmpSize +=pattern), where both the multiply and the running add can overflow. - Alignment after multiplyCheck
SkAlignTo/round-up calls applied to a product, since alignment can push an already-large value oversize_trange; preferSkSafeMath::alignUpwith anok()check.
Patch
From 82e20c2da974f5eeceb06ef92b2a404b7a74ce47 Mon Sep 17 00:00:00 2001 From: Greg Daniel <[email protected]> Date: Mon, 04 May 2026 16:02:40 +0000 Subject: [PATCH] Fix potential integer overflows in SurfaceContext using SkSafeMath Bug: b/500505046 Change-Id: I68462f3a73f2e1491151c7b9c5fc68fadb9d04a6 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1223518 Commit-Queue: Greg Daniel <[email protected]> Reviewed-by: Michael Ludwig <[email protected]> --- diff --git a/src/gpu/ganesh/SurfaceContext.cpp b/src/gpu/ganesh/SurfaceContext.cpp index 6eaa922..c6fa47f 100644 --- a/src/gpu/ganesh/SurfaceContext.cpp +++ b/src/gpu/ganesh/SurfaceContext.cpp @@ -26,6 +26,7 @@ #include "include/private/base/SkTemplates.h" #include "include/private/base/SkTo.h" #include "include/private/gpu/ganesh/GrTypesPriv.h" +#include "src/base/SkSafeMath.h" #include "src/core/SkColorSpaceXformSteps.h" #include "src/core/SkMipmap.h" #include "src/core/SkTraceEvent.h" @@ -276,7 +277,11 @@ this->colorInfo().refColorSpace(), dst.dimensions()); size_t tmpRB = tmpInfo.minRowBytes(); - size_t size = tmpRB * tmpInfo.height(); + SkSafeMath safe; + size_t size = safe.mul(tmpRB, tmpInfo.height()); + if (!safe.ok()) { + return false; + } // Chrome MSAN bots require the data to be initialized (hence the ()). tmpPixels = std::make_unique<char[]>(size); tmp = {tmpInfo, tmpPixels.get(), tmpRB}; @@ -536,16 +541,24 @@ bool mustBeTight = !caps->writePixelsRowBytesSupport(); size_t tmpSize = 0; if (mustBeTight || convertAll) { + SkSafeMath safe; for (int i = 0; i < numLevels; ++i) { if (convertAll || (mustBeTight && src[i].rowBytes() != src[i].info().minRowBytes())) { - tmpSize += src[i].info().makeColorType(allowedColorType).minRowBytes()* - src[i].height(); + size_t minRowBytes = src[i].info().makeColorType(allowedColorType).minRowBytes(); + size_t levelSize = safe.mul(minRowBytes, src[i].height()); + tmpSize = safe.add(tmpSize, levelSize); } } + if (!safe.ok()) { + return false; + } } auto tmpData = tmpSize ? SkData::MakeUninitialized(tmpSize) : nullptr; - void* tmp = tmpSize ? tmpData->writable_data() : nullptr; + if (tmpSize && !tmpData) { + return false; + } + void* tmp = tmpSize ? tmpData->writable_data() : nullptr; AutoSTArray<15, GrMipLevel> srcLevels(numLevels); bool ownAllStorage = true; for (int i = 0; i < numLevels; ++i) { @@ -1353,9 +1366,15 @@ return {}; } - size_t rowBytes = GrColorTypeBytesPerPixel(supportedRead.fColorType) * rect.width(); - rowBytes = SkAlignTo(rowBytes, this->caps()->transferBufferRowBytesAlignment()); - size_t size = rowBytes * rect.height(); + SkSafeMath safe; + size_t bytesPerPixel = GrColorTypeBytesPerPixel(supportedRead.fColorType); + size_t rowBytes = safe.mul(bytesPerPixel, rect.width()); + size_t maxTransAlignment = this->caps()->transferBufferRowBytesAlignment(); + rowBytes = safe.alignUp(rowBytes, maxTransAlignment); + size_t size = safe.mul(rowBytes, rect.height()); + if (!safe.ok()) { + return {}; + } // By using kStream_GrAccessPattern here, we are not able to cache and reuse the buffer for // multiple reads. Switching to kDynamic_GrAccessPattern would allow for this, however doing // so causes a crash in a chromium test. See skbug.com/40042671
Original Bug Report
Potential Integer Overflow in Skia SurfaceContext::readPixels leads to Heap OOB Access
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 security team.
Overview: A 32-bit integer overflow in Skia’s Ganesh backend can trigger a zero-sized heap allocation during pixel readback color conversion. This leads to a massive out-of-bounds read or write of up to 4 GiB on 32-bit platforms. This issue could potentially be exploited by a compromised renderer to leak GPU process memory or cause memory corruption.
Affected files:
third_party/skia/src/gpu/ganesh/SurfaceContext.cpp
Estimated timestamp from git blame: 2021-04-02
Summary
A potential integer overflow vulnerability exists in SurfaceContext::readPixels within Skia’s Ganesh backend. When reading pixels from a large surface that requires an intermediate color conversion, the calculation for the temporary CPU buffer size can overflow on 32-bit architectures. This results in a zero-sized heap allocation followed by a massive 4 GiB out-of-bounds (OOB) memory access, which can lead to a Denial of Service, potential Information Leak, or OOB write in the GPU process.
Technical Details
In third_party/skia/src/gpu/ganesh/SurfaceContext.cpp, if a pixel readback requires format conversion, a temporary buffer is allocated:
size_t tmpRB = tmpInfo.minRowBytes();
size_t size = tmpRB * tmpInfo.height();
// Chrome MSAN bots require the data to be initialized (hence the ()).
tmpPixels = std::make_unique<char[]>(size);
On 32-bit systems, size_t is 32 bits wide. If the readback requires a high-precision fallback format (e.g., kRGBA_F32, which is 16 bytes per pixel) and the surface dimensions are extremely large (e.g., 16384x16384), the size calculation overflows:
tmpRB= 16 bytes/pixel * 16384 = 262,144 bytes.size= 262,144 * 16384 = 4,294,967,296.4,294,967,296 % 2^32 = 0(on 32-bit systems).
std::make_unique<char[]>(0) allocates a minimal valid heap buffer. Following this, GrGLGpu::readPixels is called to write 4 GiB of data into this tiny allocation.
If the OpenGL driver blindly attempts to copy the data, it causes a 4 GiB OOB write. If the driver rejects the massive read and returns an error, Skia ignores the OpenGL error state and proceeds to GrConvertPixels. GrConvertPixels then attempts to read 4 GiB of data from this 0-sized tmpPixels buffer to convert and write 1 GiB into the renderer-supplied SharedMemory, causing a massive OOB read and Information Leak.
Potential Attacker Steps
(Note: These are suggested steps based on code analysis; we do not currently have a working proof-of-concept.)
- Allocate Surface: A compromised renderer process creates a
SharedImagewith dimensions16384 x 16384and formatR16F(16-bit float red). This requires ~512 MiB of GPU memory and is permitted on GPUs whereGL_MAX_TEXTURE_SIZE>= 16384. - Request Readback: The renderer requests a pixel readback of this
SharedImagetoRGBA_8888(4 bytes per pixel) via IPC, providing a 1 GiB SharedMemory region for the destination. - Bypass Decoder Checks: The GPU process validates the 1 GiB destination size against
UINT32_MAX(4 GiB). The check passes. - Trigger Overflow: The request reaches Skia. Because reading
R16FtoRGBA_8888requires an intermediate fallback buffer ofRGBA_F32(16 bpp) on GLES, Skia attempts to allocate16384 * 16384 * 16bytes. This overflows to0on 32-bit systems. - Exploit: The subsequent GPU read or CPU conversion operates on the 0-sized buffer, causing a massive OOB access across the GPU process heap.
Because the linear memory traversal is exactly 4 GiB, it will inevitably hit unmapped memory on a 32-bit system and cause a deterministic crash (DoS). However, an attacker might be able to exfiltrate leaked GPU memory through the SharedMemory before the crash occurs.
Suggested Fix
The size calculation in SurfaceContext::readPixels and related functions (like SurfaceContext::internalWritePixels) must be protected against integer overflow.
Instead of manual multiplication, Skia should use safe math or rely on SkImageInfo::computeByteSize, which already contains overflow checks:
size_t tmpRB = tmpInfo.minRowBytes();
size_t size = tmpInfo.computeByteSize(tmpRB);
if (size == SIZE_MAX) {
return false; // Overflow occurred
}
tmpPixels = std::make_unique<char[]>(size);
This ensures that any dimensions resulting in an overflow are cleanly rejected before memory allocation occurs.
Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234
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.