Critical patch-now ✔︎ verified CVSS 9.6 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in ANGLE
DescriptionInsufficient validation of untrusted input in ANGLE
ComponentANGLE
Sub-componentTranslator
Bug ClassInput Validation
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — 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 AIConfirmed
Reviewer notes: Root cause matches the commit message. Omits the removed comment's rationale for the numErrors() guard (avoiding stack overflow after nesting errors). The memory-corruption consequence relies on the advisory's critical rating and is not shown in the diff.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker522556145
Fix commitd2c3236e24be (angle/angle) +95/-13
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-11 (per vendor advisory)
Fix landed2026-06-18 (public git)
Disclosed2026-07-29
Patch gap41 days — fix public in source before the advisory

Background

ANGLE
the translator/validation layer that compiles attacker-controlled WebGL shaders before they reach the GPU backend.
`kWebGLMaxVariableSizeInBytes`
the upper bound ANGLE enforces on any single shader variable’s size to keep memory allocations bounded.
`checkVariableSize`
the TParseContext routine that computes a TType’s byte size and rejects it (via error(...)) when it exceeds kWebGLMaxVariableSizeInBytes.
Constructor / temporary type
a TType produced by addConstructor for an expression like T(...), which was never routed through the size check.

Root Cause Analysis

The size guard in checkVariableSize bailed out early whenever numErrors() > 0, treating the presence of any prior compile error as a license to skip validation entirely. Because an attacker can deliberately trigger a cheap, benign error earlier in the shader, this bypass let a subsequent variable whose size exceeds kWebGLMaxVariableSizeInBytes pass validation and get instantiated. Separately, checkVariableSize was only invoked on variable declarations, so constructor-produced temporary TTypes were never size-checked at all. The old design also lacked stable, per-type results: two too-large variables of the same type could be handled inconsistently, since once one produced an error the numErrors() > 0 short-circuit accepted the next. The combined effect is that oversized types could reach downstream code that trusts them to be size-bounded.
Key insight
Using global error state (numErrors()) as a validation shortcut is fragile: an attacker who induces any earlier error thereby disables the size check, so the check must depend only on the type itself and produce a stable, cached verdict per TType.

Attack Path

  1. Induce an early error Craft a WebGL shader containing a cheap, benign compile error so that numErrors() becomes greater than zero.
  2. Declare an oversized variable After the error, declare a variable (or use a constructor) whose TType size exceeds kWebGLMaxVariableSizeInBytes.
  3. Bypass the check checkVariableSize returns true via the numErrors() > 0 branch (or is never called for the constructor type), so the oversized type is accepted.
  4. Trigger instantiation The accepted oversized type flows downstream and is instantiated, driving a huge allocation or downstream overflow.

Impact Assessment

An attacker-supplied WebGL shader could defeat the kWebGLMaxVariableSizeInBytes bound and cause ANGLE to instantiate a variable far larger than intended. This yields an oversized memory allocation and potential downstream overflow, classed as insufficient validation of untrusted input in ANGLE.

Files Changed

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

Audit Directions

  • Error-state shortcuts
    Flag any validation that skips work when numErrors() > 0 (or similar global error/failure state); an attacker-triggered earlier error can disable the check.
  • Unchecked construction sites
    Look for TTypes created outside of declaration handling (constructors, temporaries, synthesized nodes) that bypass size or bounds validation applied to declared variables.
  • Non-idempotent validation
    Audit checks whose result depends on call order or prior state rather than the input alone; prefer caching a stable per-input verdict as mValidatedVariableTypeSizes does.
From d2c3236e24be3e35b80d87de7a1148cc26237f01 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Mon, 15 Jun 2026 12:16:38 -0400
Subject: [PATCH] Translator: Disallow constructing too-large variables.

Validate at construction-time that a too-large variable is invalid. This
covers the case of temporary variables instead of just variable
declarations.

Add a cache of validated TTypes. The previous logic which checked if an
error had already been generated was fragile. It would return that the
type was valid immediately after any error, if there were two variables
that were too large, the second one would be accepted and instatiated.

