CVE-2026-118319
Overview
Background
- SkSL Raster Pipeline (SkSL:RP)
- Skia’s CPU backend for SkSL shaders.
SkSLRasterPipelineCodeGeneratorlowers 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 aSlotRange {fSlotCount, nslots}and advancesfSlotCount += nslots.fSlotCountis a signedint. There are separate managers:fProgramSlots,fUniformSlots,fImmutableSlots. - `Program` slot tables / `tempStackMaxDepths` / `allocateSlotData`
- The final
ProgramcomputesfNumValueSlots,fNumUniformSlots,fNumTempStackSlots,fNumImmutableSlotsfrom these counts and, inallocateSlotData, allocates one contiguous slab sized from them.tempStackMaxDepths()sums per-instructionstack_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”.
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
- 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 usesProgramKind::kRuntimeShader) - Blow up slot count
Define nested structs with huge aggregate slot counts (each under
kVariableSlotLimit) and emit tens of thousands of call sites so cumulativefSlotCountarithmetic exceeds2^32. - Wrap the counter
createSlots’ uncheckedfSlotCount += nslotswraps to a small positive value (test:38). - Undersized allocation
Program::allocateSlotDataallocates one contiguous slab sized from the wrapped-small counts. - Out-of-bounds access
A
push_slots_indirect(dynamic indexgg[k].e) or other slot access uses the real large offsets against the tiny slab, reading/writing outside it. - 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
unsupported()/nullptr) rather than corruption.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/sksl/codegen/SkSLRasterPipelineBuilder.cpp |
modified | |
switchsrc/sksl/codegen/SkSLRasterPipelineBuilder.cpp |
modified | |
forsrc/sksl/codegen/SkSLRasterPipelineBuilder.cpp |
modified | |
ifsrc/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp |
modified | |
DEF_TESTtests/RasterPipelineCodeGeneratorTest.cpp |
modified | |
fortests/RasterPipelineCodeGeneratorTest.cpp |
modified | |
iftests/RasterPipelineCodeGeneratorTest.cpp |
modified |
Files Changed
tests/RasterPipelineCodeGeneratorTest.cppsrc/sksl/codegen/SkSLRasterPipelineBuilder.cppsrc/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 withoutSkSafeMath, especially anything later used to size an allocation. - Allocation-size vs index mismatchReview paths where a size is computed from one accumulation and indices from another (
allocateSlotDatavspush_slots_indirect); ensure both saturate/fail together. - Per-op immediate arithmeticCheck remaining
inst.fImmA/fImmBmultiplications and negations instack_usage-like functions for unchecked overflow (e.g. ternary/matrix cases). - Shader limitsVerify
kVariableSlotLimitand call-site counts cannot be composed to exceed 32-bit totals on any slot table.
Patch
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)