Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in ANGLE
DescriptionInteger overflow in ANGLE
ComponentANGLE
Sub-componentTranslator
Bug ClassInteger Overflow
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: 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.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker498400132
Fix commitd8ffa7447da0 (angle/angle) +121/-6
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-01 (per vendor advisory)
Fix landed2026-04-21 (public git)
Disclosed2026-05-12
Patch gap21 days — fix public in source before the advisory

Background

ANGLE
Chrome’s translation layer that compiles WebGL/GLSL shaders into native graphics API calls; its translator component 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

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
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.

Key insight
Emitting a compile 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

  1. Craft nested type Author a WebGL2 shader defining nested struct arrays (S: 256 floats, S2: 256 S, c: 65537 S2) whose total byte size exceeds 4*4GB and overflows 32-bit size math.
  2. Attach initializer Declare the oversized const variable with an initializer so the executeInitializer path is taken.
  3. Bypass the gate checkVariableSize calls error() but returns void, so the caller ignores it and keeps declaring the variable.
  4. Consume wrapped size Initializer and IR setup allocate and copy based on the wrapped (small) 32-bit size, under-allocating the backing buffer.
  5. 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

An attacker-supplied WebGL2 shader triggers an integer-overflow-driven under-allocation and out-of-bounds memory access inside ANGLE’s shader translator during compilation, before any GPU execution. Because shader compilation runs in the GPU process, this yields memory corruption in a privileged process reachable from any web page that can obtain a WebGL2 context, making it a viable path toward sandbox-constrained code execution or information disclosure.

Files Changed

  • src/compiler/translator/ParseContext.cpp
  • src/compiler/translator/ParseContext.h
  • src/tests/gl_tests/GLSLValidationTest.cpp

Audit Directions

  • Void validators that only `error()`
    Audit ANGLE translator checks that call error() but return void; a diagnostic without a propagated failure lets callers continue building IR from rejected input.
  • Size math on nested aggregate types
    Review 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 check
    Trace executeInitializer / addVariable and similar paths to confirm that every early-failing check short-circuits initializer allocation, copy, and IR-node creation rather than merely recording an error.
From d8ffa7447da0342d3ba77c152c3343ba633ff9f5 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <[email protected]>
Date: Mon, 20 Apr 2026 11:37:22 -0400
Subject: [PATCH] Translator: Avoid setting initializer for too-large variable

Test credit [email protected] / gemini.

Fixed: chromium:498400132
Change-Id: I4b5379de712c22fcfcdbabbaf9c3f92bae209922
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7778457
Auto-Submit: Shahbaz Youssefi <[email protected]>
Reviewed-by: Kenneth Russell <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Shahbaz Youssefi <[email protected]>
---

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
Loading diff…

Regression Test / PoC

shipped with the fix
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)
 {
Loading diff…

Original Bug Report

reported by [email protected]

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.cpp
  • third_party/angle/src/compiler/translator/ParseContext.cpp
  • third_party/angle/src/common/PoolAlloc.cpp
  • third_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

  1. Constant Amplification: Through the use of nested structures and large constant arrays, an attacker can geometrically increase the elementSize without exceeding parser depth limits. For example, a struct containing an array of structs can multiply the size at each level of nesting.
  2. Size Check Bypass: While ANGLE includes a checkVariableSize function (in ParseContext.cpp), it does not prevent this overflow. The function issues an error but returns void, and the caller declareVariable ignores the error state, returning true and allowing the process to continue to executeInitializer. Furthermore, if an error has already been recorded (e.g., from a preliminary large constant declaration or syntax error), checkVariableSize short-circuits due to a numErrors() > 0 check, skipping the size validation entirely for subsequent variables.
  3. Allocator Behavior: The allocation uses PoolAllocator, which for large allocations falls back to new (std::nothrow) uint8_t[] (in PoolAlloc.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:

  1. Craft a WebGL2 shader (ESSL 3.00) targeted at a 32-bit platform (e.g., Android ARMv7).
  2. 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]; };       
    
  3. 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(){}
    
  4. Calling gl.compileShader() with this source will trigger the heap OOB write during the constant folding of variable D.
  5. 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.

View on issue tracker