CVE-2026-11055
Overview
Files Changed
src/libANGLE/renderer/d3d/d3d11/Buffer11.cppsrc/tests/gl_tests/UniformBufferTest.cpp
Patch
From 9a6ba09c0037dc4f1f082dfcdbe1f2bd907c273d Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Wed, 15 Apr 2026 15:00:04 -0400
Subject: [PATCH] D3D11: Avoid cache self-eviction for uniform buffers.
D3D11 makes copies of buffers to handle different binding
offsets since they are not natively supported. It limits the cache to
the buffer's size * 2. glBindBufferRange allows giving sizes larger than
the buffer, this can cause the D3D11 buffer storage to trim even with
one element, evicting the newly allocated buffer.
Avoid this by checking if the buffer to evict was the newly inserted
one. It would always be the last element in the cache because it has the
most recent LRU value.
Fixed: chromium:498881735
Change-Id: Ie235f1c3efa6b3a2395bbfdf6b165c4c42d95fdc
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7765258
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---
diff --git a/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp b/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
index 64c9a43..d3ff511 100644
--- a/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
@@ -878,7 +878,14 @@
return a.second.lruCount < b.second.lruCount;
});
- ASSERT(iter->second.storage != newStorage);
+ // Don't remove the newly added storage. This can only happen if it is the last entry
+ // since it has the most recent LRU value.
+ if (iter->second.storage == newStorage)
+ {
+ ASSERT(mConstantBufferRangeStoragesCache.size() == 1);
+ break;
+ }
+
ASSERT(mConstantBufferStorageAdditionalSize >= iter->second.storage->getSize());
mConstantBufferStorageAdditionalSize -= iter->second.storage->getSize();
@@ -945,7 +952,14 @@
return a.second.lruCount < b.second.lruCount;
});
- ASSERT(iter->second.storage != newStorage);
+ // Don't remove the newly added storage. This can only happen if it is the last entry
+ // since it has the most recent LRU value.
+ if (iter->second.storage == newStorage)
+ {
+ ASSERT(mStructuredBufferRangeStoragesCache.size() == 1);
+ break;
+ }
+
ASSERT(mStructuredBufferStorageAdditionalSize >= iter->second.storage->getSize());
mStructuredBufferStorageAdditionalSize -= iter->second.storage->getSize();
diff --git a/src/tests/gl_tests/UniformBufferTest.cpp b/src/tests/gl_tests/UniformBufferTest.cpp
index 4261724..0041c10 100644
--- a/src/tests/gl_tests/UniformBufferTest.cpp
+++ b/src/tests/gl_tests/UniformBufferTest.cpp
@@ -82,6 +82,38 @@
EXPECT_PIXEL_NEAR(0, 0, 128, 191, 64, 255, 1);
}
+// Test that binding a range larger than the buffer size works when only valid ranges are accessed
+// in the shader.
+TEST_P(UniformBufferTest, BindLargerThanSize)
+{
+ constexpr size_t iterationCount = 10;
+
+ GLint alignment;
+ glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &alignment);
+
+ // Buffer filed with 0.5 floats enough for `iterationCount` different multiples of alignment. 4
+ // extra padding values are added since the shader always reads 4 floats.
+ const std::vector<float> floatData((iterationCount * alignment) + 4, 0.5f);
+ const size_t bufferSize = sizeof(float) * floatData.size();
+ glBindBuffer(GL_UNIFORM_BUFFER, mUniformBuffer);
+ glBufferData(GL_UNIFORM_BUFFER, bufferSize, floatData.data(), GL_STATIC_DRAW);
+
+ glClear(GL_COLOR_BUFFER_BIT);
+ for (size_t i = 0; i < iterationCount; i++)
+ {
+ // Each iteration, offset further into the buffer and use large (different) binding sizes.
+ size_t offset = i * alignment * sizeof(float);
+ size_t bindingSize = (i + 1) * bufferSize;
+ glBindBufferRange(GL_UNIFORM_BUFFER, 0, mUniformBuffer, offset, bindingSize);
+
+ glUniformBlockBinding(mProgram, mUniformBufferIndex, 0);
+ drawQuad(mProgram, essl3_shaders::PositionAttrib(), 0.5f);
+
+ ASSERT_GL_NO_ERROR();
+ EXPECT_PIXEL_NEAR(0, 0, 128, 128, 128, 128, 1);
+ }
+}
+
// Test a scenario that draws then update UBO (using bufferData or bufferSubData or mapBuffer) then
// draws with updated data.
TEST_P(UniformBufferTest, DrawThenUpdateThenDraw)
Regression Test / PoC
diff --git a/src/tests/gl_tests/UniformBufferTest.cpp b/src/tests/gl_tests/UniformBufferTest.cpp
index 4261724..0041c10 100644
--- a/src/tests/gl_tests/UniformBufferTest.cpp
+++ b/src/tests/gl_tests/UniformBufferTest.cpp
@@ -82,6 +82,38 @@
EXPECT_PIXEL_NEAR(0, 0, 128, 191, 64, 255, 1);
}
+// Test that binding a range larger than the buffer size works when only valid ranges are accessed
+// in the shader.
+TEST_P(UniformBufferTest, BindLargerThanSize)
+{
+ constexpr size_t iterationCount = 10;
+
+ GLint alignment;
+ glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &alignment);
+
+ // Buffer filed with 0.5 floats enough for `iterationCount` different multiples of alignment. 4
+ // extra padding values are added since the shader always reads 4 floats.
+ const std::vector<float> floatData((iterationCount * alignment) + 4, 0.5f);
+ const size_t bufferSize = sizeof(float) * floatData.size();
+ glBindBuffer(GL_UNIFORM_BUFFER, mUniformBuffer);
+ glBufferData(GL_UNIFORM_BUFFER, bufferSize, floatData.data(), GL_STATIC_DRAW);
+
+ glClear(GL_COLOR_BUFFER_BIT);
+ for (size_t i = 0; i < iterationCount; i++)
+ {
+ // Each iteration, offset further into the buffer and use large (different) binding sizes.
+ size_t offset = i * alignment * sizeof(float);
+ size_t bindingSize = (i + 1) * bufferSize;
+ glBindBufferRange(GL_UNIFORM_BUFFER, 0, mUniformBuffer, offset, bindingSize);
+
+ glUniformBlockBinding(mProgram, mUniformBufferIndex, 0);
+ drawQuad(mProgram, essl3_shaders::PositionAttrib(), 0.5f);
+
+ ASSERT_GL_NO_ERROR();
+ EXPECT_PIXEL_NEAR(0, 0, 128, 128, 128, 128, 1);
+ }
+}
+
// Test a scenario that draws then update UBO (using bufferData or bufferSubData or mapBuffer) then
// draws with updated data.
TEST_P(UniformBufferTest, DrawThenUpdateThenDraw)
Original Bug Report
Potential Use-After-Free in ANGLE D3D11 Buffer11 via Cache Self-Eviction
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 potential Use-After-Free (UAF) exists in ANGLE’s D3D11 backend due to an LRU cache self-eviction flaw when emulating buffer sub-ranges. By manipulating the size parameter in glBindBufferRange, an attacker can force the cache to free the storage object currently being processed. The code subsequently makes a virtual function call on this freed pointer, which could lead to arbitrary code execution in the GPU process.
Affected files:
third_party/angle/src/libANGLE/renderer/d3d/d3d11/Buffer11.cppthird_party/angle/src/libANGLE/renderer/d3d/d3d11/Buffer11.h
Estimated timestamp from git blame: 2025-05-14
Description
A potential Use-After-Free (UAF) vulnerability exists in ANGLE’s D3D11 backend, specifically in Buffer11::getConstantBufferRangeStorage and Buffer11::getStructuredBufferRangeSRV. When running on hardware that lacks native constant buffer offsetting support (e.g., D3D Feature Level 11.0), ANGLE emulates sub-ranges by caching them in an LRU (Least Recently Used) cache.
The vulnerability is triggered by a logic flaw in the cache eviction loop. When an attacker requests a buffer range size that expands an existing cached entry beyond an internal budget (2 * getSize()), the function enters a while loop to evict older cache entries and reclaim space. However, if the cache only contains the entry currently being processed (or if the current entry happens to have the lowest LRU count), the eviction loop will select and delete the newStorage object that is actively being used.
While an ASSERT(iter->second.storage != newStorage) exists to detect this exact self-eviction flaw, ASSERT macros are compiled out into no-ops in Release builds. Consequently, the object is freed via SafeDelete(), but the local newStorage raw pointer is left dangling. Immediately following the loop, the code invokes newStorage->resize(...). Because BufferStorage is a polymorphic class, this results in a virtual function call on freed memory.
Potential Attacker Steps
(Note: These are suggested steps based on static analysis; our tooling agent does not yet have the ability to run code or provide a working PoC.)
- Initialize WebGL: The attacker creates a WebGL 2 context on a system using the D3D11 backend without native offsetting support.
- Create Buffer: The attacker creates a uniform buffer with a specific base size, e.g., 1000 bytes.
- Populate Cache: The attacker calls
glBindBufferRangewith a size larger than the buffer (e.g., 1500 bytes). WebGL validation allows this because draw call validation only checks the clamped available size (std::min(size, bufferSize - offset)) against the shader’s requirements. A draw call is issued, causing ANGLE to cache aBufferStorageobject of size 1500. - Trigger Self-Eviction: The attacker updates the binding with an even larger size (e.g., 3000 bytes) and issues another draw call.
- UAF Execution: Inside
getConstantBufferRangeStorage, the requested size exceeds the allowed additional size budget (2000 bytes). The eviction loop triggers and deletes the 1500-byte storage object (the only item in the cache). The code then callsnewStorage->resize()on the freed pointer. - Exploitation: By using Web Workers to concurrently spray the GPU process heap, the attacker could replace the freed object with controlled data, hijacking the virtual function table (vtable) to achieve Remote Code Execution (RCE) in the GPU process.
Suggested Fix
The eviction logic must be updated to explicitly protect the actively processed storage object from being evicted.
Inside the while loops in getConstantBufferRangeStorage and getStructuredBufferRangeSRV, the std::min_element search should be modified to filter out newStorage. Alternatively, the code can explicitly check if iter->second.storage == newStorage and break the loop or skip eviction for that entry, ensuring the active object remains alive.
Additionally, evaluating whether to clamp the requested size to the buffer’s physical boundaries much earlier in the pipeline (or inside the caching logic itself) could prevent the cache from artificially inflating beyond the physical buffer’s bounds.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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.