High chrome Integer Overflow 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Skia
DescriptionMemory corruption in Skia
ComponentSkia
Bug ClassInteger Overflow
Tracker576194994
Fix commit0ef44edd0ec4 (skia) +197/-23
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-27

Background

SkSL Raster Pipeline (SkSL:RP)
Skia’s CPU backend for SkSL shaders. SkSLRasterPipelineCodeGenerator lowers a compiled SkSL program into a raster-pipeline “program” of instructions that operate over a flat array of 32-bit “slots”.
`SlotManager` / `fSlotCount` / `createSlots`
Allocates slot ranges for variables, uniforms, and immutables. createSlots(nslots) returns a SlotRange {fSlotCount, nslots} and advances fSlotCount += nslots. fSlotCount is a signed int. There are separate managers: fProgramSlots, fUniformSlots, fImmutableSlots.
`Program` slot tables / `tempStackMaxDepths` / `allocateSlotData`
The final Program computes fNumValueSlots, fNumUniformSlots, fNumTempStackSlots, fNumImmutableSlots from these counts and, in allocateSlotData, allocates one contiguous slab sized from them. tempStackMaxDepths() sums per-instruction stack_usage() deltas.
`SkSafeMath` / `SkTFitsIn<int>`
Skia helpers for checked integer arithmetic (sets a sticky overflow flag) and for verifying a value fits in a target type.

Root Cause Analysis

The root cause is a signed 32-bit integer overflow in slot accounting during raster-pipeline codegen, which the advisory generically calls “Memory corruption in Skia”. SlotManager::createSlots did fSlotCount += nslots with no overflow check; peephole combiners (discard_stack, push_constants, push_duplicates, push_slots) did lastInstruction->fImmA += count unchecked; and stack_usage() / Program::tempStackMaxDepths / the Program ctor summed stack depths and negated/multiplied fImmA fields with plain int math. A sufficiently large but individually-legal shader (many call sites of a function returning a huge struct) can drive fSlotCount past INT_MAX so it wraps around to a small value (the test crafts 43,663 * 98,364 + 99,996 + 6 = 2^32 + 38, wrapping to 38). The subsequent allocateSlotData then allocates a slab sized from the wrapped-small counts, while instructions (e.g. a push_slots_indirect from dynamic indexing gg[k].e) index slots using the pre-wrap logical offsets — a classic undersized-allocation / out-of-bounds heap access.

The patch threads SkSafeMath through the whole computation and fails codegen on overflow instead of wrapping: createSlots computes newSlotCount = safe.add(fSlotCount, nslots) and, if !safe || !SkTFitsIn<int>(newSlotCount), sets a new fOverflowed flag and bails; a hasOverflowed() aggregate over the three slot managers is checked at many codegen entry points (writeStatement, pushExpression variants, writeFunction, finish()), returning unsupported()/nullptr so no program is produced. The peephole sites now use a checked add_into() helper and only fold if it succeeds. stack_usage gains an SkSafeMath* overload so negations/multiplications (subInt(0, fImmA), mulInt(-2, fImmA), matrix mulInt/addInt) are checked; tempStackMaxDepths marks a stack depth -1 on overflow; the Program ctor propagates -1 into fNumTempStackSlots; and allocateSlotData returns std::nullopt if any of fNumValueSlots/fNumUniformSlots/fNumTempStackSlots/fNumImmutableSlots/fNumLabels is negative. The diff’s true class is IntegerOverflow leading to OOB, more specific than the advisory’s “Memory corruption”.

Key insight
SlotManager::fSlotCount (a signed int) was advanced with unchecked +=; a large-but-valid shader could wrap it past INT_MAX to a tiny value, so the later single-slab slot allocation was undersized while instructions still indexed the real (huge) slot offsets.

