High patch-now ✔︎ verified CVSS 7.5 chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Skia
DescriptionInteger overflow in Skia
ComponentSkia
Sub-componentInternals>Skia
Bug ClassInteger Overflow
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Additionally cross-checked: a second, different model independently derived the same bug class, vulnerable function and fix from the commit.
Tracker513973560
Fix commit7c9ffcb347d4 (skia) +17/-2
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-05-19 (public git)
Disclosed2026-05-27
Patch gap8 days — fix public in source before the advisory

Background

`SkPathWriter::assemble`
Skia PathOps routine that joins the partial (open) contours left after boolean path operations into a final path (SkPathWriter.cpp:207).
`fEndPtTs` / `endCount`
the array of partial-contour start/end points and its size; endCount equals twice the number of partial contours (SkPathWriter.cpp:215-217).
`entries`
the count of endpoint pairs, computed as a folded triangle endCount * (endCount - 1) / 2, used to size the distances/distLookup arrays (SkPathWriter.cpp:278-281).
`sLink` / `eLink`
link arrays indexed during contour reassembly, initialized to SK_MaxS32 (SkPathWriter.cpp:270-277).

Root Cause Analysis

Verify before building a PoC

The fix in this commit is confirmed, but the trigger and sink live in code this commit does not touch — find and confirm them against the source before building a PoC.

In the pre-fix SkPathWriter::assemble, endCount is taken from fEndPtTs.size() (SkPathWriter.cpp:215) with no upper bound, and entries is then computed in 32-bit signed arithmetic as const int entries = endCount * (endCount - 1) / 2 (SkPathWriter.cpp:278). The invariant the surrounding code assumes is that entries is a correct non-negative size for the distances, sortedDist, and distLookup arrays (SkPathWriter.cpp:279-281); but when endCount is large (the reporter estimates around 46,342), the product endCount * (endCount - 1) exceeds INT_MAX and wraps, making entries negative or too small. The loops that populate the distance arrays and link tables are driven by that value, so they can be skipped or undersized, leaving sLink/eLink at their SK_MaxS32 sentinels (SkPathWriter.cpp:276) and later using those sentinels as indices during reassembly (eIndex = eLink[sIndex], SkPathWriter.cpp:346).

The fix does not widen the arithmetic; instead it bounds the input, adding constexpr int kMaxPartialContours = 10000; if (endCount > kMaxPartialContours * 2) { return; } immediately after the entry asserts and before entries is computed (SkPathWriter.cpp diff), so endCount can be at most 20,000 and entries stays well under INT_MAX. The commit also hardens assembly: it converts a debug-only SkASSERT(!contourPts.empty()) into a runtime if (contourPts.empty()) { SkDEBUGFAIL(...); return; } (SkPathWriter.cpp:367 region) and adds SkASSERT(pt)/SkASSERT(ptT) null checks in deferredLine, deferredMove, update, and changedSlopes.

Key insight
An unbounded endCount fed a 32-bit entries = endCount*(endCount-1)/2 that could overflow and corrupt the sizing of the reassembly arrays; capping the number of partial contours keeps the product in range (and also bounds the O(N^2) work).

Attack Path

  1. Build extreme path geometry Content crafts path data that, after a boolean path op, leaves a very large number of partial contours. (inferred per reporter)
  2. Reach `assemble` PathOps calls SkPathWriter::assemble, where endCount reflects that large count (SkPathWriter.cpp:215).
  3. Overflow the size endCount * (endCount - 1) / 2 overflows 32-bit int, so entries is wrong (SkPathWriter.cpp:278).
  4. Corrupt reassembly Undersized/skipped link setup leaves SK_MaxS32 sentinels that are then used as array indices in the assembly loop (SkPathWriter.cpp:338-346), driving out-of-bounds access. (inferred that this yields memory corruption)

Impact Assessment

