CVE-2026-9998
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forresources/sksl/errors/ForLoopNEQOverflow.rts |
modified | |
forresources/sksl/errors/ForLoopOverflow.rts |
modified | |
ifsrc/sksl/analysis/SkSLGetLoopUnrollInfo.cpp |
modified |
Files Changed
gn/sksl_tests.gniresources/sksl/BUILD.bazelresources/sksl/errors/ForLoopNEQOverflow.rtsresources/sksl/errors/ForLoopOverflow.rtssrc/base/SkSafeMath.hsrc/sksl/analysis/SkSLGetLoopUnrollInfo.cpp
Patch
From 5f4f454b9662b83facd71dcbc6e8ac105660e938 Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <kjlubick@google.com>
Date: Wed, 20 May 2026 08:23:28 -0400
Subject: [PATCH] Fix for integer wraparound in sksl
I found the ES 2 spec [1] helpful for reference here.
The calculate_count_neq_int is not strictly necessary (I was unable
to find a case that tricked the existing floats with ints), but
I like the refactoring and it mirrors the gt/lt cases nicely.
[1] https://registry.khronos.org/OpenGL/specs/es/2.0/GLSL_ES_Specification_1.00.pdf
Change-Id: I0b9117f347e4b7d5d336de0f14337b9bec510ff2
Bug: 513337118
Fixed: 513337118
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1236656
Commit-Queue: Florin Malita <fmalita@google.com>
Reviewed-by: Florin Malita <fmalita@google.com>
Commit-Queue: Kaylee Lubick <kjlubick@google.com>
Auto-Submit: Kaylee Lubick <kjlubick@google.com>
---
diff --git a/gn/sksl_tests.gni b/gn/sksl_tests.gni
index 1cd9317..1a1804f 100644
--- a/gn/sksl_tests.gni
+++ b/gn/sksl_tests.gni
@@ -142,6 +142,8 @@
"errors/FloatRemainder.rts",
"errors/ForInitStmt.sksl",
"errors/ForLoopInductionVariableScope.sksl",
+ "errors/ForLoopNEQOverflow.rts",
+ "errors/ForLoopOverflow.rts",
"errors/ForTypeMismatch.rts",
"errors/FunctionParamBadType.rts",
"errors/FunctionParamShadowedByLocal.rts",
diff --git a/resources/sksl/BUILD.bazel b/resources/sksl/BUILD.bazel
index d7bfacf..df672c4 100644
--- a/resources/sksl/BUILD.bazel
+++ b/resources/sksl/BUILD.bazel
@@ -285,6 +285,8 @@
"errors/FloatRemainder.rts",
"errors/ForInitStmt.sksl",
"errors/ForLoopInductionVariableScope.sksl",
+ "errors/ForLoopNEQOverflow.rts",
+ "errors/ForLoopOverflow.rts",
"errors/ForTypeMismatch.rts",
"errors/FunctionParamBadType.rts",
"errors/FunctionParamShadowedByLocal.rts",
diff --git a/resources/sksl/errors/ForLoopNEQOverflow.rts b/resources/sksl/errors/ForLoopNEQOverflow.rts
new file mode 100644
index 0000000..12a6e86
--- /dev/null
+++ b/resources/sksl/errors/ForLoopNEQOverflow.rts
@@ -0,0 +1,11 @@
+half4 main(float2 coords) {
+ half arr[4];
+ for (int i = 2000000000; i != -2000000000; i += 1000000000) {
+ arr[i - 2000000000] = half(1);
+ }
+ return half4(0);
+}
+
+/*%%*
+loop must guarantee termination in fewer iterations
+*%%*/
diff --git a/resources/sksl/errors/ForLoopOverflow.rts b/resources/sksl/errors/ForLoopOverflow.rts
new file mode 100644
index 0000000..e73a1f9
--- /dev/null
+++ b/resources/sksl/errors/ForLoopOverflow.rts
@@ -0,0 +1,11 @@
+half4 main(float2 coords) {
+ half arr[4];
+ for (int i = 2147483640; i < 2147483647; i += 100) {
+ arr[i - 2147483640] = half(1);
+ }
+ return half4(0);
+}
+
+/*%%*
+loop must guarantee termination in fewer iterations
+*%%*/
diff --git a/src/base/SkSafeMath.h b/src/base/SkSafeMath.h
index d8f9fbc..590098a 100644
--- a/src/base/SkSafeMath.h
+++ b/src/base/SkSafeMath.h
@@ -41,6 +41,7 @@
* be set to false, and it is undefined what this returns.
*/
int addInt(int a, int b) {
+ static_assert(sizeof(int) == 4, "int is not 4 bytes");
if (b < 0 && a < std::numeric_limits<int>::min() - b) {
fOK = false;
return a;
@@ -51,6 +52,14 @@
return a + b;
}
+ int subInt(int a, int b) {
+ if (b == std::numeric_limits<int>::min()) {
+ fOK = false;
+ return a;
+ }
+ return addInt(a, -b);
+ }
+
int mulInt(int x, int y) {
int64_t result = (int64_t)x * (int64_t)y;
if (result > std::numeric_limits<int>::max() || result < std::numeric_limits<int>::min()) {
diff --git a/src/sksl/analysis/SkSLGetLoopUnrollInfo.cpp b/src/sksl/analysis/SkSLGetLoopUnrollInfo.cpp
index 70a2170..974db63 100644
--- a/src/sksl/analysis/SkSLGetLoopUnrollInfo.cpp
+++ b/src/sksl/analysis/SkSLGetLoopUnrollInfo.cpp
@@ -7,6 +7,7 @@
#include "include/core/SkTypes.h"
#include "include/private/base/SkFloatingPoint.h"
+#include "src/base/SkSafeMath.h"
#include "src/sksl/SkSLAnalysis.h"
#include "src/sksl/SkSLConstantFolder.h"
#include "src/sksl/SkSLErrorReporter.h"
@@ -35,25 +36,106 @@
// Loops that run for 100000+ iterations will exceed our program size limit.
static constexpr int kLoopTerminationLimit = 100000;
-static int calculate_count(double start, double end, double delta, bool forwards, bool inclusive) {
- if ((forwards && start > end) || (!forwards && start < end)) {
- // The loop starts in a completed state (the start has already advanced past the end).
- return 0;
- }
- if ((delta == 0.0) || forwards != (delta > 0.0)) {
- // The loop does not progress toward a completed state, and will never terminate.
- return kLoopTerminationLimit;
- }
+enum class Direction {
+ kBackwards,
+ kForwards,
+};
+
+enum class Inclusive : bool {
+ kNo = false,
+ kYes = true,
+};
+
+enum class LoopType {
+ kFloat,
+ kInt,
+};
+
+static int calculate_count_float(double start, double end, double delta,
+ Inclusive inclusive) {
double iterations = sk_ieee_double_divide(end - start, delta);
double count = std::ceil(iterations);
- if (inclusive && (count == iterations)) {
+ if (inclusive == Inclusive::kYes && (count == iterations)) {
count += 1.0;
}
if (count > kLoopTerminationLimit || !std::isfinite(count)) {
// The loop runs for more iterations than we can safely unroll.
return kLoopTerminationLimit;
}
- return (int)count;
+ return sk_double_saturate2int(count);
+}
+
+static int calculate_count_int(int32_t start, int32_t end, int32_t delta,
+ Inclusive inclusive) {
+ if (delta == 0) {
+ return kLoopTerminationLimit;
+ }
+ SkSafeMath math;
+ int roundUp = delta > 0 ? math.subInt(delta, 1) : math.addInt(delta, 1);
+ int width = math.subInt(end, start);
+ int iterations = math.addInt(width, roundUp) / delta;
+ if (inclusive == Inclusive::kYes && width % delta == 0) {
+ iterations = math.addInt(iterations, 1);
+ }
+ // Check that we won't overflow while looping
+ math.addInt(start, math.mulInt(delta, iterations));
+ if (!math || iterations < 0 || iterations > kLoopTerminationLimit) {
+ return kLoopTerminationLimit;
+ }
+ return iterations;
+}
+
+static int calculate_count(double start, double end, double delta, Direction dir,
+ Inclusive inclusive, LoopType loop) {
+ if ((dir == Direction::kForwards && start > end) ||
+ (dir == Direction::kBackwards && start < end)) {
+ // The loop starts in a completed state (the start has already advanced past the end).
+ return 0;
+ }
+ if ((delta == 0.0) ||
+ (delta > 0.0 && dir == Direction::kBackwards) ||
+ (delta < 0.0 && dir == Direction::kForwards)) {
+ // The loop does not progress toward a completed state, and will never terminate.
+ return kLoopTerminationLimit;
+ }
+ if (loop == LoopType::kInt) {
Original Bug Report
Potential Out-of-Bounds GPU Memory Access via SkSL Loop Analysis Integer Wraparound
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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: SkSL’s loop unrolling analysis incorrectly calculates iteration counts using double-precision arithmetic without accounting for 32-bit integer wraparound at runtime. This can allow malicious loops to bypass ES2 validation while performing out-of-bounds array accesses on GPU backends. This issue potentially enables memory corruption within the GPU process from a compromised renderer.
Affected files:
third_party/skia/src/sksl/analysis/SkSLGetLoopUnrollInfo.cppthird_party/skia/src/sksl/analysis/SkSLIsConstantExpression.cppthird_party/skia/src/sksl/ir/SkSLIndexExpression.cppthird_party/skia/src/sksl/codegen/SkSLSPIRVCodeGenerator.cppthird_party/skia/src/sksl/codegen/SkSLGLSLCodeGenerator.cppthird_party/skia/src/sksl/codegen/SkSLMetalCodeGenerator.cpp
Estimated timestamp from git blame: 2021-01-13
Description
A potential vulnerability exists in the SkSL (Skia Shading Language) compiler’s loop unrolling analysis. The analysis determines if a loop is valid for ES2 (which requires loops to be unrollable) by calculating its trip count. However, the calculation in src/sksl/analysis/SkSLGetLoopUnrollInfo.cpp uses 64-bit floating-point arithmetic and does not account for the 32-bit signed integer wraparound behavior that occurs on GPU hardware at runtime.
In calculate_count, the trip count is estimated as follows:
// src/sksl/analysis/SkSLGetLoopUnrollInfo.cpp
static int calculate_count(double start, double end, double delta, bool forwards, bool inclusive) {
// ...
double iterations = sk_ieee_double_divide(end - start, delta);
double count = std::ceil(iterations);
// ...
return (int)count;
}
A loop such as for (int i = 2147483640; i < 2147483647; i += 100) is analyzed as having 1 iteration because ceil((2147483647 - 2147483640) / 100) is 1. However, at runtime on a GPU, i += 100 wraps to -2147483556. The condition -2147483556 < 2147483647 remains true, causing the loop to continue for approximately $2^{30}$ iterations.
Potential Impact
This discrepancy allows a loop to pass static ES2 validation while executing many more times than expected at runtime. Because SkSL enforces that array indices in ES2 loops must be constant-index-expressions (often based on the loop index), and the compiler does not perform value-range checking on these indices relative to the unroll analysis, an attacker can trigger out-of-bounds (OOB) writes.
For example, arr[i - 2147483640] would calculate an OOB index of 100 on the second iteration if arr is a small array. Most backends (SPIR-V, GLSL, Metal) emit these loops natively without runtime safety clamps. This provides a primitive for OOB writes into shader-local memory (stack/private storage) within the GPU process.
On platforms like Android, the GPU process is a privileged process (non-isolated, sharing a UID with the browser), meaning memory corruption here could lead to a full sandbox escape.
Suggested Attack Steps
- Compromise Renderer: Gain control of a sandboxed renderer process.
- Craft SkSL: Create a malicious SkSL string using the wraparound pattern to target an array OOB.
- Submit Shader: Use an API like
SkRuntimeEffect::MakeForShadervia acc::PaintOpto force the GPU process to compile and execute the shader. - Corrupt Memory: Use the OOB write to overwrite sensitive driver metadata or shader state in the GPU process memory space.
Note: These steps are theoretical as our current tooling cannot execute code or provide a functional PoC.
Recommended Fix
The calculate_count function in SkSLGetLoopUnrollInfo.cpp should be updated to simulate the 32-bit integer arithmetic of the induction variable, including wraparound behavior. If the simulation detects a wraparound or if the iteration count deviates from the floating-point calculation, the loop should be rejected as non-unrollable in ES2 mode.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.