CVE-2026-16413
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ProgramAliasedBindingssrc/libANGLE/Program.h |
modified |
Files Changed
src/libANGLE/Program.cppsrc/libANGLE/Program.hsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/BlendFuncExtendedTest.cppsrc/tests/gl_tests/VertexAttributeTest.cpp
Patch
From 8b207281b671cebf4ed22695765299d293b8bffd Mon Sep 17 00:00:00 2001
From: Stacy Gaikovaia <gaiko@google.com>
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 <kbr@chromium.org>
Commit-Queue: Stacy Gaikovaia <gaiko@google.com>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
---
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");
Regression Test / PoC
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:
Original Bug Report
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.cppthird_party/angle/src/libANGLE/renderer/vulkan/ProgramExecutableVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.cppthird_party/angle/src/libANGLE/Program.hthird_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.hthird_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:
-
Bypass A (Aliasing in
FindUsedOutputLocation): WithinFindUsedOutputLocation, a queryVariableLocation(elementIndex, variableIndex)is constructed, whereelementIndexis calculated as the offset from the passedbaseLocationparameter (which ismaxLocationwhen 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 forname[L - maxLocation](registering aVariableLocationwitharrayIndex = L - maxLocation), a query checking a truly used slot at locationLwill incorrectly match this reserved location. The guard is misled into treating a used slot as reserved, causing it to returnfalseand allowing the link to succeed. -
Bypass B (Redirected Elements): If the attacker binds all array elements except element 0 to low, valid locations, then no slots at or above
maxLocationare occupied inoutputLocations. Consequently, the guard legitimately passes. However, downstream code ingatherOutputTypesand the Vulkan/Metal backends computes location contiguously aspod.location + elementIndexfor all elements, completely ignoring the per-element redirects. WithmaxDrawBuffers = 8and 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:
-
Intra-object Heap/Stack Overflow: The loop over
arrayIndex(0 to 7) callssetRenderPassColorAttachmentFormat(pod.location + arrayIndex, ...)which invokesRenderPassDesc::packColorAttachmentinvk_cache_utils.cpp. This writes tomAttachmentFormatson astd::array<uint8_t, 9>. For indexes 9 to 14, this results in an intra-object overflow past the array boundary into adjacent fields ofmWarmUpGraphicsPipelineDesc, while settingmColorAttachmentRangeto 15. -
Stack Buffer Overflow in
MakeRenderPass: When!preferDynamicRenderingis true,RenderPassCache::MakeRenderPassloops up tocolorAttachmentRange(15). Since the OOB-written formats at indices 9..14 are non-zero,isColorAttachmentEnabledreturns true, causingcolorAttachmentRefs.push_back(...)to execute 15 times on a stack-allocatedFixedVector<VkAttachmentReference2, 8>. BecauseFixedVector::push_backlacks runtime bounds checks in release builds, this overwrites 7 elements (~224 bytes) of stack data withVkAttachmentReference2structures.
Suggested / Potential Reproduction Steps
Note: These are potential steps as our tooling does not have the ability to execute code.
- Establish a compromised renderer process driving the passthrough GLES2 decoder with an ANGLE ES3 context supporting
EXT_blend_func_extendedand a Vulkan backend withwarmUpPipelineCacheAtLinkenabled. - 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); } - 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]"); - Trigger the Vulkan OOB write by calling
LinkProgram(p)on the program.
Suggested Fix
- Modify
FindUsedOutputLocationso that it uses the correct corresponding array index offset when constructingVariableLocationfor verification, or enforce strict bounds checks on the total potential location span (baseLocation + elementCount) directly againstmaxDrawBuffersregardless of explicit bindings during the initial link validation stage. - Add hardening checks in
RenderPassDesc::packColorAttachmentto explicitly drop or error on any attachment indices exceedingkMaxFramebufferNonResolveAttachmentsinstead of relying on debug-onlyASSERTstatements.
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.