Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in ANGLE
DescriptionInsufficient validation of untrusted input in ANGLE
ComponentANGLE
Bug ClassLogic Error
Tracker506550494
Fix commitbccf5994d766 (angle/angle) +100/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Files Changed

  • src/compiler/translator/ParseContext.cpp
  • src/tests/gl_tests/GLSLValidationTest.cpp
From bccf5994d7668b342cd7917fe3d31053fcd2e213 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
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 <syoussefi@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---

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.
Loading diff…

Regression Test / PoC

shipped with the fix
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.
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cpp
  • third_party/angle/src/compiler/translator/QualifierTypes.cpp
  • third_party/angle/src/compiler/translator/ParseContext.cpp
  • third_party/angle/src/compiler/translator/util.cpp
  • third_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:

  1. Qualifier Merging (QualifierTypes.cpp): The parser initially assigns a base scope qualifier of EvqGlobal. During qualifier merging in JoinVariableStorageQualifier (lines 353-355), the EvqGlobal state is directly overwritten by the EvqCentroid qualifier.
  2. Validation Bypass (ParseContext.cpp): The variable is declared with the EvqCentroid qualifier, which is marked as an “Incomplete” qualifier in BaseTypes.h because it lacks a direction (In/Out). Semantic checks like checkVaryingLocations rely on helpers like IsVaryingIn() (in util.cpp), which return false for incomplete qualifiers. Thus, the invalid variable silently bypasses structural validation and is added to the AST.
  3. SPIR-V Generation (OutputSPIRV.cpp): During SPIR-V generation, GetStorageClass attempts to resolve a SPIR-V storage class for EvqCentroid. Lacking a specific case, it falls through to the default: 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_ASSERTS is undefined), the assertion is a no-op, and the function incorrectly returns spv::StorageClassUniform.
  4. Malformed SPIR-V: The variable is emitted with the Uniform storage class but, because its basic type is EbtFloat and not EbtInterfaceBlock, it is not decorated with the mandatory Block decoration, directly violating Vulkan SPIR-V validation rule VUID-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)

  1. Use a browser using ANGLE’s Vulkan backend.
  2. Compile a vertex shader with a bare interpolation qualifier:
    #version 300 es
    precision mediump float;
    centroid float bogus;
    void main() { gl_Position = vec4(bogus); }
    
  3. 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.

View on issue tracker