CVE-2026-17677
Overview
Files Changed
src/compiler/translator/spirv/TranslatorSPIRV.cppsrc/tests/gl_tests/GLSLTest.cpp
Patch
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(
Regression Test / PoC
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(
Original Bug Report
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.cppthird_party/angle/src/libANGLE/renderer/vulkan/spv_utils.cppthird_party/angle/src/compiler/translator/spirv/BuildSPIRV.cppthird_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:
assignSpirvIdis not called forgl_FragCoord, meaning it is not mapped to its reserved SPIR-V ID (kIdFragCoord).- The
usesFragCoordflag remainsfalse, preventing the metadata flagHasFragCoordfrom being set inmMetadataFlags(line 995). - During SPIR-V emission in
BuildSPIRV.cpp,gl_FragCoordis assigned a generic ID instead of the reserved one. - Consequently, the
kOverviewHasFragCoordMaskbit 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.
- From a web page, create a WebGL 2.0 context.
- Create a fragment shader that references
gl_PointCoordbeforegl_FragCoordin 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); } - Trigger a draw call using this shader on an Android device where dithering emulation is required (typically devices lacking hardware dithering support).
- 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.