Medium chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in ANGLE
DescriptionType confusion in ANGLE
ComponentANGLE
Bug ClassType Confusion
Tracker517245017
Fix commitab5bee732a58 (angle/angle) +446/-32
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
ClipDistanceTestES3
src/tests/gl_tests/ClipDistanceTest.cpp
modified

Files Changed

  • src/compiler/translator/ParseContext.cpp
  • src/compiler/translator/ParseContext.h
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/ClipDistanceTest.cpp
From ab5bee732a58f6c61b93b6eea830ef6296325d98 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Mon, 20 Jul 2026 15:39:51 -0400
Subject: [PATCH] Translator: Disallow gl_Clip/CullDistance whole-array use w/o...

... an explicit declaration.  The alternative would be to record all
whole-array uses of these built-ins and at the end of the shader
validate that the implicitly-derived size matches the whole array use.
Such usage is banned instead in this change.

Bug: chromium:517245017
Change-Id: I842b2073c5132e1b1a74fd5316c4bb99c8740b2b
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8122943
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Alexey Knyazev <lexa.knyazev@gmail.com>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---

diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp
index 623f1cb..7773434 100644
--- a/src/compiler/translator/ParseContext.cpp
+++ b/src/compiler/translator/ParseContext.cpp
@@ -3269,7 +3269,7 @@
     for (size_t i = 0; i < fnCandidate->getParamCount(); ++i)
     {
         TQualifier qual        = fnCandidate->getParam(i)->getType().getQualifier();
-        TIntermTyped *argument = (*(fnCall->getSequence()))[i]->getAsTyped();
+        TIntermTyped *argument = (*fnCall->getSequence())[i]->getAsTyped();
         bool argumentIsRead    = (IsQualifierUnspecified(qual) || qual == EvqParamIn ||
                                qual == EvqParamInOut || qual == EvqParamConst);
         if (argumentIsRead)
@@ -3299,6 +3299,45 @@
     }
 }
 
+void TParseContext::checkClipCullDistanceWholeArrayUse(const TSourceLoc &location,
+                                                       TQualifier qualifier,
+                                                       const char *message)
+{
+    switch (qualifier)
+    {
+        case EvqClipDistance:
+            if (mClipDistanceInfo.size == 0)
+            {
+                error(location, message, "gl_ClipDistance");
+                return;
+            }
+            break;
+        case EvqCullDistance:
+            if (mCullDistanceInfo.size == 0)
+            {
+                error(location, message, "gl_CullDistance");
+                return;
+            }
+            break;
+        default:
+            break;
+    }
+}
+
+void TParseContext::functionCallClipCullDistanceCheck(const TFunction *fnCandidate,
+                                                      TIntermAggregate *fnCall)
+{
+    // If clip/cull distance is not redeclared, they can't be passed to a function because their
+    // size is unknown.  Per EXT_clip_cull_distance, only indexing with constants can implicitly
+    // size the built-ins, passing to a function shouldn't try to size them.
+    for (size_t i = 0; i < fnCandidate->getParamCount(); ++i)
+    {
+        TIntermTyped *argument = (*fnCall->getSequence())[i]->getAsTyped();
+        checkClipCullDistanceWholeArrayUse(argument->getLine(), argument->getQualifier(),
+                                           "Cannot pass to function unless it is explicitly sized");
+    }
+}
+
 void TParseContext::checkInvariantVariableQualifier(bool invariant,
                                                     const TQualifier qualifier,
                                                     const TSourceLoc &invariantLocation)
@@ -9038,6 +9077,16 @@
             error(loc, "array size mismatch", GetOperatorString(op));
             return false;
         }
+
+        // If either side is gl_Clip/CullDistance but the built-in is not sized, that's not allowed.
+        // Per EXT_clip_cull_distance, only indexing with constants can implicitly size the
+        // built-ins, using them in whole-array assignment shouldn't try to size them.
+        checkClipCullDistanceWholeArrayUse(
+            loc, left->getType().getQualifier(),
+            "Cannot use as left-hand side of assignment unless it is explicitly sized");
+        checkClipCullDistanceWholeArrayUse(
+            loc, right->getType().getQualifier(),
+            "Cannot use as right-hand side of assignment unless it is explicitly sized");
     }
 
     // Check ops which require integer / ivec parameters
