Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in ANGLE
DescriptionInappropriate implementation in ANGLE
ComponentANGLE
Bug ClassLogic Error
Tracker513921488
Fix commitf0d2ebc2635c (angle/angle) +96/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • src/compiler/translator/spirv/TranslatorSPIRV.cpp
  • src/tests/gl_tests/GLSLTest.cpp
From f0d2ebc2635c41149ef9f686b0db095339dc48e1 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Mon, 01 Jun 2026 15:24:45 -0400
Subject: [PATCH] SPIR-V: Fix combination of gl_SampleID/PointCoord/FragCoord

The loop that checked these `break`ed early instead of `continue`ing, so
these built-ins didn't really work together.

Bug: chromium:513921488
Change-Id: I14de6d5e3355a0630134bde1de78c69559ece51c
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7891093
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Yuxin Hu <yuxinhu@google.com>
---

diff --git a/src/compiler/translator/spirv/TranslatorSPIRV.cpp b/src/compiler/translator/spirv/TranslatorSPIRV.cpp
index 76ad9db..dbaf91e 100644
--- a/src/compiler/translator/spirv/TranslatorSPIRV.cpp
+++ b/src/compiler/translator/spirv/TranslatorSPIRV.cpp
@@ -60,6 +60,7 @@
 namespace
 {
 constexpr ImmutableString kFlippedPointCoordName    = ImmutableString("flippedPointCoord");
+constexpr ImmutableString kFlippedSamplePositionName = ImmutableString("flippedSamplePosition");
 constexpr ImmutableString kFlippedFragCoordName     = ImmutableString("flippedFragCoord");
 constexpr ImmutableString kDefaultUniformsBlockName = ImmutableString("defaultUniforms");
 
@@ -898,13 +899,13 @@
                         static_cast<const TVariable *>(getSymbolTable().findBuiltIn(
                             ImmutableString("gl_SampleID"), getShaderVersion()));
                     assignSpirvId(sampleID->uniqueId(), vk::spirv::kIdSampleID);
-                    break;
+                    continue;
                 }
 
                 if (inputVarying.name == "gl_PointCoord")
                 {
                     usesPointCoord = true;
-                    break;
+                    continue;
                 }
 
                 if (inputVarying.name == "gl_FragCoord")
@@ -914,7 +915,7 @@
                         static_cast<const TVariable *>(getSymbolTable().findBuiltIn(
                             ImmutableString("gl_FragCoord"), getShaderVersion()));
                     assignSpirvId(fragCoord->uniqueId(), vk::spirv::kIdFragCoord);
-                    break;
+                    continue;
                 }
             }
 
@@ -977,7 +978,7 @@
                         ImmutableString("gl_SamplePosition"), getShaderVersion()));
                 if (!RotateAndFlipBuiltinVariable(this, root, GetMainSequence(root), swapXY, flipXY,
                                                   &getSymbolTable(), samplePositionBuiltin,
-                                                  kFlippedPointCoordName, pivot))
+                                                  kFlippedSamplePositionName, pivot))
                 {
                     return false;
                 }
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 9723062..5060578 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -11231,6 +11231,32 @@
     EXPECT_EQ(userFBOData, backbufferData);
 }
 
+// Test gl_PointCoord used before gl_FragCoord when dithering might be emulated.
+TEST_P(GLSLTest_ES3, PointCoordBeforeFragCoord)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+out vec4 colorOut;
+void main()
+{
+    vec2 p = gl_PointCoord;
+    colorOut = vec4(abs(p) + vec2(1, 1), gl_FragCoord.x * 0.000001, 1.0);
+})";
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+    GLTexture tex565;
+    glBindTexture(GL_TEXTURE_2D, tex565);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGB565, 10, 20);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex565, 0);
+
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor::yellow, 1);
+    ASSERT_GL_NO_ERROR();
+}
+
 bool SubrectEquals(const std::vector<GLColor> &bigArray,
                    const std::vector<GLColor> &smallArray,
                    int bigSize,
@@ -19447,7 +19473,63 @@
     color = vec4(gl_SamplePosition.yx, float(gl_SampleID), float(gl_MaxSamples + gl_NumSamples));
 })";
 
