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
Tracker536681676
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

Potential ANGLE cross-stage LinkValidateBuiltInVaryings bypass via redeclare-after-use

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 potential vulnerability in ANGLE’s D3D11 backend allows a WebGL fragment shader to bypass cross-stage varying size validation using a redeclare-after-use pattern on gl_ClipDistance. This desynchronizes the recorded varying size from the internal AST size, defeating LinkValidateBuiltInVaryings. As a result, ANGLE emits out-of-bounds array writes in the generated HLSL pixel prologue.

Affected files:

  • third_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp
  • third_party/angle/src/compiler/translator/hlsl/OutputHLSL.cpp
  • third_party/angle/src/libANGLE/ProgramLinkedResources.cpp
  • third_party/angle/src/compiler/translator/CollectVariables.cpp

Estimated timestamp from git blame: Unknown (Google3 checkout)

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

A potential security vulnerability exists in ANGLE’s D3D11 shader-translation backend where the cross-stage built-in array validation is completely bypassed. When a WebGL fragment shader references gl_ClipDistance or gl_CullDistance before redeclaring it with a smaller array size (a “redeclare-after-use” pattern), the preorder traversal in CollectVariables registers the larger default built-in size rather than the smaller redeclared size.

As a result, the cross-stage linkage validator LinkValidateBuiltInVaryings is fooled into validating matching sizes and allows compilation/linkage to succeed. ANGLE’s code generator subsequently declares a static array with the smaller redeclared size but emits copy operations in the pixel shader prologue matching the larger vertex-shader size. This leads to potential out-of-bounds shader register writes on indexable temporary variables in the emitted HLSL. Under Chromium severity guidelines, emitting OOB array accesses in HLSL from attacker-controlled GLSL that reaches D3DCompile in the sandboxed GPU process is classified as a High (S1) severity vulnerability.


2. Proof-of-Concept & Detailed Execution Flow

(Note: These are suggested steps outlining the theoretical execution flow based on static analysis, as our tooling agent cannot execute code natively).

Root Cause Analysis & Execution Flow

  1. Attacker Input: The attacker (via web content) provides a vertex shader and a fragment shader utilizing the WEBGL_clip_cull_distance extension.
  2. Vertex Shader Setup: The vertex shader is written to explicitly set a large size for gl_ClipDistance (e.g., by assigning to gl_ClipDistance[7]), causing its clipDistanceSize property to evaluate to 8.
  3. Fragment Shader Setup: The fragment shader is written using a “redeclare-after-use” pattern: it accesses gl_ClipDistance within a function (e.g., gl_ClipDistance[0]) textually before providing a global redeclaration of the array with a smaller size (e.g., in float gl_ClipDistance[4];).
  4. AST Generation (Initial Reference): During AST generation, third_party/angle/src/compiler/translator/ParseContext.cpp parses the initial use. It resolves to the default built-in TVariable with arraySizes = [8] (derived from gl_MaxClipDistances in builtin_variables.json).
  5. AST Generation (Redeclaration): The parser encounters the global redeclaration. Inside ParseContext::declareVariable (ParseContext.cpp:2153-2284), the switch case handling EvqClipDistance only checks extension availability and limits. It lacks a use-before-redeclare guard (unlike gl_Position at lines 2291-2310).
  6. New TVariable Creation: At ParseContext.cpp:2172, a entirely new TVariable is allocated for the redeclared array (size = 4) and inserted into the user-defined symbol table via symbolTable.declare(*variable) (ParseContext.cpp:2367), hiding the built-in lookup.
  7. AST Simplification Bypass: MarkClipCullRedeclaredSize (ParseContext.cpp:292-298) updates mClipDistanceInfo.size = 4. Consequently, parseContext.isClipDistanceRedeclared() becomes true. During AST transformations (Compiler.cpp:799-801), this state short-circuits the SizeClipCullDistance pass (Compiler.cpp:804), meaning the initial AST reference retains its default size of 8.
  8. Preorder Variable Collection: CollectVariablesTraverser performs a preorder traversal (CollectVariables.cpp:281). It encounters the pre-redeclaration reference (size 8) first.
  9. Latching the Size: CollectVariablesTraverser::visitSymbol (CollectVariables.cpp:661-665) calls recordBuiltInVaryingUsed. Inside (CollectVariables.cpp:369-384), it extracts the arraySize of 8, appends it to inputVaryings, and latches mClipDistanceAdded = true.
  10. Redeclaration Ignored: The traverser reaches the redeclared float[4] node. Because !(*addedFlag) evaluates to false (the latch is already true), the node is ignored. inputVaryings["gl_ClipDistance"].arraySize permanently records 8, while internal tracking mClipDistanceSize correctly holds 4.
  11. Defeated Cross-Stage Guard: During program link-validation (third_party/angle/src/libANGLE/ProgramLinkedResources.cpp:2202-2272), LinkValidateBuiltInVaryings compares the vertex output size (sizeClipDistance = 8) against the fragment input size (varying.getOutermostArraySize()). Because the collected input size was erroneously latched at 8, 8 != 8 is false, and validation incorrectly succeeds.
  12. Vulnerable HLSL Emission: In third_party/angle/src/compiler/translator/hlsl/OutputHLSL.cpp:843-852, the static array is declared using mClipDistanceSize (4): static float gl_ClipDistance[4] = {0, 0, 0, 0};.
  13. Out-Of-Bounds Pixel Prologue: DynamicHLSL.cpp:822-858 generates the pixel shader input prologue. It pulls the loop extent from the vertex shader (pixelBuiltins.glClipDistance.indexOrSize = 8). It emits HLSL C-style array casting to cram the vector inputs into the array: ((float4[2])gl_ClipDistance)[1] = input.gl_ClipDistance1;.
  14. Execution: This widening lvalue cast forcibly re-dimensions the float[4] array to float4[2] and writes to elements 4 through 7. Passing this generated HLSL to Microsoft’s D3DCompile API leads to an OOB write into shader-private indexable temporary registers (dcl_indexableTemp x0[4]) in the sandboxed GPU process.

