Medium CVSS 4.3 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in ANGLE
DescriptionUninitialized Use in ANGLE
ComponentANGLE
Bug ClassUninitialized Memory
Tracker498827800
Fix commitdbfb7aa2898b (angle) +165/-46
Related commits671687dcca43 previously mapped (not the fix) · angle
50b36f53a772 candidate · angle
CWECWE-457 (Use of uninitialized)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-02 (per vendor advisory)
Fix landed2026-06-05 (public git)
Disclosed2026-07-29
Patch gap54 days — fix public in source before the advisory
Re-mapped on 2026-10-03: our pipeline had picked the earliest commit citing this bug (671687dcca43), which an audit found was a preparatory/helper change, not the fix. This page now points to the commit whose diff removes the bug. No root-cause analysis is published for this CVE.

Changed Functions

FunctionChangeNotes
RobustResourceInitTestES31
src/tests/gl_tests/RobustResourceInitTest.cpp
modified
RobustResourceInitWithPaddingTest
src/tests/gl_tests/RobustResourceInitTest.cpp
modified

Files Changed

  • src/libANGLE/renderer/vulkan/BufferVk.cpp
  • src/libANGLE/renderer/vulkan/ContextVk.cpp
  • src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • src/libANGLE/renderer/vulkan/vk_helpers.h
  • src/libANGLE/renderer/vulkan/vk_renderer.cpp
  • src/libANGLE/renderer/vulkan/vk_utils.cpp
  • src/libANGLE/renderer/vulkan/vk_utils.h
  • src/tests/gl_tests/RobustResourceInitTest.cpp
diff --git a/src/libANGLE/renderer/vulkan/BufferVk.cpp b/src/libANGLE/renderer/vulkan/BufferVk.cpp
index 675cc0c..7dcfda0 100644
--- a/src/libANGLE/renderer/vulkan/BufferVk.cpp
+++ b/src/libANGLE/renderer/vulkan/BufferVk.cpp
@@ -485,19 +485,7 @@
 
         return angle::Result::Continue;
     }
-
-    const void *dataForImpl = data;
-    if (zeroFillRequired == gl::ZeroFillRequired::Yes)
-    {
-        const angle::MemoryBuffer *scratchBuffer = nullptr;
-        ANGLE_VK_CHECK(contextVk,
-                       context->getZeroFilledBuffer(static_cast<size_t>(size), &scratchBuffer),
-                       VK_ERROR_OUT_OF_HOST_MEMORY);
-        dataForImpl = scratchBuffer->data();
-    }
-
-    return setDataWithMemoryType(context, target, dataForImpl, size, memoryPropertyFlags, usage,
-                                 feedback);
+    return setDataWithMemoryType(context, target, data, size, memoryPropertyFlags, usage, feedback);
 }
 
 angle::Result BufferVk::setData(const gl::Context *context,
diff --git a/src/libANGLE/renderer/vulkan/ContextVk.cpp b/src/libANGLE/renderer/vulkan/ContextVk.cpp
index 47205db..b040e72 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.cpp
+++ b/src/libANGLE/renderer/vulkan/ContextVk.cpp
@@ -7092,10 +7092,15 @@
                                                            alignment, bufferUsageType, pool);
     if (ANGLE_LIKELY(result == VK_SUCCESS))
     {
-        if (mRenderer->getFeatures().allocateNonZeroMemory.enabled)
+        if (isRobustResourceInitEnabled())
+        {
+            ANGLE_TRY(bufferHelper->initializeRobustMemory(
+                this, GetDefaultBufferUsageFlags(mRenderer), bufferHelper->getSize()));
+        }
+        else if (mRenderer->getFeatures().allocateNonZeroMemory.enabled)
         {
             ANGLE_TRY(bufferHelper->initializeNonZeroMemory(
-                this, GetDefaultBufferUsageFlags(mRenderer), allocationSize));
+                this, GetDefaultBufferUsageFlags(mRenderer), bufferHelper->getSize()));
         }
 
         return angle::Result::Continue;
@@ -7141,11 +7146,16 @@
     // returned.
     ANGLE_VK_CHECK(this, result == VK_SUCCESS, result);
 