-    ANGLE_GL_PROGRAM(testProgram, essl3_shaders::vs::Simple(), kFS);
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(127, 127, 0, 255), 1);
+}
+
+// Test that gl_SampleID can be used before gl_SampleMaskIn.
+TEST_P(GLSLTest_ES3, SampleIDBeforeSampleMaskIn)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_OES_sample_variables"));
+
+    const char kFS[] = R"(#version 300 es
+#extension GL_OES_sample_variables : require
+precision highp float;
+out vec4 color;
+void main()
+{
+    int sampleId = gl_SampleID;
+    gl_SampleMask[0] = gl_SampleMaskIn[0] & 0x55555555;
+    color = vec4(gl_SamplePosition.yx, float(sampleId), float(gl_MaxSamples + gl_NumSamples));
+})";
+
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(127, 127, 0, 255), 1);
+}
+
+// Test that gl_FragCoord can be used after gl_SampleID.
+TEST_P(GLSLTest_ES3, SampleIDBeforeFragCoord)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_OES_sample_variables"));
+
+    const char kFS[] = R"(#version 300 es
+#extension GL_OES_sample_variables : require
+precision highp float;
+uniform vec2 dim;
+out vec4 color;
+void main()
+{
+    vec2 useSampleIDFirst = vec2(gl_SampleID);
+    vec2 coords = gl_FragCoord.xy / dim + useSampleIDFirst;
+    color = vec4(coords.x > 0.5, coords.y > 0.5, 0, 1);
+})";
+
+    const int w  = getWindowWidth();
+    const int h  = getWindowHeight();
+    const int w2 = w / 2;
+    const int h2 = h / 2;
+
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    glUseProgram(program);
+    glUniform2f(glGetUniformLocation(program, "dim"), w, h);
+    drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+
+    EXPECT_PIXEL_RECT_EQ(0, 0, w2 - 1, h2 - 1, GLColor::black);
+    EXPECT_PIXEL_RECT_EQ(w2 + 1, 0, w - (w2 + 1), h2 - 1, GLColor::red);
+    EXPECT_PIXEL_RECT_EQ(0, h2 + 1, w2 - 1, h - (h2 + 1), GLColor::green);
+    EXPECT_PIXEL_RECT_EQ(w2 + 1, h2 + 1, w - (w2 + 1), h - (h2 + 1), GLColor::yellow);
 }
 
 // Test that shader caching maintains uniforms across compute shader compilations.
@@ -24090,7 +24172,10 @@
     ES3_VULKAN().enable(Feature::AvoidOpSelectWithMismatchingRelaxedPrecision),
     ES3_VULKAN().enable(Feature::ForceInitShaderVariables),
     ES3_VULKAN().disable(Feature::SupportsSPIRV14),
-    ES2_VULKAN().enable(Feature::VaryingsRequireMatchingPrecisionInSpirv));
+    ES2_VULKAN().enable(Feature::VaryingsRequireMatchingPrecisionInSpirv),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation90),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation180),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation270));
 
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(GLSLTestNoValidation);
 
@@ -24102,7 +24187,10 @@
     ES3_OPENGLES().enable(Feature::ScalarizeVecAndMatConstructorArgs),
     ES3_VULKAN().enable(Feature::AvoidOpSelectWithMismatchingRelaxedPrecision),
     ES3_VULKAN().enable(Feature::ForceInitShaderVariables),
-    ES3_VULKAN().disable(Feature::SupportsSPIRV14));
+    ES3_VULKAN().disable(Feature::SupportsSPIRV14),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation90),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation180),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation270));
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(GLSLPrecisionTest_ES3);
 ANGLE_INSTANTIATE_TEST_ES3_AND(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 9723062..5060578 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -11231,6 +11231,32 @@
     EXPECT_EQ(userFBOData, backbufferData);
 }
 
