Medium chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in ANGLE
DescriptionUninitialized Use in ANGLE
ComponentANGLE
Bug ClassUninitialized Memory
Tracker498827800
Fix commit671687dcca43 (angle/angle) +157/-62
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • src/libANGLE/Buffer.cpp
  • src/libANGLE/angletypes.h
  • src/libANGLE/renderer/BufferImpl.cpp
  • src/libANGLE/renderer/BufferImpl.h
  • src/libANGLE/renderer/BufferImpl_mock.h
  • src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
From 671687dcca438df06a1031f046f950221dcd013c Mon Sep 17 00:00:00 2001
From: Amirali Abdolrashidi <[email protected]>
Date: Thu, 28 May 2026 17:36:39 -0700
Subject: [PATCH] Move robust init clear to the backends

  Currently, to clear the buffers for robust resource initialization,
ANGLE allocates a zero-filled buffer in the frontend and uses it to
fill the buffer if there is no initial data (as if glBufferSubData()
has been called with an all-zero buffer). However, the backend may
allocate a different size due to padding or alignment requirements.
To prevent uninitialized space, it is more accurate for the backend
to clear the allocated memory.

In this change, the frontend robust init is moved to the backends.
After that, each backend can optimize the clearing op independently.

* Added a new object as arg to BufferImpl::setDataWithUsageFlags() and
  setData() for all backends: gl::ZeroFillRequired

  * It indicates whether the buffer content should be zeroed in case
    of unspecified data.

* Moved the robust clear from the frontend to the backends.

  * It is performed in case gl::ZeroFillRequired is set, one of its
    conditions being that it should not be an external buffer.

    * (Similar to the original method where the clear was in
      bufferDataImpl() and not in bufferExternalDataImpl())

* Added UNREACHABLE() to BufferVk::setData(), as it is not used
  in the Vulkan backend.

Bug: chromium:498827800
Change-Id: I443e65174adb0b89debe6135b5ee2b45d9026e8b
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7884236
Commit-Queue: Amirali Abdolrashidi <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
Reviewed-by: Charlie Lao <[email protected]>
---

diff --git a/src/libANGLE/Buffer.cpp b/src/libANGLE/Buffer.cpp
index 8470826..590dc6a 100644
--- a/src/libANGLE/Buffer.cpp
+++ b/src/libANGLE/Buffer.cpp
@@ -189,9 +189,18 @@
                                             GLbitfield flags,
                                             gl::BufferStorage bufferStorage)
 {
+    // If we are using robust resource init, make sure the buffer starts cleared.
+    // Note: The context is checked for nullptr because of some testing code.
+    gl::ZeroFillRequired zeroFillRequired =
+        (context != nullptr && context->isRobustResourceInitEnabled() && clientBuffer == nullptr &&
+         data == nullptr && size > 0)
+            ? gl::ZeroFillRequired::Yes
+            : gl::ZeroFillRequired::No;
+
     rx::BufferFeedback feedback;
-    angle::Result result = mImpl->setDataWithUsageFlags(context, target, clientBuffer, data, size,
-                                                        usage, flags, bufferStorage, &feedback);
+    angle::Result result =
+        mImpl->setDataWithUsageFlags(context, target, clientBuffer, data, size, usage, flags,
+                                     bufferStorage, &feedback, zeroFillRequired);
 
     applyImplFeedback(context, feedback);
 
@@ -215,8 +224,6 @@
                                      GLbitfield flags,
                                      BufferStorage bufferStorage)
 {
-    const void *dataForImpl = data;
-
     if (mState.isMapped())
     {
         // Per the OpenGL ES 3.0 spec, buffers are implicity unmapped when a call to
@@ -230,19 +237,8 @@
         ANGLE_TRY(unmap(context, &dontCare));
     }
 
-    // If we are using robust resource init, make sure the buffer starts cleared.
-    // Note: the Context is checked for nullptr because of some testing code.
-    // TODO(jmadill): Investigate lazier clearing.
-    if (context && context->isRobustResourceInitEnabled() && !data && size > 0)
-    {
-        const angle::MemoryBuffer *scratchBuffer = nullptr;
-        ANGLE_CHECK_GL_ALLOC(
-            context, context->getZeroFilledBuffer(static_cast<size_t>(size), &scratchBuffer));
-        dataForImpl = scratchBuffer->data();
-    }
-
-    ANGLE_TRY(setDataWithUsageFlags(context, target, nullptr, dataForImpl, size, usage, flags,
-                                    bufferStorage));
+    ANGLE_TRY(
+        setDataWithUsageFlags(context, target, nullptr, data, size, usage, flags, bufferStorage));
 
     bool wholeBuffer = size == mState.mSize;
 
diff --git a/src/libANGLE/angletypes.h b/src/libANGLE/angletypes.h
index f9aa01c..2c53b6c 100644
--- a/src/libANGLE/angletypes.h
+++ b/src/libANGLE/angletypes.h
@@ -1631,6 +1631,14 @@
     Immutable,
 };
 