An attacker able to feed such geometry into Skia PathOps could cause out-of-bounds memory access; the reporter situates this in the PrintCompositor utility process reached via the printing flow (reachability into that process is inferred, not shown in the Skia material). The advisory classifies this as an integer overflow in Skia, high severity, with no in-the-wild exploitation; escalation to code execution or Site Isolation bypass is (inferred).

Changed Functions

FunctionChangeNotes
if
src/pathops/SkPathWriter.cpp
modified
for
src/pathops/SkPathWriter.cpp
modified

Files Changed

  • src/pathops/SkPathWriter.cpp

Audit Directions

  • Unbounded N in N-squared sizing
    Flag n * (n - 1) / 2 or similar quadratic size computations in 32-bit int where n comes from untrusted input; cap n or widen the arithmetic.
  • Sentinel-initialized index arrays
    Where arrays are pre-filled with SK_MaxS32/max sentinels and later used as indices, verify every element is overwritten before use.
  • Debug-only invariants on attacker-influenced data
    Convert SkASSERTs that guard geometry-dependent conditions into runtime checks so malformed input fails safe in release builds.
From 7c9ffcb347d4169fdca223d21cc62c2c0420db66 Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <[email protected]>
Date: Tue, 19 May 2026 15:07:23 +0000
Subject: [PATCH] Avoid overflow and timeout in SkPathWriter::assemble

I was unable to make a test case that caused an overflow
and didn't timeout, but I think the possibility exists
for both.

This removes that, adds a few defensive asserts, and
makes one assert actually checked at runtime, just to be safe.

Change-Id: I4ce112624cb346ac7597466ac9c78402c5061e1c
Bug: 513973560
Fixed: 513973560
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1239556
Reviewed-by: Thomas Smith <[email protected]>
---

