Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in ANGLE
DescriptionOut of bounds write in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker517359779
Fix commit8b207281b671 (angle/angle) +94/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-21

Changed Functions

FunctionChangeNotes
ProgramAliasedBindings
src/libANGLE/Program.h
modified

Files Changed

  • src/libANGLE/Program.cpp
  • src/libANGLE/Program.h
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/BlendFuncExtendedTest.cpp
  • src/tests/gl_tests/VertexAttributeTest.cpp
From 8b207281b671cebf4ed22695765299d293b8bffd Mon Sep 17 00:00:00 2001
From: Stacy Gaikovaia <[email protected]>
Date: Mon, 29 Jun 2026 15:08:18 -0400
Subject: [PATCH] Ignore most indexing in attribute and output bindings

In ProgramBindings::bindLocation(for attributes) and
ProgramAliasedBindings::bindLocation (for outputs), ignore any binding
name if there's [ in it, unless it's a [0] suffix. Adjust getter
expectations to match. If ANGLE has no explicit binding recorded,
array[1] will receive the consecutive location following array[0]
meaning that it's no longer possible to bind arrays data to arbitrary
memory addresses.

This change impacts the GLES APIs glBindAttribLocation,
glGetFragDataLocation (inputs), and glBindFragDataLocationEXT (outputs).

Bug: angleproject:517359779
Change-Id: I1b538a772a5c9a834059ef93a648be115ebb28c2
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7952070
Reviewed-by: Kenneth Russell <[email protected]>
Commit-Queue: Stacy Gaikovaia <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
---

diff --git a/src/libANGLE/Program.cpp b/src/libANGLE/Program.cpp
index 81bdd00..bf0a66a 100644
--- a/src/libANGLE/Program.cpp
+++ b/src/libANGLE/Program.cpp
@@ -343,7 +343,21 @@
 
 void ProgramBindings::bindLocation(GLuint index, const std::string &name)
 {
-    mBindings[name] = index;
+    if (name.find('[') != std::string::npos)
+    {
+        if (angle::EndsWith(name, "[0]") && (name.find(']') == name.length() - 1))
+        {
+            mBindings[name.substr(0, name.length() - 3)] = index;
+        }
+        else
+        {
+            return;
+        }
+    }
+    else
+    {
+        mBindings[name] = index;
+    }
 }
 
 int ProgramBindings::getBindingByName(const std::string &name) const
@@ -378,8 +392,16 @@
 
 ProgramAliasedBindings::~ProgramAliasedBindings() {}
 
-void ProgramAliasedBindings::bindLocation(GLuint index, const std::string &name)
+void ProgramAliasedBindings::bindLocation(GLuint index,
+                                          const std::string &name,
+                                          BindLocationPolicy policy)
 {
+    if (policy == BindLocationPolicy::IgnoreIndexing && name.find('[') != std::string::npos &&
+        (!angle::EndsWith(name, "[0]") || name.find(']') != name.length() - 1))
+    {
+        return;
+    }
+
     mBindings[name] = ProgramBinding(index);
 
     // EXT_blend_func_extended spec: "If it specifies the base name of an array,
@@ -853,19 +875,20 @@
                                   const char *name)
 {
     ASSERT(!mLinkingState);
-    mState.mUniformLocationBindings.bindLocation(location.value, name);
+    mState.mUniformLocationBindings.bindLocation(location.value, name,
+                                                 BindLocationPolicy::AcceptIndexing);
 }
 
 void Program::bindFragmentOutputLocation(const Context *context, GLuint index, const char *name)
 {
     ASSERT(!mLinkingState);
-    mState.mFragmentOutputLocations.bindLocation(index, name);
+    mState.mFragmentOutputLocations.bindLocation(index, name, BindLocationPolicy::IgnoreIndexing);
 }
 
 void Program::bindFragmentOutputIndex(const Context *context, GLuint index, const char *name)
 {
     ASSERT(!mLinkingState);
-    mState.mFragmentOutputIndexes.bindLocation(index, name);
+    mState.mFragmentOutputIndexes.bindLocation(index, name, BindLocationPolicy::IgnoreIndexing);
 }
 
 void Program::makeNewExecutable(const Context *context)
diff --git a/src/libANGLE/Program.h b/src/libANGLE/Program.h
index a0f0dd4..3efbed0 100644
--- a/src/libANGLE/Program.h
+++ b/src/libANGLE/Program.h
@@ -188,6 +188,12 @@
     angle::HashMap<std::string, GLuint> mBindings;
 };
 
