CVE-2026-79138
Overview
Files Changed
src/compiler/translator/ParseContext.cppsrc/tests/gl_tests/GLSLValidationTest.cpp
Patch
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 "
Regression Test / PoC
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)
{
Original Bug Report
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.cppthird_party/angle/src/compiler/translator/hlsl/OutputHLSL.cppthird_party/angle/src/libANGLE/ProgramLinkedResources.cppthird_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
- Attacker Input: The attacker (via web content) provides a vertex shader and a fragment shader utilizing the
WEBGL_clip_cull_distanceextension. - Vertex Shader Setup: The vertex shader is written to explicitly set a large size for
gl_ClipDistance(e.g., by assigning togl_ClipDistance[7]), causing itsclipDistanceSizeproperty to evaluate to 8. - Fragment Shader Setup: The fragment shader is written using a “redeclare-after-use” pattern: it accesses
gl_ClipDistancewithin 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];). - AST Generation (Initial Reference): During AST generation,
third_party/angle/src/compiler/translator/ParseContext.cppparses the initial use. It resolves to the default built-inTVariablewitharraySizes = [8](derived fromgl_MaxClipDistancesinbuiltin_variables.json). - AST Generation (Redeclaration): The parser encounters the global redeclaration. Inside
ParseContext::declareVariable(ParseContext.cpp:2153-2284), the switch case handlingEvqClipDistanceonly checks extension availability and limits. It lacks a use-before-redeclare guard (unlikegl_Positionat lines 2291-2310). - New TVariable Creation: At
ParseContext.cpp:2172, a entirely newTVariableis allocated for the redeclared array (size = 4) and inserted into the user-defined symbol table viasymbolTable.declare(*variable)(ParseContext.cpp:2367), hiding the built-in lookup. - AST Simplification Bypass:
MarkClipCullRedeclaredSize(ParseContext.cpp:292-298) updatesmClipDistanceInfo.size = 4. Consequently,parseContext.isClipDistanceRedeclared()becomestrue. During AST transformations (Compiler.cpp:799-801), this state short-circuits theSizeClipCullDistancepass (Compiler.cpp:804), meaning the initial AST reference retains its default size of 8. - Preorder Variable Collection:
CollectVariablesTraverserperforms a preorder traversal (CollectVariables.cpp:281). It encounters the pre-redeclaration reference (size 8) first. - Latching the Size:
CollectVariablesTraverser::visitSymbol(CollectVariables.cpp:661-665) callsrecordBuiltInVaryingUsed. Inside (CollectVariables.cpp:369-384), it extracts thearraySizeof 8, appends it toinputVaryings, and latchesmClipDistanceAdded = true. - Redeclaration Ignored: The traverser reaches the redeclared
float[4]node. Because!(*addedFlag)evaluates tofalse(the latch is already true), the node is ignored.inputVaryings["gl_ClipDistance"].arraySizepermanently records 8, while internal trackingmClipDistanceSizecorrectly holds 4. - Defeated Cross-Stage Guard: During program link-validation (
third_party/angle/src/libANGLE/ProgramLinkedResources.cpp:2202-2272),LinkValidateBuiltInVaryingscompares the vertex output size (sizeClipDistance = 8) against the fragment input size (varying.getOutermostArraySize()). Because the collected input size was erroneously latched at 8,8 != 8is false, and validation incorrectly succeeds. - Vulnerable HLSL Emission: In
third_party/angle/src/compiler/translator/hlsl/OutputHLSL.cpp:843-852, the static array is declared usingmClipDistanceSize(4):static float gl_ClipDistance[4] = {0, 0, 0, 0};. - Out-Of-Bounds Pixel Prologue:
DynamicHLSL.cpp:822-858generates 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;. - Execution: This widening lvalue cast forcibly re-dimensions the
float[4]array tofloat4[2]and writes to elements 4 through 7. Passing this generated HLSL to Microsoft’sD3DCompileAPI 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: VerifiedmClipDistanceAddedlatch initialization (line 303) and its usage insiderecordBuiltInVaryingUsed(lines 369-384) whereif (!(*addedFlag))guards the addition tovaryings->push_back(info)and subsequently latches*addedFlag = true.ParseContext.cpp&ParseContext.h: Confirmed no use-before-redeclare checks exist forEvqClipDistancein lines 2153-2284 (unlikegl_Position). ConfirmedMarkClipCullRedeclaredSizesetsinfo->sizeandisClipDistanceRedeclared()becomes true.Compiler.cpp: VerifiedSizeClipCullDistancecall is short-circuited ifparseContext.isClipDistanceRedeclared()(lines 799-808).ProgramLinkedResources.cpp: VerifiedLinkValidateBuiltInVaryings(lines 2202-2272) invokesvarying.getOutermostArraySize()and performs a strict equality check (sizeClipDistance != varying.getOutermostArraySize()), relying on the flawedCollectVariablesoutput.DynamicHLSL.cpp: Verified pixel shader prologue generation (lines 822-858) loops overpixelBuiltins.glClipDistance.indexOrSizeand emits explicit array casting (e.g.,((float4[2])gl_ClipDistance)[1] = input.gl_ClipDistance1;).OutputHLSL.cpp: Verified the variable is statically allocated usingmClipDistanceSize(lines 843-852).- Environmental Assumptions:
WEBGL_clip_cull_distanceis shipping (non-draft) in WebGL2 (webgl2_rendering_context.cc:106). The D3D11 backend advertisesclipCullDistanceANGLE = 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.