Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in ANGLE
DescriptionOut of bounds write in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker536444272
Fix commit10dcf44dc8e0 (angle/angle) +294/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Files Changed

  • src/compiler/translator/ParseContext.cpp
  • src/tests/gl_tests/GLSLValidationTest.cpp
From 10dcf44dc8e0d44596fa5a999117a04fddfcab8b Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Mon, 20 Jul 2026 14:21:15 -0400
Subject: [PATCH] Translator: Disallow gl_Clip/CullDistance declaration after use

If these built-ins are redeclared after being referenced, the AST
inconsistently uses two TVariables to refer to the built-in.

This could potentially be fixed by a pass over the AST.  With IR, it
should be easier to support by simply sizing the existing VariableID.
However, support for this use case requires additional validation to
ensure that the implicitly derived size for these built-ins is no bigger
than the redeclared size.

Bug: angleproject:42266961
Bug: chromium:536444272
Bug: chromium:536681676
Change-Id: I1d8faa95280b5183a9581447051364cfc96479b7
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8123200
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Alexey Knyazev <lexa.knyazev@gmail.com>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp
index 7745cbe..623f1cb 100644
--- a/src/compiler/translator/ParseContext.cpp
+++ b/src/compiler/translator/ParseContext.cpp
@@ -281,6 +281,11 @@
     return IsSampler(type->getBasicType()) || type->isStructureContainingOnlySamplers();
 }
 
+bool IsClipCullEncountered(const ClipCullDistanceInfo &info)
+{
+    return info.maxIndex >= 0 || info.hasNonConstIndex || info.hasArrayLengthMethodCall;
+}
+
 void MarkClipCullFirstEncounter(const TSourceLoc &line, ClipCullDistanceInfo *info)
 {
     if (info->firstEncounter.first_line < 0)
@@ -2346,12 +2351,29 @@
             }
         }
 
+        // Record the redeclared size of gl_Clip/CullDistance.  Do not allow redeclaration after
+        // these built-ins are already referenced to avoid having to fix the AST after the fact.
+        // With IR, this could be more easily supported if needed.
         switch (expectedType.getQualifier())
         {
             case EvqClipDistance:
+                if (IsClipCullEncountered(mClipDistanceInfo))
+                {
+                    error(line,
+                          "redeclaration of gl_ClipDistance after it is referenced is not allowed",
+                          identifier);
+                    return false;
+                }
                 MarkClipCullRedeclaredSize(line, type->getOutermostArraySize(), &mClipDistanceInfo);
                 break;
             case EvqCullDistance:
+                if (IsClipCullEncountered(mCullDistanceInfo))
+                {
+                    error(line,
+                          "redeclaration of gl_CullDistance after it is referenced is not allowed",
+                          identifier);
+                    return false;
+                }
                 MarkClipCullRedeclaredSize(line, type->getOutermostArraySize(), &mCullDistanceInfo);
                 break;
             default:
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 28a44aa..dc5e920 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -7060,6 +7060,278 @@
     }
 }
 
