CVE-2026-11191
Overview
Files Changed
src/compiler/translator/ParseContext.cppsrc/compiler/translator/ParseContext.hsrc/tests/gl_tests/GLSLValidationTest.cpp
Patch
From 58107d63948a28819bf25c1bb8e273755c2faa8d Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> Date: Mon, 27 Apr 2026 14:38:29 -0400 Subject: [PATCH] Translator: Fix gl_FragData index check when dual-src blending Bug: chromium:503392431 Change-Id: I0e7dda15d238b926b996f5df1d62b52fb3ab51f3 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7797472 Reviewed-by: Yuxin Hu <[email protected]> Commit-Queue: Shahbaz Youssefi <[email protected]> --- diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp index 5e41089..e913976 100644 --- a/src/compiler/translator/ParseContext.cpp +++ b/src/compiler/translator/ParseContext.cpp @@ -545,6 +545,7 @@ ShouldEnforceESSL100LoopAndIndexingLimitations(spec, mShaderVersion, options)), mFragmentOutputIndex1Used(false), mFragmentOutputFragDepthUsed(false), + mMaxFragDataArrayIndexUsed(0), mGeometryShaderInputPrimitiveType(EptUndefined), mGeometryShaderOutputPrimitiveType(EptUndefined), mGeometryShaderInvocations(0), @@ -7097,18 +7098,16 @@ if (!baseExpression->getType().isUnsizedArray()) { - if (baseExpression->isArray()) + if (baseExpression->isArray() && baseExpression->getQualifier() == EvqFragData) { - if (baseExpression->getQualifier() == EvqFragData && index > 0) + mMaxFragDataArrayIndexUsed = std::max(mMaxFragDataArrayIndexUsed, index); + if (index > 0 && !isExtensionEnabled(TExtension::EXT_draw_buffers)) { - if (!isExtensionEnabled(TExtension::EXT_draw_buffers)) - { - outOfRangeError(outOfRangeIndexIsError, location, - "array index for gl_FragData must be zero when " - "GL_EXT_draw_buffers is disabled", - "[]"); - safeIndex = 0; - } + outOfRangeError(outOfRangeIndexIsError, location, + "array index for gl_FragData must be zero when " + "GL_EXT_draw_buffers is disabled", + "[]"); + safeIndex = 0; } } // Only do generic out-of-range check if similar error hasn't already been reported. @@ -10276,6 +10275,24 @@ variable.variable->name()); } } + + // If gl_SecondaryFragDataEXT is used, then indices to gl_FragData must be smaller than + // gl_MaxDualSourceDrawBuffersEXT. This cannot be validated until the end of the shader because + // it would be unknown if gl_SecondaryFragDataEXT is ever used. + // + // Note that gl_SecondaryFragColorEXT is not checked, because simultaneous use of gl_FragData + // and gl_SecondaryFragColorEXT is already forbidden. Additionally, mFragmentOutputIndex1Used + // is not checked as it pertains to ESSL 300+. + const bool secondaryFragDataUsed = + symbolTable.gl_SecondaryFragDataEXT() != nullptr && + symbolTable.isStaticallyUsed(*symbolTable.gl_SecondaryFragDataEXT()); + if (secondaryFragDataUsed && mMaxFragDataArrayIndexUsed >= mResources.MaxDualSourceDrawBuffers) + { + mDiagnostics->globalError( + "array index for gl_FragData must be less than " + "GL_MAX_DUAL_SOURCE_DRAW_BUFFERS_EXT when " + "gl_SecondaryFragDataEXT is used"); + } } bool TParseContext::postParseChecks() diff --git a/src/compiler/translator/ParseContext.h b/src/compiler/translator/ParseContext.h index 1f83d31..c4280f4 100644 --- a/src/compiler/translator/ParseContext.h +++ b/src/compiler/translator/ParseContext.h @@ -977,6 +977,7 @@ TVector<VariableAndLocation> mFragmentOutputsYuv; bool mFragmentOutputIndex1Used; bool mFragmentOutputFragDepthUsed; + int mMaxFragDataArrayIndexUsed; // Track the geometry shader global parameters declared in layout. TLayoutPrimitiveType mGeometryShaderInputPrimitiveType; diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp index 7afe0d0..e1346f1 100644 --- a/src/tests/gl_tests/GLSLValidationTest.cpp +++ b/src/tests/gl_tests/GLSLValidationTest.cpp @@ -4539,6 +4539,19 @@ "'i' : Loop index cannot be statically assigned to within the body of the loop"); } +// Shader that writes to FragData at index >= gl_MaxDrawBuffers. +TEST_P(GLSLValidationTest, FragDataIndexTooLarge) +{ + ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_draw_buffers")); + + constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require +precision mediump float; +void main() { + gl_FragData[gl_MaxDrawBuffers] = vec4(0.1); +})"; + validateError(GL_FRAGMENT_SHADER, kFS, "array index out of range"); +} + // Shader that writes to SecondaryFragColor and SecondaryFragData does not compile. TEST_P(GLSLValidationTest, BlendFuncExtendedSecondaryColorAndData) { @@ -4582,13 +4595,49 @@ precision mediump float; void main() { gl_SecondaryFragColorEXT = vec4(1.0); - gl_FragData[gl_MaxDrawBuffers - 1] = vec4(0.1); + gl_FragData[gl_MaxDualSourceDrawBuffersEXT - 1] = vec4(0.1); })"; validateError(GL_FRAGMENT_SHADER, kFS, "cannot use both output variable sets (gl_FragData, gl_SecondaryFragDataEXT) and " "(gl_FragColor, gl_SecondaryFragColorEXT)"); } +// Shader that writes to SecondaryFragData and FragData at an index >= than +// gl_MaxDualSourceDrawBuffersEXT. +TEST_P(GLSLValidationTest, BlendFuncExtendedDataArrayAndSecondaryData) +{ + ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_blend_func_extended")); + ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_draw_buffers")); + + constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require +#extension GL_EXT_blend_func_extended : require +precision mediump float; +void main() { + gl_SecondaryFragDataEXT[0] = vec4(1.0); + gl_FragData[gl_MaxDualSourceDrawBuffersEXT] = vec4(0.1); +})"; + validateError(GL_FRAGMENT_SHADER, kFS, + "array index for gl_FragData must be less than " + "GL_MAX_DUAL_SOURCE_DRAW_BUFFERS_EXT when gl_SecondaryFragDataEXT is used"); +} + +// Shader that writes to FragData at an index >= than gl_MaxDualSourceDrawBuffersEXT is fine if +// SecondaryFragData is not used. Note that gl_MaxDualSourceDrawBuffersEXT is typically 1, while +// the size of gl_FragData (gl_MaxDrawBuffers) is larger. +TEST_P(GLSLValidationTest, BlendFuncExtendedDataArrayOnly) +{ + ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_blend_func_extended")); + ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_draw_buffers")); + + constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require +#extension GL_EXT_blend_func_extended : require +precision mediump float; +void main() { + gl_FragData[gl_MaxDrawBuffers - 1] = vec4(0.1); +})"; + validateSuccess(GL_FRAGMENT_SHADER, kFS); +} + // Dynamic indexing of SecondaryFragData is not allowed in WebGL 2.0. TEST_P(WebGL2GLSLValidationTest, BlendFuncExtendedSecondaryDataIndexing) {
Regression Test / PoC
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 7afe0d0..e1346f1 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -4539,6 +4539,19 @@
"'i' : Loop index cannot be statically assigned to within the body of the loop");
}
+// Shader that writes to FragData at index >= gl_MaxDrawBuffers.
+TEST_P(GLSLValidationTest, FragDataIndexTooLarge)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_draw_buffers"));
+
+ constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require
+precision mediump float;
+void main() {
+ gl_FragData[gl_MaxDrawBuffers] = vec4(0.1);
+})";
+ validateError(GL_FRAGMENT_SHADER, kFS, "array index out of range");
+}
+
// Shader that writes to SecondaryFragColor and SecondaryFragData does not compile.
TEST_P(GLSLValidationTest, BlendFuncExtendedSecondaryColorAndData)
{
@@ -4582,13 +4595,49 @@
precision mediump float;
void main() {
gl_SecondaryFragColorEXT = vec4(1.0);
- gl_FragData[gl_MaxDrawBuffers - 1] = vec4(0.1);
+ gl_FragData[gl_MaxDualSourceDrawBuffersEXT - 1] = vec4(0.1);
})";
validateError(GL_FRAGMENT_SHADER, kFS,
"cannot use both output variable sets (gl_FragData, gl_SecondaryFragDataEXT) and "
"(gl_FragColor, gl_SecondaryFragColorEXT)");
}
+// Shader that writes to SecondaryFragData and FragData at an index >= than
+// gl_MaxDualSourceDrawBuffersEXT.
+TEST_P(GLSLValidationTest, BlendFuncExtendedDataArrayAndSecondaryData)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_blend_func_extended"));
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_draw_buffers"));
+
+ constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require
+#extension GL_EXT_blend_func_extended : require
+precision mediump float;
+void main() {
+ gl_SecondaryFragDataEXT[0] = vec4(1.0);
+ gl_FragData[gl_MaxDualSourceDrawBuffersEXT] = vec4(0.1);
+})";
+ validateError(GL_FRAGMENT_SHADER, kFS,
+ "array index for gl_FragData must be less than "
+ "GL_MAX_DUAL_SOURCE_DRAW_BUFFERS_EXT when gl_SecondaryFragDataEXT is used");
+}
+
+// Shader that writes to FragData at an index >= than gl_MaxDualSourceDrawBuffersEXT is fine if
+// SecondaryFragData is not used. Note that gl_MaxDualSourceDrawBuffersEXT is typically 1, while
+// the size of gl_FragData (gl_MaxDrawBuffers) is larger.
+TEST_P(GLSLValidationTest, BlendFuncExtendedDataArrayOnly)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_blend_func_extended"));
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_draw_buffers"));
+
+ constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require
+#extension GL_EXT_blend_func_extended : require
+precision mediump float;
+void main() {
+ gl_FragData[gl_MaxDrawBuffers - 1] = vec4(0.1);
+})";
+ validateSuccess(GL_FRAGMENT_SHADER, kFS);
+}
+
// Dynamic indexing of SecondaryFragData is not allowed in WebGL 2.0.
TEST_P(WebGL2GLSLValidationTest, BlendFuncExtendedSecondaryDataIndexing)
{
Original Bug Report
OOB access in ANGLE shader translation via gl_FragData array resizing
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 go/chrome-ai-generated-security-bugs-faq for more information.
Overview: ANGLE’s shader translator can emit structurally malformed shaders with out-of-bounds array accesses when a fragment shader uses both GL_EXT_draw_buffers and GL_EXT_blend_func_extended. This occurs because the gl_FragData array is resized to a smaller value during AST transformation without re-validating or clamping existing constant indices. The resulting malformed shader code is passed to the native GPU driver, potentially causing memory corruption in the GPU process during shader compilation.
Affected files:
third_party/angle/src/compiler/translator/tree_ops/spirv/EmulateFragColorData.cppthird_party/angle/src/compiler/translator/spirv/TranslatorSPIRV.cppthird_party/angle/src/compiler/translator/glsl/TranslatorGLSL.cppthird_party/angle/src/compiler/translator/msl/TranslatorMSL.cppthird_party/angle/src/compiler/translator/ValidateAST.cppthird_party/angle/src/compiler/translator/ParseContext.cpp
Estimated timestamp from git blame: 2023-07-17
Vulnerability Details
A logic error exists in ANGLE’s shader translation process that allows the generation of structurally malformed backend shaders (such as SPIR-V) containing out-of-bounds array accesses.
This issue manifests through the following sequence of events:
- Initial Parsing: A WebGL fragment shader accesses a high valid index of
gl_FragData, such asgl_FragData[7]. InParseContext.cpp, this is validated againstMaxDrawBuffers(typically 8). Since7 < 8, the bounds check passes, and anEOpIndexDirectAST node is created with the constant literal index7. - Array Resizing: If the shader also uses
gl_SecondaryFragDataEXT(provided byGL_EXT_blend_func_extended), theEmulateFragColorDatatransformation pass is triggered during compilation. - Size Reduction:
EmulateFragColorDataTraverser::visitSymbolreplacesgl_FragDatawith a new internal array and resizes it toMaxDualSourceDrawBuffers. On most hardware, this limits the array size to1. - Missing Re-validation: The transformation does not traverse the parent tree to clamp or re-evaluate existing
EOpIndexDirectnodes.ValidateASTalso lacks bounds-checking for constant indices. Thus, an index of7pointing into an array of size1remains in the AST. - Malformed Output Emission: During SPIR-V generation (
OutputSPIRV.cpp), the literal index7is extracted from theEOpIndexDirectnode and directly emitted into anOpAccessChaininstruction without further bounds checking. - Validation Bypass: In Chromium Release builds,
ANGLE_ENABLE_ASSERTSis false, meaning ANGLE’s internalspirv-valvalidation step is skipped for performance reasons.
The resulting structurally invalid SPIR-V is passed directly to the native Vulkan GPU driver via vkCreateShaderModule. Native drivers typically assume SPIR-V from trusted environments is structurally valid. Processing an out-of-bounds OpAccessChain leads to undefined behavior within the driver’s shader compiler, frequently resulting in out-of-bounds memory reads or writes in the GPU process.
Impact
Compromising the GPU process allows an attacker to bypass the renderer sandbox on Desktop platforms (Windows, macOS, Linux). On Android, the GPU process is unsandboxed by default, meaning this could lead directly to arbitrary code execution on the host device.
Potential Reproduction Steps
Note: These are suggested steps to trigger the bug. We have not executed a full working proof-of-concept against a live driver.
- Initialize a WebGL context.
- Enable the required extensions in JavaScript:
gl.getExtension('WEBGL_draw_buffers'); gl.getExtension('WEBGL_blend_func_extended'); - Compile a fragment shader that writes to both extensions:
#extension GL_EXT_draw_buffers : require #extension GL_EXT_blend_func_extended : require precision mediump float; void main() { // Passes ParseContext validation (7 < MaxDrawBuffers) gl_FragData[7] = vec4(1.0); // Triggers EmulateFragColorData, reducing gl_FragData size to 1 gl_SecondaryFragDataEXT[0] = vec4(0.0); } - Bind the shader and issue a draw call. On a Vulkan backend, ANGLE will submit the malformed SPIR-V to the driver.
Suggested Fix
The AST needs to remain consistent after the EmulateFragColorData transformation. We suggest updating EmulateFragColorData so that when gl_FragData is resized, the compiler traverses the AST to identify EOpIndexDirect nodes associated with this variable. Any indices that now exceed the new array size (MaxDualSourceDrawBuffers) should be clamped to MaxDualSourceDrawBuffers - 1, or alternatively, the compilation should be explicitly failed with an out-of-range error at the transformation stage.
Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09
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.