CVE-2026-9969
Overview
Files Changed
src/compiler/translator/ParseContext.cppsrc/tests/gl_tests/GLSLValidationTest.cpp
Patch
From bccf5994d7668b342cd7917fe3d31053fcd2e213 Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> Date: Tue, 05 May 2026 16:37:25 -0400 Subject: [PATCH] Translator: Verify that qualifiers are paired with in/out `centroid vec4 global` is an invalid declaration, but was not validated. Bug: chromium:506550494 Change-Id: I5ed61c6642ac0dfa4c60c636904f33f77616adad Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7818778 Commit-Queue: Shahbaz Youssefi <[email protected]> Reviewed-by: Geoff Lang <[email protected]> --- diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp index b3c959f..ef8d290 100644 --- a/src/compiler/translator/ParseContext.cpp +++ b/src/compiler/translator/ParseContext.cpp @@ -4367,11 +4367,28 @@ checkEarlyFragmentTestsIsNotSpecified(typeSpecifier.getLine(), returnType.layoutQualifier.earlyFragmentTests); - if (returnType.qualifier == EvqSampleIn || returnType.qualifier == EvqSampleOut || - returnType.qualifier == EvqNoPerspectiveSampleIn || - returnType.qualifier == EvqNoPerspectiveSampleOut) + switch (returnType.qualifier) { - mSampleQualifierSpecified = true; + case EvqSmooth: + case EvqFlat: + case EvqNoPerspective: + case EvqCentroid: + case EvqSample: + case EvqNoPerspectiveCentroid: + case EvqNoPerspectiveSample: + // These qualifiers must be merged with |in| or |out| qualifiers. + error(typeSpecifier.getLine(), "qualifier can only be used with in and out variables", + getQualifierString(returnType.qualifier)); + break; + + case EvqSampleIn: + case EvqSampleOut: + case EvqNoPerspectiveSampleIn: + case EvqNoPerspectiveSampleOut: + mSampleQualifierSpecified = true; + break; + default: + break; } if (mShaderVersion < 300) @@ -4410,7 +4427,7 @@ } if (returnType.qualifier == EvqComputeIn) { - error(typeSpecifier.getLine(), "'in' can be only used to specify the local group size", + error(typeSpecifier.getLine(), "'in' can only be used to specify the local group size", "in"); } } diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp index 2568a07..fdcd682 100644 --- a/src/tests/gl_tests/GLSLValidationTest.cpp +++ b/src/tests/gl_tests/GLSLValidationTest.cpp @@ -201,6 +201,83 @@ "'in' : function must have the same parameter qualifiers in all of its declarations"); } +// Auxiliary/interpolation qualifiers must always be paired with storage qualifiers. +TEST_P(GLSLValidationTest_ES3, NoAuxOrInterpQualifierWithoutStorageQualifier) +{ + { + constexpr char kVS[] = R"(#version 300 es +precision mediump float; +centroid float invalid; +void main() { gl_Position = vec4(invalid); } + )"; + + validateError(GL_VERTEX_SHADER, kVS, + "'centroid' : qualifier can only be used with in and out variables"); + } + + { + constexpr char kVS[] = R"(#version 300 es +precision mediump float; +flat int invalid; +void main() { gl_Position = vec4(invalid); } + )"; + + validateError(GL_VERTEX_SHADER, kVS, + "'flat' : qualifier can only be used with in and out variables"); + } + + { + constexpr char kVS[] = R"(#version 300 es +precision mediump float; +smooth float invalid; +void main() { gl_Position = vec4(invalid); } + )"; + + validateError(GL_VERTEX_SHADER, kVS, + "'smooth' : qualifier can only be used with in and out variables"); + } + + if (IsGLExtensionEnabled("GL_NV_shader_noperspective_interpolation")) + { + constexpr char kVS[] = R"(#version 300 es +#extension GL_NV_shader_noperspective_interpolation : require +precision mediump float; +noperspective float invalid; +void main() { gl_Position = vec4(invalid); } + )"; + + validateError(GL_VERTEX_SHADER, kVS, + "'noperspective' : qualifier can only be used with in and out variables"); + } + + if (IsGLExtensionEnabled("GL_NV_shader_noperspective_interpolation")) + { + constexpr char kVS[] = R"(#version 300 es +#extension GL_NV_shader_noperspective_interpolation : require +precision mediump float; +noperspective centroid float invalid; +void main() { gl_Position = vec4(invalid); } + )"; + + validateError( + GL_VERTEX_SHADER, kVS, + "'noperspective centroid' : qualifier can only be used with in and out variables"); + } + + if (IsGLExtensionEnabled("GL_OES_shader_multisample_interpolation")) + { + constexpr char kFS[] = R"(#version 300 es +#extension GL_OES_shader_multisample_interpolation : require +precision mediump float; +sample float invalid; +out vec4 color; +void main() { color = vec4(invalid); } + )"; + + validateError(GL_FRAGMENT_SHADER, kFS, + "'sample' : qualifier can only be used with in and out variables"); + } +} // Assignment and equality are undefined for structures containing arrays (ESSL 1.00 section 5.7) TEST_P(GLSLValidationTest, CompareStructsContainingArrays) { @@ -2101,7 +2178,7 @@ })"; validateError(GL_COMPUTE_SHADER, kCS, - "'in' : 'in' can be only used to specify the local group size"); + "'in' : 'in' can only be used to specify the local group size"); } // Invalid use of the in storage qualifier. Can be only used to describe the local block size.
Regression Test / PoC
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 2568a07..fdcd682 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -201,6 +201,83 @@
"'in' : function must have the same parameter qualifiers in all of its declarations");
}
+// Auxiliary/interpolation qualifiers must always be paired with storage qualifiers.
+TEST_P(GLSLValidationTest_ES3, NoAuxOrInterpQualifierWithoutStorageQualifier)
+{
+ {
+ constexpr char kVS[] = R"(#version 300 es
+precision mediump float;
+centroid float invalid;
+void main() { gl_Position = vec4(invalid); }
+ )";
+
+ validateError(GL_VERTEX_SHADER, kVS,
+ "'centroid' : qualifier can only be used with in and out variables");
+ }
+
+ {
+ constexpr char kVS[] = R"(#version 300 es
+precision mediump float;
+flat int invalid;
+void main() { gl_Position = vec4(invalid); }
+ )";
+
+ validateError(GL_VERTEX_SHADER, kVS,
+ "'flat' : qualifier can only be used with in and out variables");
+ }
+
+ {
+ constexpr char kVS[] = R"(#version 300 es
+precision mediump float;
+smooth float invalid;
+void main() { gl_Position = vec4(invalid); }
+ )";
+
+ validateError(GL_VERTEX_SHADER, kVS,
+ "'smooth' : qualifier can only be used with in and out variables");
+ }
+
+ if (IsGLExtensionEnabled("GL_NV_shader_noperspective_interpolation"))
+ {
+ constexpr char kVS[] = R"(#version 300 es
+#extension GL_NV_shader_noperspective_interpolation : require
+precision mediump float;
+noperspective float invalid;
+void main() { gl_Position = vec4(invalid); }
+ )";
+
+ validateError(GL_VERTEX_SHADER, kVS,
+ "'noperspective' : qualifier can only be used with in and out variables");
+ }
+
+ if (IsGLExtensionEnabled("GL_NV_shader_noperspective_interpolation"))
+ {
+ constexpr char kVS[] = R"(#version 300 es
+#extension GL_NV_shader_noperspective_interpolation : require
+precision mediump float;
+noperspective centroid float invalid;
+void main() { gl_Position = vec4(invalid); }
+ )";
+
+ validateError(
+ GL_VERTEX_SHADER, kVS,
+ "'noperspective centroid' : qualifier can only be used with in and out variables");
+ }
+
+ if (IsGLExtensionEnabled("GL_OES_shader_multisample_interpolation"))
+ {
+ constexpr char kFS[] = R"(#version 300 es
+#extension GL_OES_shader_multisample_interpolation : require
+precision mediump float;
+sample float invalid;
+out vec4 color;
+void main() { color = vec4(invalid); }
+ )";
+
+ validateError(GL_FRAGMENT_SHADER, kFS,
+ "'sample' : qualifier can only be used with in and out variables");
+ }
+}
// Assignment and equality are undefined for structures containing arrays (ESSL 1.00 section 5.7)
TEST_P(GLSLValidationTest, CompareStructsContainingArrays)
{
@@ -2101,7 +2178,7 @@
})";
validateError(GL_COMPUTE_SHADER, kCS,
- "'in' : 'in' can be only used to specify the local group size");
+ "'in' : 'in' can only be used to specify the local group size");
}
// Invalid use of the in storage qualifier. Can be only used to describe the local block size.
Original Bug Report
Malformed SPIR-V generation via bare interpolation qualifiers in ANGLE
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: ANGLE’s GLSL parser fails to reject global variables declared with only an interpolation qualifier (e.g., centroid). This bypasses semantic validation and generates malformed SPIR-V in release builds, which is passed to the host Vulkan driver and can potentially cause memory corruption in the GPU process.
Affected files:
third_party/angle/src/compiler/translator/spirv/OutputSPIRV.cppthird_party/angle/src/compiler/translator/QualifierTypes.cppthird_party/angle/src/compiler/translator/ParseContext.cppthird_party/angle/src/compiler/translator/util.cppthird_party/angle/src/libANGLE/renderer/vulkan/spv_utils.cpp
Estimated timestamp from git blame: 2024-05-28
Description
A potential vulnerability exists in ANGLE’s GLSL frontend where it fails to properly validate and reject global variable declarations that use a bare interpolation or auxiliary qualifier (such as centroid, flat, or smooth) without a corresponding storage qualifier like in or out.
Root Cause Analysis
When parsing a declaration like centroid float bogus; at the global scope:
- Qualifier Merging (
QualifierTypes.cpp): The parser initially assigns a base scope qualifier ofEvqGlobal. During qualifier merging inJoinVariableStorageQualifier(lines 353-355), theEvqGlobalstate is directly overwritten by theEvqCentroidqualifier. - Validation Bypass (
ParseContext.cpp): The variable is declared with theEvqCentroidqualifier, which is marked as an “Incomplete” qualifier inBaseTypes.hbecause it lacks a direction (In/Out). Semantic checks likecheckVaryingLocationsrely on helpers likeIsVaryingIn()(inutil.cpp), which returnfalsefor incomplete qualifiers. Thus, the invalid variable silently bypasses structural validation and is added to the AST. - SPIR-V Generation (
OutputSPIRV.cpp): During SPIR-V generation,GetStorageClassattempts to resolve a SPIR-V storage class forEvqCentroid. Lacking a specificcase, it falls through to thedefault:branch. This branch contains an assertion (ASSERT(type.getInterfaceBlock() != nullptr || qualifier == EvqUniform);) that catches the error in debug builds. However, in official release builds (ANGLE_ENABLE_ASSERTSis undefined), the assertion is a no-op, and the function incorrectly returnsspv::StorageClassUniform. - Malformed SPIR-V: The variable is emitted with the
Uniformstorage class but, because its basic type isEbtFloatand notEbtInterfaceBlock, it is not decorated with the mandatoryBlockdecoration, directly violating Vulkan SPIR-V validation ruleVUID-StandaloneSpirv-Uniform-06807.
Impact
This vulnerability allows a malicious webpage (via WebGL or WebGPU) to provide structurally invalid SPIR-V to the host Vulkan driver via vkCreateShaderModule. Depending on the robustness of the vendor-specific Vulkan driver’s shader compiler, processing this malformed input can potentially lead to out-of-bounds memory access or memory corruption. Because the Vulkan driver executes within Chrome’s GPU process—which is unsandboxed on platforms like Android—this could lead to full remote code execution (RCE) and system compromise.
Potential Reproduction Steps
(Note: These are suggested steps to trigger the compilation bug; a full driver exploit requires a vulnerable vendor driver)
- Use a browser using ANGLE’s Vulkan backend.
- Compile a vertex shader with a bare interpolation qualifier:
#version 300 es precision mediump float; centroid float bogus; void main() { gl_Position = vec4(bogus); } - Link the program. In a release build, the malformed SPIR-V is generated and passed to the Vulkan driver.
Suggested Fix
Update TParseContext::declareVariable or GetVariableTypeQualifierFromSortedSequence to explicitly reject global variable declarations that result in incomplete qualifiers (like EvqCentroid, EvqFlat, EvqSmooth) without a corresponding In or Out storage qualifier.
Evaluated with Chrome root at commit: a1e33f5848218e21d4a16ae2c1bc94e815c30c7f
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.