Fixed: chromium:522556145
Fixed: chromium:522811428
Change-Id: Ie0f6d4b940cd06bc9426bdeaf3371b3c9cbf756c
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7939998
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp
index 7f5b6a6..51a90ba 100644
--- a/src/compiler/translator/ParseContext.cpp
+++ b/src/compiler/translator/ParseContext.cpp
@@ -1855,16 +1855,19 @@
     // The size check does not take std430 into account as it is intended for WebGL shaders.  For
     // the same reason, other shader stages than vertex/fragment are ignored as defer-sized
     // variables e.g. in geometry shaders are not handled.
-    //
-    // Additionally, if the shader has already failed compilation, do not validate the type sizes.
-    // For example, if previously an error is generated due to too-deep struct nesting the
-    // calculation here could overflow the stack if performed.
-    if (!mCompileOptions.rejectWebglShadersWithLargeVariables || numErrors() > 0 ||
+    if (!mCompileOptions.rejectWebglShadersWithLargeVariables ||
         (mShaderType != GL_VERTEX_SHADER && mShaderType != GL_FRAGMENT_SHADER))
     {
         return true;
     }
 
+    // Check the cache of validated types first.
+    auto preValidatedIter = mValidatedVariableTypeSizes.find(*type);
+    if (preValidatedIter != mValidatedVariableTypeSizes.end())
+    {
+        return preValidatedIter->second;
+    }
+
     // Note: the only allowed interface block in webgl shaders is UBOs in std140 mode, so the size
     // is unconditionally calculated with std140 rules if the variable is an interface block.
     // Uniform variables are treated the same way as UBOs, as they are often packed the same way
@@ -1875,6 +1878,7 @@
     if (variableSize > kWebGLMaxVariableSizeInBytes)
     {
         error(line, "Size of declared variable exceeds implementation-defined limit", identifier);
+        mValidatedVariableTypeSizes[*type] = false;
         return false;
     }
 
@@ -1921,6 +1925,7 @@
                 error(line,
                       "Size of declared private variable exceeds implementation-defined limit",
                       identifier);
+                mValidatedVariableTypeSizes[*type] = false;
                 return false;
             }
             mTotalPrivateVariablesSize += variableSize;
@@ -1928,6 +1933,8 @@
         default:
             break;
     }
+
+    mValidatedVariableTypeSizes[*type] = true;
     return true;
 }
 