-    // Initialize with non-zero value if needed.
-    if (mRenderer->getFeatures().allocateNonZeroMemory.enabled)
+    // Initialize with value if needed.
+    if (isRobustResourceInitEnabled())
+    {
+        ANGLE_TRY(bufferHelper->initializeRobustMemory(this, GetDefaultBufferUsageFlags(mRenderer),
+                                                       bufferHelper->getSize()));
+    }
+    else if (mRenderer->getFeatures().allocateNonZeroMemory.enabled)
     {
         ANGLE_TRY(bufferHelper->initializeNonZeroMemory(this, GetDefaultBufferUsageFlags(mRenderer),
-                                                        allocationSize));
+                                                        bufferHelper->getSize()));
     }
 
     return angle::Result::Continue;
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
index e1f4396..c1856bd 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
@@ -4737,7 +4737,7 @@
 
     if (renderer->getFeatures().allocateNonZeroMemory.enabled)
     {
-        ANGLE_TRY(initializeNonZeroMemory(context, createInfo->usage, createInfo->size));
+        ANGLE_TRY(initializeNonZeroMemory(context, createInfo->usage, sizeOut));
     }
 
     return angle::Result::Continue;
@@ -4898,10 +4898,27 @@
     mCurrentReadStages       = 0;
 }
 
+angle::Result BufferHelper::initializeRobustMemory(ErrorContext *context,
+                                                   VkBufferUsageFlags usage,
+                                                   VkDeviceSize size)
+{
+    constexpr int kInitZeroValue = 0;
+    return initializeMemoryWithValueImpl(context, usage, size, kInitZeroValue);
+}
+
 angle::Result BufferHelper::initializeNonZeroMemory(ErrorContext *context,
                                                     VkBufferUsageFlags usage,
                                                     VkDeviceSize size)
 {
+    constexpr int kInitNonZeroValue = 55;
+    return initializeMemoryWithValueImpl(context, usage, size, kInitNonZeroValue);
+}
+
+angle::Result BufferHelper::initializeMemoryWithValueImpl(ErrorContext *context,
+                                                          VkBufferUsageFlags usage,
+                                                          VkDeviceSize size,
+                                                          const int value)
+{
     Renderer *renderer = context->getRenderer();
 
     // This memory can't be mapped, so the buffer must be marked as a transfer destination so we
@@ -4910,9 +4927,8 @@
     if (!isHostVisible() && (usage & VK_BUFFER_USAGE_TRANSFER_DST_BIT) != 0)
     {
         ASSERT((usage & VK_BUFFER_USAGE_TRANSFER_DST_BIT) != 0);
-        // Staging buffer memory is non-zero-initialized in 'init'.
         StagingBuffer stagingBuffer;
-        ANGLE_TRY(stagingBuffer.init(context, size, StagingUsage::Both));
+        ANGLE_TRY(stagingBuffer.init(context, size, StagingUsage::Both, value));
 
         // Queue a DMA copy.
         VkBufferCopy copyRegion = {};
@@ -4928,7 +4944,7 @@
         commandBuffer.copyBuffer(stagingBuffer.getBuffer(), getBuffer(), 1, &copyRegion);
 
         renderer->insertSubmitDebugMarkerInCommandBuffer(commandBuffer,
-                                                         QueueSubmitReason::InitNonZeroMemory);
+                                                         QueueSubmitReason::InitializeMemory);
         ANGLE_VK_TRY(context, commandBuffer.end());
 
         QueueSerial queueSerial;
@@ -4943,11 +4959,9 @@
     }
     else if (isHostVisible())
     {
-        // Can map the memory.
-        // Pick an arbitrary value to initialize non-zero memory for sanitization.
-        constexpr int kNonZeroInitValue = 55;
+        // Can map the memory to initialize non-zero memory for sanitization.
         uint8_t *mapPointer             = mSuballocation.getMappedMemory();
-        memset(mapPointer, kNonZeroInitValue, static_cast<size_t>(getSize()));
+        memset(mapPointer, value, static_cast<size_t>(size));
         if (!isCoherent())
         {
             mSuballocation.flush(renderer);
@@ -6236,7 +6250,10 @@
         // If format is compressed, set its contents through buffer copies.
 
         // The staging buffer memory is non-zero-initialized in 'init'.
-        ANGLE_TRY(stagingBuffer.init(context, size, StagingUsage::Write));
+        // Pick an arbitrary value to initialize non-zero memory for sanitization.
+        // Note that 0x3F3F3F3F as float is about 0.75.
+        constexpr int kInitNonZeroValue = 0x3F;
+        ANGLE_TRY(stagingBuffer.init(context, size, StagingUsage::Write, kInitNonZeroValue));
 
         for (LevelIndex level(0); level < LevelIndex(mLevelCount); ++level)
         {
@@ -6303,7 +6320,7 @@
     }
 
     renderer->insertSubmitDebugMarkerInCommandBuffer(commandBuffer,
-                                                     QueueSubmitReason::InitNonZeroMemory);
+                                                     QueueSubmitReason::InitializeMemory);
     ANGLE_VK_TRY(context, commandBuffer.end());
 
     QueueSerial queueSerial;
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.h b/src/libANGLE/renderer/vulkan/vk_helpers.h
index ced685b..a9a23a9 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.h
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.h
@@ -1021,6 +1021,10 @@
         mDescriptorSetCacheManager.addKey(sharedCacheKey);
     }
 
