CVE-2026-13841
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/graphite/KeyHelpers.cpp |
modified | |
FloatStorageManagersrc/gpu/graphite/PipelineData.h |
modified | |
ifsrc/gpu/graphite/PipelineData.h |
modified |
Files Changed
src/gpu/graphite/KeyHelpers.cppsrc/gpu/graphite/PipelineData.h
Patch
From 5ecba665f5d1cf8b0fadd5986f7eaf579cca282e Mon Sep 17 00:00:00 2001 From: Michael Ludwig <[email protected]> Date: Fri, 22 May 2026 16:44:17 -0400 Subject: [PATCH] [graphite] Drop excessively large gradient draws This skips recording draws with more than 1M color stops, primarily as a way to avoid worrying about overflowing during intermediate calculations. We can increase it if necessary, but hopefully this is healthy enough no one is trying to make shaders this large. This also skips recording draws when the FSM has maxed out its allocatable size for a single buffer. Given how large that is, we shouldn't encounter it in the wild but this lets us fail semi gracefully. If needed, we can revisit by either flushing the entire Recorder when reaching a limit, or by allowing a recording to use multiple buffers Bug: 515467789 Fixed: 515467789 Change-Id: Ie032f9ed35b6cf0316a18b32bb36e3ec3c047097 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1243936 Commit-Queue: Michael Ludwig <[email protected]> Reviewed-by: Robert Phillips <[email protected]> Reviewed-by: Thomas Smith <[email protected]> --- diff --git a/src/gpu/graphite/KeyHelpers.cpp b/src/gpu/graphite/KeyHelpers.cpp index 36ebed3..1e94cc7 100644 --- a/src/gpu/graphite/KeyHelpers.cpp +++ b/src/gpu/graphite/KeyHelpers.cpp @@ -289,6 +289,9 @@ // Writes the color and offset data directly in the gatherer gradient buffer and returns the // offset the data begins at in the buffer. +// +// Returns a negative offset to signal failure, in which case the paint key must be poisoned +// to drop the draw. static int write_color_and_offset_bufdata(int numStops, const SkPMColor4f* colors, const float* offsets, @@ -296,6 +299,7 @@ FloatStorageManager* floatStorageManager) { auto [dstData, bufferOffset] = floatStorageManager->allocateGradientData(numStops, shader); if (dstData) { + SkASSERT(bufferOffset >= 0); // Data doesn't already exist so we need to write it. // Writes all offset data, then color data. This way when binary searching through the // offsets, there is better cache locality. @@ -388,16 +392,24 @@ void GradientShaderBlocks::AddBlock(const KeyContext& keyContext, const GradientData& gradData) { int bufferOffset = 0; if (gradData.fNumStops > GradientData::kNumInternalStorageStops && keyContext.recorder()) { + bool hasStorage; if (gradData.fUseStorageBuffer) { bufferOffset = write_color_and_offset_bufdata(gradData.fNumStops, gradData.fSrcColors, gradData.fSrcOffsets, gradData.fSrcShader, keyContext.floatStorageManager()); + hasStorage = bufferOffset >= 0; } else { - SkASSERT(gradData.fColorsAndOffsetsProxy); keyContext.pipelineDataGatherer()->add(gradData.fColorsAndOffsetsProxy, {SkFilterMode::kNearest, SkTileMode::kClamp}); + hasStorage = SkToBool(gradData.fColorsAndOffsetsProxy); + } + + if (!hasStorage) { + keyContext.paintParamsKeyBuilder()->addErrorBlock(); + SKGPU_LOG_W("Couldn't upload large gradient color stop data"); + return; } } diff --git a/src/gpu/graphite/PipelineData.h b/src/gpu/graphite/PipelineData.h index 236a3f1..bc25542 100644 --- a/src/gpu/graphite/PipelineData.h +++ b/src/gpu/graphite/PipelineData.h @@ -462,6 +462,23 @@ * DrawPass. It de-duplicates gradient data by caching based on the SkGradientBaseShader pointer. */ class FloatStorageManager : public SkRefCnt { + // Size limit for individual gradients (anything larger will be dropped) + static constexpr int kMaxGradientStops = 1024 * 1024; // ~5MB of data in the shader + + // Size limit for the max buffer size. If a new draw would exceed this limit, we drop the draw. + // The float storage manager is used by all Devices in a Recorder and is reset at snap(), + // requiring a global flush to otherwise get a new buffer (which is undesirable). Instead, we + // assume that exceeding this limit happens in two situations: + // 1. Adversarial content, at which point correctness is not critical. + // 2. A truly bespoke application requiring 4GB of gradient color data should be having its + // workload managed at the application level where it can snap Recordings. + // + // It is also likely that even if we accumulate this much CPU data, a GPU driver will fail to + // create a buffer for us to copy to, causing the snap() to fail. + static constexpr int kMaxStorageFloats = + static_cast<int>(std::numeric_limits<uint32_t>::max() / sizeof(float)); + static_assert(std::numeric_limits<uint32_t>::max() / sizeof(float) + <= (uint32_t) std::numeric_limits<int>::max()); public: FloatStorageManager() = default; @@ -473,14 +490,26 @@ // Checks if data already exists for the requested gradient shader. If so, it returns // a nullptr and the existing offset. If not, it allocates space, caches the offset, // and returns a pointer to the start of the new data and the calculated offset. + // + // If it was not possible to store the gradient data, a nullptr and negative offset + // are returned to signal the error state. std::pair<float*, int> allocateGradientData(int numStops, const SkGradientBaseShader* shader) { SkASSERT(!this->isFinalized()); + if (numStops > kMaxGradientStops) { + return {nullptr, -1}; + } + int* existingOffset = fGradientOffsetCache.find(shader->uniqueID()); if (existingOffset) { return {nullptr, *existingOffset}; } auto [ptr, offset] = this->allocateFloatData(numStops * 5); // 4 for color, 1 for offset - fGradientOffsetCache.set(shader->uniqueID(), offset); + + // Only cache the storage if it was allocated successfully. + if (ptr) { + SkASSERT(offset >= 0); + fGradientOffsetCache.set(shader->uniqueID(), offset); + } return {ptr, offset}; } @@ -488,6 +517,7 @@ bool finalize(DrawBufferManager* bufferMgr) { SkASSERT(!this->isFinalized()); if (!fGradientStorage.empty()) { + SkASSERT(fGradientStorage.size() <= kMaxStorageFloats); auto [writer, bufferInfo, _] = bufferMgr->getMappedStorageBuffer(fGradientStorage.size(), sizeof(float)); if (writer) { @@ -512,6 +542,9 @@ // of the new allocation and its offset from the beginning of the buffer. std::pair<float*, int> allocateFloatData(int floatCount) { int currentSize = fGradientStorage.size(); + if (kMaxStorageFloats - floatCount < currentSize) { + return {nullptr, -1}; // We've accumulated too much + } fGradientStorage.resize(currentSize + floatCount); float* startPtr = fGradientStorage.begin() + currentSize;
Original Bug Report
Potential Heap Out-of-Bounds Write in Skia Graphite via Integer Overflow in FloatStorageManager
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A series of integer overflows in Skia’s Graphite backend allow a compromised renderer to trigger a heap out-of-bounds write in the GPU process. This occurs when aggregating large numbers of gradient stops into a shared buffer, leading to container state corruption and undersized allocations. On Android, where the GPU process is unsandboxed, this could potentially lead to a full sandbox escape.
Affected files:
third_party/skia/src/gpu/graphite/PipelineData.hthird_party/skia/src/gpu/graphite/KeyHelpers.cppthird_party/skia/src/base/SkTDArray.cppthird_party/skia/include/private/base/SkTDArray.h
Estimated timestamp from git blame: 2024-06-11
Summary
In Skia’s Graphite rendering backend, the FloatStorageManager aggregates gradient color and offset data for all draw calls within a single DrawPass into a shared SkTDArray<float>. By accumulating a large number of gradient stops across multiple drawing commands, a compromised renderer can trigger a 32-bit signed integer overflow in the buffer’s size calculation. This leads to a logic error in SkTDArray where the buffer is not correctly resized, resulting in a subsequent out-of-bounds write of attacker-controlled float data.
Technical Details
1. Integer Overflow in Size Calculation
In third_party/skia/src/gpu/graphite/PipelineData.h, the allocateFloatData function calculates the new required size for the shared gradient buffer:
// PipelineData.h:515
void allocateFloatData(int floatCount) {
int currentSize = fGradientStorage.size();
fGradientStorage.resize(currentSize + floatCount);
float* startPtr = fGradientStorage.begin() + currentSize;
return {startPtr, currentSize};
}
currentSize and floatCount are signed 32-bit integers. By sending many draw calls within a single Graphite recorder session, an attacker can cause currentSize to approach INT_MAX. A final draw call with a sufficient floatCount will cause the addition currentSize + floatCount to overflow and wrap to a negative value.
2. Logic Error in SkTDStorage::resize
When fGradientStorage.resize() is called with a negative value, it reaches SkTDStorage::resize in third_party/skia/src/base/SkTDArray.cpp:
void SkTDStorage::resize(int newSize) {
SkASSERT(newSize >= 0); // Compiled out in release builds
if (newSize > fCapacity) {
this->reserve(newSize);
}
fSize = newSize;
}
In release builds, the SkASSERT is removed. Because newSize is negative and fCapacity is a large positive value, the reserve() call is skipped. The buffer is not enlarged, but the logical size fSize is corrupted to the negative value.
3. Out-of-Bounds Write
Back in allocateFloatData, a pointer startPtr is returned pointing into the existing buffer at the old currentSize. The function write_color_and_offset_bufdata in KeyHelpers.cpp then writes the gradient data (RGBA floats and offsets) into this pointer. Since the buffer was never resized to accommodate the new data, this results in a heap out-of-bounds write of attacker-controlled floats.
Potential Exploitation Steps
- Use a compromised renderer process to send a stream of unique gradient draw commands via OOP-Rasterization.
- Accumulate approximately 2GB of gradient data in the GPU process’s Graphite recorder to reach the 32-bit signed integer limit.
- Send a final drawing command that triggers the signed integer overflow in
allocateFloatData. - Leverage the resulting heap out-of-bounds write to corrupt adjacent heap objects (e.g., virtual function tables) in the GPU process.
Suggested Fix
- In
PipelineData.h, usebase::CheckedNumericorSkSafeMathto perform the size calculation inallocateFloatDataand handle overflows by failing the draw pass or discarding the data. - Harden
SkTDStorage::resizeto explicitly check and fail ifnewSize < 0in all build types, or transition the container to usesize_tfor its size and capacity members.
Evaluated with Chrome root at commit: 29093e11cf509e3593f6229e4b1b075cca356049
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.