+// Shader redeclares gl_ClipDistance, but after it's been referenced with a constant index.
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareClipDistanceAfterReferenceConstantIndex)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+    gl_ClipDistance[0] = 1.0;
+}
+out highp float gl_ClipDistance[3];
+)";
+    constexpr char kExpect[] =
+        "'gl_ClipDistance' : redeclaration of gl_ClipDistance after it is referenced is not "
+        "allowed";
+
+    if (hasAngle)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_ANGLE_clip_cull_distance", kVS, kExpect);
+    }
+
+    if (hasExt)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_EXT_clip_cull_distance", kVS, kExpect);
+    }
+}
+
+// Shader redeclares gl_CullDistance, but after it's been referenced with a constant index.
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareCullDistanceAfterReferenceConstantIndex)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+    gl_CullDistance[0] = 1.0;
+}
+out highp float gl_CullDistance[3];
+)";
+    constexpr char kExpect[] =
+        "'gl_CullDistance' : redeclaration of gl_CullDistance after it is referenced is not "
+        "allowed";
+
+    if (hasAngle)
+    {
+        GLint maxCullDistances = 0;
+        glGetIntegerv(GL_MAX_CULL_DISTANCES_EXT, &maxCullDistances);
+        if (maxCullDistances > 0)
+        {
+            validateErrorWithExt(GL_VERTEX_SHADER, "GL_ANGLE_clip_cull_distance", kVS, kExpect);
+        }
+    }
+
+    if (hasExt)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_EXT_clip_cull_distance", kVS, kExpect);
+    }
+}
+
+// Shader redeclares gl_ClipDistance, but after it's been referenced with a non-constant index.
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareClipDistanceAfterReferenceNonConstantIndex)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+    for (int i = 0; i < 2; ++i)
+    {
+        gl_ClipDistance[i] = 1.0;
+    }
+}
+out highp float gl_ClipDistance[3];
+)";
+    constexpr char kExpect[] =
+        "'gl_ClipDistance' : redeclaration of gl_ClipDistance after it is referenced is not "
+        "allowed";
+
+    if (hasAngle)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_ANGLE_clip_cull_distance", kVS, kExpect);
+    }
+
+    if (hasExt)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_EXT_clip_cull_distance", kVS, kExpect);
+    }
+}
+
+// Shader redeclares gl_CullDistance, but after it's been referenced with a non-constant index.
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareCullDistanceAfterReferenceNonConstantIndex)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+    for (int i = 0; i < 2; ++i)
+    {
+        gl_CullDistance[i] = 1.0;
+    }
+}
+out highp float gl_CullDistance[3];
+)";
+    constexpr char kExpect[] =
+        "'gl_CullDistance' : redeclaration of gl_CullDistance after it is referenced is not "
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 28a44aa..dc5e920 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -7060,6 +7060,278 @@
     }
 }
 