+enum class ZeroFillRequired : bool
+{
+    // The buffer should remain unchanged after initialization if there is no specified data.
+    No,
+    // The buffer should be zero-filled after initialization if there is no specified data.
+    Yes,
+};
+
 }  // namespace gl
 
 #endif  // LIBANGLE_ANGLETYPES_H_
diff --git a/src/libANGLE/renderer/BufferImpl.cpp b/src/libANGLE/renderer/BufferImpl.cpp
index 0ede6fb..0aeb9dd 100644
--- a/src/libANGLE/renderer/BufferImpl.cpp
+++ b/src/libANGLE/renderer/BufferImpl.cpp
@@ -28,9 +28,10 @@
                                                 gl::BufferUsage usage,
                                                 GLbitfield flags,
                                                 gl::BufferStorage bufferStorage,
-                                                BufferFeedback *feedback)
+                                                BufferFeedback *feedback,
+                                                gl::ZeroFillRequired zeroFillRequired)
 {
-    return setData(context, target, data, size, usage, feedback);
+    return setData(context, target, data, size, usage, feedback, zeroFillRequired);
 }
 
 angle::Result BufferImpl::onLabelUpdate(const gl::Context *context)
diff --git a/src/libANGLE/renderer/BufferImpl.h b/src/libANGLE/renderer/BufferImpl.h
index eebc237..855ad44 100644
--- a/src/libANGLE/renderer/BufferImpl.h
+++ b/src/libANGLE/renderer/BufferImpl.h
@@ -53,13 +53,15 @@
                                                 gl::BufferUsage usage,
                                                 GLbitfield flags,
                                                 gl::BufferStorage bufferStorage,
