Medium watch CVSS 8.8 chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in ANGLE
DescriptionType confusion in ANGLE
ComponentANGLE
Sub-componentTranslator
Bug ClassType Confusion
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517245017
Fix commitab5bee732a58 (angle/angle) +446/-32
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-07-22 (public git)
Disclosed2026-08-25
Patch gap34 days — fix public in source before the advisory

Background

`gl_ClipDistance` / `gl_CullDistance`
built-in shader output arrays (from EXT_clip_cull_distance / APPLE_clip_distance) whose size may be left implicit and derived from the highest constant index used, or fixed by an explicit redeclaration.
Implicit sizing
per EXT_clip_cull_distance, only indexing these built-ins with a constant may implicitly establish their array size; whole-array use (passing the whole array to a function, or whole-array assignment) does not carry a size.
`TParseContext`
ANGLE’s GLSL translator parse context that performs semantic validation while building the intermediate representation (IR).
`mClipDistanceInfo.size` / `mCullDistanceInfo.size`
the recorded (possibly still 0, i.e. not-yet-sized) array sizes for these built-ins during translation.

Root Cause Analysis

The ANGLE shader translator allowed gl_ClipDistance/gl_CullDistance to be used as a whole array — passed to a user-defined function, or used on either side of a whole-array assignment — even when the built-in had not been explicitly sized (its recorded .size was still 0). Such a whole-array use has no associated size, yet downstream translation (function-argument type matching / IR construction and the eventual backend) assumes a concrete array type, producing a mismatch between the size the built-in is treated as having and its actual (unknown/zero) size — a type/size confusion. The regression GLSLValidationTest cases show the exact trigger: a shader that writes gl_ClipDistance[0] (which would only implicitly size it to 1) and then passes the whole gl_ClipDistance to a function void f(float d[8]), mismatching an 8-element parameter.

The fix adds checkClipCullDistanceWholeArrayUse, which emits an error() (and returns) when the qualifier is EvqClipDistance/EvqCullDistance and the corresponding info .size == 0; it is invoked from the new functionCallClipCullDistanceCheck for each function-call argument (wired into the user-defined function-call path alongside functionCallRValueLValueErrorCheck) and for both operands of whole-array assignment (in the binary/assignment validation hunk near line 9077). This rejects the unsized whole-array use at compile time instead of letting an ill-sized array type flow into codegen.

Key insight
The single mistake was permitting whole-array use (function argument or whole-array assignment) of an implicitly-sized gl_ClipDistance/gl_CullDistance whose size was still 0, letting an unsized built-in be treated as a concretely-sized array; the fix bans such use via checkClipCullDistanceWholeArrayUse whenever the recorded size is 0.

Attack Path

  1. Craft a malicious shader A web page/app submits a GLSL shader (via WebGL/WebGL2 with EXT/ANGLE/APPLE clip-cull-distance) that uses gl_ClipDistance (or gl_CullDistance) without an explicit redeclaration so its size stays implicit/0.
  2. Use the whole array without sizing The shader passes the entire built-in array to a function whose parameter has a different fixed size (e.g. void f(float d[8])), or assigns the whole array, so no valid size is established for the built-in.
  3. Translator accepts mismatched sizing Pre-patch, the translator does not reject this, and the built-in’s whole-array use is carried into the IR/backend with an assumed array size that does not match its true (unsized) type — a type confusion.
  4. Type confusion in the pipeline The size/type mismatch propagates into shader translation/codegen, producing inconsistent handling of the clip/cull array (inferred; the diff demonstrates the missing validation and the mismatched-size test, not a concrete memory-corruption sink).

Impact Assessment

A remote attacker who can supply a GLSL shader (e.g. through WebGL in a renderer, reaching ANGLE) can trigger a type confusion over gl_ClipDistance/gl_CullDistance by using the whole, unsized array, in the GPU/shader-translation context. The advisory classifies this as a type confusion in ANGLE; no exploited-in-the-wild status is stated, and any escalation from the type confusion to memory corruption or code execution is (inferred). The precondition is a driver/config where the relevant clip/cull-distance extension is available.

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/gl_tests/GLSLValidationTest.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/ClipDistanceTest.cpp

Audit Directions

  • Implicitly-sized built-in arrays used whole
    Audit other built-ins whose size is derived from constant indexing (clip/cull distance, and similar) for paths that permit whole-array use (function args, assignment, returns, initializers) before the size is fixed.
  • Missing size validation before type propagation
    Search the translator for places that consume an array type/qualifier and assume a concrete size without checking a still-0 recorded size, where the mismatched size can flow into IR/codegen.
  • Function-argument type/size matching for special variables
    Review argument-matching logic (e.g. near functionCallRValueLValueErrorCheck) to ensure special GLSL variables with deferred sizing are validated against parameter array sizes rather than silently accepted.
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)
 {
-    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);
+}
... (file diff truncated)
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