Attack Path

  1. Deliver a shader Attacker supplies a crafted SkSL runtime shader (e.g. via a web API that compiles SkSL, such as CSS/canvas/PaintWorklet-style effects routed to Skia). (web reachability inferred; the test uses ProgramKind::kRuntimeShader)
  2. Blow up slot count Define nested structs with huge aggregate slot counts (each under kVariableSlotLimit) and emit tens of thousands of call sites so cumulative fSlotCount arithmetic exceeds 2^32.
  3. Wrap the counter createSlots’ unchecked fSlotCount += nslots wraps to a small positive value (test: 38).
  4. Undersized allocation Program::allocateSlotData allocates one contiguous slab sized from the wrapped-small counts.
  5. Out-of-bounds access A push_slots_indirect (dynamic index gg[k].e) or other slot access uses the real large offsets against the tiny slab, reading/writing outside it.
  6. Primitive Controlled heap out-of-bounds read/write from shader execution, i.e. memory corruption in the process running Skia raster-pipeline codegen.

Impact Assessment

Runs wherever SkSL raster-pipeline programs are compiled/executed from attacker-controlled shader source — typically the renderer or GPU/utility process, no special privilege beyond serving a page that reaches Skia SkSL. The achieved primitive is an out-of-bounds heap read/write caused by an integer-overflow-driven undersized allocation, a strong memory-corruption primitive (the test demonstrates deterministic 32-bit wraparound). Preconditions: ability to feed a large crafted SkSL program and shader inputs large enough to overflow slot counts; the fix converts the condition into a clean codegen failure (unsupported()/nullptr) rather than corruption.

Changed Functions

FunctionChangeNotes
if
src/sksl/codegen/SkSLRasterPipelineBuilder.cpp
modified
switch
src/sksl/codegen/SkSLRasterPipelineBuilder.cpp
modified
for
src/sksl/codegen/SkSLRasterPipelineBuilder.cpp
modified
if
src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp
modified
DEF_TEST
tests/RasterPipelineCodeGeneratorTest.cpp
modified
for
tests/RasterPipelineCodeGeneratorTest.cpp
modified
if
tests/RasterPipelineCodeGeneratorTest.cpp
modified

Files Changed

  • tests/RasterPipelineCodeGeneratorTest.cpp
  • src/sksl/codegen/SkSLRasterPipelineBuilder.cpp
  • src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp

Audit Directions

  • Other `int` counters advanced with `+=`
    Audit Skia codegen and buffer-sizing code for signed counters (fSlotCount, stack depths, instruction immediates) accumulated without SkSafeMath, especially anything later used to size an allocation.
  • Allocation-size vs index mismatch
    Review paths where a size is computed from one accumulation and indices from another (allocateSlotData vs push_slots_indirect); ensure both saturate/fail together.
  • Per-op immediate arithmetic
    Check remaining inst.fImmA/fImmB multiplications and negations in stack_usage-like functions for unchecked overflow (e.g. ternary/matrix cases).
  • Shader limits
    Verify kVariableSlotLimit and call-site counts cannot be composed to exceed 32-bit totals on any slot table.
From 0ef44edd0ec4498a1599e6987422bca461fdd6ab Mon Sep 17 00:00:00 2001
From: Thomas Smith <thomsmit@google.com>
Date: Thu, 22 Oct 2026 13:04:03 -0400
Subject: [PATCH] [SkSL:RP] Prevent slot count overflow in Raster Pipeline codegen

Guard SlotManager::createSlots against signed integer overflow using SkSafeMath and SkTFitsIn<int>, and fail codegen if any slot table (program, uniform, or immutable slots) overflows. Also guard temporary stack depth calculations in Program::tempStackMaxDepths and Program::Program against integer overflow and reject negative slot counts in Program::allocateSlotData.

Bug: b/574776938
Bug: b/576194994
Change-Id: Iac9d0a729b78eb2af4195a0f6b47511344b40754
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1401636
Commit-Queue: Thomas Smith <thomsmit@google.com>
Auto-Submit: Thomas Smith <thomsmit@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
---
diff --git a/src/sksl/codegen/SkSLRasterPipelineBuilder.cpp b/src/sksl/codegen/SkSLRasterPipelineBuilder.cpp
index 9babcd3..2ad9eee 100644
--- a/src/sksl/codegen/SkSLRasterPipelineBuilder.cpp
+++ b/src/sksl/codegen/SkSLRasterPipelineBuilder.cpp
@@ -345,6 +345,18 @@
     return false;
 }
 