-                                                BufferFeedback *feedback);
+                                                BufferFeedback *feedback,
+                                                gl::ZeroFillRequired zeroFillRequired);
     virtual angle::Result setData(const gl::Context *context,
                                   gl::BufferBinding target,
                                   const void *data,
                                   size_t size,
                                   gl::BufferUsage usage,
-                                  BufferFeedback *feedback)     = 0;
+                                  BufferFeedback *feedback,
+                                  gl::ZeroFillRequired zeroFillRequired) = 0;
     virtual angle::Result setSubData(const gl::Context *context,
                                      gl::BufferBinding target,
                                      const void *data,
diff --git a/src/libANGLE/renderer/BufferImpl_mock.h b/src/libANGLE/renderer/BufferImpl_mock.h
index 8079e4c..625920e 100644
--- a/src/libANGLE/renderer/BufferImpl_mock.h
+++ b/src/libANGLE/renderer/BufferImpl_mock.h
@@ -22,13 +22,14 @@
     MockBufferImpl() : BufferImpl(mMockState) {}
     ~MockBufferImpl() { destructor(); }
 
-    MOCK_METHOD6(setData,
+    MOCK_METHOD7(setData,
                  angle::Result(const gl::Context *,
                                gl::BufferBinding,
                                const void *,
                                size_t,
                                gl::BufferUsage,
-                               BufferFeedback *));
+                               BufferFeedback *,
+                               gl::ZeroFillRequired));
     MOCK_METHOD6(setSubData,
                  angle::Result(const gl::Context *,
                                gl::BufferBinding,
diff --git a/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp b/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
index 6dff1d3..567adde 100644
--- a/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/Buffer11.cpp
@@ -353,10 +353,21 @@
                                 const void *data,
                                 size_t size,
                                 gl::BufferUsage usage,
-                                BufferFeedback *feedback)
+                                BufferFeedback *feedback,
+                                gl::ZeroFillRequired zeroFillRequired)
 {
+    const void *dataForImpl = data;
+    if (zeroFillRequired == gl::ZeroFillRequired::Yes)
+    {
+        const angle::MemoryBuffer *scratchBuffer = nullptr;
+        ANGLE_CHECK_GL_ALLOC(
+            GetImplAs<Context11>(context),
+            context->getZeroFilledBuffer(static_cast<size_t>(size), &scratchBuffer));
+        dataForImpl = scratchBuffer->data();
+    }
+
     updateD3DBufferUsage(context, usage, feedback);
-    return setSubData(context, target, data, size, 0, feedback);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/perf_tests/IndexDataManagerTest.cpp b/src/tests/perf_tests/IndexDataManagerTest.cpp
index 8084612..a0b0f85 100644
--- a/src/tests/perf_tests/IndexDataManagerTest.cpp
+++ b/src/tests/perf_tests/IndexDataManagerTest.cpp
@@ -91,7 +91,8 @@
                           const void *data,
                           size_t size,
                           gl::BufferUsage,
-                          rx::BufferFeedback *feedback) override
+                          rx::BufferFeedback *feedback,
+                          gl::ZeroFillRequired zeroFillRequired) override
     {
         mData.resize(size);
         if (data && size > 0)
Loading diff…

Original Bug Report

reported by [email protected]

Cross-origin GPU memory leak via uninitialized vertex buffer padding in ANGLE

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: On AMD and Samsung GPUs, ANGLE’s Vulkan backend adds a 2KB uninitialized padding to vertex buffers to work around driver issues. Because this padding is included in the Vulkan buffer binding size, a malicious WebGL application can read the uninitialized padding using out-of-bounds vertex fetches. This potentially allows an attacker to leak stale GPU memory from other origins.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_renderer.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp

Estimated timestamp from git blame: 2025-11-05

Description

A potential cross-origin GPU memory leak exists in ANGLE’s Vulkan backend for AMD and Samsung Exynos GPUs. To work around driver bugs on these platforms, ANGLE artificially pads vertex buffers by 2048 bytes (mMaxVertexAttribStride). However, this padding region is never initialized and contains raw, stale GPU memory.

Because ANGLE includes this padded size when binding vertex buffers to the Vulkan API, and relies on the GPU’s hardware robust buffer access rather than validating indices on the CPU, an attacker can safely index past their user-requested buffer size and read the uninitialized padding. This bypasses WebGL’s robust resource initialization guarantees and leaks cross-origin memory.

Root Cause Analysis

  1. Padding Applied: In vk_renderer.cpp, padBuffersToMaxVertexAttribStride is enabled for AMD and Samsung GPUs, setting mMaxVertexAttribStride to 2048. When allocating buffers, BufferHelper::initSuballocation inflates the size by this amount.
  2. Incomplete Initialization: WebGL enforces robust resource initialization. Buffer::bufferDataImpl handles this by creating a zero-filled scratch buffer, but it only sizes it to the user’s requested length. The 2048-byte padding appended by the backend is skipped and remains uninitialized physical GPU memory (which may contain data from other processes/origins).
  3. CPU Validation Bypassed: During Context creation, if the Vulkan driver supports robustBufferAccessBehaviorKHR, ANGLE disables its own CPU-side bounds checking (mBufferAccessValidationEnabled = false).
  4. Padded Size Bound to Vulkan: When preparing a draw call, VertexArrayVk::syncDirtyEnabledNonStreamingAttrib computes the bound size as user_size + 2048. This padded size is passed down to vkCmdBindVertexBuffers2EXT.
  5. Hardware Validation Fooled: When an out-of-bounds fetch occurs, the GPU hardware checks the requested index against the Vulkan bound size. Since the bound size includes the padding, the hardware considers the out-of-bounds read valid, successfully feeding stale memory into the shader.

Potential Attacker Steps

Note: These are suggested/potential steps derived from code analysis; our tooling does not currently have the ability to run or verify live exploits.

  1. Initialize a WebGL context on a vulnerable device (AMD or Samsung GPU).
  2. Create a vertex buffer with a specific size S using gl.bufferData.
  3. Configure a WebGL Transform Feedback object to capture the outputs of a vertex shader.
  4. Issue a gl.drawArrays or gl.drawElements call that intentionally fetches vertex indices located past the buffer size S, but within the S + 2048 boundary.
  5. Because CPU validation is disabled and the hardware considers the read in-bounds, the vertex shader will process the uninitialized GPU memory.
  6. Read back the leaked memory from the Transform Feedback buffer using gl.getBufferSubData into a JavaScript TypedArray to exfiltrate the data.

Suggested Fix

Ensure that the padding region is zero-initialized when robust resource initialization is required. This can be done by:

  1. Modifying the ANGLE frontend (Buffer::bufferDataImpl) to query and include the backend padding size when allocating the zero-filled scratch buffer.
  2. Alternatively, explicitly issuing a Vulkan vkCmdFillBuffer or similar zeroing command on the padded region [user_size, user_size + padding] within the Vulkan backend when a suballocation is initialized and robust access is requested.

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.

View on issue tracker