+    angle::Result initializeRobustMemory(ErrorContext *context,
+                                         VkBufferUsageFlags usage,
+                                         VkDeviceSize size);
+
     angle::Result initializeNonZeroMemory(ErrorContext *context,
                                           VkBufferUsageFlags usage,
                                           VkDeviceSize size);
@@ -1082,6 +1086,11 @@
                                               VkDeviceSize actualDataSize,
                                               VkDeviceSize *offsetOut);
 
+    angle::Result initializeMemoryWithValueImpl(ErrorContext *context,
+                                                VkBufferUsageFlags usage,
+                                                VkDeviceSize size,
+                                                const int value);
+
     // Suballocation object.
     BufferSuballocation mSuballocation;
     // This normally is invalid. We always use the BufferBlock's buffer and offset combination. But
diff --git a/src/libANGLE/renderer/vulkan/vk_renderer.cpp b/src/libANGLE/renderer/vulkan/vk_renderer.cpp
index e7b0b11..cb51036 100644
--- a/src/libANGLE/renderer/vulkan/vk_renderer.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_renderer.cpp
@@ -140,8 +140,7 @@
     {QueueSubmitReason::ForceSubmitStagedTexture,
      "Queue submission imminent due to staged texture updates"},
     {QueueSubmitReason::DrawOverlay, "Queue submission imminent due to drawing overlay"},
-    {QueueSubmitReason::InitNonZeroMemory,
-     "Queue submission imminent due to initializing non-zero memory"},
+    {QueueSubmitReason::InitializeMemory, "Queue submission imminent due to initializing memory"},
 }};
 }  // namespace
 
diff --git a/src/libANGLE/renderer/vulkan/vk_utils.cpp b/src/libANGLE/renderer/vulkan/vk_utils.cpp
index 765b3e2..4779d79 100644
--- a/src/libANGLE/renderer/vulkan/vk_utils.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_utils.cpp
@@ -46,10 +46,6 @@
 {
 namespace
 {
-// Pick an arbitrary value to initialize non-zero memory for sanitization.  Note that 0x3F3F3F3F
-// as float is about 0.75.
-constexpr int kNonZeroInitValue = 0x3F;
-
 VkImageUsageFlags GetStagingBufferUsageFlags(vk::StagingUsage usage)
 {
     switch (usage)
@@ -520,7 +516,10 @@
     mSize = 0;
 }
 
-angle::Result StagingBuffer::init(ErrorContext *context, VkDeviceSize size, StagingUsage usage)
+angle::Result StagingBuffer::init(ErrorContext *context,
+                                  VkDeviceSize size,
+                                  StagingUsage usage,
+                                  const int initValue)
 {
     VkBufferCreateInfo createInfo    = {};
     createInfo.sType                 = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
@@ -545,15 +544,7 @@
                                         &memoryTypeIndex, &mBuffer, &mAllocation));
     mSize = static_cast<size_t>(size);
 
-    // Wipe memory to an invalid value when the 'allocateNonZeroMemory' feature is enabled. The
-    // invalid values ensures our testing doesn't assume zero-initialized memory.
-    if (renderer->getFeatures().allocateNonZeroMemory.enabled)
-    {
-        ANGLE_TRY(InitMappableAllocation(context, allocator, &mAllocation, size, kNonZeroInitValue,
-                                         requiredFlags));
-    }
-
-    return angle::Result::Continue;
+    return InitMappableAllocation(context, allocator, &mAllocation, size, initValue, requiredFlags);
 }
 
 void StagingBuffer::release(ContextVk *contextVk)