+// Shader redeclares gl_ClipDistance, but after it's been referenced with a constant index.
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareClipDistanceAfterReferenceConstantIndex)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+    gl_ClipDistance[0] = 1.0;
+}
+out highp float gl_ClipDistance[3];
+)";
+    constexpr char kExpect[] =
+        "'gl_ClipDistance' : redeclaration of gl_ClipDistance after it is referenced is not "
+        "allowed";
+
+    if (hasAngle)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_ANGLE_clip_cull_distance", kVS, kExpect);
+    }
+
+    if (hasExt)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_EXT_clip_cull_distance", kVS, kExpect);
+    }
+}
+
+// Shader redeclares gl_CullDistance, but after it's been referenced with a constant index.
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareCullDistanceAfterReferenceConstantIndex)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+    gl_CullDistance[0] = 1.0;
+}
+out highp float gl_CullDistance[3];
+)";
+    constexpr char kExpect[] =
+        "'gl_CullDistance' : redeclaration of gl_CullDistance after it is referenced is not "
+        "allowed";
+
+    if (hasAngle)
+    {
+        GLint maxCullDistances = 0;
+        glGetIntegerv(GL_MAX_CULL_DISTANCES_EXT, &maxCullDistances);
+        if (maxCullDistances > 0)
+        {
+            validateErrorWithExt(GL_VERTEX_SHADER, "GL_ANGLE_clip_cull_distance", kVS, kExpect);
+        }
+    }
+
+    if (hasExt)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_EXT_clip_cull_distance", kVS, kExpect);
+    }
+}
+
+// Shader redeclares gl_ClipDistance, but after it's been referenced with a non-constant index.
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareClipDistanceAfterReferenceNonConstantIndex)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+    for (int i = 0; i < 2; ++i)
+    {
+        gl_ClipDistance[i] = 1.0;
+    }
+}
+out highp float gl_ClipDistance[3];
+)";
+    constexpr char kExpect[] =
+        "'gl_ClipDistance' : redeclaration of gl_ClipDistance after it is referenced is not "
+        "allowed";
+
+    if (hasAngle)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_ANGLE_clip_cull_distance", kVS, kExpect);
+    }
+
+    if (hasExt)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_EXT_clip_cull_distance", kVS, kExpect);
+    }
+}
+
+// Shader redeclares gl_CullDistance, but after it's been referenced with a non-constant index.
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareCullDistanceAfterReferenceNonConstantIndex)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+    for (int i = 0; i < 2; ++i)
+    {
+        gl_CullDistance[i] = 1.0;
+    }
+}
+out highp float gl_CullDistance[3];
+)";
+    constexpr char kExpect[] =
+        "'gl_CullDistance' : redeclaration of gl_CullDistance after it is referenced is not "
+        "allowed";
+
+    if (hasAngle)
+    {
+        GLint maxCullDistances = 0;
+        glGetIntegerv(GL_MAX_CULL_DISTANCES_EXT, &maxCullDistances);
+        if (maxCullDistances > 0)
+        {
+            validateErrorWithExt(GL_VERTEX_SHADER, "GL_ANGLE_clip_cull_distance", kVS, kExpect);
+        }
+    }
+
+    if (hasExt)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_EXT_clip_cull_distance", kVS, kExpect);
+    }
+}
+
+// Shader redeclares gl_ClipDistance, but after it's been referenced with .length().
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareClipDistanceAfterReferenceLength)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+    gl_Position.z = gl_ClipDistance.length();
+}
+out highp float gl_ClipDistance[3];
+)";
+    constexpr char kExpect[] =
+        "'gl_ClipDistance' : redeclaration of gl_ClipDistance after it is referenced is not "
+        "allowed";
+
+    if (hasAngle)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_ANGLE_clip_cull_distance", kVS, kExpect);
+    }
+
+    if (hasExt)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_EXT_clip_cull_distance", kVS, kExpect);
+    }
+}
+
+// Shader redeclares gl_CullDistance, but after it's been referenced with .length().
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareCullDistanceAfterReferenceLength)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+    gl_Position.z = gl_CullDistance.length();
+}
+out highp float gl_CullDistance[3];
+)";
+    constexpr char kExpect[] =
+        "'gl_CullDistance' : redeclaration of gl_CullDistance after it is referenced is not "
+        "allowed";
+
+    if (hasAngle)
+    {
+        GLint maxCullDistances = 0;
+        glGetIntegerv(GL_MAX_CULL_DISTANCES_EXT, &maxCullDistances);
+        if (maxCullDistances > 0)
+        {
+            validateErrorWithExt(GL_VERTEX_SHADER, "GL_ANGLE_clip_cull_distance", kVS, kExpect);
+        }
+    }
+
+    if (hasExt)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_EXT_clip_cull_distance", kVS, kExpect);
+    }
+}
+
+// Shader redeclares gl_ClipDistance twice.
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareClipDistanceTwice)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+out highp float gl_ClipDistance[3];
+out highp float gl_ClipDistance[3];
+void main()
+{
+    gl_Position = aPosition;
+    gl_ClipDistance[0] = 1.0;
+}
+)";
+    constexpr char kExpect[] = "'gl_ClipDistance' : redefinition";
+
+    if (hasAngle)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_ANGLE_clip_cull_distance", kVS, kExpect);
+    }
+
+    if (hasExt)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_EXT_clip_cull_distance", kVS, kExpect);
+    }
+}
+
+// Shader redeclares gl_CullDistance twice.
+TEST_P(GLSLValidationClipDistanceTest_ES3, RedeclareCullDistanceTwice)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    constexpr char kVS[] =
+        R"(in vec4 aPosition;
+out highp float gl_CullDistance[3];
+out highp float gl_CullDistance[3];
+void main()
+{
+    gl_Position = aPosition;
+    gl_CullDistance[0] = 1.0;
+}
+)";
+    constexpr char kExpect[] = "'gl_CullDistance' : redefinition";
+
+    if (hasAngle)
+    {
+        GLint maxCullDistances = 0;
+        glGetIntegerv(GL_MAX_CULL_DISTANCES_EXT, &maxCullDistances);
+        if (maxCullDistances > 0)
+        {
+            validateErrorWithExt(GL_VERTEX_SHADER, "GL_ANGLE_clip_cull_distance", kVS, kExpect);
+        }
+    }
+
+    if (hasExt)
+    {
+        validateErrorWithExt(GL_VERTEX_SHADER, "GL_EXT_clip_cull_distance", kVS, kExpect);
+    }
+}
+
 // In compute shader, redeclaring gl_ClipDistance should be denied.
 TEST_P(GLSLValidationClipDistanceTest_ES31, ComputeDeclareClipDistance)
 {
Loading diff…

Original Bug Report

reported by aw...@chromium.org

OOB write via gl_ClipDistance use-before-redeclare identity mismatch in ANGLE

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

Overview: A WebGL2 vertex shader can trigger a potential out-of-bounds write in ANGLE by indexing gl_ClipDistance before redeclaring it with a smaller size. This creates two distinct AST variables, causing variable replacement to miss the original access and emit an out-of-bounds constant index into the Output storage class in SPIR-V. In release builds, this unvalidated SPIR-V reaches the Vulkan driver where robustBufferAccess does not protect output storage, leading to a potential GPU-process driver memory corruption.

Affected files:

  • third_party/angle/src/compiler/translator/tree_util/ReplaceClipCullDistanceVariable.cpp
  • third_party/angle/src/compiler/translator/ParseContext.cpp
  • third_party/angle/src/compiler/translator/ParseContext.h
  • third_party/angle/src/compiler/translator/tree_ops/DeclarePerVertexBlocks.cpp
  • third_party/angle/src/compiler/translator/spirv/OutputSPIRV.cpp

Estimated timestamp from git blame: Unknown (Google3 checkout)

1. Summary of the Issue (Meant for Human Triage)

There is a potential out-of-bounds (OOB) write vulnerability in ANGLE’s Vulkan backend caused by an identity mismatch during AST variable replacement. When a WebGL2 vertex shader accesses a built-in array like gl_ClipDistance textually before redeclaring it with a smaller, explicit size, ANGLE’s parser instantiates two distinct TVariable objects: one for the implicit, larger global built-in array, and another for the user-redeclared array.

During the ReplaceClipCullDistanceVariable transformation pass, ANGLE attempts to replace references to the built-in array using strict pointer-identity checks. Because the pre-redeclaration access points to the original built-in TVariable and not the newly allocated redeclared TVariable, it survives the replacement pass. Subsequently, the DeclarePerVertexBlocks pass merges this surviving built-in symbol into the gl_PerVertex structure field, which was appropriately downsized to the redeclared size.

As a result, the code generator (OutputSPIRV) emits a direct index operation (EOpIndexDirect) using the original large constant index against the new, smaller output interface block array. In release builds, AST and SPIR-V validation are compiled out. This allows the spec-invalid SPIR-V to reach the Vulkan driver (vkCreateShaderModule). Because the Vulkan robustBufferAccess feature explicitly does not cover the Output storage class, this results in an unmitigated driver-level OOB write within the GPU process.

2. Proof-of-Concept & Detailed Execution Flow

Suggested Attacker Steps (Potential Proof-of-Concept): Note: Our tooling agent does not have the ability to run code, so these are suggested steps based on static analysis.

  1. The attacker serves a malicious webpage containing a WebGL2 context that requests the WEBGL_clip_cull_distance extension.
  2. This extension is registered without a draft flag and is enabled by default on Vulkan backends if the physical device supports shaderClipDistance with maxClipDistances >= 8 (e.g., third_party/blink/renderer/modules/webgl/webgl2_rendering_context.cc:106 and libANGLE/renderer/vulkan/vk_caps_utils.cpp:1300-1308).
  3. The attacker provides the following malicious vertex shader string that contains an early access to a large index of gl_ClipDistance, followed by a redeclaration with a smaller size:
    #version 300 es
    #extension GL_ANGLE_clip_cull_distance : require
    void f() { 
        gl_ClipDistance[7] = 1.0; 
    }
    out highp float gl_ClipDistance[2];
    void main() { 
        f(); 
        gl_ClipDistance[0] = 0.0; 
        gl_Position = vec4(1); 
    }
    

Detailed Execution Flow:

  1. Parsing Phase & Bounds Check: During parsing of the function f(), parseVariableIdentifier (in third_party/angle/src/compiler/translator/ParseContext.cpp) resolves the first access gl_ClipDistance[7].
  2. Since the redeclared variable does not exist yet, getNamedVariable searches the symbol table and returns the implicit built-in TVariable m_gl_ClipDistance, which is defined with array size MaxClipDistances (e.g., 8) via third_party/angle/src/compiler/translator/SymbolTable_autogen.cpp.
  3. The parser performs an index bounds check for the direct index [7] at ParseContext.cpp:7276. It checks this against the TVariable’s array size (baseExpression->getOutermostArraySize(), which is 8). checkIndexLessThan(7, 8) successfully passes.
  4. Redeclaration & Tracking Gap: Later, the parser encounters the redeclaration: out highp float gl_ClipDistance[2];.
  5. In ParseContext.cpp:2172, declareVariable processes this redeclaration and allocates a completely new TVariable instance on the heap (new TVariable(...)) with array size 2.
  6. The redeclaration logic calls MarkClipCullRedeclaredSize (ParseContext.cpp:292-298), which sets info->size = 2 directly, without validating it against info->maxIndex (which is tracked as 7 from the earlier access).
  7. The new TVariable (size 2) is inserted into the symbol table via symbolTable.declare(*variable) (ParseContext.cpp:2280), shadowing the built-in definition.
  8. Bypassing Normalization: At the end of parsing, TCompiler::compile sets mClipDistanceSize to 2 (Compiler.cpp:544) via parseContext.getClipDistanceArraySize().
  9. At Compiler.cpp:795-805, the translator checks whether to run the SizeClipCullDistance pass (which normalizes and resizes implicit built-in arrays).
  10. Because parseContext.isClipDistanceRedeclared() returns true (since mClipDistanceInfo.size > 0), the condition short-circuits and the SizeClipCullDistance pass is skipped.
  11. Pointer Identity Mismatch: The AST proceeds with two divergent TVariable references. TranslatorSPIRV.cpp invokes ReplaceClipDistanceAssignments -> ReplaceClipCullDistanceAssignmentsImpl.
  12. In ReplaceClipCullDistanceVariable.cpp:508-513, builtInVar is strictly assigned the pointer of the user-redeclared TVariable (builtInVar = &redeclaredBuiltIn->variable();).
  13. ReplaceVariableExceptOneTraverser::visitSymbol (ReplaceClipCullDistanceVariable.cpp:188-194) attempts to match nodes using pointer identity: if (&node->variable() == mToBeReplaced).
  14. When the traverser visits f()’s gl_ClipDistance[7], &node->variable() points to the implicit size-8 TVariable, while mToBeReplaced points to the new size-2 TVariable. The identity check fails. The pre-redeclaration symbol node survives unmodified.
  15. Per-Vertex Merging: The pipeline runs DeclarePerVertexBlocksTraverser (tree_ops/DeclarePerVertexBlocks.cpp). It creates the gl_PerVertex block with a gl_ClipDistance field sized to 2 (DeclarePerVertexBlocks.cpp:346-372).
  16. DeclarePerVertexBlocksTraverser::visitSymbol (DeclarePerVertexBlocks.cpp:227-274) encounters the surviving m_gl_ClipDistance node. It matches by qualifier (EvqClipDistance) rather than pointer identity, successfully resolving it to field index 2 of the gl_PerVertex block.
  17. This generates an EOpIndexDirect operation where the base is gl_PerVertex field 2 (type float[2]), but the direct index child node retains the original literal 7.
  18. Unvalidated SPIR-V Emission: During SPIR-V code generation, OutputSPIRV::visitBinary (spirv/OutputSPIRV.cpp:5208-5219) processes the EOpIndexDirect node.
  19. The AST transformation ClampIndirectIndices (tree_ops/ClampIndirectIndices.cpp:37-40) is explicitly bypassed because it only operates on EOpIndexIndirect (non-constant indices).
  20. visitBinary extracts the constant literal 7 (OutputSPIRV.cpp:5207) and calls accessChainOnPush (OutputSPIRV.cpp:808-830). Since the parent is an array type, it blindly accepts the literal index without any bounds checking or clamping.
  21. The generator emits the literal index verbatim (accessChainPushLiteral). The output module now contains an OpAccessChain with a constant index of 7 accessing a OpTypeArray %float %uint_2 inside the Output storage class.
  22. Driver Sink Reached: In Chromium Release builds, ANGLE_ENABLE_ASSERTS is disabled (third_party/angle/src/common/log_utils.h:209-211), meaning options.validateAST is false (libANGLE/Shader.cpp:666-668).
  23. Because validateAST is false, AST validation immediately returns true, and spirv::Validate is compiled out inside the ASSERT macro.
  24. The spec-invalid SPIR-V is passed unvalidated to vkCreateShaderModule. Under Vulkan spec, robustBufferAccess mitigates OOB writes on Uniform, StorageBuffer, and vertex inputs, but does not cover the Output storage class, granting a driver-resident GPU memory out-of-bounds write primitive.

Suggested Fix: To mitigate this issue, ensure that ANGLE correctly handles built-in array redeclarations that shrink the size of the array after it has been indexed. This can be resolved by modifying ParseContext.cpp inside MarkClipCullRedeclaredSize or during post-parse validation to emit a compilation error if the user-redeclared size is strictly less than the tracked maxIndex + 1. Alternatively, update ReplaceVariableExceptOneTraverser (ReplaceClipCullDistanceVariable.cpp:188) to match EvqClipDistance nodes by qualifier rather than strict pointer identity so the prior accesses are properly replaced or unified.

3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)

