CVE-2026-9932
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
SimpleUniformBufferTestsrc/tests/gl_tests/UniformBufferTest.cpp |
modified |
Files Changed
src/libANGLE/renderer/d3d/d3d11/Buffer11.cppsrc/tests/gl_tests/UniformBufferTest.cpp
Patch
From ba4afac4c523d50f5639014cc50f35052f125913 Mon Sep 17 00:00:00 2001 From: Antonio Maiorano <[email protected]> Date: Thu, 23 Apr 2026 12:56:06 -0400 Subject: [PATCH] [d3d11] Fail on potential delete of mapped buffer during draw Before this change, it was possible on D3D11 to map a buffer, then draw in such a way that garbage collection attempts to delete the mapped buffer, which would then result in a UAF on unmapping that buffer. For now we detect this situation and return an invalid operation; however, we should instead catch this in validation as per http://anglebug.com/505771894. Bug: chromium:501563323 Change-Id: I26f0a10564cc04ffd1eba31bcb542f0c5529a0ae Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7790252 Reviewed-by: Geoff Lang <[email protected]> Commit-Queue: Antonio Maiorano <[email protected]> --- diff --git a/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp b/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp index d3ff511..1cbfd32 100644 --- a/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp +++ b/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp @@ -622,6 +622,15 @@ mIdleness[usage]++; BufferStorage *&storage = mBufferStorages[usage]; + + // TODO(http://anglebug.com/505771894): Add validation that a buffer is not mapped in calls that + // use it (draw, etc.). Once fixed, turn this into an assert. + if (storage != nullptr && storage == mMappedStorage) + { + ANGLE_TRY_HR(SafeGetImplAs<Context11>(context), E_FAIL, + "Error deallocating mapped storage"); + } + if (storage != nullptr && mIdleness[usage] > mDeallocThresholds[usage]) { BufferStorage *latestStorage = nullptr; diff --git a/src/tests/gl_tests/UniformBufferTest.cpp b/src/tests/gl_tests/UniformBufferTest.cpp index 36d5d79..45de0dd 100644 --- a/src/tests/gl_tests/UniformBufferTest.cpp +++ b/src/tests/gl_tests/UniformBufferTest.cpp @@ -5007,4 +5007,107 @@ GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(WebGL2UniformBufferTest); ANGLE_INSTANTIATE_TEST_ES3(WebGL2UniformBufferTest); +class SimpleUniformBufferTest : public ANGLETest<> +{ + protected: + SimpleUniformBufferTest() + { + setWindowWidth(128); + setWindowHeight(128); + setConfigRedBits(8); + setConfigGreenBits(8); + setConfigBlueBits(8); + setConfigAlphaBits(8); + } +}; + +// Test that maps a buffer as staging, then performs a draw inducing a garbage collect of the +// staging buffer. This would formerly UAF, but now returns an invalid operation. +// TODO(http://anglebug.com/505771894): Update test once we validate that drawing with a mapped +// buffer is invalid. +TEST_P(SimpleUniformBufferTest, MappedUBOStagingGarbageCollection) +{ + // Only applies to D3D11 backend. + ANGLE_SKIP_TEST_IF(!IsD3D11()); + + // Create a program with 10 uniform blocks. + constexpr char kVS[] = R"(#version 300 es +layout(std140) uniform block0 { vec4 data0; }; +layout(std140) uniform block1 { vec4 data1; }; +layout(std140) uniform block2 { vec4 data2; }; +layout(std140) uniform block3 { vec4 data3; }; +layout(std140) uniform block4 { vec4 data4; }; +layout(std140) uniform block5 { vec4 data5; }; +layout(std140) uniform block6 { vec4 data6; }; +layout(std140) uniform block7 { vec4 data7; }; +layout(std140) uniform block8 { vec4 data8; }; +layout(std140) uniform block9 { vec4 data9; }; + +void main() { + vec4 total = vec4(0.0); + total += data0; + total += data1; + total += data2; + total += data3; + total += data4; + total += data5; + total += data6; + total += data7; + total += data8; + total += data9; + gl_Position = vec4(total.xyz, 1.0); +} + )"; + + constexpr char kFS[] = R"(#version 300 es +precision highp float; +out vec4 color; +void main() { + color = vec4(1.0); +} +)"; + + ANGLE_GL_PROGRAM(program, kVS, kFS); + glUseProgram(program); + + // Create a buffer and bind it as a COPY_READ_BUFFER. + GLBuffer buffer; + glBindBuffer(GL_COPY_READ_BUFFER, buffer); + + // Use GL_DYNAMIC_DRAW and provide data to ensure it uses SYSTEM_MEMORY storage + // and sets it as mLatestBufferStorage. + std::vector<uint8_t> data(1024, 0); + glBufferData(GL_COPY_READ_BUFFER, 1024, data.data(), GL_DYNAMIC_DRAW); + + // Bind the buffer as a UBO to all 10 slots. + for (int i = 0; i < 10; ++i) + { + glBindBufferBase(GL_UNIFORM_BUFFER, i, buffer); + } + + // Map the buffer for reading. Since it's SYSTEM_MEMORY and we are mapping for read, + // it should allocate a STAGING buffer. + void *ptr = glMapBufferRange(GL_COPY_READ_BUFFER, 0, 1024, GL_MAP_READ_BIT); + ASSERT_NE(ptr, nullptr); + + // Now perform a draw call. + // This will trigger StateManager11::syncUniformBuffersForShader. + // It will iterate through the 10 slots. + // For each slot, it calls Buffer11::getConstantBufferRange -> getBufferStorage(UNIFORM) -> + // garbageCollection. garbageCollection(UNIFORM) calls checkForDeallocation(STAGING). On the 9th + // slot, STAGING's idleness will be 9, exceeding the threshold of 8. checkForDeallocation would + // then attempt to delete the STAGING storage because latestStorage (SYSTEM_MEMORY) != storage + // (STAGING), however, it currently detects this and returns failure. + glDrawArrays(GL_POINTS, 0, 1); + + // Now unmap. This calls Buffer11::unmap() which uses mMappedStorage and would formerly UAF on + // accessing mMappedBuffer. + glUnmapBuffer(GL_COPY_READ_BUFFER); + + ASSERT_GL_ERROR(GL_INVALID_OPERATION); +} + +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(SimpleUniformBufferTest); +ANGLE_INSTANTIATE_TEST_ES3(SimpleUniformBufferTest); + } // namespace
Regression Test / PoC
diff --git a/src/tests/gl_tests/UniformBufferTest.cpp b/src/tests/gl_tests/UniformBufferTest.cpp
index 36d5d79..45de0dd 100644
--- a/src/tests/gl_tests/UniformBufferTest.cpp
+++ b/src/tests/gl_tests/UniformBufferTest.cpp
@@ -5007,4 +5007,107 @@
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(WebGL2UniformBufferTest);
ANGLE_INSTANTIATE_TEST_ES3(WebGL2UniformBufferTest);
+class SimpleUniformBufferTest : public ANGLETest<>
+{
+ protected:
+ SimpleUniformBufferTest()
+ {
+ setWindowWidth(128);
+ setWindowHeight(128);
+ setConfigRedBits(8);
+ setConfigGreenBits(8);
+ setConfigBlueBits(8);
+ setConfigAlphaBits(8);
+ }
+};
+
+// Test that maps a buffer as staging, then performs a draw inducing a garbage collect of the
+// staging buffer. This would formerly UAF, but now returns an invalid operation.
+// TODO(http://anglebug.com/505771894): Update test once we validate that drawing with a mapped
+// buffer is invalid.
+TEST_P(SimpleUniformBufferTest, MappedUBOStagingGarbageCollection)
+{
+ // Only applies to D3D11 backend.
+ ANGLE_SKIP_TEST_IF(!IsD3D11());
+
+ // Create a program with 10 uniform blocks.
+ constexpr char kVS[] = R"(#version 300 es
+layout(std140) uniform block0 { vec4 data0; };
+layout(std140) uniform block1 { vec4 data1; };
+layout(std140) uniform block2 { vec4 data2; };
+layout(std140) uniform block3 { vec4 data3; };
+layout(std140) uniform block4 { vec4 data4; };
+layout(std140) uniform block5 { vec4 data5; };
+layout(std140) uniform block6 { vec4 data6; };
+layout(std140) uniform block7 { vec4 data7; };
+layout(std140) uniform block8 { vec4 data8; };
+layout(std140) uniform block9 { vec4 data9; };
+
+void main() {
+ vec4 total = vec4(0.0);
+ total += data0;
+ total += data1;
+ total += data2;
+ total += data3;
+ total += data4;
+ total += data5;
+ total += data6;
+ total += data7;
+ total += data8;
+ total += data9;
+ gl_Position = vec4(total.xyz, 1.0);
+}
+ )";
+
+ constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+out vec4 color;
+void main() {
+ color = vec4(1.0);
+}
+)";
+
+ ANGLE_GL_PROGRAM(program, kVS, kFS);
+ glUseProgram(program);
+
+ // Create a buffer and bind it as a COPY_READ_BUFFER.
+ GLBuffer buffer;
+ glBindBuffer(GL_COPY_READ_BUFFER, buffer);
+
+ // Use GL_DYNAMIC_DRAW and provide data to ensure it uses SYSTEM_MEMORY storage
+ // and sets it as mLatestBufferStorage.
+ std::vector<uint8_t> data(1024, 0);
+ glBufferData(GL_COPY_READ_BUFFER, 1024, data.data(), GL_DYNAMIC_DRAW);
+
+ // Bind the buffer as a UBO to all 10 slots.
+ for (int i = 0; i < 10; ++i)
+ {
+ glBindBufferBase(GL_UNIFORM_BUFFER, i, buffer);
+ }
+
+ // Map the buffer for reading. Since it's SYSTEM_MEMORY and we are mapping for read,
+ // it should allocate a STAGING buffer.
+ void *ptr = glMapBufferRange(GL_COPY_READ_BUFFER, 0, 1024, GL_MAP_READ_BIT);
+ ASSERT_NE(ptr, nullptr);
+
+ // Now perform a draw call.
+ // This will trigger StateManager11::syncUniformBuffersForShader.
+ // It will iterate through the 10 slots.
+ // For each slot, it calls Buffer11::getConstantBufferRange -> getBufferStorage(UNIFORM) ->
+ // garbageCollection. garbageCollection(UNIFORM) calls checkForDeallocation(STAGING). On the 9th
+ // slot, STAGING's idleness will be 9, exceeding the threshold of 8. checkForDeallocation would
+ // then attempt to delete the STAGING storage because latestStorage (SYSTEM_MEMORY) != storage
+ // (STAGING), however, it currently detects this and returns failure.
+ glDrawArrays(GL_POINTS, 0, 1);
+
+ // Now unmap. This calls Buffer11::unmap() which uses mMappedStorage and would formerly UAF on
+ // accessing mMappedBuffer.
+ glUnmapBuffer(GL_COPY_READ_BUFFER);
+
+ ASSERT_GL_ERROR(GL_INVALID_OPERATION);
+}
+
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(SimpleUniformBufferTest);
+ANGLE_INSTANTIATE_TEST_ES3(SimpleUniformBufferTest);
+
} // namespace
Original Bug Report
Potential Use-After-Free in ANGLE D3D11 Buffer11 via STAGING storage deallocation
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.
Overview: A Use-After-Free vulnerability exists in ANGLE’s D3D11 backend when a mapped buffer’s staging storage is prematurely garbage-collected. This occurs if a read-only mapped buffer is bound to multiple uniform blocks and drawn, causing its idleness counter to exceed the deallocation threshold. An attacker could potentially exploit this from a compromised renderer to execute arbitrary code in the GPU process.
Affected files:
third_party/angle/src/libANGLE/renderer/d3d/d3d11/Buffer11.cppthird_party/angle/src/libANGLE/renderer/d3d/d3d11/Buffer11.hthird_party/angle/src/libANGLE/renderer/d3d/d3d11/StateManager11.cpp
Estimated timestamp from git blame: 2025-05-14
Vulnerability Details
In ANGLE’s D3D11 backend, Buffer11 manages multiple storage representations for a single GL buffer (e.g., SYSTEM_MEMORY, STAGING, UNIFORM). To optimize memory, Buffer11::garbageCollection periodically deallocates unused storages.
A Use-After-Free (UAF) condition can occur because the raw pointer Buffer11::mMappedStorage can be left dangling if its underlying storage object is reclaimed by garbage collection while the buffer is mapped. This relies on two distinct logic flaws:
- Priority Inversion in Mapping: When a buffer is mapped with
GL_MAP_READ_BITand itsmLatestBufferStorageisSYSTEM_MEMORY,Buffer11::mapRangecreates aSTAGINGstorage to provide CPU access. However, becauseBUFFER_USAGE_STAGING(index 1) does not have a strictly lower priority index thanBUFFER_USAGE_SYSTEM_MEMORY(index 0),mLatestBufferStorageremains pointing toSYSTEM_MEMORY.mMappedStorageis set to the newSTAGINGstorage. - Lack of UBO Mapping Validation: ANGLE’s draw-time validation (
ValidateDrawStates) correctly prevents drawing if vertex or index buffers are mapped, but fails to validate the mapped state of Uniform Buffers (UBOs).
If a draw call is issued while the buffer is mapped and bound to multiple uniform blocks, StateManager11::syncUniformBuffersForShader iterates through the uniform bindings. For each binding, it calls Buffer11::getConstantBufferRange, which ultimately triggers garbageCollection and checkForDeallocation(BUFFER_USAGE_STAGING).
Each iteration increments mIdleness[BUFFER_USAGE_STAGING]. If the shader uses more than 8 uniform block bindings (the default deallocation threshold), the idleness exceeds the threshold. Because mLatestBufferStorage is SYSTEM_MEMORY, latestStorage != storage evaluates to true, and SafeDelete(storage) destroys the STAGING object.
mMappedStorage is completely unaware of this deletion. A subsequent call to glUnmapBuffer will trigger the virtual function mMappedStorage->unmap(), resulting in a virtual call on a freed object.
Potential Reproduction Steps
Note: These are suggested steps based on static analysis. Our tooling does not yet execute code to provide a working proof-of-concept.
- From a compromised renderer process, establish a WebGL2/GLES3 context that uses the passthrough command decoder (default on Windows for ANGLE D3D11).
- Create a GL buffer and initialize it with
GL_DYNAMIC_DRAWorGL_STREAM_DRAW. This sets the internal D3D usage to dynamic, forcing ANGLE to useSYSTEM_MEMORYas themLatestBufferStorage. - Compile a shader program that actively uses at least 9 uniform block bindings.
- Bind the buffer to all 9 uniform block binding points (
glBindBufferBase(GL_UNIFORM_BUFFER, 0 through 8)). - Map the buffer for reading via
MapBufferRangewithGL_MAP_READ_BIT. ANGLE allocatesSTAGINGstorage and pointsmMappedStorageto it. - Issue a
DrawArrayscommand. ANGLE fails to validate that the UBO is mapped, allowing the draw to proceed. - The loop in
syncUniformBuffersForShaderfetches the storage for each of the 9 bindings, callinggarbageCollection9 times. - On the 9th iteration,
mIdleness[STAGING]reaches 9, exceeding the threshold of 8. TheSTAGINGstorage isSafeDeleted. - (Optional) Spray the GPU process heap to reclaim the freed
NativeStorageobject and counterfeit its vtable. - Unmap the buffer (
glUnmapBuffer). The GPU process executesmMappedStorage->unmap(), dereferencing the attacker’s payload and achieving Remote Code Execution in the GPU process.
Suggested Fix
There are multiple ways to address this issue:
- Prevent Deallocation of Mapped Storage: Update
Buffer11::checkForDeallocationto explicitly prevent deleting the storage if it is currently mapped:if (storage != nullptr && storage != mMappedStorage && mIdleness[usage] > mDeallocThresholds[usage]) { // ... existing deletion logic ... } - Validate UBO Mapped State: Update ANGLE’s top-level API validation (e.g.,
ValidateProgramDrawStates) to return an error (likekBufferMapped) if any active Uniform Buffer Object is currently mapped, bringing UBOs in line with vertex and index buffer validation. - Modernize Pointers: ANGLE relies extensively on raw pointers. Migrating ANGLE’s pointers (like
mMappedStorage) tobase::raw_ptrwould mitigate the exploitability of this and similar UAFs by transforming them into safe, non-exploitable crashes via MiraclePtr/BackupRefPtr.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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.