diff --git a/src/libANGLE/renderer/vulkan/vk_utils.h b/src/libANGLE/renderer/vulkan/vk_utils.h
index 9c2b533..1384864 100644
--- a/src/libANGLE/renderer/vulkan/vk_utils.h
+++ b/src/libANGLE/renderer/vulkan/vk_utils.h
@@ -581,7 +581,10 @@
     void collectGarbage(Renderer *renderer, const QueueSerial &queueSerial);
     void destroy(Renderer *renderer);
 
-    angle::Result init(ErrorContext *context, VkDeviceSize size, StagingUsage usage);
+    angle::Result init(ErrorContext *context,
+                       VkDeviceSize size,
+                       StagingUsage usage,
+                       const int initValue);
 
     Buffer &getBuffer() { return mBuffer; }
     const Buffer &getBuffer() const { return mBuffer; }
@@ -1724,7 +1727,7 @@
     CopySurfaceImageToBuffer,
     ForeignImageRelease,
     ImageUseThenReleaseToExternal,
-    InitNonZeroMemory,
+    InitializeMemory,
     TextureReformatToRenderable,
     CopyTextureOnCPU,
     GenerateMipmapOnCPU,
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 0c5d42c..3369af2 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -312,6 +312,9 @@
 class RobustResourceInitTestES31 : public RobustResourceInitTest
 {};
 
+class RobustResourceInitWithPaddingTest : public RobustResourceInitTest
+{};
+
 // Robust resource initialization is not based on hardware support or native extensions, check that
 // it only works on the implemented renderers
 TEST_P(RobustResourceInitTest, ExpectedRendererSupport)
@@ -419,6 +422,98 @@
     EXPECT_GL_TRUE(initState);
 }
 