Potential Proof-of-Concept Shaders

Vertex Shader:

#version 300 es
#extension GL_ANGLE_clip_cull_distance : require
void main() {
    for(int i = 0; i < 8; i++) {
        gl_ClipDistance[i] = 0.0;
    }
    gl_Position = vec4(0.0);
}

Fragment Shader:

#version 300 es
#extension GL_ANGLE_clip_cull_distance : require
precision highp float;
out vec4 fragColor;

float helper() {
    return gl_ClipDistance[0]; // References built-in float[8] first
}

in float gl_ClipDistance[4];   // Redeclared after use with size 4

void main() {
    fragColor = vec4(helper());
}

Suggested Fix

Update ParseContext.cpp to explicitly forbid redeclaring gl_ClipDistance or gl_CullDistance after it has already been used in the shader, mirroring the existing safeguards enforced for gl_Position and gl_PointSize. Alternatively, ensure CollectVariables un-latches or gracefully resizes collected built-in tracking when encountering a subsequent valid redeclaration.


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

> The report accurately describes a bypass of ANGLE’s LinkValidateBuiltInVaryings cross-stage size check. By referencing the gl_ClipDistance built-in in the fragment shader before redeclaring it with a smaller size, CollectVariables (using a preorder traversal) visits the built-in reference first, latches arraySizes = 8, and ignores the subsequent redeclaration node. Meanwhile, mClipDistanceSize takes the smaller redeclared size. This causes LinkValidateBuiltInVaryings to incorrectly see matching sizes and pass, while DynamicHLSL.cpp emits HLSL that copies up to the vertex shader’s extent (e.g., 8 elements) into an array declared by OutputHLSL.cpp with the smaller redeclared size, resulting in an OOB array write in the generated HLSL. > > While the reporter conservatively rated this as Medium (S2) because they expected Microsoft’s D3DCompile to reject the invalid HLSL (acting as an unverified defense-in-depth), the Chromium severity guidelines in the Knowledge Base explicitly classify this exact pattern as High (S1). The S1 ceiling explicitly lists: ‘ANGLE shader translator emits SPIR-V/HLSL with an OOB OpAccessChain / array index from attacker GLSL (built-in redeclared with a smaller size…) … The emitted IR reaches vkCreateShaderModule/D3DCompile in the GPU process. This is web-content-reachable.’ Because this is the D3D11 backend (running in the Windows GPU process, which is sandboxed), it meets the criteria for High (S1) severity (only Metal is explicitly downgraded to S2 due to its separate sandboxed compiler process).

Codebase Investigator Output & Reachability Proofs:

  • CollectVariables.cpp: Verified mClipDistanceAdded latch initialization (line 303) and its usage inside recordBuiltInVaryingUsed (lines 369-384) where if (!(*addedFlag)) guards the addition to varyings->push_back(info) and subsequently latches *addedFlag = true.
  • ParseContext.cpp & ParseContext.h: Confirmed no use-before-redeclare checks exist for EvqClipDistance in lines 2153-2284 (unlike gl_Position). Confirmed MarkClipCullRedeclaredSize sets info->size and isClipDistanceRedeclared() becomes true.
  • Compiler.cpp: Verified SizeClipCullDistance call is short-circuited if parseContext.isClipDistanceRedeclared() (lines 799-808).
  • ProgramLinkedResources.cpp: Verified LinkValidateBuiltInVaryings (lines 2202-2272) invokes varying.getOutermostArraySize() and performs a strict equality check (sizeClipDistance != varying.getOutermostArraySize()), relying on the flawed CollectVariables output.
  • DynamicHLSL.cpp: Verified pixel shader prologue generation (lines 822-858) loops over pixelBuiltins.glClipDistance.indexOrSize and emits explicit array casting (e.g., ((float4[2])gl_ClipDistance)[1] = input.gl_ClipDistance1;).
  • OutputHLSL.cpp: Verified the variable is statically allocated using mClipDistanceSize (lines 843-852).
  • Environmental Assumptions: WEBGL_clip_cull_distance is shipping (non-draft) in WebGL2 (webgl2_rendering_context.cc:106). The D3D11 backend advertises clipCullDistanceANGLE = true (renderer11_utils.cpp:1403). Code generation targets the sandboxed Windows GPU process, meeting S1 severity.

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