+// Test gl_PointCoord used before gl_FragCoord when dithering might be emulated.
+TEST_P(GLSLTest_ES3, PointCoordBeforeFragCoord)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+out vec4 colorOut;
+void main()
+{
+    vec2 p = gl_PointCoord;
+    colorOut = vec4(abs(p) + vec2(1, 1), gl_FragCoord.x * 0.000001, 1.0);
+})";
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+    GLTexture tex565;
+    glBindTexture(GL_TEXTURE_2D, tex565);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGB565, 10, 20);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex565, 0);
+
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor::yellow, 1);
+    ASSERT_GL_NO_ERROR();
+}
+
 bool SubrectEquals(const std::vector<GLColor> &bigArray,
                    const std::vector<GLColor> &smallArray,
                    int bigSize,
@@ -19447,7 +19473,63 @@
     color = vec4(gl_SamplePosition.yx, float(gl_SampleID), float(gl_MaxSamples + gl_NumSamples));
 })";
 
-    ANGLE_GL_PROGRAM(testProgram, essl3_shaders::vs::Simple(), kFS);
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(127, 127, 0, 255), 1);
+}
+
+// Test that gl_SampleID can be used before gl_SampleMaskIn.
+TEST_P(GLSLTest_ES3, SampleIDBeforeSampleMaskIn)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_OES_sample_variables"));
+
+    const char kFS[] = R"(#version 300 es
+#extension GL_OES_sample_variables : require
+precision highp float;
+out vec4 color;
+void main()
+{
+    int sampleId = gl_SampleID;
+    gl_SampleMask[0] = gl_SampleMaskIn[0] & 0x55555555;
+    color = vec4(gl_SamplePosition.yx, float(sampleId), float(gl_MaxSamples + gl_NumSamples));
+})";
+
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(127, 127, 0, 255), 1);
+}
+
+// Test that gl_FragCoord can be used after gl_SampleID.
+TEST_P(GLSLTest_ES3, SampleIDBeforeFragCoord)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_OES_sample_variables"));
+
+    const char kFS[] = R"(#version 300 es
+#extension GL_OES_sample_variables : require
+precision highp float;
+uniform vec2 dim;
+out vec4 color;
+void main()
+{
+    vec2 useSampleIDFirst = vec2(gl_SampleID);
+    vec2 coords = gl_FragCoord.xy / dim + useSampleIDFirst;
+    color = vec4(coords.x > 0.5, coords.y > 0.5, 0, 1);
+})";
+
+    const int w  = getWindowWidth();
+    const int h  = getWindowHeight();
+    const int w2 = w / 2;
+    const int h2 = h / 2;
+
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    glUseProgram(program);
+    glUniform2f(glGetUniformLocation(program, "dim"), w, h);
+    drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+
+    EXPECT_PIXEL_RECT_EQ(0, 0, w2 - 1, h2 - 1, GLColor::black);
+    EXPECT_PIXEL_RECT_EQ(w2 + 1, 0, w - (w2 + 1), h2 - 1, GLColor::red);
+    EXPECT_PIXEL_RECT_EQ(0, h2 + 1, w2 - 1, h - (h2 + 1), GLColor::green);
+    EXPECT_PIXEL_RECT_EQ(w2 + 1, h2 + 1, w - (w2 + 1), h - (h2 + 1), GLColor::yellow);
 }
 
 // Test that shader caching maintains uniforms across compute shader compilations.
@@ -24090,7 +24172,10 @@
     ES3_VULKAN().enable(Feature::AvoidOpSelectWithMismatchingRelaxedPrecision),
     ES3_VULKAN().enable(Feature::ForceInitShaderVariables),
     ES3_VULKAN().disable(Feature::SupportsSPIRV14),
-    ES2_VULKAN().enable(Feature::VaryingsRequireMatchingPrecisionInSpirv));
+    ES2_VULKAN().enable(Feature::VaryingsRequireMatchingPrecisionInSpirv),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation90),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation180),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation270));
 
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(GLSLTestNoValidation);
 
@@ -24102,7 +24187,10 @@
     ES3_OPENGLES().enable(Feature::ScalarizeVecAndMatConstructorArgs),
     ES3_VULKAN().enable(Feature::AvoidOpSelectWithMismatchingRelaxedPrecision),
     ES3_VULKAN().enable(Feature::ForceInitShaderVariables),