+// Tests that the padding is also cleared out for a new uninitialized buffer in Vulkan.
+TEST_P(RobustResourceInitWithPaddingTest, BufferDataWithinPaddingRange)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    constexpr size_t kWidth  = 4;
+    constexpr size_t kHeight = 4;
+
+    GLTexture readTex;
+    glBindTexture(GL_TEXTURE_2D, readTex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, readTex, 0);
+
+    // The array buffers are initialized to fill the whole FBO with a single color.
+    std::vector<float> colorInitData(128 * 1024 / sizeof(float), 1.0f);
+    std::vector<float> posData = {
+        0.0f,  0.0f,  -1.0f, -1.0f, -1.0f, -0.5f, -1.0f, 0.0f,  -1.0f, 0.5f,  -1.0f, 1.0f,
+        -0.5f, 1.0f,  0.0f,  1.0f,  0.5f,  1.0f,  1.0f,  1.0f,  1.0f,  0.5f,  1.0f,  0.0f,
+        1.0f,  -0.5f, 1.0f,  -1.0f, 0.5f,  -1.0f, 0.0f,  -1.0f, -0.5f, -1.0f, -1.0f, -1.0f,
+    };
+
+    GLBuffer bufferColorData1;
+    glBindBuffer(GL_ARRAY_BUFFER, bufferColorData1);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * colorInitData.size(), colorInitData.data(),
+                 GL_STATIC_DRAW);
+
+    GLBuffer bufferPos;
+    glBindBuffer(GL_ARRAY_BUFFER, bufferPos);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * posData.size(), posData.data(), GL_STATIC_DRAW);
+
+    constexpr char kVS[] = R"(
+        attribute vec2 position;
+        attribute float dataIn;
+        varying vec4 colorOut;
+        void main() {
+            gl_Position = vec4(position, 0.0, 1.0);
+            // Return blue if input data is 0.0, and yellow otherwise.
+            colorOut = dataIn == 0.0 ? vec4(0.0, 0.0, 1.0, 1.0)
+                                     : vec4(1.0, 1.0, 0.0, 1.0);
+        })";
+    constexpr char kFS[] = R"(
+        varying mediump vec4 colorOut;
+        void main() {
+            gl_FragColor = colorOut;
+        })";
+
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    glUseProgram(program);
+
+    GLint dataInLoc = glGetAttribLocation(program, "dataIn");
+    ASSERT_NE(-1, dataInLoc);
+    GLint posLoc = glGetAttribLocation(program, "position");
+    ASSERT_NE(-1, posLoc);
+
+    glBindBuffer(GL_ARRAY_BUFFER, bufferColorData1);
+    glVertexAttribPointer(dataInLoc, 1, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(dataInLoc);
+
+    glBindBuffer(GL_ARRAY_BUFFER, bufferPos);
+    glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(posLoc);
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
+    glDrawArrays(GL_TRIANGLE_FAN, 0, 18);
+    ASSERT_GL_NO_ERROR();
+
+    EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::yellow);
+    bufferColorData1.reset();
+    glFinish();
+
+    // At this point, the VMA suballocation for the color data buffer should be deleted, although
+    // the underlying buffer block remains. A much smaller buffer is now allocated for the color
+    // data. However, it is not initialized with data. Therefore, with robust resource init, it is
+    // expected for the whole buffer and its padding to be initialized to 0. Therefore, even if the
+    // draw cuts into the padding space in the backend, it should still have 0 in it.
+    GLBuffer bufferColorData2;
+    glBindBuffer(GL_ARRAY_BUFFER, bufferColorData2);
+    glBufferData(GL_ARRAY_BUFFER, 4, nullptr, GL_STATIC_DRAW);
+    glVertexAttribPointer(dataInLoc, 1, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(dataInLoc);
+    glBindBuffer(GL_ARRAY_BUFFER, bufferPos);
+    glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(posLoc);
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
+    glDrawArrays(GL_TRIANGLE_FAN, 0, 18);
+    ASSERT_GL_NO_ERROR();
+
+    EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::blue);
+}
+
 // Regression test for passing a zero size init buffer with the extension.
 TEST_P(RobustResourceInitTest, BufferDataZeroSize)
 {
@@ -3428,4 +3523,11 @@
 ANGLE_INSTANTIATE_TEST_ES31_AND(RobustResourceInitTestES31,
                                 ES31_VULKAN().enable(Feature::AllocateNonZeroMemory));
 
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(RobustResourceInitWithPaddingTest);
+ANGLE_INSTANTIATE_TEST(RobustResourceInitWithPaddingTest,
+                       ES3_VULKAN().enable(Feature::PadBuffersToMaxVertexAttribStride),
+                       ES3_VULKAN_SWIFTSHADER().enable(Feature::PadBuffersToMaxVertexAttribStride),
+                       ES3_VULKAN()
+                           .enable(Feature::PadBuffersToMaxVertexAttribStride)
+                           .enable(Feature::AllocateNonZeroMemory));
 }  // namespace angle
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 0c5d42c..3369af2 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -312,6 +312,9 @@
 class RobustResourceInitTestES31 : public RobustResourceInitTest
 {};
 
+class RobustResourceInitWithPaddingTest : public RobustResourceInitTest
+{};
+
 // Robust resource initialization is not based on hardware support or native extensions, check that
 // it only works on the implemented renderers
 TEST_P(RobustResourceInitTest, ExpectedRendererSupport)
@@ -419,6 +422,98 @@
     EXPECT_GL_TRUE(initState);
 }
 
