CVE-2026-17677
Overview
Background
- SPIR-V translator
- ANGLE’s
TranslatorSPIRVcomponent that lowers ESSL shaders to SPIR-V for the Vulkan backend, including remapping certain fragment built-ins to emulated (flipped/rotated) variables. - `assignSpirvId`
- assigns a fixed SPIR-V id (e.g.
kIdSampleID,kIdFragCoord) to a built-in variable so later passes can reference it consistently. - Emulated built-ins
gl_SampleID,gl_PointCoord, andgl_FragCoordrequire special handling (id assignment and, for coordinates, flip/rotate emulation under pre-rotation) when a shader uses them.- `RotateAndFlipBuiltinVariable`
- the transform that inserts a flipped/rotated replacement variable (named via constants like
kFlippedFragCoordName) for a coordinate built-in.
Root Cause Analysis
Inside the input-varying scan loop in src/compiler/translator/spirv/TranslatorSPIRV.cpp (enclosing function not named in the hunk), each recognized built-in (gl_SampleID, gl_PointCoord, gl_FragCoord) ended its handling with break, which terminated the entire loop after the first matching built-in instead of proceeding to the remaining input varyings.
As a result, when a shader used more than one of these built-ins together, only the first was processed — the others never received their SPIR-V id assignment or usesPointCoord flag — so combinations such as gl_PointCoord before gl_FragCoord, or gl_SampleID before gl_FragCoord, were mistranslated.
The fix replaces each break with continue, so the loop keeps scanning and every used built-in is handled. A second, related copy-paste defect is fixed: the sample-position emulation passed kFlippedPointCoordName to RotateAndFlipBuiltinVariable instead of the correct kFlippedSamplePositionName, meaning the flipped sample-position variable collided with / reused the point-coord name.
break (which exits the loop) where continue (advance to the next varying) was intended, so only the first of several fragment built-ins was translated; changing the three breaks to continue — plus correcting the flipped-sample-position variable name — lets the built-ins work in combination.Attack Path
- Author a multi-built-in shader
Write a fragment shader that reads two or more of
gl_SampleID,gl_PointCoord,gl_FragCoord(and/orgl_SamplePosition) in the samemain, ordered so a later one follows an earlier one. - Compile through the Vulkan/SPIR-V path
ANGLE’s
TranslatorSPIRVscans input varyings and, pre-patch,breaks after the first recognized built-in, leaving the rest without id assignment or emulation. - Produce incorrect SPIR-V The generated shader references a built-in that was never properly set up (or a flipped variable under the wrong name), yielding inappropriate/incorrect rendering results.
- No memory-corruption primitive shown The patch and regression tests demonstrate only wrong pixel output; any behavior beyond miscompilation is (inferred) and not evidenced by the diff.
Impact Assessment
Files Changed
src/compiler/translator/spirv/TranslatorSPIRV.cppsrc/tests/gl_tests/GLSLTest.cpp
Audit Directions
- `break` vs `continue` in matcher loopsAudit loops that classify/handle multiple independent items (built-ins, attributes, varyings) for stray
breaks that stop after the first match instead of continuing. - Copy-paste of emulation variable namesReview calls to
RotateAndFlipBuiltinVariableand similar helpers for reusedkFlipped*Nameconstants that don’t match the built-in being transformed. - Combinatorial built-in coverageEnsure translator tests exercise fragment built-ins in combination and in varied source order, not only in isolation, to surface early-exit and name-collision bugs.
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.