-    ES3_VULKAN().disable(Feature::SupportsSPIRV14));
+    ES3_VULKAN().disable(Feature::SupportsSPIRV14),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation90),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation180),
+    ES3_VULKAN().enable(Feature::EmulatedPrerotation270));
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(GLSLPrecisionTest_ES3);
 ANGLE_INSTANTIATE_TEST_ES3_AND(
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential memory corruption in unsandboxed GPU process via duplicate SPIR-V built-in variables

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: A logic error in ANGLE’s SPIR-V translator causes it to prematurely terminate its scan of input variables, leading to incorrect SPIR-V metadata. On Android, this triggers the injection of duplicate built-in variables into the shader module, violating the Vulkan specification. Providing such malformed SPIR-V to production graphics drivers in the unsandboxed GPU process could lead to memory corruption and remote code execution.

Affected files:

  • third_party/angle/src/compiler/translator/spirv/TranslatorSPIRV.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/spv_utils.cpp
  • third_party/angle/src/compiler/translator/spirv/BuildSPIRV.cpp
  • third_party/angle/src/compiler/translator/spirv/OutputSPIRV.cpp

Estimated timestamp from git blame: Unknown (Google3 checkout)

Description

A logic error exists in ANGLE’s TranslatorSPIRV::translateImpl within third_party/angle/src/compiler/translator/spirv/TranslatorSPIRV.cpp. When scanning the shader’s input varyings to assign reserved SPIR-V IDs and set metadata flags, the implementation uses break instead of continue for several built-in variables. This causes the scanner to stop processing the remaining variables in the list if it encounters certain built-ins early.

Specifically, the loop at lines 878-921 handles built-ins like gl_SampleID, gl_PointCoord, and gl_FragCoord. If gl_SampleID or gl_PointCoord appears in the mInputVaryings list before gl_FragCoord, the scanner executes a break and never reaches the code that handles gl_FragCoord (lines 912-920).

Impact on SPIR-V Generation

When the scanner is cut short:

  1. assignSpirvId is not called for gl_FragCoord, meaning it is not mapped to its reserved SPIR-V ID (kIdFragCoord).
  2. The usesFragCoord flag remains false, preventing the metadata flag HasFragCoord from being set in mMetadataFlags (line 995).
  3. During SPIR-V emission in BuildSPIRV.cpp, gl_FragCoord is assigned a generic ID instead of the reserved one.
  4. Consequently, the kOverviewHasFragCoordMask bit in the non-semantic overview marker is not set.

On Android, when the emulateDithering feature is active, ANGLE’s SpirvDitherEmulationTransformer (in third_party/angle/src/libANGLE/renderer/vulkan/spv_utils.cpp) inspects these flags. If hasFragCoord() returns false, the transformer proceeds to inject a second OpVariable decorated with BuiltIn FragCoord using the reserved ID. This results in a SPIR-V module with two distinct variables both decorated as the FragCoord built-in, violating Vulkan standalone SPIR-V requirements (VUID-StandaloneSpirv-OpEntryPoint-08721).

Security Impact

This malformed SPIR-V is passed directly to the production Vulkan driver’s vkCreateShaderModule call. In Chrome on Android, the GPU process is currently unsandboxed. Driver shader compilers are highly complex and typically assume SPIR-V validity; providing a module that violates fundamental uniqueness constraints for built-in variables can lead to undefined behavior, including memory corruption (such as out-of-bounds writes or use-after-frees) within the driver’s JIT compiler. This allows for a potential sandbox escape from a compromised renderer to the privileged GPU process.

Potential Reproduction Steps

Note: These are potential steps based on code analysis.

  1. From a web page, create a WebGL 2.0 context.
  2. Create a fragment shader that references gl_PointCoord before gl_FragCoord in its execution path (or simply by lexical order if the compiler preserves it). Example:
    #version 300 es
    precision mediump float;
    out vec4 color;
    void main() {
        float x = gl_PointCoord.x;
        color = vec4(gl_FragCoord.xyz, x);
    }
    
  3. Trigger a draw call using this shader on an Android device where dithering emulation is required (typically devices lacking hardware dithering support).
  4. The resulting invalid SPIR-V module will be submitted to the driver in the GPU process.

Suggested Fix

In third_party/angle/src/compiler/translator/spirv/TranslatorSPIRV.cpp, change the break statements to continue at lines 903, 909, and 919 within the mInputVaryings loop in TranslatorSPIRV::translateImpl.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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