+// This function is used to guard peephole optimization sites. If it fails, fInstructions adds both
+// both instructions, but the count goes over INT_MAX and is caught downstream.
+static bool add_into(int* target, int delta) {
+    SkSafeMath safe;
+    int sum = safe.addInt(*target, delta);
+    if (!safe) {
+        return false;
+    }
+    *target = sum;
+    return true;
+}
+
 void Builder::discard_stack(int32_t count, int stackID) {
     // If we pushed something onto the stack and then immediately discarded part of it, we can
     // shrink or eliminate the push.
@@ -357,8 +369,10 @@
         switch (lastInstruction->fOp) {
             case BuilderOp::discard_stack:
                 // Our last op was actually a separate discard_stack; combine the discards.
-                lastInstruction->fImmA += count;
-                return;
+                if (add_into(&lastInstruction->fImmA, count)) {
+                    return;
+                }
+                break;
 
             case BuilderOp::push_clone:
             case BuilderOp::push_clone_from_stack:
@@ -648,8 +662,9 @@
     if (count > 0) {
         if (Instruction* lastInstruction = this->lastInstruction()) {
             // If the previous op is pushing the same value, we can just push more of them.
-            if (lastInstruction->fOp == BuilderOp::push_constant && lastInstruction->fImmB == val) {
-                lastInstruction->fImmA += count;
+            if (lastInstruction->fOp == BuilderOp::push_constant &&
+                lastInstruction->fImmB == val &&
+                add_into(&lastInstruction->fImmA, count)) {
                 return;
             }
         }
@@ -660,8 +675,8 @@
 void Builder::push_duplicates(int count) {
     if (Instruction* lastInstruction = this->lastInstruction()) {
         // If the previous op is pushing a constant, we can just push more of them.
-        if (lastInstruction->fOp == BuilderOp::push_constant) {
-            lastInstruction->fImmA += count;
+        if (lastInstruction->fOp == BuilderOp::push_constant &&
+            add_into(&lastInstruction->fImmA, count)) {
             return;
         }
     }
@@ -689,9 +704,9 @@
     if (numSlots == 1 && offsetFromStackTop == 0) {
         // ... and the previous op is pushing a constant...
         if (Instruction* lastInstruction = this->lastInstruction()) {
-            if (lastInstruction->fOp == BuilderOp::push_constant) {
+            if (lastInstruction->fOp == BuilderOp::push_constant &&
+                add_into(&lastInstruction->fImmA, 1)) {
                 // ... we can just push more of them.
-                lastInstruction->fImmA += 1;
                 return;
             }
         }
@@ -1285,7 +1300,7 @@
                                      numImmutableSlots, fNumLabels, debugTrace);
 }
 
-static int stack_usage(const Instruction& inst) {
+static int stack_usage(const Instruction& inst, SkSafeMath* safe) {
     switch (inst.fOp) {
         case BuilderOp::push_condition_mask:
         case BuilderOp::push_loop_mask:
@@ -1324,20 +1339,20 @@
         case ALL_MULTI_SLOT_BINARY_OP_CASES:
         case BuilderOp::discard_stack:
         case BuilderOp::select:
-            return -inst.fImmA;
+            return safe->subInt(0, inst.fImmA);
 
         case ALL_N_WAY_TERNARY_OP_CASES:
         case ALL_MULTI_SLOT_TERNARY_OP_CASES:
-            return 2 * -inst.fImmA;
+            return safe->mulInt(-2, inst.fImmA);
 
         case BuilderOp::swizzle_1:
-            return 1 - inst.fImmA;  // consumes immA slots and emits a scalar
+            return safe->subInt(1, inst.fImmA);  // consumes immA slots and emits a scalar
         case BuilderOp::swizzle_2:
-            return 2 - inst.fImmA;  // consumes immA slots and emits a 2-slot vector
+            return safe->subInt(2, inst.fImmA);  // consumes immA slots and emits a 2-slot vector
         case BuilderOp::swizzle_3:
-            return 3 - inst.fImmA;  // consumes immA slots and emits a 3-slot vector
+            return safe->subInt(3, inst.fImmA);  // consumes immA slots and emits a 3-slot vector
         case BuilderOp::swizzle_4:
-            return 4 - inst.fImmA;  // consumes immA slots and emits a 4-slot vector
+            return safe->subInt(4, inst.fImmA);  // consumes immA slots and emits a 4-slot vector
 
         case BuilderOp::dot_2_floats:
             return -3;  // consumes two 2-slot vectors and emits one scalar
@@ -1353,12 +1368,13 @@
         case BuilderOp::matrix_multiply_3:
         case BuilderOp::matrix_multiply_4:
             // consumes the left- and right-matrices; emits result over existing padding slots
-            return -(inst.fImmA * inst.fImmB + inst.fImmC * inst.fImmD);
+            return safe->subInt(0, safe->addInt(safe->mulInt(inst.fImmA, inst.fImmB),
+                                                safe->mulInt(inst.fImmC, inst.fImmD)));
 
         case BuilderOp::shuffle: {
             int consumed = inst.fImmA;
             int generated = inst.fImmB;
-            return generated - consumed;
+            return safe->subInt(generated, consumed);
         }
         case ALL_SINGLE_SLOT_UNARY_OP_CASES:
         case ALL_MULTI_SLOT_UNARY_OP_CASES:
@@ -1368,6 +1384,11 @@
     }
 }
 
+static int stack_usage(const Instruction& inst) {
+    SkSafeMath safe;
+    return stack_usage(inst, &safe);
+}
+
 Program::StackDepths Program::tempStackMaxDepths() const {
     // Count the number of separate temp stacks that the program uses.
     int numStacks = 1;
@@ -1380,9 +1401,14 @@
     largest.push_back_n(numStacks, 0);
     current.push_back_n(numStacks, 0);
 
+    SkSafeMath safe;
     for (const Instruction& inst : fInstructions) {
         int stackID = inst.fStackID;
-        current[stackID] += stack_usage(inst);
+        current[stackID] = safe.addInt(current[stackID], stack_usage(inst, &safe));
+        if (!safe) {
+            largest[stackID] = -1;
+            return largest;
+        }
         largest[stackID] = std::max(current[stackID], largest[stackID]);
         // If we assert here, the generated program has popped off the top of the stack.
         SkASSERTF_RELEASE(current[stackID] >= 0, "unbalanced temp stack push/pop on stack");
@@ -1411,9 +1437,18 @@
         , fDebugTrace(debugTrace) {
     fTempStackMaxDepths = this->tempStackMaxDepths();
 
+    SkSafeMath safe;
     fNumTempStackSlots = 0;
     for (const int depth : fTempStackMaxDepths) {
-        fNumTempStackSlots += depth;
+        if (depth < 0) {
+            fNumTempStackSlots = -1;
+            break;
+        }
+        fNumTempStackSlots = safe.addInt(fNumTempStackSlots, depth);
+        if (!safe) {
+            fNumTempStackSlots = -1;
+            break;
+        }
     }
 
     if (fDebugTrace) {
@@ -1674,6 +1709,13 @@
 }
 
 std::optional<Program::SlotData> Program::allocateSlotData(SkArenaAlloc* alloc) const {
+    if (fNumValueSlots < 0 ||
+        fNumUniformSlots < 0 ||
+        fNumTempStackSlots < 0 ||
+        fNumImmutableSlots < 0 ||
+        fNumLabels < 0) {
+        return std::nullopt;
+    }
     // Allocate a contiguous slab of slot data for immutables, values, and stack entries.
     const size_t N = SkOpts::raster_pipeline_highp_stride;
     const int scalarWidth = 1 * sizeof(float);
diff --git a/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp b/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp
index 113f9c4..c87c9b7 100644
--- a/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp
+++ b/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp
@@ -12,7 +12,9 @@
 #include "include/private/SkEnumBitMask.h"
 #include "include/private/SkOnlyStackAllocatable.h"
 #include "include/private/SkTArray.h"
+#include "include/private/SkTFitsIn.h"
 #include "include/private/SkTo.h"
+#include "src/core/SkSafeMath.h"
 #include "src/core/SkStringView.h"
 #include "src/core/SkTHash.h"
 #include "src/core/SkUtils.h"
@@ -140,9 +142,13 @@
     /** Returns the total number of slots consumed. */
     int slotCount() const { return fSlotCount; }
 
+    /** Returns true if the slot count overflowed. */
+    bool overflowed() const { return fOverflowed; }
+
 private:
     THashMap<const IRNode*, SlotRange> fSlotMap;
     int fSlotCount = 0;
+    bool fOverflowed = false;
     std::vector<SlotDebugInfo>* fSlotDebugInfo;
 };
 
@@ -219,6 +225,13 @@
      */
     int getFunctionDebugInfo(const FunctionDeclaration& decl);
 
+    /** Returns true if any slot table overflowed. */
+    bool hasOverflowed() const {
+        return fProgramSlots.overflowed() ||
+               fUniformSlots.overflowed() ||
+               fImmutableSlots.overflowed();
+    }
+
     /** Returns true for variables with slots in fProgramSlots; immutables or uniforms are false. */
     bool hasVariableSlots(const Variable& v) {
         return !IsUniform(v) && !fImmutableVariables.contains(&v);
@@ -1177,25 +1190,31 @@
     if (nslots == 0) {
         return {};
     }
+    SkSafeMath safe;
+    size_t newSlotCount = safe.add(fSlotCount, nslots);
+    if (!safe || !SkTFitsIn<int>(newSlotCount)) {
+        fOverflowed = true;
+        return {0, SkToInt(std::min<size_t>(nslots, INT_MAX))};
+    }
     if (fSlotDebugInfo) {
         // Our debug slot-info table should have the same length as the actual slot table.
         SkASSERT(fSlotDebugInfo->size() == (size_t)fSlotCount);
 
         // Append slot names and types to our debug slot-info table.
-        fSlotDebugInfo->reserve(fSlotCount + nslots);
+        fSlotDebugInfo->reserve(newSlotCount);
         this->addSlotDebugInfo(name, type, pos, isFunctionReturnValue);
 
         // Confirm that we added the expected number of slots.
-        SkASSERT(fSlotDebugInfo->size() == (size_t)(fSlotCount + nslots));
+        SkASSERT(fSlotDebugInfo->size() == newSlotCount);
     }
 
     SlotRange result = {fSlotCount, (int)nslots};
-    fSlotCount += nslots;
+    fSlotCount = SkToInt(newSlotCount);
     return result;
 }
 
 std::optional<SlotRange> SlotManager::mapVariableToSlots(const Variable& v, SlotRange range) {
-    SkASSERT(v.type().slotCount() == SkToSizeT(range.count));
+    SkASSERT(fOverflowed || v.type().slotCount() == SkToSizeT(range.count));
     const SlotRange* existingEntry = fSlotMap.find(&v);
     std::optional<SlotRange> originalRange = existingEntry ? std::optional(*existingEntry)
                                                            : std::nullopt;
@@ -1490,6 +1509,9 @@
     // Set up a slot range dedicated to this function's return value.
     SlotRange lastFunctionResult = fCurrentFunctionResult;
     fCurrentFunctionResult = this->getFunctionSlots(callSite, function.declaration());
+    if (this->hasOverflowed()) {
+        return std::nullopt;
+    }
 
     // Save off the return mask.
     if (this->needsReturnMask(&function)) {
@@ -1660,10 +1682,17 @@
         }
     }
 
+    if (this->hasOverflowed()) {
+        return unsupported();
+    }
+
     return true;
 }
 
 bool Generator::writeStatement(const Statement& s) {
+    if (this->hasOverflowed()) {
+        return unsupported();
+    }
     switch (s.kind()) {
         case Statement::Kind::kBlock:
             // The debugger will stop on statements inside Blocks; there's no need for an additional
@@ -3966,6 +3995,9 @@
     if (IsUniform(var)) {
         // Push a uniform.
         r = this->getUniformSlots(var);
+        if (this->hasOverflowed()) {
+            return unsupported();
+        }
         SkASSERT(r.count == (int)var.type().slotCount());
         r.index += subset.index;
         r.count = subset.count;
@@ -3983,6 +4015,9 @@
         }
         // Push the immutable slot range.
         r = this->getImmutableSlots(var);
+        if (this->hasOverflowed()) {
+            return unsupported();
+        }
         SkASSERT(r.count == (int)var.type().slotCount());
         r.index += subset.index;
         r.count = subset.count;
@@ -3990,6 +4025,9 @@
     } else {
         // Push the variable.
         r = this->getVariableSlots(var);
+        if (this->hasOverflowed()) {
+            return unsupported();
+        }
         SkASSERT(r.count == (int)var.type().slotCount());
         r.index += subset.index;
         r.count = subset.count;
@@ -4081,10 +4119,17 @@
         fTraceMask->exit();
     }
 
+    if (this->hasOverflowed()) {
+        return unsupported();
+    }
+
     return true;
 }
 
 std::unique_ptr<RP::Program> Generator::finish() {
+    if (this->hasOverflowed()) {
+        return nullptr;
+    }
     return fBuilder.finish(fProgramSlots.slotCount(),
                            fUniformSlots.slotCount(),
                            fImmutableSlots.slotCount(),
diff --git a/tests/RasterPipelineCodeGeneratorTest.cpp b/tests/RasterPipelineCodeGeneratorTest.cpp
index 63089e0..c8dde52 100644
--- a/tests/RasterPipelineCodeGeneratorTest.cpp
+++ b/tests/RasterPipelineCodeGeneratorTest.cpp
@@ -380,3 +380,90 @@
             SkSL::MakeRasterPipelineProgram(*program, *main->definition());
     REPORTER_ASSERT(r, rasterProg);
 }
+
+DEF_TEST(SkSLRasterPipelineSlotCountWraparound, r) {
+    auto names = [](const char* prefix, int n) {
+        std::stringstream s;
+        for (int i = 0; i < n; ++i) {
+            if (i > 0) {
+                s << ",";
+            }
+            s << prefix << i;
+        }
+        return s.str();
+    };
+
+    // Struct E has 98,364 slots and struct G has 99,996 slots (both under kVariableSlotLimit).
+    // Without overflow protection in SlotManager::createSlots, 43,663 call sites of f() allocate
+    // 43,663 * 98,364 + 99,996 + 6 = 4,294,967,334 = 2^32 + 38 value slots, wrapping fSlotCount
+    // around 32-bit int to 38. Dynamic indexing via uniform `k` in `gg[k].e` emits a single
+    // push_slots_indirect instruction per call.
+    constexpr int kCalls = 43663;
+    std::stringstream str;
+    str << "struct A{float4x4 " << names("a", 64) << ";};";
+    str << "struct E{A " << names("b", 96) << ";float " << names("c", 60) << ";};";
+    str << "struct G{E e;float4x4 " << names("y", 102) << ";};";
+    str << "uniform int k;";
+    str << "G gg[1];";
+    str << "noinline E f(){return gg[k].e;}";
+    str << "half4 main(float2 p){if(k>0){";
+    for (int i = 0; i < kCalls; ++i) {
+        str << "f();";
+    }
+    str << "}return half4(0);}";
+    std::string src = str.str();
+
+    SkSL::Compiler compiler;
+    SkSL::ProgramSettings settings;
+    settings.fMaxVersionAllowed = SkSL::Version::k300;
+    std::unique_ptr<SkSL::Program> program =
+            compiler.convertProgram(SkSL::ProgramKind::kRuntimeShader, src, settings);
... (file diff truncated)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.