CVE-2026-7353
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
SK_APIinclude/core/SkRegion.h |
modified | |
ifsrc/core/SkRegion.cpp |
modified | |
switchsrc/core/SkRegion.cpp |
modified |
Files Changed
include/core/SkRegion.hsrc/core/SkRegion.cpp
Patch
From 63dbf37c3700b34b5050d418d7cc9a3cb1708d5b Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <kjlubick@google.com>
Date: Fri, 24 Apr 2026 14:04:56 +0000
Subject: [PATCH] Reject SkRegions with multiple empty slices in a row
This also adds some documentation that I found helpful when
poking through this file.
I added SkRegion_Iterator_StepsThroughAllScanlines to make sure
I understood how the SkRegion class works and what iteration
is supposed to do. Then, I made
SkRegion_ReadFromMemory_ConsecutiveEmptySlices_Invalid which
reproduced the linked bug (e.g. crash in debug and ASAN issue
in release). I added the fix and a final test to make sure
the empty regions still work.
Bug: b/498809718
Change-Id: Idc68540170a3440f45f8f58890e36e81a36a05ba
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1217257
Reviewed-by: Florin Malita <fmalita@google.com>
---
diff --git a/include/core/SkRegion.h b/include/core/SkRegion.h
index 22a7216..041cd96 100644
--- a/include/core/SkRegion.h
+++ b/include/core/SkRegion.h
@@ -447,8 +447,9 @@
#endif
/** \class SkRegion::Iterator
- Returns sequence of rectangles, sorted along y-axis, then x-axis, that make
- up SkRegion.
+ Goes through the region one rectangle at a time. For each "strip" of one or more contiguous
+ Y values (scanlines) in ascending order, the iterator returns each rectangle in that strip
+ (from left to right) before advancing to the next strip (which may or may not have a gap).
*/
class SK_API Iterator {
public:
@@ -494,9 +495,12 @@
bool done() const { return fDone; }
/** Advances SkRegion::Iterator to next SkIRect in SkRegion if it is not done.
-
- example: https://fiddle.skia.org/c/@Region_Iterator_next
- */
+ * This moves to the next rectangle to the right within the current
+ * horizontal strip. If the end of the strip is reached, it automatically
+ * advances to the first rectangle in the next strip, skipping any vertical gaps.
+ *
+ * example: https://fiddle.skia.org/c/@Region_Iterator_next
+ */
void next();
/** Returns SkIRect element in SkRegion. Does not return predictable results if SkRegion
@@ -514,6 +518,7 @@
private:
const SkRegion* fRgn;
+ // See top of SkRegion.cpp for more on the RLE encoding
const SkRegion::RunType* fRuns;
SkIRect fRect = {0, 0, 0, 0};
bool fDone;
diff --git a/src/core/SkRegion.cpp b/src/core/SkRegion.cpp
index f8e7c62..64bdcb3 100644
--- a/src/core/SkRegion.cpp
+++ b/src/core/SkRegion.cpp
@@ -23,14 +23,30 @@
using namespace skia_private;
-/* Region Layout
+/* SkRegion Run-Length Encoding (RLE) Format:
*
- * TOP
+ * A region is stored as a series of non-overlapping horizontal strips (scanlines).
+ * All rectangles in a strip share the same Top and Bottom Y-coordinates.
*
- * [ Bottom, X-Intervals, [Left, Right]..., X-Sentinel ]
- * ...
+ * Data layout:
+ * [Top-Y]
+ * [Bottom-Y, Interval-Count (N), Left-1, Right-1, ..., Left-N, Right-N, X-Sentinel]
+ * [Bottom-Y, Interval-Count (M), Left-1, Right-1, ..., Left-M, Right-M, X-Sentinel]
+ * ...
+ * [Y-Sentinel]
*
- * Y-Sentinel
+ * - Sentinels are 0x7FFFFFFF (SkRegion_kRunTypeSentinel).
+ * - Interval-Count can be 0 (representing a vertical gap between strips).
+ * - Strips are sorted by Y; Intervals within a strip are sorted by X.
+ *
+ * Example: Two rects [10, 10, 20, 20] and [30, 10, 40, 20] on one line,
+ * then a gap until Y=30, then [15, 30, 25, 40] would be:
+ *
+ * 10 // Global Top-Y
+ * 20, 2, 10, 20, 30, 40, S // Strip 1: Y 10-20, 2 rects, X-Sentinel
+ * 30, 0, S // Strip 2: Y 20-30, 0 rects (gap), X-Sentinel
+ * 40, 1, 15, 25, S // Strip 3: Y 30-40, 1 rect, X-Sentinel
+ * S // Y-Sentinel
*/
/////////////////////////////////////////////////////////////////////////////////////////////////
@@ -98,6 +114,10 @@
return const_cast<SkRegionPriv::RunType*>(runs);
}
+static inline void assert_sentinel(int32_t value, bool isSentinel) {
+ SkASSERT(SkRegionValueIsSentinel(value) == isSentinel);
+}
+
bool SkRegion::RunsAreARect(const SkRegion::RunType runs[], int count,
SkIRect* bounds) {
assert_sentinel(runs[0], false); // top
@@ -299,7 +319,7 @@
if (runs[3] == SkRegion_kRunTypeSentinel) { // should be first left...
runs += 3; // skip empty initial span
runs[0] = runs[-2]; // set new top to prev bottom
- assert_sentinel(runs[1], false); // bot: a sentinal would mean two in a row
+ assert_sentinel(runs[1], false); // bot: a sentinel would mean two in a row
assert_sentinel(runs[2], false); // intervalcount
assert_sentinel(runs[3], false); // left
assert_sentinel(runs[4], false); // right
@@ -879,7 +899,7 @@
int flush() {
(*fArray)[fStartDst] = fTop;
- // Previously reserved enough for TWO sentinals.
+ // Previously reserved enough for TWO sentinels.
SkASSERT(fArray->count() > SkToInt(fPrevDst + fPrevLen));
(*fArray)[fPrevDst + fPrevLen] = SkRegion_kRunTypeSentinel;
return (int)(fPrevDst - fStartDst + fPrevLen + 1);
@@ -1050,20 +1070,18 @@
// swith to using pointers, so we can swap them as needed
const SkRegion* rgna = &rgnaOrig;
const SkRegion* rgnb = &rgnbOrig;
- // after this point, do not refer to rgnaOrig or rgnbOrig!!!
- // collaps difference and reverse-difference into just difference
+ // collapse difference and reverse-difference into just difference
if (kReverseDifference_Op == op) {
- using std::swap;
- swap(rgna, rgnb);
+ std::swap(rgna, rgnb);
op = kDifference_Op;
}
SkIRect bounds;
- bool a_empty = rgna->isEmpty();
- bool b_empty = rgnb->isEmpty();
- bool a_rect = rgna->isRect();
- bool b_rect = rgnb->isRect();
+ bool a_empty = rgna->isEmpty();
+ bool b_empty = rgnb->isEmpty();
+ bool a_rect = rgna->isRect();
+ bool b_rect = rgnb->isRect();
switch (op) {
case kDifference_Op:
@@ -1188,9 +1206,11 @@
}
SkSafeMath safeMath;
int sum = 2;
+ // 3 bytes per ySpan (stop Y, how many intervals, sentinel)
sum = safeMath.addInt(sum, ySpanCount);
sum = safeMath.addInt(sum, ySpanCount);
sum = safeMath.addInt(sum, ySpanCount);
+ // 2 bytes per interval (startX, stop X)
sum = safeMath.addInt(sum, intervalCount);
sum = safeMath.addInt(sum, intervalCount);
return safeMath && sum == runCount;
@@ -1218,6 +1238,7 @@
const int32_t* const end = runs + runCount;
SkIRect bounds = {0, 0, 0 ,0}; // calulated bounds
SkIRect rect = {0, 0, 0, 0}; // current rect
+ bool prevWasEmpty = true; // If we start with an empty slice, that's corrupted data.
rect.fTop = *runs++;
if (rect.fTop == SkRegion_kRunTypeSentinel) {
return false; // no rect can contain SkRegion_kRunTypeSentinel
@@ -1246,6 +1267,15 @@
if (xIntervals < 0 || xIntervals > intervalCount || runs + 1 + 2 * xIntervals > end) {
return false;
}
+ if (xIntervals == 0) {
+ if (prevWasEmpty) {
+ // back to back empty spans are invalid; our serialization always has them together
+ return false;
+ }
+ prevWasEmpty = true;
+ } else {
+ prevWasEmpty = false;
+ }
intervalCount -= xIntervals;
bool firstInterval = true;
int32_t lastRight = 0; // check that x-intervals are distinct and ordered.
@@ -1263,7 +1293,7 @@
bounds.join(rect);
}
if (*runs++ != SkRegion_kRunTypeSentinel) {
- return false; // required check sentinal.
+ return false; // required check sentinel.
Original Bug Report
Heap OOB write in GPU process via SkRegion::Iterator desynchronization
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 logic error in SkRegion::Iterator causes it to desynchronize when parsing a region with empty spans, yielding more rectangles than its reported interval count. When such a crafted region is sent to the GPU process and drawn using RegionOp, these excess rectangles cause an out-of-bounds write in the VertexWriter. This leads to a potential heap-based buffer overflow and RCE in the privileged GPU process.
Affected files:
third_party/skia/src/gpu/ganesh/ops/RegionOp.cppthird_party/skia/src/core/SkRegion.cppthird_party/skia/src/gpu/BufferWriter.h
Estimated timestamp from git blame: 2021-10-26
Summary
A vulnerability in Skia’s SkRegion deserialization and iteration logic allows a compromised renderer process to trigger a heap-based out-of-bounds write in the highly privileged GPU process.
By crafting a serialized SkRegion containing empty y-spans, an attacker can cause SkRegion::Iterator to desynchronize during iteration. This desynchronization results in the iterator yielding more rectangles than the region’s reported total interval count. The Ganesh RegionOp uses the trusted interval count to allocate its vertex buffer, and the subsequent iteration with the desynchronized iterator overflows this buffer.
Vulnerability Details
1. Incomplete Validation in SkRegion::readFromMemory
The validate_run function in third_party/skia/src/core/SkRegion.cpp, called during readFromMemory, checks that the number of x-intervals in each span is within bounds. However, it does not prevent a region from containing consecutive empty y-spans (where xIntervals == 0). While a single empty span is a valid structural element in Skia to represent vertical gaps, consecutive empty spans or empty spans at the end of a region can trigger parsing bugs downstream.
2. Iterator Desynchronization in SkRegion::Iterator::next()
The SkRegion::Iterator::next() function (third_party/skia/src/core/SkRegion.cpp:1409) has a critical logic flaw when handling empty spans.
if (runs[0] < SkRegion_kRunTypeSentinel) { // valid Y value
int intervals = runs[1];
if (0 == intervals) { // empty line
fRect.fTop = runs[0];
runs += 3; // Advance past the empty span
} else {
fRect.fTop = fRect.fBottom;
}
// BUG: Execution unconditionally falls through to here.
fRect.fBottom = runs[0];
assert_sentinel(runs[2], false);
// ...
When it encounters an empty span (intervals == 0), it sets fRect.fTop and advances the run pointer by 3 (runs += 3) to skip it. However, it fails to return or continue. It unconditionally proceeds to read the “next” span’s data within the same next() call. Because runs was already advanced, it misinterprets the subsequent structural metadata (like sentinels or the next span’s headers) as rectangle coordinates. The iterator yields a “phantom” rectangle and its internal state becomes completely desynchronized from the true run array structure.
3. OOB Write in Ganesh RegionOp
In third_party/skia/src/gpu/ganesh/ops/RegionOp.cpp, RegionOpImpl::onPrepareDraws calculates the required vertex buffer size using computeRegionComplexity(), which returns the region’s fIntervalCount (trusted from the deserialized header). It then allocates a vertex buffer for exactly this number of quads using a QuadHelper.
Subsequently, it iterates through the region using SkRegion::Iterator in a while (!iter.done()) loop, calling vertices.writeQuad() for each rectangle yielded.
Because the iterator desynchronizes on empty spans, it consumes multiple structural elements to produce single phantom rectangles, failing to terminate correctly. Consequently, it yields more rectangles than the originally allocated fIntervalCount.
VertexWriter (third_party/skia/src/gpu/BufferWriter.h) relies entirely on SkASSERT for bounds checking, which compiles to a no-op in Release builds. When writeQuad is called for the excess rectangles, it writes attacker-controlled floating-point data past the end of the allocated vertex buffer on the heap.
Potential Attack Steps
Note: These steps are based on static analysis, as our tooling agent does not currently run exploit code.
- Initial Compromise: An attacker gains arbitrary code execution in the sandboxed Renderer process.
- Crafting the Payload: The attacker crafts a serialized
SkRegionbyte array. The header specifies a smallintervalCount(e.g., 1). The run array is structured with empty spans followed by carefully chosen data designed to be misinterpreted as attacker-controlled rectangle coordinates. - Embedding in IPC: The attacker embeds this
SkRegioninto anAlphaThresholdPaintFilteras part of acc::PaintOpBufferstream. - Triggering the Bug: The compromised Renderer sends this
PaintOpBufferto the GPU process for Out-of-Process Rasterization (OOP-R) via thekRasterCHROMIUMcommand. - Execution: The GPU process deserializes the
PaintOpBuffer, instantiates the filter, and attempts to draw the region using Ganesh. The iterator desynchronizes, yielding excess rectangles, and theVertexWriteroverflows the heap buffer, leading to Remote Code Execution (RCE) in the GPU process.
Suggested Fix
- Fix the Iterator: In
SkRegion::Iterator::next()(third_party/skia/src/core/SkRegion.cpp), modify the empty line handling block to either loop to find the next valid interval or return immediately after advancing the pointer.
if (0 == intervals) { // empty line
fRect.fTop = runs[0];
runs += 3;
fRuns = runs;
// RECURSE OR LOOP HERE to find the next valid rectangle instead of falling through.
this->next();
return;
}
- Harden Validation: Update
validate_runto explicitly reject consecutive empty spans or empty spans at the end of a region if they are considered malformed.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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.