@@ -6415,7 +6422,9 @@
 //
 TIntermTyped *TParseContext::addConstructor(TFunctionLookup *fnCall, const TSourceLoc &line)
 {
-    TType type                 = fnCall->constructorType();
+    TType type = fnCall->constructorType();
+    checkVariableSize(line, ImmutableString(""), &type);
+
     TIntermSequence &arguments = fnCall->arguments();
     if (type.isUnsizedArray())
     {
diff --git a/src/compiler/translator/ParseContext.h b/src/compiler/translator/ParseContext.h
index 653a5ac..9052005 100644
--- a/src/compiler/translator/ParseContext.h
+++ b/src/compiler/translator/ParseContext.h
@@ -915,6 +915,8 @@
     // Keeps track of the total size of shader-private variables, if validating that this size
     // should not exceed a sensible threshold.
     angle::base::CheckedNumeric<size_t> mTotalPrivateVariablesSize;
+    // Tracks if a type has been validated as safe in checkVariableSize.
+    TMap<TType, bool> mValidatedVariableTypeSizes;
 
     // Track state related to control flow, used for various validation:
     //
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 8f9b2c0..2fe9f45 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -2986,12 +2986,8 @@
                   "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 using a large constant that is declared inline. Construction of such a large object, even if
+// it may be constant folded is not allowed.
 TEST_P(WebGL2GLSLValidationTest, InlineLargeConstant)
 {
     const int N1 = 256;
@@ -3040,7 +3036,82 @@
        << "const S2 sB = S2(b);\n"
        << "void main(){ " << s2.str() << "[0].b[0].a[0]; }\n";
 
-    validateSuccess(GL_FRAGMENT_SHADER, fs.str().c_str());
+    validateError(GL_FRAGMENT_SHADER, fs.str().c_str(),
+                  "'' : Size of declared variable exceeds implementation-defined limit");
+}
+
+// Validate that too-large structures cannot be instantiated as temporaries.
+TEST_P(WebGL2GLSLValidationTest, LargeStructConstructorOnly)
+{
+    const int N1 = 1024;
+    const int N2 = 1024;
+    const int N3 = 64;
+
+    std::ostringstream fs;
+    fs << "#version 300 es\n";
+    fs << "precision highp float;\n";
+    fs << "struct S1 { mat4 m[" << N1 << "]; };\n";
+    fs << "struct S2 {\n";
+    for (int i = 0; i < N2; i++)
+    {
+        fs << "    S1 m" << i << ";\n";
+    }
+    fs << "};\n";
+    fs << "struct S3 {\n";
+    for (int i = 0; i < N3; i++)
+    {
+        fs << "    S2 m" << i << ";\n";
+    }
+    fs << "};\n";
+    fs << "out vec4 color;\n";
+    fs << "void main() {\n";
+    fs << "    S1 s1;\n";
+    fs << "    color = S3(";
+    for (int i = 0; i < N3; i++)
+    {
+        fs << "S2(";
+        for (int j = 0; j < N2; j++)
+        {
+            fs << "s1";
+            if (j != N2 - 1)
+            {
+                fs << ",";
+            }
+        }
+        fs << ")";
+        if (i != N3 - 1)
+        {
+            fs << ",";
+        }
+    }
+    fs << ").m0.m0.m[0][0];\n";
+    fs << "}\n";
+
+    validateError(GL_FRAGMENT_SHADER, fs.str().c_str(),
+                  "'' : Size of declared private variable exceeds implementation-defined limit");
+}
+
+// Validate that too-large structures cannot be used as function return types
+TEST_P(WebGL2GLSLValidationTest, LargeVariableFunctionReturnType)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+
+struct largestruct {
+    mat4 m[1000000000u];
+};
+
+largestruct func() {
+    largestruct s;
+    return s;
+}
+
+void main() {
+    func();
+})";
+
+    validateError(GL_FRAGMENT_SHADER, kFS,
+                  "'s' : Size of declared variable exceeds implementation-defined limit");
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 8f9b2c0..2fe9f45 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -2986,12 +2986,8 @@
                   "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 using a large constant that is declared inline. Construction of such a large object, even if
+// it may be constant folded is not allowed.
 TEST_P(WebGL2GLSLValidationTest, InlineLargeConstant)
 {
     const int N1 = 256;
@@ -3040,7 +3036,82 @@
        << "const S2 sB = S2(b);\n"
        << "void main(){ " << s2.str() << "[0].b[0].a[0]; }\n";
 
-    validateSuccess(GL_FRAGMENT_SHADER, fs.str().c_str());
+    validateError(GL_FRAGMENT_SHADER, fs.str().c_str(),
+                  "'' : Size of declared variable exceeds implementation-defined limit");
+}
+
+// Validate that too-large structures cannot be instantiated as temporaries.
+TEST_P(WebGL2GLSLValidationTest, LargeStructConstructorOnly)
+{
+    const int N1 = 1024;
+    const int N2 = 1024;
+    const int N3 = 64;
+
+    std::ostringstream fs;
+    fs << "#version 300 es\n";
+    fs << "precision highp float;\n";
+    fs << "struct S1 { mat4 m[" << N1 << "]; };\n";
+    fs << "struct S2 {\n";
+    for (int i = 0; i < N2; i++)
+    {
+        fs << "    S1 m" << i << ";\n";
+    }
+    fs << "};\n";
+    fs << "struct S3 {\n";
+    for (int i = 0; i < N3; i++)
+    {
+        fs << "    S2 m" << i << ";\n";
+    }
+    fs << "};\n";
+    fs << "out vec4 color;\n";
+    fs << "void main() {\n";
+    fs << "    S1 s1;\n";
+    fs << "    color = S3(";
+    for (int i = 0; i < N3; i++)
+    {
+        fs << "S2(";
+        for (int j = 0; j < N2; j++)
+        {
+            fs << "s1";
+            if (j != N2 - 1)
+            {
+                fs << ",";
+            }
+        }
+        fs << ")";
+        if (i != N3 - 1)
+        {
+            fs << ",";
+        }
+    }
+    fs << ").m0.m0.m[0][0];\n";
+    fs << "}\n";
+
+    validateError(GL_FRAGMENT_SHADER, fs.str().c_str(),
+                  "'' : Size of declared private variable exceeds implementation-defined limit");
+}
+
+// Validate that too-large structures cannot be used as function return types
+TEST_P(WebGL2GLSLValidationTest, LargeVariableFunctionReturnType)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+
+struct largestruct {
+    mat4 m[1000000000u];
+};
+
+largestruct func() {
+    largestruct s;
+    return s;
+}
+
+void main() {
+    func();
+})";
+
+    validateError(GL_FRAGMENT_SHADER, kFS,
+                  "'s' : Size of declared variable exceeds implementation-defined limit");
 }
 
 // Test that too large color outputs are rejected
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential GPU Process RCE via missing size validation on temporary constructor objects in ANGLE