+// Tests that the padding is also cleared out for a new uninitialized buffer in Vulkan.
+TEST_P(RobustResourceInitWithPaddingTest, BufferDataWithinPaddingRange)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    constexpr size_t kWidth  = 4;
+    constexpr size_t kHeight = 4;
+
+    GLTexture readTex;
+    glBindTexture(GL_TEXTURE_2D, readTex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, readTex, 0);
+
+    // The array buffers are initialized to fill the whole FBO with a single color.
+    std::vector<float> colorInitData(128 * 1024 / sizeof(float), 1.0f);
+    std::vector<float> posData = {
+        0.0f,  0.0f,  -1.0f, -1.0f, -1.0f, -0.5f, -1.0f, 0.0f,  -1.0f, 0.5f,  -1.0f, 1.0f,
+        -0.5f, 1.0f,  0.0f,  1.0f,  0.5f,  1.0f,  1.0f,  1.0f,  1.0f,  0.5f,  1.0f,  0.0f,
+        1.0f,  -0.5f, 1.0f,  -1.0f, 0.5f,  -1.0f, 0.0f,  -1.0f, -0.5f, -1.0f, -1.0f, -1.0f,
+    };
+
+    GLBuffer bufferColorData1;
+    glBindBuffer(GL_ARRAY_BUFFER, bufferColorData1);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * colorInitData.size(), colorInitData.data(),
+                 GL_STATIC_DRAW);
+
+    GLBuffer bufferPos;
+    glBindBuffer(GL_ARRAY_BUFFER, bufferPos);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * posData.size(), posData.data(), GL_STATIC_DRAW);
+
+    constexpr char kVS[] = R"(
+        attribute vec2 position;
+        attribute float dataIn;
+        varying vec4 colorOut;
+        void main() {
+            gl_Position = vec4(position, 0.0, 1.0);
+            // Return blue if input data is 0.0, and yellow otherwise.
+            colorOut = dataIn == 0.0 ? vec4(0.0, 0.0, 1.0, 1.0)
+                                     : vec4(1.0, 1.0, 0.0, 1.0);
+        })";
+    constexpr char kFS[] = R"(
+        varying mediump vec4 colorOut;
+        void main() {
+            gl_FragColor = colorOut;
+        })";
+
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    glUseProgram(program);
+
+    GLint dataInLoc = glGetAttribLocation(program, "dataIn");
+    ASSERT_NE(-1, dataInLoc);
+    GLint posLoc = glGetAttribLocation(program, "position");
+    ASSERT_NE(-1, posLoc);
+
+    glBindBuffer(GL_ARRAY_BUFFER, bufferColorData1);
+    glVertexAttribPointer(dataInLoc, 1, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(dataInLoc);
+
+    glBindBuffer(GL_ARRAY_BUFFER, bufferPos);
+    glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(posLoc);
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
+    glDrawArrays(GL_TRIANGLE_FAN, 0, 18);
+    ASSERT_GL_NO_ERROR();
+
+    EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::yellow);
+    bufferColorData1.reset();
+    glFinish();
+
+    // At this point, the VMA suballocation for the color data buffer should be deleted, although
+    // the underlying buffer block remains. A much smaller buffer is now allocated for the color
+    // data. However, it is not initialized with data. Therefore, with robust resource init, it is
+    // expected for the whole buffer and its padding to be initialized to 0. Therefore, even if the
+    // draw cuts into the padding space in the backend, it should still have 0 in it.
+    GLBuffer bufferColorData2;
+    glBindBuffer(GL_ARRAY_BUFFER, bufferColorData2);
+    glBufferData(GL_ARRAY_BUFFER, 4, nullptr, GL_STATIC_DRAW);
+    glVertexAttribPointer(dataInLoc, 1, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(dataInLoc);
+    glBindBuffer(GL_ARRAY_BUFFER, bufferPos);
+    glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(posLoc);
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
+    glDrawArrays(GL_TRIANGLE_FAN, 0, 18);
+    ASSERT_GL_NO_ERROR();
+
+    EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::blue);
+}
+
 // Regression test for passing a zero size init buffer with the extension.
 TEST_P(RobustResourceInitTest, BufferDataZeroSize)
 {
@@ -3428,4 +3523,11 @@
 ANGLE_INSTANTIATE_TEST_ES31_AND(RobustResourceInitTestES31,
                                 ES31_VULKAN().enable(Feature::AllocateNonZeroMemory));
 
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(RobustResourceInitWithPaddingTest);
+ANGLE_INSTANTIATE_TEST(RobustResourceInitWithPaddingTest,
+                       ES3_VULKAN().enable(Feature::PadBuffersToMaxVertexAttribStride),
+                       ES3_VULKAN_SWIFTSHADER().enable(Feature::PadBuffersToMaxVertexAttribStride),
+                       ES3_VULKAN()
+                           .enable(Feature::PadBuffersToMaxVertexAttribStride)
+                           .enable(Feature::AllocateNonZeroMemory));
 }  // namespace angle
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