diff --git a/src/pathops/SkPathWriter.cpp b/src/pathops/SkPathWriter.cpp
index 6577d9d..b374f9c 100644
--- a/src/pathops/SkPathWriter.cpp
+++ b/src/pathops/SkPathWriter.cpp
@@ -56,6 +56,7 @@
 }
 
 bool SkPathWriter::deferredLine(const SkOpPtT* pt) {
+    SkASSERT(pt);
     SkASSERT(fFirstPtT);
     SkASSERT(fDefer[0]);
     if (fDefer[0] == pt) {
@@ -78,6 +79,7 @@
 }
 
 void SkPathWriter::deferredMove(const SkOpPtT* pt) {
+    SkASSERT(pt);
     if (!fDefer[1]) {
         fFirstPtT = fDefer[0] = pt;
         return;
@@ -161,6 +163,7 @@
 // if last point to be written matches the current path's first point, alter the
 // last to avoid writing a degenerate lineTo when the path is closed
 SkPoint SkPathWriter::update(const SkOpPtT* pt) {
+    SkASSERT(pt);
     if (!fDefer[1]) {
         this->moveTo();
     } else if (!this->matchedLast(fDefer[0])) {
@@ -180,6 +183,7 @@
 }
 
 bool SkPathWriter::changedSlopes(const SkOpPtT* ptT) const {
+    SkASSERT(ptT);
     if (matchedLast(fDefer[0])) {
         return false;
     }
@@ -214,7 +218,15 @@
     SkOpPtT const* const* runs = fEndPtTs.begin();  // starts, ends of partial contours
     int endCount = fEndPtTs.size(); // all starts and ends
     SkASSERT(endCount > 0);
-    SkASSERT(endCount == fPartials.size() * 2);
+    SkASSERT(endCount == (int)fPartials.size() * 2);
+
+    // Limit the number of partial contours to avoid O(N^2) complexity and integer overflows.
+    // 10,000 partial contours results in 20,000 ends and ~200,000,000 distance entries.
+    constexpr int kMaxPartialContours = 10000;
+    if (endCount > kMaxPartialContours * 2) {
+        return;
+    }
+
 #if DEBUG_ASSEMBLE
     for (int index = 0; index < endCount; index += 2) {
         const SkOpPtT* eStart = runs[index];
@@ -364,7 +376,10 @@
                 if (forward) {
                     next = contourPts.empty() ? SkPoint{0, 0} : contourPts.front();
                 } else {
-                    SkASSERT(!contourPts.empty());
+                    if (contourPts.empty()) {
+                        SkDEBUGFAIL("unexpected empty contour");
+                        return;
+                    }
                     next = contourPts.back();
                 }
                 if (*prior != next) {
Loading diff…

Original Bug Report

reported by [email protected]

Heap memory corruption in PrintCompositor via Skia PathOps 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: An integer overflow in Skia’s PathOps engine can be triggered by providing highly complex path geometry during PDF printing. This flaw leads to out-of-bounds memory access in the PrintCompositor utility process, potentially allowing for Site Isolation bypass or Remote Code Execution.

Affected files:

  • third_party/skia/src/utils/SkClipStackUtils.cpp
  • third_party/skia/src/pdf/SkPDFDevice.cpp
  • third_party/skia/src/pdf/SkPDFGraphicStackState.cpp
  • components/services/print_compositor/print_compositor_impl.cc
  • printing/common/metafile_utils.cc

Estimated timestamp from git blame: 2019-12-20

Potential Root Cause

A vulnerability has been identified in the Skia PathOps engine, specifically within the SkPathWriter::assemble() function in third_party/skia/src/pathops/SkPathWriter.cpp. This function is responsible for joining “partial contours” (open path segments) that remain after complex boolean operations (like Simplify or Op).

The function calculates the number of potential endpoint pairs using 32-bit signed integer arithmetic:

const int entries = endCount * (endCount - 1) / 2;

If endCount (the number of endpoints) is approximately 46,342 or greater, the product endCount * (endCount - 1) overflows the maximum value for a 32-bit signed integer (INT_MAX). This results in entries becoming negative or incorrectly small.

Technical Details

When this overflow occurs, the following memory corruption primitives are triggered:

  1. Skipped Initializations: The loop that populates the distance and link arrays (for (rIndex = 0; rIndex < entries; ++rIndex)) is skipped if entries is negative. This leaves the sLink and eLink arrays populated with their default value of SK_MaxS32 (2,147,483,647).
  2. Massive Out-of-Bounds Read: In the subsequent assembly loop, the code uses these default values as indices: eIndex = eLink[sIndex]. If sIndex is SK_MaxS32, this performs an enormous out-of-bounds read from the heap.
  3. Heap Memory Corruption: The resulting eIndex (likely containing arbitrary heap data) is then used to index into the fPartials array (a collection of SkPathBuilder objects). The subsequent call to fPartials[rIndex].snapshot() results in the engine treating arbitrary heap memory as a valid SkPathBuilder object, leading to memory corruption.
  4. Out-of-Bounds Write: Additionally, the cumulative row offset rRow used to index distLookup can also overflow, causing negative indices to be stored and later used to trigger out-of-bounds writes in the linkOne or linkTwo arrays.

Attack Vector and Reachability

An attacker who has already compromised a renderer process could potentially trigger this sink by:

  1. Generating a malicious SkMultiPictureDocument (MSKP) containing thousands of self-intersecting SkPath objects designed to create high complexity in PathOps.
  2. Submitting this document to the browser via the printing flow, which forwards the content to the PrintCompositor utility process for PDF conversion.
  3. As the PrintCompositor process replays the drawing commands, the complex geometry triggers the PathOps engine and the overflow in SkPathWriter::assemble().

Note that these are potential steps identified through code analysis; our current environment does not support executing proof-of-concept code.

Impact

The PrintCompositor service is a critical component for Site Isolation, as it aggregates content from multiple cross-origin renderer processes for printing. Compromising this process would allow an attacker to bypass Site Isolation and read cross-origin printed data.

Suggested Potential Fix

The calculations for entries and rRow in SkPathWriter::assemble should be updated to use 64-bit integers (int64_t) or Chromium’s base::CheckedNumeric to prevent overflows. Furthermore, a reasonable limit should be placed on the number of partial contours processed to mitigate both memory corruption and potential Denial of Service attacks via resource exhaustion.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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.

View on issue tracker