Critical chrome Integer Overflow 🔧 Commit mapped

Overview

Critical
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Skia
DescriptionImproper input validation in Skia
ComponentSkia
Bug ClassInteger Overflow
Tracker574776938
Fix commit0ef44edd0ec4 (skia) +197/-23
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-27

Background

SkSL Raster Pipeline (RP) backend
Skia’s software path for executing SkSL shaders. SkSLRasterPipelineCodeGenerator.cpp lowers an SkSL program into an RP::Program, and SkSLRasterPipelineBuilder.cpp builds/optimizes the instruction stream.
Slots
the RP backend allocates fixed-width “slots” for every scalar value, uniform, immutable, and temp-stack entry. Counts (fSlotCount, fNumValueSlots, fNumUniformSlots, fNumImmutableSlots, fNumTempStackSlots) are tracked as signed int.
`SlotManager::createSlots`
allocates a contiguous range of nslots slots and advances fSlotCount. The returned SlotRange{index, count} later drives the size of the single allocated slot-data slab in Program::allocateSlotData.
`SkSafeMath` / `SkTFitsIn<int>`
Skia’s checked-arithmetic helper and a fits-in-type predicate; SkSafeMath latches an overflow flag when any tracked add/mul/sub overflows.
Peephole optimizations
Builder methods (discard_stack, push_constant, push_duplicates, etc.) fold repeated pushes by doing lastInstruction->fImmA += count, another place an int counter can overflow.

Root Cause Analysis

Across the RP codegen, slot and stack counts were accumulated in signed 32-bit int with no overflow checking. The central defect is in SlotManager::createSlots, which did SlotRange result = {fSlotCount, nslots}; fSlotCount += nslots; unconditionally. A program with enough large structs and call sites can drive the running total past INT_MAX, wrapping fSlotCount around to a small (even positive) value. The new regression test makes this concrete: struct E has 98,364 slots, and 43,663 call sites of a function returning E allocate 43663 * 98364 + ... = 4,294,967,334 = 2^32 + 38 value slots, wrapping fSlotCount to 38. Downstream, Program::allocateSlotData uses these counts to size one contiguous slab and hands out ranges whose index values were recorded before the wrap (large), while the slab is sized to the wrapped (tiny) count — a classic integer-overflow-to-undersized-allocation leading to out-of-bounds slot access during pipeline execution. The same unchecked fImmA += count pattern in the builder’s peephole folds, and the stack_usage/tempStackMaxDepths/fNumTempStackSlots accumulations in Program, offered additional overflow paths.

The patch instruments all of these with SkSafeMath. createSlots now computes newSlotCount = safe.add(fSlotCount, nslots) and, if !safe || !SkTFitsIn<int>(newSlotCount), sets a new fOverflowed flag and returns a clamped range instead of advancing. Generator gains hasOverflowed() (OR of the program/uniform/immutable slot managers) and checks it at many codegen entry points (getFunctionSlots, writeStatement, variable/uniform/immutable pushes, writeProgram, finish), bailing out via unsupported()/nullptr so an overflowed program is never emitted. A new add_into(int*, int) helper wraps the peephole fImmA += count folds with SkSafeMath, declining the fold (and falling through to emitting a fresh instruction) on overflow. stack_usage is threaded through an SkSafeMath* so negative/large stack deltas are computed checked; tempStackMaxDepths and the Program constructor propagate -1 sentinels on overflow; and allocateSlotData now returns std::nullopt if any of fNumValueSlots, fNumUniformSlots, fNumTempStackSlots, fNumImmutableSlots, or fNumLabels is negative. The advisory calls this “improper input validation,” which is accurate at the surface — but the diff shows the specific mechanism is a signed integer overflow of slot/stack counts that would otherwise corrupt allocation sizing.

Key insight
Slot and temp-stack counts were summed in unchecked signed 32-bit int, so a large-but-individually-legal SkSL program could overflow fSlotCount (demonstrated: 2^32 + 38 value slots wrapping to 38), causing the RP backend to size its slot-data slab from the wrapped-small total while handing out pre-wrap-large indices — an out-of-bounds write/read primitive.

Attack Path

  1. Web/content attacker supplies an SkSL program that reaches the Raster Pipeline codegen (e.g. a CSS/canvas/SkRuntimeEffect runtime shader processed in software), which any page that can run shaders can trigger (inferred: standard runtime-shader reachability).
  2. Build an oversized program authors a shader with large structs and many call sites / pushes so the total slot count exceeds INT_MAX while each individual construct stays under per-construct limits (as the SkSLRasterPipelineSlotCountWraparound test does with struct E and 43,663 calls).
  3. Overflow the counter SkSL codegen accumulates fSlotCount (or a temp-stack depth) past 2^32, wrapping it to a small value; old code emits the program without noticing.
  4. Undersized allocation Program::allocateSlotData sizes the single slot slab from the wrapped-small total, while instructions reference slot indices computed before the wrap.
  5. Primitive out-of-bounds reads/writes into the slot-data slab during raster-pipeline execution (heap OOB in the process running Skia, matching advisory “memory corruption”); exact write controllability depends on the shader and is not fully characterized by the diff.

Impact Assessment

This is an integer-overflow-induced out-of-bounds condition in Skia’s SkSL Raster Pipeline codegen, reachable wherever untrusted SkSL/runtime effects are compiled and executed in software (renderer or GPU/utility process depending on configuration) with no special privilege. The overflow is the root mechanism; the exploitable sink is the resulting undersized slot-data slab accessed with stale large indices — a heap out-of-bounds primitive. Strength is gated by the need to construct a program whose aggregate slot/stack count exceeds INT_MAX while passing other per-construct limits (kVariableSlotLimit etc.), which is nontrivial but demonstrated feasible by the attached test. The patch converts every overflow into a clean codegen failure (unsupported()/nullptr/std::nullopt), so a malicious program is rejected rather than mis-sized; the advisory’s “improper input validation” is really a signed-integer-overflow-to-OOB, and the fix is precisely added bounds/overflow validation.

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 count accumulators in SkSL codegen
    audit remaining int/size_t running totals in the RP backend and sibling backends (e.g. GPU/SkVM codegen) that sum per-slot or per-instruction counts without SkSafeMath.
  • Pre-computed index vs. post-wrap size
    look for any allocator that records element indices/ranges and separately sizes a backing buffer from a total; a mismatch after overflow is the exploitable shape here, not just the wrap itself.
  • Peephole `fImmA += count` sites
    grep the builder for other instruction-folding spots that mutate immediate counters in place without the new add_into guard.
  • Per-construct limits vs. aggregate limits
    review limits like kVariableSlotLimit: enforcing a per-struct/per-variable cap does not bound the aggregate slot count across many call sites, which is exactly the gap exploited.
From 0ef44edd0ec4498a1599e6987422bca461fdd6ab Mon Sep 17 00:00:00 2001
From: Thomas Smith <[email protected]>
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 <[email protected]>
Auto-Submit: Thomas Smith <[email protected]>
Reviewed-by: Robert Phillips <[email protected]>
---
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.