CVE-2026-10009
Overview
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;
endCountequals 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 thedistances/distLookuparrays (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
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.
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
- 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)
- Reach `assemble`
PathOps calls
SkPathWriter::assemble, whereendCountreflects that large count (SkPathWriter.cpp:215). - Overflow the size
endCount * (endCount - 1) / 2overflows 32-bitint, soentriesis wrong (SkPathWriter.cpp:278). - Corrupt reassembly
Undersized/skipped link setup leaves
SK_MaxS32sentinels 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
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
| Function | Change | Notes |
|---|---|---|
ifsrc/pathops/SkPathWriter.cpp |
modified | |
forsrc/pathops/SkPathWriter.cpp |
modified |
Files Changed
src/pathops/SkPathWriter.cpp
Audit Directions
- Unbounded N in N-squared sizingFlag
n * (n - 1) / 2or similar quadratic size computations in 32-bitintwherencomes from untrusted input; capnor widen the arithmetic. - Sentinel-initialized index arraysWhere 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 dataConvert
SkASSERTs that guard geometry-dependent conditions into runtime checks so malformed input fails safe in release builds.
Patch
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) {
Original Bug Report
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.cppthird_party/skia/src/pdf/SkPDFDevice.cppthird_party/skia/src/pdf/SkPDFGraphicStackState.cppcomponents/services/print_compositor/print_compositor_impl.ccprinting/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:
- Skipped Initializations: The loop that populates the distance and link arrays (
for (rIndex = 0; rIndex < entries; ++rIndex)) is skipped ifentriesis negative. This leaves thesLinkandeLinkarrays populated with their default value ofSK_MaxS32(2,147,483,647). - Massive Out-of-Bounds Read: In the subsequent assembly loop, the code uses these default values as indices:
eIndex = eLink[sIndex]. IfsIndexisSK_MaxS32, this performs an enormous out-of-bounds read from the heap. - Heap Memory Corruption: The resulting
eIndex(likely containing arbitrary heap data) is then used to index into thefPartialsarray (a collection ofSkPathBuilderobjects). The subsequent call tofPartials[rIndex].snapshot()results in the engine treating arbitrary heap memory as a validSkPathBuilderobject, leading to memory corruption. - Out-of-Bounds Write: Additionally, the cumulative row offset
rRowused to indexdistLookupcan also overflow, causing negative indices to be stored and later used to trigger out-of-bounds writes in thelinkOneorlinkTwoarrays.
Attack Vector and Reachability
An attacker who has already compromised a renderer process could potentially trigger this sink by:
- Generating a malicious
SkMultiPictureDocument(MSKP) containing thousands of self-intersectingSkPathobjects designed to create high complexity in PathOps. - Submitting this document to the browser via the printing flow, which forwards the content to the
PrintCompositorutility process for PDF conversion. - As the
PrintCompositorprocess replays the drawing commands, the complex geometry triggers the PathOps engine and the overflow inSkPathWriter::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.