Flapjack, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: ANGLE’s GLSL compiler enforces a 2GB limit on variable sizes to prevent integer overflows in native graphics drivers. However, TParseContext::addConstructor fails to check the size of anonymous temporary objects created via inline constructors. An attacker can instantiate nested structures exceeding 2GB inline, bypassing validation and potentially causing overflows and memory corruption in the native GPU driver.

Affected files:

  • third_party/angle/src/compiler/translator/ParseContext.cpp

Estimated timestamp from git blame: Unknown (Google3 checkout)

Description

ANGLE’s GLSL compiler implements security mitigations to prevent unprivileged WebGL shaders from triggering integer overflows in underlying native graphics drivers. Specifically, the function TParseContext::checkVariableSize limits individual variables to 2GB (kWebGLMaxVariableSizeInBytes) and private/temporary variables to 64KB (kWebGLMaxPrivateVariableSizeInBytes).

While explicitly declared variables are properly validated in TParseContext::declareVariable, this size validation is completely missing when a massive object is instantiated as an anonymous temporary expression. In third_party/angle/src/compiler/translator/ParseContext.cpp, the TParseContext::addConstructor function creates the AST node for a constructor call but fails to invoke checkVariableSize on the constructed type. Furthermore, because it never enters checkVariableSize, it bypasses the accumulator mTotalPrivateVariablesSize, evading the 16MB kWebGLMaxTotalPrivateVariableSizeInBytes limit.

Impact

By generating GLSL that contains a massive anonymous temporary object, ANGLE translates and passes the shader to the native graphics driver (e.g., Adreno, Mali, Metal). Native driver compilers frequently use 32-bit signed or unsigned integers for layout and offset calculations. Parsing a >2GB or >4GB temporary object will predictably trigger integer overflows, potentially resulting in undersized heap allocations or out-of-bounds stack accesses during compilation or runtime execution.

Memory corruption in the GPU process can lead to Remote Code Execution. On platforms where the GPU process is unsandboxed (e.g., Android), this translates to an immediate sandbox escape.

Potential Steps to Reproduce

Note: These are theoretical steps, as our tooling cannot currently execute code to produce a working PoC.

  1. Initialize a WebGL 2.0 context.
  2. Compile a GLSL shader that defines nested structures to construct a 4GB type without exceeding the kWebGLMaxStructNesting (4) or kMaxFieldCount (16383) limits.
#version 300 es
precision highp float;

struct S1 { mat4 m[1024]; };  // 64KB, depth 1
struct S2 { S1 f0; S1 f1; /* ... 1024 fields ... */ S1 f1023; }; // 64MB, depth 2
struct S3 { S2 g0; S2 g1; /* ... 64 fields ... */ S2 g63; }; // 4GB, depth 3

out vec4 color;
void main() {
    S1 s1;
    // The constructor for S3 bypasses checkVariableSize because it is an inline temporary.
    color = S3(S2(s1, s1 /* ... */), S2(s1, s1 /* ... */) /* ... */).g0.f0.m[0][0];
}
  1. Pass this shader to gl.compileShader. ANGLE will parse the massive S3(...) temporary and create the AST without throwing a size error.
  2. The translated code is passed to the native driver, where the 4GB allocation likely overflows 32-bit offset/size calculations.

Suggested Fix

In third_party/angle/src/compiler/translator/ParseContext.cpp, update TParseContext::addConstructor to call checkVariableSize on the constructor’s return type before creating the AST node.

TIntermTyped *TParseContext::addConstructor(TFunctionLookup *fnCall, const TSourceLoc &line)
{
    TType type = fnCall->constructorType();
    
    if (!checkVariableSize(line, ImmutableString(""), &type))
    {
        return CreateZeroNode(type);
    }
    
    // ... proceed with checking arguments

Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff


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.

View on issue tracker