CVE-2026-78905
Overview
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.
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
- Craft a malicious shader
A web page/app submits a GLSL shader (via WebGL/WebGL2 with
EXT/ANGLE/APPLEclip-cull-distance) that usesgl_ClipDistance(orgl_CullDistance) without an explicit redeclaration so its size stays implicit/0. - 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. - 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.
- 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
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
| Function | Change | Notes |
|---|---|---|
ClipDistanceTestES3src/tests/gl_tests/ClipDistanceTest.cpp |
modified |
Files Changed
src/compiler/translator/ParseContext.cppsrc/compiler/translator/ParseContext.hsrc/tests/gl_tests/GLSLValidationTest.cppsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/ClipDistanceTest.cpp
Audit Directions
- Implicitly-sized built-in arrays used wholeAudit 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 propagationSearch the translator for places that consume an array type/qualifier and assume a concrete size without checking a still-
0recorded size, where the mismatched size can flow into IR/codegen. - Function-argument type/size matching for special variablesReview 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.
Patch
From ab5bee732a58f6c61b93b6eea830ef6296325d98 Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> 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 <[email protected]> Reviewed-by: Alexey Knyazev <[email protected]> Reviewed-by: Geoff Lang <[email protected]> --- 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)
Regression Test / PoC
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)
Original Bug Report
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.cppthird_party/angle/src/compiler/translator/ParseContext.cppthird_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.
- Initialize a WebGL2 context and request the
GL_ANGLE_clip_cull_distanceorGL_EXT_clip_cull_distanceextension on a platform using ANGLE’s Vulkan backend. - Query the maximum allowed clip distances (
N, typically 8). - Compile and attempt to link a vertex shader that references the whole
gl_ClipDistancearray (either via assignment to another array of sizeN, or by passing it to a function expecting an array of sizeN), 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); } - 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.