Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds memory access in ANGLE
DescriptionOut of bounds memory access in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker503392431
Fix commit58107d63948a (angle/angle) +78/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • src/compiler/translator/ParseContext.cpp
  • src/compiler/translator/ParseContext.h
  • src/tests/gl_tests/GLSLValidationTest.cpp
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)
 {
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 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)
 {
Loading diff…

Original Bug Report

reported by [email protected]

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.cpp
  • third_party/angle/src/compiler/translator/spirv/TranslatorSPIRV.cpp
  • third_party/angle/src/compiler/translator/glsl/TranslatorGLSL.cpp
  • third_party/angle/src/compiler/translator/msl/TranslatorMSL.cpp
  • third_party/angle/src/compiler/translator/ValidateAST.cpp
  • third_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:

  1. Initial Parsing: A WebGL fragment shader accesses a high valid index of gl_FragData, such as gl_FragData[7]. In ParseContext.cpp, this is validated against MaxDrawBuffers (typically 8). Since 7 < 8, the bounds check passes, and an EOpIndexDirect AST node is created with the constant literal index 7.
  2. Array Resizing: If the shader also uses gl_SecondaryFragDataEXT (provided by GL_EXT_blend_func_extended), the EmulateFragColorData transformation pass is triggered during compilation.
  3. Size Reduction: EmulateFragColorDataTraverser::visitSymbol replaces gl_FragData with a new internal array and resizes it to MaxDualSourceDrawBuffers. On most hardware, this limits the array size to 1.
  4. Missing Re-validation: The transformation does not traverse the parent tree to clamp or re-evaluate existing EOpIndexDirect nodes. ValidateAST also lacks bounds-checking for constant indices. Thus, an index of 7 pointing into an array of size 1 remains in the AST.
  5. Malformed Output Emission: During SPIR-V generation (OutputSPIRV.cpp), the literal index 7 is extracted from the EOpIndexDirect node and directly emitted into an OpAccessChain instruction without further bounds checking.
  6. Validation Bypass: In Chromium Release builds, ANGLE_ENABLE_ASSERTS is false, meaning ANGLE’s internal spirv-val validation 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.

  1. Initialize a WebGL context.
  2. Enable the required extensions in JavaScript:
    gl.getExtension('WEBGL_draw_buffers');
    gl.getExtension('WEBGL_blend_func_extended');
    
  3. 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);
    }
    
  4. 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.

View on issue tracker