CVE-2026-8556
Overview
Files Changed
src/libANGLE/Context.cppsrc/tests/gl_tests/UniformBufferTest.cpp
Patch
From a812b1d2c8f5e2223a56cdc9d346fb2af1e40235 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Wed, 08 Apr 2026 12:56:56 -0400
Subject: [PATCH] Vulkan: Fix UBO index remap after offset-only update
Bug: chromium:500052361
Change-Id: I08700488e1a571f3c5d9897c4b81b7dedee92d51
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7736311
Reviewed-by: Charlie Lao <cclao@google.com>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/src/libANGLE/Context.cpp b/src/libANGLE/Context.cpp
index 5bb6d0f..9614024 100644
--- a/src/libANGLE/Context.cpp
+++ b/src/libANGLE/Context.cpp
@@ -9341,6 +9341,7 @@
{
mState.mDirtyBits.set(state::DIRTY_BIT_UNIFORM_BUFFER_BINDINGS);
mState.mDirtyUniformBlocks.set(uniformBlockIndex);
+ mState.mUniformBufferBlocksDirtyTypeMask.set(BufferDirtyType::Binding);
mStateCache.onUniformBufferStateChange(this);
}
diff --git a/src/tests/gl_tests/UniformBufferTest.cpp b/src/tests/gl_tests/UniformBufferTest.cpp
index e3bdf40..4261724 100644
--- a/src/tests/gl_tests/UniformBufferTest.cpp
+++ b/src/tests/gl_tests/UniformBufferTest.cpp
@@ -4815,6 +4815,72 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
}
+// Test that rebinding UBOs with |glUniformBlockBinding| a buffer offset has changed works.
+TEST_P(UniformBufferTest, BlockBindChangeAfterOffsetChange)
+{
+ constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+layout(std140) uniform Block0 { vec4 u0; };
+layout(std140) uniform Block1 { vec4 u1; };
+out vec4 fragColor;
+void main() {
+ fragColor = u0 + u1;
+})";
+
+ ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+ glUseProgram(program);
+
+ const GLuint block0Index = glGetUniformBlockIndex(program, "Block0");
+ const GLuint block1Index = glGetUniformBlockIndex(program, "Block1");
+
+ // Map block bindings to something explicit
+ glUniformBlockBinding(program, block0Index, 0);
+ glUniformBlockBinding(program, block1Index, 1);
+
+ constexpr GLuint kSmallBufferSize = 256;
+ constexpr GLuint kLargeBufferSize = 16 * 1024 * 1024;
+
+ const std::vector<float> kBuffer0InitData(kSmallBufferSize, 0.25);
+ const std::vector<float> kBuffer1InitData(kSmallBufferSize, 0.1);
+ const std::vector<float> kBuffer2InitData(kLargeBufferSize, 0.5);
+
+ GLBuffer buffer0;
+ glBindBuffer(GL_UNIFORM_BUFFER, buffer0);
+ glBufferData(GL_UNIFORM_BUFFER, sizeof(float) * kSmallBufferSize, kBuffer0InitData.data(),
+ GL_STATIC_DRAW);
+
+ GLBuffer buffer1;
+ glBindBuffer(GL_UNIFORM_BUFFER, buffer1);
+ glBufferData(GL_UNIFORM_BUFFER, sizeof(float) * kSmallBufferSize, kBuffer1InitData.data(),
+ GL_STATIC_DRAW);
+
+ GLBuffer buffer2;
+ glBindBuffer(GL_UNIFORM_BUFFER, buffer2);
+ glBufferData(GL_UNIFORM_BUFFER, sizeof(float) * kLargeBufferSize, kBuffer2InitData.data(),
+ GL_STATIC_DRAW);
+
+ // Bind all the buffers. Note that binding 2 is unused by the program. Bind the large buffer
+ // at an offset near the end.
+ glBindBufferRange(GL_UNIFORM_BUFFER, 0, buffer0, 0, kSmallBufferSize);
+ glBindBufferRange(GL_UNIFORM_BUFFER, 1, buffer1, 0, kSmallBufferSize);
+ glBindBufferRange(GL_UNIFORM_BUFFER, 2, buffer2, kLargeBufferSize - kSmallBufferSize,
+ kSmallBufferSize);
+
+ // Issue a draw call to sync all dirty bits.
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+
+ // Change the offset of binding 0 only. This takes a special fast-path in the Vulkan backend.
+ glBindBufferRange(GL_UNIFORM_BUFFER, 0, buffer0, kSmallBufferSize, kSmallBufferSize);
+
+ // Switch the binding of the other buffer to the huge buffer.
+ glUniformBlockBinding(program, block1Index, 2);
+
+ // Draw again. It must correctly read from buffer0 and buffer2.
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(191, 191, 191, 191), 1);
+ ASSERT_GL_NO_ERROR();
+}
+
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(UniformBufferTest);
ANGLE_INSTANTIATE_TEST_ES3(UniformBufferTest);
Regression Test / PoC
diff --git a/src/tests/gl_tests/UniformBufferTest.cpp b/src/tests/gl_tests/UniformBufferTest.cpp
index e3bdf40..4261724 100644
--- a/src/tests/gl_tests/UniformBufferTest.cpp
+++ b/src/tests/gl_tests/UniformBufferTest.cpp
@@ -4815,6 +4815,72 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
}
+// Test that rebinding UBOs with |glUniformBlockBinding| a buffer offset has changed works.
+TEST_P(UniformBufferTest, BlockBindChangeAfterOffsetChange)
+{
+ constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+layout(std140) uniform Block0 { vec4 u0; };
+layout(std140) uniform Block1 { vec4 u1; };
+out vec4 fragColor;
+void main() {
+ fragColor = u0 + u1;
+})";
+
+ ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+ glUseProgram(program);
+
+ const GLuint block0Index = glGetUniformBlockIndex(program, "Block0");
+ const GLuint block1Index = glGetUniformBlockIndex(program, "Block1");
+
+ // Map block bindings to something explicit
+ glUniformBlockBinding(program, block0Index, 0);
+ glUniformBlockBinding(program, block1Index, 1);
+
+ constexpr GLuint kSmallBufferSize = 256;
+ constexpr GLuint kLargeBufferSize = 16 * 1024 * 1024;
+
+ const std::vector<float> kBuffer0InitData(kSmallBufferSize, 0.25);
+ const std::vector<float> kBuffer1InitData(kSmallBufferSize, 0.1);
+ const std::vector<float> kBuffer2InitData(kLargeBufferSize, 0.5);
+
+ GLBuffer buffer0;
+ glBindBuffer(GL_UNIFORM_BUFFER, buffer0);
+ glBufferData(GL_UNIFORM_BUFFER, sizeof(float) * kSmallBufferSize, kBuffer0InitData.data(),
+ GL_STATIC_DRAW);
+
+ GLBuffer buffer1;
+ glBindBuffer(GL_UNIFORM_BUFFER, buffer1);
+ glBufferData(GL_UNIFORM_BUFFER, sizeof(float) * kSmallBufferSize, kBuffer1InitData.data(),
+ GL_STATIC_DRAW);
+
+ GLBuffer buffer2;
+ glBindBuffer(GL_UNIFORM_BUFFER, buffer2);
+ glBufferData(GL_UNIFORM_BUFFER, sizeof(float) * kLargeBufferSize, kBuffer2InitData.data(),
+ GL_STATIC_DRAW);
+
+ // Bind all the buffers. Note that binding 2 is unused by the program. Bind the large buffer
+ // at an offset near the end.
+ glBindBufferRange(GL_UNIFORM_BUFFER, 0, buffer0, 0, kSmallBufferSize);
+ glBindBufferRange(GL_UNIFORM_BUFFER, 1, buffer1, 0, kSmallBufferSize);
+ glBindBufferRange(GL_UNIFORM_BUFFER, 2, buffer2, kLargeBufferSize - kSmallBufferSize,
+ kSmallBufferSize);
+
+ // Issue a draw call to sync all dirty bits.
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+
+ // Change the offset of binding 0 only. This takes a special fast-path in the Vulkan backend.
+ glBindBufferRange(GL_UNIFORM_BUFFER, 0, buffer0, kSmallBufferSize, kSmallBufferSize);
+
+ // Switch the binding of the other buffer to the huge buffer.
+ glUniformBlockBinding(program, block1Index, 2);
+
+ // Draw again. It must correctly read from buffer0 and buffer2.
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(191, 191, 191, 191), 1);
+ ASSERT_GL_NO_ERROR();
+}
+
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(UniformBufferTest);
ANGLE_INSTANTIATE_TEST_ES3(UniformBufferTest);
Original Bug Report
ANGLE/Vulkan: glUniformBlockBinding bypasses UBO dirty-type mask causing GPU OOB read
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 security team.
Overview: A logic error in ANGLE’s Vulkan backend allows glUniformBlockBinding to bypass necessary descriptor set updates by failing to update the UBO dirty-type mask. This can result in an out-of-bounds GPU memory read by applying a large dynamic offset to an incorrect, smaller buffer descriptor.
Affected files:
third_party/angle/src/libANGLE/Context.cppthird_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.cppthird_party/angle/src/libANGLE/renderer/vulkan/ProgramExecutableVk.cppthird_party/angle/src/libANGLE/State.cppthird_party/angle/src/libANGLE/ProgramExecutable.cpp
Estimated timestamp from git blame: 2025-07-10
Summary
A potential vulnerability in ANGLE’s Vulkan backend allows for out-of-bounds (OOB) GPU memory reads. The issue arises because glUniformBlockBinding fails to properly update the Uniform Buffer Object (UBO) dirty-type mask (mUniformBufferBlocksDirtyTypeMask). This leads the Vulkan backend to incorrectly use an ‘offset-only’ fast path during state synchronization, applying a new dynamic offset to a stale VkBuffer descriptor. This violates Vulkan safety requirements (VUID-01979) and can be exploited to leak cross-origin GPU memory data.
Technical Details
In ANGLE, State::mUniformBufferBlocksDirtyTypeMask tracks whether UBO updates involve only offset changes or full binding changes. The Vulkan backend uses this mask to optimize descriptor set updates. If the mask is exactly {Offset}, ContextVk::syncState may take the updateUniformBufferBlocksOffset fast path. This fast path only updates the mDynamicOffsets array sent to Vulkan without refreshing the underlying VkBuffer handles or ranges in the descriptor set.
When glUniformBlockBinding is called, it triggers Context::onUniformBlockBindingUpdated. This function marks the uniform block as dirty (DIRTY_BIT_UNIFORM_BUFFER_BINDINGS) but crucially fails to update mUniformBufferBlocksDirtyTypeMask. If an attacker ensures this mask is currently {Offset} (e.g., by changing the offset of a different buffer), a subsequent remapping via glUniformBlockBinding will leave the mask as {Offset}.
During the next draw call, ContextVk::syncState sees the {Offset} mask and takes the fast path. It retrieves the offset for the new binding and applies it to the mDynamicOffsets array. However, because a full update wasn’t triggered, the descriptor set still contains the VkBuffer from the previous binding.
If the new binding’s offset is large, the combined dynamicOffset + range can exceed the size of the original VkBuffer. In vk_cache_utils.cpp, DescriptorSetDescBuilder::updateOneUniformBufferOffset contains an assertion to catch this serial mismatch, but it is compiled out in release builds, allowing the mismatched offset to reach the Vulkan driver.
Potential Trigger Steps
(Note: These are suggested steps based on code analysis; our tooling has not executed a working proof of concept.)
- Setup: Create a WebGL2 context. Compile a shader program using multiple active
std140uniform blocks. Keep the number of active blocks small (e.g., <= 6) to ensureProgramExecutableVk::usesDynamicUniformBufferDescriptors()returns true. - Initial Bindings: Bind buffers using
gl.bindBufferRange:- Buffer A to binding index 0.
- Buffer B (small) to binding index 1.
- Buffer C (large, with a large offset) to binding index 5.
- Sync: Issue
gl.drawArraysto synchronize state and clear the dirty masks in the backend. - Set Offset Mask: Call
gl.bindBufferRangeon binding index 0, changing only the offset. This setsmUniformBufferBlocksDirtyTypeMaskto exactly{Offset}. - Trigger Bug: Call
gl.uniformBlockBinding(program, 1, 5)to remap uniform block 1 from binding 1 (Buffer B) to binding 5 (Buffer C).Context::onUniformBlockBindingUpdatedfails to update the mask, leaving it as{Offset}. - Exploit: Issue another
gl.drawArrays.ContextVk::syncStatetakes the fast path. It applies Buffer C’s massive dynamic offset to Buffer B’sVkBufferdescriptor. - Leak: The GPU reads out-of-bounds from Buffer B’s memory pool. The shader processes this data, and the attacker reads it back via
gl.readPixels, leaking cross-origin GPU memory.
Suggested Fix
In third_party/angle/src/libANGLE/Context.cpp, update Context::onUniformBlockBindingUpdated to properly flag the binding as changed:
void Context::onUniformBlockBindingUpdated(GLuint uniformBlockIndex)
{
mState.mDirtyBits.set(state::DIRTY_BIT_UNIFORM_BUFFER_BINDINGS);
mState.mDirtyUniformBlocks.set(uniformBlockIndex);
// Add this line:
mState.mUniformBufferBlocksDirtyTypeMask.set(gl::BufferDirtyType::Binding);
mStateCache.onUniformBufferStateChange(this);
}
Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad
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.