Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Skia
DescriptionInteger overflow in Skia
ComponentSkia
Sub-componentGanesh
Bug ClassInteger Overflow
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld 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
Reviewer notes: Overflow mechanism matches diff exactly. OOB-write/corruption outcome is inferred: diff shows only the SkSafeMath guard and that counts feed kMaxLines/kMaxQuadsAndConics sizing, not the actual out-of-bounds sink.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker382786791
Fix commit8b030e47588a (skia) +18/-5
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedHan Zheng (HexHive)
Reported2024-12-08 · bounty $3000 (per vendor advisory)
Fix landed2024-12-17 (public git)
Disclosed2025-01-14
Patch gap28 days — fix public in source before the advisory

Background

AAHairline ops
Ganesh GPU-backend operations that render anti-aliased hairlines by tessellating paths into lines, quadratics, and conics before emitting vertices.
`gather_lines_and_quads`
a helper that walks a single path’s segments, appends them to shared buffers, and returns the number of quads contributed by that path.
`SkSafeMath`
a Skia utility that performs integer arithmetic while latching a sticky overflow flag queried via SkSafeMath::ok.
Op combining
Ganesh merges compatible draw ops into one, so onPrepareDraws must sum primitive counts across every path instance folded into the op.

Root Cause Analysis

When multiple AAHairlineOp instances are chained together, AAHairlineOp::onPrepareDraws iterates over every folded path in fPaths and accumulates the per-path quad totals into the plain int accumulator quadCount, then computes int quadAndConicCount = conicCount + quadCount. Because these were ordinary signed-int additions with no bounds checking, a sufficiently large or crafted set of combined paths could push quadCount (and the subsequent conicCount + quadCount) past SK_MaxS32, producing signed integer overflow and a wrapped, undersized count. That corrupted count was then used to size vertex buffers and drive subsequent tessellation loops, so a too-small allocation would be indexed as if it were huge, yielding out-of-bounds writes.

The fix routes every accumulation through an SkSafeMath instance: the loop guard checks safeMath.ok(), each contribution is added with SkSafeMath::addInt, and the final conicCount + quadCount is likewise computed with addInt followed by an early return if the sticky overflow flag has tripped. This preserves the invariant that the primitive counts used for allocation and iteration never exceed the representable/allocatable range, aborting the draw instead of wrapping.

Key insight
The single core mistake was accumulating and summing tessellated primitive counts across combined ops with unchecked signed-int arithmetic; the fix makes every addition overflow-aware via SkSafeMath::addInt and bails out with SkSafeMath::ok before any wrapped count can size a buffer.

Attack Path

  1. Craft oversized geometry An attacker supplies content (e.g., canvas/SVG/WebGL-adjacent draws) that produces many hairline paths with a very large aggregate number of quads and conics.
  2. Force op combining The paths are batched so Ganesh folds many AAHairlineOp instances together, filling fPaths so their per-path quad counts are summed in one onPrepareDraws pass.
  3. Overflow the accumulator The unchecked quadCount += ... and conicCount + quadCount additions exceed SK_MaxS32 and wrap to a small or negative value.
  4. Undersize the allocation The wrapped quadAndConicCount sizes the vertex buffer far smaller than the real primitive count.
  5. Write out of bounds Tessellation then emits the true number of vertices into the undersized buffer, corrupting adjacent heap memory for a potential exploit primitive.

Impact Assessment

An attacker gains a controlled heap buffer overflow inside the GPU rasterization path, executing in whatever process hosts Skia’s Ganesh backend (typically the GPU process, or the renderer where GPU work is in-process). The precondition is that the attacker can drive rendering of enough combined anti-aliased hairline geometry to overflow the 32-bit primitive counts, which is reachable from untrusted web content that controls drawing commands.

Changed Functions

FunctionChangeNotes
for
src/gpu/ganesh/ops/AAHairLinePathRenderer.cpp
modified

Files Changed

  • src/gpu/ganesh/ops/AAHairLinePathRenderer.cpp

Audit Directions

  • Unchecked count accumulation across combined ops
    Audit other Ganesh op renderers that sum per-instance primitive/vertex counts over fPaths-style lists with plain int and feed the result into allocation sizing.
  • Loop-summed geometry totals
    Look for tessellation and path-gathering helpers whose returned counts are added in a loop without SkSafeMath, especially where the total later multiplies a per-primitive vertex stride.
  • 32-bit sizing near SK_MaxS32 caps
    Inspect code that compares accumulated counts against SK_MaxS32-derived limits (like kMaxLines/kMaxQuadsAndConics) but computes those counts with non-saturating arithmetic.
From 8b030e47588af50f56ef380d81a17667baeb582b Mon Sep 17 00:00:00 2001
From: James Godfrey-Kittle <jamesgk@google.com>
Date: Tue, 17 Dec 2024 12:14:17 -0500
Subject: [PATCH] [ganesh] Avoid overflow when combining AAHairlineOps

Bug: b/382786791
Change-Id: I955d943015cce76f75221df9fab0897a6f22fe4b
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/930577
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: James Godfrey-Kittle <jamesgk@google.com>
---