+enum class BindLocationPolicy
+{
+    AcceptIndexing,
+    IgnoreIndexing,
+};
+
 // Uniforms and Fragment Outputs require special treatment due to array notation (e.g., "[0]")
 class ProgramAliasedBindings final : angle::NonCopyable
 {
@@ -195,7 +201,7 @@
     ProgramAliasedBindings();
     ~ProgramAliasedBindings();
 
-    void bindLocation(GLuint index, const std::string &name);
+    void bindLocation(GLuint index, const std::string &name, BindLocationPolicy policy);
     int getBindingByName(const std::string &name) const;
     int getBindingByLocation(GLuint location) const;
     template <typename T>
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 3b5a103..be933fa 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -666,6 +666,7 @@
 42267082 PIXEL4ORXL GLES : EGLBlobCacheTest.FragmentOutputLocationKey/* = SKIP
 42267082 PIXEL4ORXL GLES : EXTBlendFuncExtendedDrawTestES3.ES3GettersArray/* = SKIP
 42267082 PIXEL4ORXL GLES : EXTBlendFuncExtendedDrawTestES3.ESSL3BindSimpleVarAsArrayNoBind/* = SKIP
+42267082 PIXEL4ORXL GLES : EXTBlendFuncExtendedDrawTestES3.BindFragDataLocationBracketReject/* = SKIP
 42267082 PIXEL4ORXL GLES : EXTBlendFuncExtendedDrawTestES3.MultipleDrawBuffersAPI/* = SKIP
 42267082 PIXEL4ORXL GLES : EXTBlendFuncExtendedTestES3.FragmentOutputLocationsPartiallyAutomatic/* = SKIP
 
diff --git a/src/tests/gl_tests/BlendFuncExtendedTest.cpp b/src/tests/gl_tests/BlendFuncExtendedTest.cpp
index 0dcc862..05816a8 100644
--- a/src/tests/gl_tests/BlendFuncExtendedTest.cpp
+++ b/src/tests/gl_tests/BlendFuncExtendedTest.cpp
@@ -827,7 +827,9 @@
         EXPECT_EQ(0, glGetFragDataIndexEXT(mProgram, "FragData"));
         EXPECT_EQ(kFragData0Location, glGetFragDataLocation(mProgram, "FragData[0]"));
         EXPECT_EQ(0, glGetFragDataIndexEXT(mProgram, "FragData[0]"));
-        EXPECT_EQ(kFragData1Location, glGetFragDataLocation(mProgram, "FragData[1]"));
+        // Binding FragData[1] to kFragData1Location ignored because indexing is ignored.
+        // It receives the consecutive location following FragData[0] (kFragData0Location + 1).
+        EXPECT_EQ(kFragData0Location + 1, glGetFragDataLocation(mProgram, "FragData[1]"));
         EXPECT_EQ(0, glGetFragDataIndexEXT(mProgram, "FragData[1]"));
         // Index bigger than the GLSL variable array length does not find anything.
         EXPECT_EQ(-1, glGetFragDataLocation(mProgram, "FragData[3]"));
@@ -869,6 +871,40 @@
     LinkProgram();
 }
 
+// Test that glBindFragDataLocationEXT and glBindFragDataLocationIndexedEXT ignores names with '['
+// unless ending with '[0]'.
+TEST_P(EXTBlendFuncExtendedDrawTestES3, BindFragDataLocationBracketReject)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_blend_func_extended"));
+
+    constexpr char kFragDataShader[] = R"(#version 300 es
+#extension GL_EXT_blend_func_extended : require
+precision mediump float;
+uniform vec4 src;
+uniform vec4 src1;
+out vec4 FragData;
+out vec4 SecondaryFragData;
+void main() {
+    FragData = src;
+    SecondaryFragData = src1;
+})";
+
+    mProgram = CompileProgram(essl3_shaders::vs::Simple(), kFragDataShader, [](GLuint program) {
+        glBindFragDataLocationEXT(program, 0, "FragData");
+        // Bind invalid names with [1] suffix or multidimensional indices (should be ignored)
+        glBindFragDataLocationEXT(program, 2, "FragData[1]");
+        glBindFragDataLocationIndexedEXT(program, 0, 1, "SecondaryFragData[1]");
+        glBindFragDataLocationEXT(program, 2, "FragData[1][0]");
+        glBindFragDataLocationIndexedEXT(program, 0, 1, "SecondaryFragData[1][0]");
+    });
+
+    LinkProgram();
+
+    EXPECT_EQ(0, glGetFragDataLocation(mProgram, "FragData"));
+    EXPECT_EQ(-1, glGetFragDataLocation(mProgram, "FragData[1]"));
+    EXPECT_EQ(-1, glGetFragDataLocation(mProgram, "FragData[1][0]"));
+}
+
 // Test an ESSL 3.00 program with a link-time fragment output location conflict.
 TEST_P(EXTBlendFuncExtendedTestES3, FragmentOutputLocationConflict)
 {
diff --git a/src/tests/gl_tests/VertexAttributeTest.cpp b/src/tests/gl_tests/VertexAttributeTest.cpp
index 3dbf463..987bedc 100644
--- a/src/tests/gl_tests/VertexAttributeTest.cpp
+++ b/src/tests/gl_tests/VertexAttributeTest.cpp
@@ -1323,6 +1323,27 @@
     EXPECT_PIXEL_NEAR(0, 0, 128, 0, 0, 255, 1);
 }
 
+// Test that glBindAttribLocation rejects names with '[' unless ending with '[0]'.
+TEST_P(VertexAttributeTest, BindAttribLocationBracketReject)
+{
+    GLuint program = compileMultiAttribProgram(1);
+    glBindAttribLocation(program, 2, "position");
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 3b5a103..be933fa 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -666,6 +666,7 @@
 42267082 PIXEL4ORXL GLES : EGLBlobCacheTest.FragmentOutputLocationKey/* = SKIP
 42267082 PIXEL4ORXL GLES : EXTBlendFuncExtendedDrawTestES3.ES3GettersArray/* = SKIP
 42267082 PIXEL4ORXL GLES : EXTBlendFuncExtendedDrawTestES3.ESSL3BindSimpleVarAsArrayNoBind/* = SKIP
+42267082 PIXEL4ORXL GLES : EXTBlendFuncExtendedDrawTestES3.BindFragDataLocationBracketReject/* = SKIP
 42267082 PIXEL4ORXL GLES : EXTBlendFuncExtendedDrawTestES3.MultipleDrawBuffersAPI/* = SKIP
 42267082 PIXEL4ORXL GLES : EXTBlendFuncExtendedTestES3.FragmentOutputLocationsPartiallyAutomatic/* = SKIP
diff --git a/src/tests/gl_tests/BlendFuncExtendedTest.cpp b/src/tests/gl_tests/BlendFuncExtendedTest.cpp
index 0dcc862..05816a8 100644
--- a/src/tests/gl_tests/BlendFuncExtendedTest.cpp
+++ b/src/tests/gl_tests/BlendFuncExtendedTest.cpp
@@ -827,7 +827,9 @@
         EXPECT_EQ(0, glGetFragDataIndexEXT(mProgram, "FragData"));
         EXPECT_EQ(kFragData0Location, glGetFragDataLocation(mProgram, "FragData[0]"));
         EXPECT_EQ(0, glGetFragDataIndexEXT(mProgram, "FragData[0]"));
-        EXPECT_EQ(kFragData1Location, glGetFragDataLocation(mProgram, "FragData[1]"));
+        // Binding FragData[1] to kFragData1Location ignored because indexing is ignored.
+        // It receives the consecutive location following FragData[0] (kFragData0Location + 1).
+        EXPECT_EQ(kFragData0Location + 1, glGetFragDataLocation(mProgram, "FragData[1]"));
         EXPECT_EQ(0, glGetFragDataIndexEXT(mProgram, "FragData[1]"));
         // Index bigger than the GLSL variable array length does not find anything.
         EXPECT_EQ(-1, glGetFragDataLocation(mProgram, "FragData[3]"));
@@ -869,6 +871,40 @@
     LinkProgram();
 }
 
+// Test that glBindFragDataLocationEXT and glBindFragDataLocationIndexedEXT ignores names with '['
+// unless ending with '[0]'.
+TEST_P(EXTBlendFuncExtendedDrawTestES3, BindFragDataLocationBracketReject)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_blend_func_extended"));
+
+    constexpr char kFragDataShader[] = R"(#version 300 es
+#extension GL_EXT_blend_func_extended : require
+precision mediump float;
+uniform vec4 src;
+uniform vec4 src1;
+out vec4 FragData;
+out vec4 SecondaryFragData;
+void main() {
+    FragData = src;
+    SecondaryFragData = src1;
+})";
+
+    mProgram = CompileProgram(essl3_shaders::vs::Simple(), kFragDataShader, [](GLuint program) {
+        glBindFragDataLocationEXT(program, 0, "FragData");
+        // Bind invalid names with [1] suffix or multidimensional indices (should be ignored)
+        glBindFragDataLocationEXT(program, 2, "FragData[1]");
+        glBindFragDataLocationIndexedEXT(program, 0, 1, "SecondaryFragData[1]");
+        glBindFragDataLocationEXT(program, 2, "FragData[1][0]");
+        glBindFragDataLocationIndexedEXT(program, 0, 1, "SecondaryFragData[1][0]");
+    });
+
+    LinkProgram();
+
+    EXPECT_EQ(0, glGetFragDataLocation(mProgram, "FragData"));
+    EXPECT_EQ(-1, glGetFragDataLocation(mProgram, "FragData[1]"));
+    EXPECT_EQ(-1, glGetFragDataLocation(mProgram, "FragData[1][0]"));
+}
+
 // Test an ESSL 3.00 program with a link-time fragment output location conflict.
 TEST_P(EXTBlendFuncExtendedTestES3, FragmentOutputLocationConflict)
 {
diff --git a/src/tests/gl_tests/VertexAttributeTest.cpp b/src/tests/gl_tests/VertexAttributeTest.cpp
index 3dbf463..987bedc 100644
--- a/src/tests/gl_tests/VertexAttributeTest.cpp
+++ b/src/tests/gl_tests/VertexAttributeTest.cpp
@@ -1323,6 +1323,27 @@
     EXPECT_PIXEL_NEAR(0, 0, 128, 0, 0, 255, 1);
 }
 
+// Test that glBindAttribLocation rejects names with '[' unless ending with '[0]'.
+TEST_P(VertexAttributeTest, BindAttribLocationBracketReject)
+{
+    GLuint program = compileMultiAttribProgram(1);
+    glBindAttribLocation(program, 2, "position");
+    // Bind invalid name with [1] suffix (should be ignored)
+    glBindAttribLocation(program, 5, "a0[1]");
+    // name with [0] suffix should be considered valid and bound
+    glBindAttribLocation(program, 3, "a0[0]");
+    // Bind invalid name with multidimensional index (should be ignored)
+    glBindAttribLocation(program, 4, "a0[1][0]");
+    glLinkProgram(program);
+
+    EXPECT_EQ(2, glGetAttribLocation(program, "position"));
+    EXPECT_EQ(3, glGetAttribLocation(program, "a0"));
+
+    // These were not bound.
+    EXPECT_EQ(-1, glGetAttribLocation(program, "a0[1]"));
+    EXPECT_EQ(-1, glGetAttribLocation(program, "a0[1][0]"));
+}
+
 class VertexAttributeOORTest : public VertexAttributeTest
 {
   public:
Loading diff…

Original Bug Report

reported by [email protected]

OOB writes in ANGLE Vulkan backend via flawed linkValidateOutputVariables validation

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 potential validation logic error in ANGLE’s fragment shader output variable linking allows output arrays to exceed the maximum draw buffers limit. When certain per-element bindings are used, this bypasses validation and triggers out-of-bounds stack and heap writes in the Vulkan pipeline cache warm-up path. This could potentially allow arbitrary code execution in the GPU process from a compromised renderer.

Affected files:

  • third_party/angle/src/libANGLE/ProgramExecutable.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/ProgramExecutableVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp
  • third_party/angle/src/libANGLE/Program.h
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.h
  • third_party/angle/src/common/FixedVector.h

Estimated timestamp from git blame: 2021-10-28

Summary

A potential logic flaw in ANGLE’s output variable validation allows a fragment shader output array to span past IMPLEMENTATION_MAX_DRAW_BUFFERS (which is 8) when per-element API bindings are used via glBindFragDataLocationEXT. This validation bypass subsequently triggers out-of-bounds (OOB) writes on both the heap and stack within the Vulkan backend during pipeline cache warm-up, presenting a potential path to arbitrary code execution (RCE) in the GPU process.

Vulnerability Analysis

In ProgramExecutable::linkValidateOutputVariables (third_party/angle/src/libANGLE/ProgramExecutable.cpp), ANGLE processes per-element API bindings of the form name[k] (where k >= 1). It records VariableLocation(arrayIndex=k, varIdx) in reservedLocations and validates that they are under maxDrawBuffers. The base location is then assigned, and a guard calls FindUsedOutputLocation(outputLocations, maxLocation, baseLocation + elementCount - maxLocation, reservedLocations, idx) to detect if any indices exceed maxLocation.

There are two potential bypasses for this validation:

  1. Bypass A (Aliasing in FindUsedOutputLocation): Within FindUsedOutputLocation, a query VariableLocation(elementIndex, variableIndex) is constructed, where elementIndex is calculated as the offset from the passed baseLocation parameter (which is maxLocation when called by the over-max guard). However, VariableLocation::operator== (third_party/angle/src/libANGLE/Program.h) only compares {arrayIndex, index}. Thus, if an attacker has reserved a location for name[L - maxLocation] (registering a VariableLocation with arrayIndex = L - maxLocation), a query checking a truly used slot at location L will incorrectly match this reserved location. The guard is misled into treating a used slot as reserved, causing it to return false and allowing the link to succeed.

  2. Bypass B (Redirected Elements): If the attacker binds all array elements except element 0 to low, valid locations, then no slots at or above maxLocation are occupied in outputLocations. Consequently, the guard legitimately passes. However, downstream code in gatherOutputTypes and the Vulkan/Metal backends computes location contiguously as pod.location + elementIndex for all elements, completely ignoring the per-element redirects. With maxDrawBuffers = 8 and an array size of 8, the contiguous locations reach up to index 14 (i.e., 7 + 8 - 1).

Downstream Impact (Vulkan Backend)

When linking a program with the Vulkan backend feature warmUpPipelineCacheAtLink enabled, ANGLE calls SetupDefaultPipelineState in ProgramExecutableVk.cpp. This triggers potential memory corruption:

  1. Intra-object Heap/Stack Overflow: The loop over arrayIndex (0 to 7) calls setRenderPassColorAttachmentFormat(pod.location + arrayIndex, ...) which invokes RenderPassDesc::packColorAttachment in vk_cache_utils.cpp. This writes to mAttachmentFormats on a std::array<uint8_t, 9>. For indexes 9 to 14, this results in an intra-object overflow past the array boundary into adjacent fields of mWarmUpGraphicsPipelineDesc, while setting mColorAttachmentRange to 15.

  2. Stack Buffer Overflow in MakeRenderPass: When !preferDynamicRendering is true, RenderPassCache::MakeRenderPass loops up to colorAttachmentRange (15). Since the OOB-written formats at indices 9..14 are non-zero, isColorAttachmentEnabled returns true, causing colorAttachmentRefs.push_back(...) to execute 15 times on a stack-allocated FixedVector<VkAttachmentReference2, 8>. Because FixedVector::push_back lacks runtime bounds checks in release builds, this overwrites 7 elements (~224 bytes) of stack data with VkAttachmentReference2 structures.

Suggested / Potential Reproduction Steps

Note: These are potential steps as our tooling does not have the ability to execute code.

  1. Establish a compromised renderer process driving the passthrough GLES2 decoder with an ANGLE ES3 context supporting EXT_blend_func_extended and a Vulkan backend with warmUpPipelineCacheAtLink enabled.
  2. Create a GL program p, attach a vertex shader and a fragment shader containing:
    #version 300 es
    precision mediump float;
    out vec4 fragData[8];
    void main() {
        for(int i = 0; i < 8; ++i) fragData[i] = vec4(1);
    }
    
  3. Issue the following explicit GLES2 command buffer bindings:
    BindFragDataLocationEXTBucket(p, 7, "fragData");
    BindFragDataLocationEXTBucket(p, 0, "fragData[1]");
    BindFragDataLocationEXTBucket(p, 1, "fragData[2]");
    BindFragDataLocationEXTBucket(p, 2, "fragData[3]");
    BindFragDataLocationEXTBucket(p, 3, "fragData[4]");
    BindFragDataLocationEXTBucket(p, 4, "fragData[5]");
    BindFragDataLocationEXTBucket(p, 5, "fragData[6]");
    BindFragDataLocationEXTBucket(p, 6, "fragData[7]");
    
  4. Trigger the Vulkan OOB write by calling LinkProgram(p) on the program.

Suggested Fix

  1. Modify FindUsedOutputLocation so that it uses the correct corresponding array index offset when constructing VariableLocation for verification, or enforce strict bounds checks on the total potential location span (baseLocation + elementCount) directly against maxDrawBuffers regardless of explicit bindings during the initial link validation stage.
  2. Add hardening checks in RenderPassDesc::packColorAttachment to explicitly drop or error on any attachment indices exceeding kMaxFramebufferNonResolveAttachments instead of relying on debug-only ASSERT statements.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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