Prior Critic Verdict: > “The vulnerability report correctly identifies an issue where ANGLE’s SPIR-V translator emits an out-of-bounds OpAccessChain write. > > 1. By accessing gl_ClipDistance[K] before redeclaring it with a smaller size N (where N <= K < gl_MaxClipDistances), two distinct TVariable objects exist in the AST. > 2. ReplaceClipCullDistanceVariable replaces the redeclared variable using pointer identity (&node->variable() == mToBeReplaced), leaving the built-in variable referencing the larger array size in the AST. > 3. DeclarePerVertexBlocks processes the remaining built-in variable by qualifier (EvqClipDistance), converting it into an access to the gl_PerVertex block’s field 2, which was sized to N. > 4. OutputSPIRV then emits OpAccessChain for the constant index K into the float[N] array without any clamping, since ClampIndirectIndices only handles non-constant indices. > 5. In release builds, spirv::Validate and validateAST are ASSERT-only. Thus, the spec-invalid SPIR-V containing the OOB OpStore into the Output storage class reaches vkCreateShaderModule on the Vulkan backend. > > Severity Justification: High (S1). > Per the security severity guidelines: > - “ANGLE shader translator emits SPIR-V/HLSL with an OOB OpAccessChain / array index from attacker GLSL … The emitted IR reaches vkCreateShaderModule/D3DCompile in the GPU process. This is web-content-reachable (shader source is page-supplied). -> S1” > - The OOB write is in driver-managed GPU memory (vertex output storage) rather than Chrome’s host memory heap. Therefore, it qualifies as High (S1) rather than Critical (S0), even on Android where the GPU process is unsandboxed. > - The report accurately notes that robustBufferAccess does not cover the Output storage class, leaving the OOB write unmitigated in the driver.”

