Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker501563323
Fix commitba4afac4c523 (angle/angle) +112/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
SimpleUniformBufferTest
src/tests/gl_tests/UniformBufferTest.cpp
modified

Files Changed

  • src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
  • src/tests/gl_tests/UniformBufferTest.cpp
From ba4afac4c523d50f5639014cc50f35052f125913 Mon Sep 17 00:00:00 2001
From: Antonio Maiorano <amaiorano@google.com>
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 <geofflang@chromium.org>
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
---

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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cpp
  • third_party/angle/src/libANGLE/renderer/d3d/d3d11/Buffer11.h
  • third_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:

  1. Priority Inversion in Mapping: When a buffer is mapped with GL_MAP_READ_BIT and its mLatestBufferStorage is SYSTEM_MEMORY, Buffer11::mapRange creates a STAGING storage to provide CPU access. However, because BUFFER_USAGE_STAGING (index 1) does not have a strictly lower priority index than BUFFER_USAGE_SYSTEM_MEMORY (index 0), mLatestBufferStorage remains pointing to SYSTEM_MEMORY. mMappedStorage is set to the new STAGING storage.
  2. 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.

  1. From a compromised renderer process, establish a WebGL2/GLES3 context that uses the passthrough command decoder (default on Windows for ANGLE D3D11).
  2. Create a GL buffer and initialize it with GL_DYNAMIC_DRAW or GL_STREAM_DRAW. This sets the internal D3D usage to dynamic, forcing ANGLE to use SYSTEM_MEMORY as the mLatestBufferStorage.
  3. Compile a shader program that actively uses at least 9 uniform block bindings.
  4. Bind the buffer to all 9 uniform block binding points (glBindBufferBase(GL_UNIFORM_BUFFER, 0 through 8)).
  5. Map the buffer for reading via MapBufferRange with GL_MAP_READ_BIT. ANGLE allocates STAGING storage and points mMappedStorage to it.
  6. Issue a DrawArrays command. ANGLE fails to validate that the UBO is mapped, allowing the draw to proceed.
  7. The loop in syncUniformBuffersForShader fetches the storage for each of the 9 bindings, calling garbageCollection 9 times.
  8. On the 9th iteration, mIdleness[STAGING] reaches 9, exceeding the threshold of 8. The STAGING storage is SafeDeleted.
  9. (Optional) Spray the GPU process heap to reclaim the freed NativeStorage object and counterfeit its vtable.
  10. Unmap the buffer (glUnmapBuffer). The GPU process executes mMappedStorage->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:

  1. Prevent Deallocation of Mapped Storage: Update Buffer11::checkForDeallocation to explicitly prevent deleting the storage if it is currently mapped:
    if (storage != nullptr && storage != mMappedStorage && mIdleness[usage] > mDeallocThresholds[usage]) {
        // ... existing deletion logic ...
    }
    
  2. Validate UBO Mapped State: Update ANGLE’s top-level API validation (e.g., ValidateProgramDrawStates) to return an error (like kBufferMapped) if any active Uniform Buffer Object is currently mapped, bringing UBOs in line with vertex and index buffer validation.
  3. Modernize Pointers: ANGLE relies extensively on raw pointers. Migrating ANGLE’s pointers (like mMappedStorage) to base::raw_ptr would 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.

View on issue tracker