@@ -10012,6 +10061,7 @@
             callNode->setLine(loc);
             checkImageMemoryAccessForUserDefinedFunctions(fnCandidate, callNode);
             functionCallRValueLValueErrorCheck(fnCandidate, callNode);
+            functionCallClipCullDistanceCheck(fnCandidate, callNode);
 
             mCallGraph[mCurrentFunction].insert(fnCandidate);
             mIRBuilder.callFunction(mFunctionToId.at(fnCandidate));
diff --git a/src/compiler/translator/ParseContext.h b/src/compiler/translator/ParseContext.h
index d086955..956bcf3 100644
--- a/src/compiler/translator/ParseContext.h
+++ b/src/compiler/translator/ParseContext.h
@@ -216,6 +216,10 @@
     bool checkWorkGroupSizeIsNotSpecified(const TSourceLoc &location,
                                           const TLayoutQualifier &layoutQualifier);
     void functionCallRValueLValueErrorCheck(const TFunction *fnCandidate, TIntermAggregate *fnCall);
+    void checkClipCullDistanceWholeArrayUse(const TSourceLoc &location,
+                                            TQualifier qualifier,
+                                            const char *message);
+    void functionCallClipCullDistanceCheck(const TFunction *fnCandidate, TIntermAggregate *fnCall);
     void checkInvariantVariableQualifier(bool invariant,
                                          const TQualifier qualifier,
                                          const TSourceLoc &invariantLocation);
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index ac1f736..417e268 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -211,6 +211,7 @@
 463961767 WIN INTEL OPENGL : EGLSurfacelessContextTest.Switcheroo/* = SKIP
 494350632 WIN INTEL OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
 355607623 WIN INTEL OPENGL : WebGL2GLSLTest.BasicInfiniteLoop/* = SKIP
+537235696 WIN INTEL OPENGL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
 // Some Intel Vulkan drivers report incorrect VkPhysicalDeviceLimits::maxSamplerLodBias on Windows.
 498165510 WIN INTEL VULKAN : QCOMTextureLodBiasTest.BiasClamping/* = SKIP
 
@@ -288,6 +289,7 @@
 379758210 LINUX INTEL OPENGL : GLSLTest_ES3.SwizzledToBoolCoercion/* = SKIP
 437259106 LINUX INTEL : EGLDisplayTest.ContextLeakAfterTerminate/* = SKIP
 448675904 LINUX INTEL OPENGL : GLSLTestLoops.ForContinueInSwitchComplex/* = SKIP
+537235696 LINUX INTEL OPENGL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
 
 //Angle has bugs for sampler validation
 401546698 : TextureCubeTest.CubeMapBug2/* = SKIP
@@ -343,6 +345,7 @@
 536046166 NVIDIA VULKAN : FramebufferTest_ES31_MSAA.MultisampleStencilSampling/*EmulatedPrerotation* = SKIP
 42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixCopyBug/* = SKIP
 42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixMultiplicationBug/* = SKIP
+537235696 NVIDIA OPENGL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
 
 // Intel Vulkan
 
@@ -460,6 +463,7 @@
 515709506 MAC METAL : DrawBaseVertexBaseInstanceTest_ES3.BaseInstanceSmallDivisorClientMemory/* = SKIP
 524008572 MAC METAL : VertexAttributeTestES3.LargeAttribPointerOffsetNoCrash/* = SKIP
 534815900 MAC METAL : TextureCubeTestES3.RedefinedCubemapLevelsOnlyFaceZeroCompatible/* = SKIP
+536936861 MAC METAL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
 
 // The workaround is not intended to be enabled in this configuration so
 // skip it as the failure is likely a driver bug.
diff --git a/src/tests/gl_tests/ClipDistanceTest.cpp b/src/tests/gl_tests/ClipDistanceTest.cpp
index 46c5623..069d16d 100644
--- a/src/tests/gl_tests/ClipDistanceTest.cpp
+++ b/src/tests/gl_tests/ClipDistanceTest.cpp
@@ -28,8 +28,16 @@
         setConfigDepthBits(24);
         setExtensionsEnabled(false);
     }
+
+    // Given the vertex and fragment shader, writes position to |a_position| and three planes in
+    // |u_plane[i]|.  The vertex shader should evaluate |gl_ClipDistance[i]| as
+    // |dot(position, u_plane[i])|.  The fragment shader should output red.
+    void threeClipDistancesRedeclared(const char *vs, const char *fs);
 };
 
+class ClipDistanceTestES3 : public ClipDistanceAPPLETest
+{};
+
 // Query max clip distances and enable, disable states of clip distances
 TEST_P(ClipDistanceAPPLETest, StateQuery)
 {
@@ -218,11 +226,15 @@
 
 attribute vec2 a_position;
 
+void write(out float distance)
+{
+    distance = dot(gl_Position, u_plane);
+}
+
 void main()
 {
     gl_Position = vec4(a_position, 0.0, 1.0);
-
-    gl_ClipDistance[0] = dot(gl_Position, u_plane);
+    write(gl_ClipDistance[0]);
 })";
 
     ANGLE_GL_PROGRAM(programRed, kVS, essl1_shaders::fs::Red());
@@ -647,36 +659,9 @@
     }
 }
 
-// Redeclare gl_ClipDistance in shader with explicit size, also use it in a global function
-// outside main()
-TEST_P(ClipDistanceAPPLETest, ThreeClipDistancesRedeclared)
+void ClipDistanceAPPLETest::threeClipDistancesRedeclared(const char *vs, const char *fs)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index ac1f736..417e268 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -211,6 +211,7 @@
 463961767 WIN INTEL OPENGL : EGLSurfacelessContextTest.Switcheroo/* = SKIP
 494350632 WIN INTEL OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
 355607623 WIN INTEL OPENGL : WebGL2GLSLTest.BasicInfiniteLoop/* = SKIP
+537235696 WIN INTEL OPENGL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
 // Some Intel Vulkan drivers report incorrect VkPhysicalDeviceLimits::maxSamplerLodBias on Windows.
 498165510 WIN INTEL VULKAN : QCOMTextureLodBiasTest.BiasClamping/* = SKIP
 
@@ -288,6 +289,7 @@
 379758210 LINUX INTEL OPENGL : GLSLTest_ES3.SwizzledToBoolCoercion/* = SKIP
 437259106 LINUX INTEL : EGLDisplayTest.ContextLeakAfterTerminate/* = SKIP
 448675904 LINUX INTEL OPENGL : GLSLTestLoops.ForContinueInSwitchComplex/* = SKIP
+537235696 LINUX INTEL OPENGL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
 
 //Angle has bugs for sampler validation
 401546698 : TextureCubeTest.CubeMapBug2/* = SKIP
@@ -343,6 +345,7 @@
 536046166 NVIDIA VULKAN : FramebufferTest_ES31_MSAA.MultisampleStencilSampling/*EmulatedPrerotation* = SKIP
 42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixCopyBug/* = SKIP
 42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixMultiplicationBug/* = SKIP
+537235696 NVIDIA OPENGL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
 
 // Intel Vulkan
 
@@ -460,6 +463,7 @@
 515709506 MAC METAL : DrawBaseVertexBaseInstanceTest_ES3.BaseInstanceSmallDivisorClientMemory/* = SKIP
 524008572 MAC METAL : VertexAttributeTestES3.LargeAttribPointerOffsetNoCrash/* = SKIP
 534815900 MAC METAL : TextureCubeTestES3.RedefinedCubemapLevelsOnlyFaceZeroCompatible/* = SKIP
+536936861 MAC METAL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
 
 // The workaround is not intended to be enabled in this configuration so
 // skip it as the failure is likely a driver bug.
diff --git a/src/tests/gl_tests/ClipDistanceTest.cpp b/src/tests/gl_tests/ClipDistanceTest.cpp
index 46c5623..069d16d 100644
--- a/src/tests/gl_tests/ClipDistanceTest.cpp
+++ b/src/tests/gl_tests/ClipDistanceTest.cpp
@@ -28,8 +28,16 @@
         setConfigDepthBits(24);
         setExtensionsEnabled(false);
     }
+
+    // Given the vertex and fragment shader, writes position to |a_position| and three planes in
+    // |u_plane[i]|.  The vertex shader should evaluate |gl_ClipDistance[i]| as
+    // |dot(position, u_plane[i])|.  The fragment shader should output red.
+    void threeClipDistancesRedeclared(const char *vs, const char *fs);
 };
 
+class ClipDistanceTestES3 : public ClipDistanceAPPLETest
+{};
+
 // Query max clip distances and enable, disable states of clip distances
 TEST_P(ClipDistanceAPPLETest, StateQuery)
 {
@@ -218,11 +226,15 @@
 
 attribute vec2 a_position;
 
+void write(out float distance)
+{
+    distance = dot(gl_Position, u_plane);
+}
+
 void main()
 {
     gl_Position = vec4(a_position, 0.0, 1.0);
-
-    gl_ClipDistance[0] = dot(gl_Position, u_plane);
+    write(gl_ClipDistance[0]);
 })";
 
     ANGLE_GL_PROGRAM(programRed, kVS, essl1_shaders::fs::Red());
@@ -647,36 +659,9 @@
     }
 }
 
-// Redeclare gl_ClipDistance in shader with explicit size, also use it in a global function
-// outside main()
-TEST_P(ClipDistanceAPPLETest, ThreeClipDistancesRedeclared)
+void ClipDistanceAPPLETest::threeClipDistancesRedeclared(const char *vs, const char *fs)
 {
-    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_APPLE_clip_distance"));
-
-    constexpr char kVS[] = R"(
-#extension GL_APPLE_clip_distance : require
-
-varying highp float gl_ClipDistance[3];
-
-void computeClipDistances(in vec4 position, in vec4 plane[3])
-{
-    gl_ClipDistance[0] = dot(position, plane[0]);
-    gl_ClipDistance[1] = dot(position, plane[1]);
-    gl_ClipDistance[2] = dot(position, plane[2]);
-}
-
-uniform vec4 u_plane[3];
-
-attribute vec2 a_position;
-
-void main()
-{
-    gl_Position = vec4(a_position, 0.0, 1.0);
-
-    computeClipDistances(gl_Position, u_plane);
-})";
-
-    ANGLE_GL_PROGRAM(programRed, kVS, essl1_shaders::fs::Red());
+    ANGLE_GL_PROGRAM(programRed, vs, fs);
     glUseProgram(programRed);
     ASSERT_GL_NO_ERROR();
 
@@ -741,6 +726,104 @@
     }
 }
 
+// Redeclare gl_ClipDistance in shader with explicit size, also use it in a global function
+// outside main()
+TEST_P(ClipDistanceAPPLETest, ThreeClipDistancesRedeclared)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_APPLE_clip_distance"));
+
+    constexpr char kVS[] = R"(
+#extension GL_APPLE_clip_distance : require
+
+varying highp float gl_ClipDistance[3];
+
+void computeClipDistances(in vec4 position, in vec4 plane[3])
+{
+    gl_ClipDistance[0] = dot(position, plane[0]);
+    gl_ClipDistance[1] = dot(position, plane[1]);
+    gl_ClipDistance[2] = dot(position, plane[2]);
+}
+
+uniform vec4 u_plane[3];
+
+attribute vec2 a_position;
+
+void main()
+{
+    gl_Position = vec4(a_position, 0.0, 1.0);
+
+    computeClipDistances(gl_Position, u_plane);
+})";
+
+    threeClipDistancesRedeclared(kVS, essl1_shaders::fs::Red());
+}
+
+// Redeclare gl_ClipDistance in shader with explicit size, also pass it to a function.
+TEST_P(ClipDistanceAPPLETest, ThreeClipDistancesRedeclaredAndPassedToFunction)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_APPLE_clip_distance"));
+
+    constexpr char kVS[] = R"(
+#extension GL_APPLE_clip_distance : require
+
+varying highp float gl_ClipDistance[3];
+
+void computeClipDistances(out float distance[3], in vec4 position, in vec4 plane[3])
+{
+    distance[0] = dot(position, plane[0]);
+    distance[1] = dot(position, plane[1]);
+    distance[2] = dot(position, plane[2]);
+}
+
+uniform vec4 u_plane[3];
+
+attribute vec2 a_position;
+
+void main()
+{
+    gl_Position = vec4(a_position, 0.0, 1.0);
+
+    computeClipDistances(gl_ClipDistance, gl_Position, u_plane);
+})";
+
+    threeClipDistancesRedeclared(kVS, essl1_shaders::fs::Red());
+}
+
+// Redeclare gl_ClipDistance in shader with explicit size, also pass it to a function.
+TEST_P(ClipDistanceTestES3, ThreeClipDistancesRedeclaredAndPassedToFunction)
+{
+    const bool hasExt   = IsGLExtensionEnabled("GL_EXT_clip_cull_distance");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_clip_cull_distance");
+    ANGLE_SKIP_TEST_IF(!hasExt && !hasAngle);
+
+    std::stringstream vs;
+    vs << R"(#version 300 es
+#extension )"
+       << (hasExt ? "GL_EXT_clip_cull_distance" : "GL_ANGLE_clip_cull_distance") << R"( : require
+
+varying highp float gl_ClipDistance[3];
+
+void computeClipDistances(out float distance[3], in vec4 position, in vec4 plane[3])
+{
+    distance[0] = dot(position, plane[0]);
+    distance[1] = dot(position, plane[1]);
+    distance[2] = dot(position, plane[2]);
+}
+
+uniform vec4 u_plane[3];
+
+in vec2 a_position;
+
+void main()
+{
+    gl_Position = vec4(a_position, 0.0, 1.0);
+
+    computeClipDistances(gl_ClipDistance, gl_Position, u_plane);
+})";
+
+    threeClipDistancesRedeclared(vs.str().c_str(), essl3_shaders::fs::Red());
+}
+
 using ClipCullDistanceTestParams = std::tuple<angle::PlatformParameters, bool>;
 
 std::string PrintToStringParamName(const ::testing::TestParamInfo<ClipCullDistanceTestParams> &info)
@@ -2918,6 +3001,9 @@
 
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(ClipDistanceAPPLETest);
 
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(ClipDistanceTestES3);
+ANGLE_INSTANTIATE_TEST_ES3(ClipDistanceTestES3);
+
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(ClipCullDistanceTest);
 ANGLE_INSTANTIATE_TEST_COMBINE_1(ClipCullDistanceTest,
                                  PrintToStringParamName,
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index dc5e920..c7197e5 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -7332,6 +7332,276 @@
     }
 }
 
+// Shader passes gl_ClipDistance to function without explicitly sizing it
+TEST_P(GLSLValidationClipDistanceTest_ES3, UnsizedClipDistancePassedToFunction)
+{
+    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 f(float d[8]) {}
+void main()
+{
+    gl_Position = aPosition;
+    gl_ClipDistance[0] = 1.0;
+    f(gl_ClipDistance);
+}
+)";
+    constexpr char kExpect[] =
+        "'gl_ClipDistance' : Cannot pass to function unless it is explicitly sized";
+
+    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 passes gl_CullDistance to function without explicitly sizing it
+TEST_P(GLSLValidationClipDistanceTest_ES3, UnsizedCullDistancePassedToFunction)
+{
+    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 f(float d[8]) {}
+void main()
+{
+    gl_Position = aPosition;
+    gl_CullDistance[0] = 1.0;
+    f(gl_CullDistance);
+}
+)";
+    constexpr char kExpect[] =
+        "'gl_CullDistance' : Cannot pass to function unless it is explicitly sized";
+
+    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)
+    {
... (truncated)
Loading diff…

Original Bug Report

reported by vm...@google.com

Type mismatch in ANGLE SPIR-V generation via whole-array gl_ClipDistance references

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: ANGLE fails to re-promote parent AST node types after shrinking the built-in gl_ClipDistance or gl_CullDistance arrays, creating an AST type desynchronization. In production builds of Chrome where AST and SPIR-V validations are disabled, this results in malformed SPIR-V instructions being passed to the underlying platform Vulkan driver. Depending on the Vulkan compiler’s robustness, this could potentially lead to out-of-bounds compiler memory accesses within the GPU process.

Affected files:

  • third_party/angle/src/compiler/translator/SizeClipCullDistance.cpp
  • third_party/angle/src/compiler/translator/ParseContext.cpp
  • third_party/angle/src/compiler/translator/spirv/OutputSPIRV.cpp

Estimated timestamp from git blame: 2025-10-01

Description

A potential vulnerability has been identified in ANGLE’s handling of the built-in gl_ClipDistance and gl_CullDistance arrays. The compiler pre-declares gl_ClipDistance with the maximum supported size (typically float[8]). During parsing, if the array is only indexed up to a smaller boundary (e.g., gl_ClipDistance[0] = 1.0;), the SizeClipCullDistance compiler pass resizes the array variable to the actual used size (e.g., float[3]).

While the pass replaces the variable associated with the underlying TIntermSymbol nodes, the parent nodes in the AST (such as TIntermBinary representing variable initializations or TIntermAggregate representing function calls) retain their original cached types (float[8]) determined during parsing. This leads to a type desynchronization within the AST.

In release builds of Chrome, where AST and SPIR-V validations are compiled out, this malformed AST is translated directly into SPIR-V. The mismatch manifests as an invalid OpStore instruction where a value of the resized type (float[3]) is stored into a pointer of the unresized type (float[8]):

OpStore %ptr_float_8 %val_float_3

This violates Section 3.42.8 of the SPIR-V Specification, which mandates that the type of the Object operand must match the type pointed to by the Pointer operand. This malformed SPIR-V is then passed directly to the platform’s Vulkan driver via vkCreateShaderModule in the GPU process.

Potential Impact

Passing structurally malformed SPIR-V to a Vulkan driver compiler can result in undefined behavior, including out-of-bounds memory writes or memory corruption during shader compilation. On platforms like Android, where the GPU process is currently unsandboxed, memory corruption in the Vulkan compiler could potentially facilitate a direct sandbox escape.

Potential Steps to Reproduce

Note: The following steps are theoretical and have not been executed on a live environment as this analysis was conducted via static code review.

  1. Initialize a WebGL2 context and request the GL_ANGLE_clip_cull_distance or GL_EXT_clip_cull_distance extension on a platform using ANGLE’s Vulkan backend.
  2. Query the maximum allowed clip distances (N, typically 8).
  3. Compile and attempt to link a vertex shader that references the whole gl_ClipDistance array (either via assignment to another array of size N, or by passing it to a function expecting an array of size N), while only writing to a single index:
    #version 300 es
    #extension GL_ANGLE_clip_cull_distance : require
    void f(highp float a[8]) { gl_Position = vec4(a[7]); }
    void main() {
        gl_ClipDistance[0] = 1.0;
        f(gl_ClipDistance);
    }
    
  4. Trigger program linkage to force the SPIR-V binary generation and subsequent submission to vkCreateShaderModule.

Suggested Fix

When SizeClipCullDistance modifies the array bounds of the built-in variables, the AST must be traversed to update and re-promote the types of any parent nodes that reference the resized symbol. Alternatively, the compiler should ensure that any parent TIntermExpression caching type definitions (such as TIntermBinary and TIntermAggregate nodes) are forced to re-derive their types when a child variable is replaced, preventing type desynchronization prior to SPIR-V code generation.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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