Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in Skia
DescriptionHeap buffer overflow in Skia
ComponentSkia
Bug ClassOOB
Tracker498809718
Fix commit63dbf37c3700 (skia) +167/-25
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-28

Changed Functions

FunctionChangeNotes
SK_API
include/core/SkRegion.h
modified
if
src/core/SkRegion.cpp
modified
switch
src/core/SkRegion.cpp
modified

Files Changed

  • include/core/SkRegion.h
  • src/core/SkRegion.cpp
From 63dbf37c3700b34b5050d418d7cc9a3cb1708d5b Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <[email protected]>
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 <[email protected]>
---

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.
Loading diff…

Original Bug Report

reported by [email protected]

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.cpp
  • third_party/skia/src/core/SkRegion.cpp
  • third_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.

  1. Initial Compromise: An attacker gains arbitrary code execution in the sandboxed Renderer process.
  2. Crafting the Payload: The attacker crafts a serialized SkRegion byte array. The header specifies a small intervalCount (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.
  3. Embedding in IPC: The attacker embeds this SkRegion into an AlphaThresholdPaintFilter as part of a cc::PaintOpBuffer stream.
  4. Triggering the Bug: The compromised Renderer sends this PaintOpBuffer to the GPU process for Out-of-Process Rasterization (OOP-R) via the kRasterCHROMIUM command.
  5. 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 the VertexWriter overflows the heap buffer, leading to Remote Code Execution (RCE) in the GPU process.

Suggested Fix

  1. 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;
        }
  1. Harden Validation: Update validate_run to 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.

View on issue tracker