Automated Execution Log / Code Reachability Proofs:

  • ParseContext.cpp:7276: Bounds check validates against getOutermostArraySize() (8). Verified by investigation of TParseContext::addIndexExpression and checkIndexLessThan.
  • ParseContext.cpp:2172: declareVariable allocates new TVariable(&symbolTable, identifier, type, symbolType);. Verified that pre-redeclaration accesses point to a different heap object than post-redeclaration accesses.
  • ParseContext.cpp:292-298: MarkClipCullRedeclaredSize sets info->size = arraySize with no check against info->maxIndex.
  • Compiler.cpp:804: parseContext.isClipDistanceRedeclared() evaluates to true, skipping SizeClipCullDistance.
  • ReplaceClipCullDistanceVariable.cpp:188-194: Pointer identity check &node->variable() == mToBeReplaced. Verified to fail for pre-redeclaration gl_ClipDistance symbols.
  • DeclarePerVertexBlocks.cpp:227-274: Merge by qualifier (variable->symbolType() == SymbolType::BuiltIn and GetPerVertexFieldIndex mapping EvqClipDistance to field 2).
  • OutputSPIRV.cpp:5208-5219: EOpIndexDirect handler. Extracting literal via getAsConstantUnion()->getIConst(0), passing directly to accessChainOnPush and accessChainPushLiteral with no bounds assertions.
  • Release Checks: libANGLE/Shader.cpp:666-668 and common/log_utils.h:209-211 verify that ANGLE_ENABLE_ASSERTS drives validateAST. Without NDEBUG overridden, it is false. Compiler.cpp:689-710 skips validation if false.

Environmental Assumptions:

  • Platform: Vulkan backend (default on Android, Linux, ChromeOS).
  • Build Target: Chromium Release build where ANGLE_ENABLE_ASSERTS is disabled, allowing bypassed AST validation.
  • Spec Design Reference: Vulkan robustBufferAccess restricts to Uniform, StorageBuffer, and input attributes. Output class is explicitly unmanaged. SPIR-V Validation constraints (spirv-val) are bypassed as they run strictly in debug paths.

Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4


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