CVE-2026-79188
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 <[email protected]> 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 <[email protected]> Reviewed-by: Alexey Knyazev <[email protected]> Commit-Queue: Shahbaz Youssefi <[email protected]> --- 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
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.cppthird_party/angle/src/compiler/translator/ParseContext.cppthird_party/angle/src/compiler/translator/ParseContext.hthird_party/angle/src/compiler/translator/tree_ops/DeclarePerVertexBlocks.cppthird_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.
- The attacker serves a malicious webpage containing a WebGL2 context that requests the
WEBGL_clip_cull_distanceextension. - This extension is registered without a draft flag and is enabled by default on Vulkan backends if the physical device supports
shaderClipDistancewithmaxClipDistances >= 8(e.g.,third_party/blink/renderer/modules/webgl/webgl2_rendering_context.cc:106andlibANGLE/renderer/vulkan/vk_caps_utils.cpp:1300-1308). - 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:
- Parsing Phase & Bounds Check: During parsing of the function
f(),parseVariableIdentifier(inthird_party/angle/src/compiler/translator/ParseContext.cpp) resolves the first accessgl_ClipDistance[7]. - Since the redeclared variable does not exist yet,
getNamedVariablesearches the symbol table and returns the implicit built-inTVariablem_gl_ClipDistance, which is defined with array sizeMaxClipDistances(e.g., 8) viathird_party/angle/src/compiler/translator/SymbolTable_autogen.cpp. - The parser performs an index bounds check for the direct index
[7]atParseContext.cpp:7276. It checks this against theTVariable’s array size (baseExpression->getOutermostArraySize(), which is 8).checkIndexLessThan(7, 8)successfully passes. - Redeclaration & Tracking Gap: Later, the parser encounters the redeclaration:
out highp float gl_ClipDistance[2];. - In
ParseContext.cpp:2172,declareVariableprocesses this redeclaration and allocates a completely newTVariableinstance on the heap (new TVariable(...)) with array size 2. - The redeclaration logic calls
MarkClipCullRedeclaredSize(ParseContext.cpp:292-298), which setsinfo->size = 2directly, without validating it againstinfo->maxIndex(which is tracked as 7 from the earlier access). - The new
TVariable(size 2) is inserted into the symbol table viasymbolTable.declare(*variable)(ParseContext.cpp:2280), shadowing the built-in definition. - Bypassing Normalization: At the end of parsing,
TCompiler::compilesetsmClipDistanceSizeto 2 (Compiler.cpp:544) viaparseContext.getClipDistanceArraySize(). - At
Compiler.cpp:795-805, the translator checks whether to run theSizeClipCullDistancepass (which normalizes and resizes implicit built-in arrays). - Because
parseContext.isClipDistanceRedeclared()returnstrue(sincemClipDistanceInfo.size > 0), the condition short-circuits and theSizeClipCullDistancepass is skipped. - Pointer Identity Mismatch: The AST proceeds with two divergent
TVariablereferences.TranslatorSPIRV.cppinvokesReplaceClipDistanceAssignments->ReplaceClipCullDistanceAssignmentsImpl. - In
ReplaceClipCullDistanceVariable.cpp:508-513,builtInVaris strictly assigned the pointer of the user-redeclaredTVariable(builtInVar = &redeclaredBuiltIn->variable();). ReplaceVariableExceptOneTraverser::visitSymbol(ReplaceClipCullDistanceVariable.cpp:188-194) attempts to match nodes using pointer identity:if (&node->variable() == mToBeReplaced).- When the traverser visits
f()’sgl_ClipDistance[7],&node->variable()points to the implicit size-8TVariable, whilemToBeReplacedpoints to the new size-2TVariable. The identity check fails. The pre-redeclaration symbol node survives unmodified. - Per-Vertex Merging: The pipeline runs
DeclarePerVertexBlocksTraverser(tree_ops/DeclarePerVertexBlocks.cpp). It creates thegl_PerVertexblock with agl_ClipDistancefield sized to 2 (DeclarePerVertexBlocks.cpp:346-372). DeclarePerVertexBlocksTraverser::visitSymbol(DeclarePerVertexBlocks.cpp:227-274) encounters the survivingm_gl_ClipDistancenode. It matches by qualifier (EvqClipDistance) rather than pointer identity, successfully resolving it to field index 2 of thegl_PerVertexblock.- This generates an
EOpIndexDirectoperation where the base isgl_PerVertexfield 2 (typefloat[2]), but the direct index child node retains the original literal7. - Unvalidated SPIR-V Emission: During SPIR-V code generation,
OutputSPIRV::visitBinary(spirv/OutputSPIRV.cpp:5208-5219) processes theEOpIndexDirectnode. - The AST transformation
ClampIndirectIndices(tree_ops/ClampIndirectIndices.cpp:37-40) is explicitly bypassed because it only operates onEOpIndexIndirect(non-constant indices). visitBinaryextracts the constant literal 7 (OutputSPIRV.cpp:5207) and callsaccessChainOnPush(OutputSPIRV.cpp:808-830). Since the parent is an array type, it blindly accepts the literal index without any bounds checking or clamping.- The generator emits the literal index verbatim (
accessChainPushLiteral). The output module now contains anOpAccessChainwith a constant index of 7 accessing aOpTypeArray %float %uint_2inside theOutputstorage class. - Driver Sink Reached: In Chromium Release builds,
ANGLE_ENABLE_ASSERTSis disabled (third_party/angle/src/common/log_utils.h:209-211), meaningoptions.validateASTis false (libANGLE/Shader.cpp:666-668). - Because
validateASTis false, AST validation immediately returns true, andspirv::Validateis compiled out inside theASSERTmacro. - The spec-invalid SPIR-V is passed unvalidated to
vkCreateShaderModule. Under Vulkan spec,robustBufferAccessmitigates OOB writes onUniform,StorageBuffer, and vertex inputs, but does not cover theOutputstorage 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 againstgetOutermostArraySize()(8). Verified by investigation ofTParseContext::addIndexExpressionandcheckIndexLessThan.ParseContext.cpp:2172:declareVariableallocatesnew TVariable(&symbolTable, identifier, type, symbolType);. Verified that pre-redeclaration accesses point to a different heap object than post-redeclaration accesses.ParseContext.cpp:292-298:MarkClipCullRedeclaredSizesetsinfo->size = arraySizewith no check againstinfo->maxIndex.Compiler.cpp:804:parseContext.isClipDistanceRedeclared()evaluates to true, skippingSizeClipCullDistance.ReplaceClipCullDistanceVariable.cpp:188-194: Pointer identity check&node->variable() == mToBeReplaced. Verified to fail for pre-redeclarationgl_ClipDistancesymbols.DeclarePerVertexBlocks.cpp:227-274: Merge by qualifier (variable->symbolType() == SymbolType::BuiltInandGetPerVertexFieldIndexmappingEvqClipDistanceto field 2).OutputSPIRV.cpp:5208-5219:EOpIndexDirecthandler. Extracting literal viagetAsConstantUnion()->getIConst(0), passing directly toaccessChainOnPushandaccessChainPushLiteralwith no bounds assertions.- Release Checks:
libANGLE/Shader.cpp:666-668andcommon/log_utils.h:209-211verify thatANGLE_ENABLE_ASSERTSdrivesvalidateAST. WithoutNDEBUGoverridden, it is false.Compiler.cpp:689-710skips validation if false.
Environmental Assumptions:
- Platform: Vulkan backend (default on Android, Linux, ChromeOS).
- Build Target: Chromium Release build where
ANGLE_ENABLE_ASSERTSis disabled, allowing bypassed AST validation. - Spec Design Reference: Vulkan
robustBufferAccessrestricts toUniform,StorageBuffer, and input attributes.Outputclass 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.