diff --git a/src/gpu/ganesh/ops/AAHairLinePathRenderer.cpp b/src/gpu/ganesh/ops/AAHairLinePathRenderer.cpp
index 3527904..2c04f31 100644
--- a/src/gpu/ganesh/ops/AAHairLinePathRenderer.cpp
+++ b/src/gpu/ganesh/ops/AAHairLinePathRenderer.cpp
@@ -27,6 +27,7 @@
 #include "include/private/base/SkPoint_impl.h"
 #include "include/private/base/SkTArray.h"
 #include "include/private/gpu/ganesh/GrTypesPriv.h"
+#include "src/base/SkSafeMath.h"
 #include "src/core/SkGeometry.h"
 #include "src/core/SkMatrixPriv.h"
 #include "src/core/SkPointPriv.h"
@@ -1219,16 +1220,28 @@
 
     int instanceCount = fPaths.size();
     bool convertConicsToQuads = !target->caps().shaderCaps()->fFloatIs32Bits;
-    for (int i = 0; i < instanceCount; i++) {
+    SkSafeMath safeMath;
+    for (int i = 0; i < instanceCount && safeMath.ok(); i++) {
         const PathData& args = fPaths[i];
-        quadCount += gather_lines_and_quads(args.fPath, args.fViewMatrix, args.fDevClipBounds,
-                                            args.fCapLength, convertConicsToQuads, &lines, &quads,
-                                            &conics, &qSubdivs, &cWeights);
+        quadCount = safeMath.addInt(quadCount,
+                                    gather_lines_and_quads(args.fPath,
+                                                           args.fViewMatrix,
+                                                           args.fDevClipBounds,
+                                                           args.fCapLength,
+                                                           convertConicsToQuads,
+                                                           &lines,
+                                                           &quads,
+                                                           &conics,
+                                                           &qSubdivs,
+                                                           &cWeights));
     }
 
     int lineCount = lines.size() / 2;
     int conicCount = conics.size() / 3;
-    int quadAndConicCount = conicCount + quadCount;
+    int quadAndConicCount = safeMath.addInt(conicCount, quadCount);
+    if (!safeMath.ok()) {
+        return;
+    }
 
     static constexpr int kMaxLines = SK_MaxS32 / kLineSegNumVertices;
     static constexpr int kMaxQuadsAndConics = SK_MaxS32 / kQuadNumVertices;
Loading diff…

Original Bug Report

reported by kd...@gmail.com

Security: Skia integer overflow (results in OOB) at AAHairlineOp::onPrepareDraws

Steps to reproduce the problem

  1. [optional] apply skia patch (only accelerate the computation to enhance bug reproduce)
  2. build skpbench with ubsan
  3. run script to generate path.skp and run ./skpbench –src poc/path.skp –config gles

Problem Description

In function AAHairlineOp::onPrepareDraws, the quadCount iterate every path and add gather_lines_and_quads, which can be controlled by fPath. by creating crafted skia path, we’re able to increase the gather_lines_and_quads results to 0xf0f4 in the poc. and there are no additional verification for the quadCount to prevent overflow.

    int instanceCount = fPaths.size();
    bool convertConicsToQuads = !target->caps().shaderCaps()->fFloatIs32Bits;
    for (int i = 0; i < instanceCount; i++) {
        const PathData& args = fPaths[i];
        int quadCount += gather_lines_and_quads(args.fPath, args.fViewMatrix, args.fDevClipBounds,
                                             args.fCapLength, convertConicsToQuads, &lines, &quads,
                                             &conics, &qSubdivs, &cWeights);
        // [1] gather_lines_and_quads can be super large (e.g. 0xf0f4)
        // therefore the quadCount may overflow
    }

Subsequently, the quadCount is used for allocation at [2-3], endup being used to allocate buffer at [4]. Therefore, the OOB will happen at [5]


    int quadAndConicCount = conicCount + quadCount;          //[2]

    static constexpr int kMaxLines = SK_MaxS32 / kLineSegNumVertices;
    static constexpr int kMaxQuadsAndConics = SK_MaxS32 / kQuadNumVertices;
    if (lineCount > kMaxLines || quadAndConicCount > kMaxQuadsAndConics) {
        return;
    }

        int vertexCount = kQuadNumVertices * quadAndConicCount; // [3]
        void* vertices = target->makeVertexSpace(sizeof(BezierVertex), vertexCount, &vertexBuffer,
                                                 &firstVertex); // [4]

        if (!vertices || !quadsIndexBuffer) {
            SkDebugf("Could not allocate vertices\n");
            return;
        }

        BezierVertex* bezVerts = reinterpret_cast<BezierVertex*>(vertices);

        int unsubdivQuadCnt = quads.size() / 3;
        for (int i = 0; i < unsubdivQuadCnt; ++i) {
            SkASSERT(qSubdivs[i] >= 0);
            if (!quads[3*i].isFinite() || !quads[3*i+1].isFinite() || !quads[3*i+2].isFinite()) {
                return;
            }
            add_quads(&quads[3*i], qSubdivs[i], toDevice, toSrc, &bezVerts); //[5]
        }

Summary

Security: Skia integer overflow (results in OOB) at AAHairlineOp::onPrepareDraws

Custom Questions

Type of crash:

gpu

Reporter credit:

Han Zheng (HexHive)

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A

View on issue tracker
Uploaded PoC files