CVE-2026-17740
Overview
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
| Function | Change | Notes |
|---|---|---|
RobustResourceInitTestES31src/tests/gl_tests/RobustResourceInitTest.cpp |
modified | |
RobustResourceInitWithPaddingTestsrc/tests/gl_tests/RobustResourceInitTest.cpp |
modified |
Files Changed
src/libANGLE/renderer/vulkan/BufferVk.cppsrc/libANGLE/renderer/vulkan/ContextVk.cppsrc/libANGLE/renderer/vulkan/vk_helpers.cppsrc/libANGLE/renderer/vulkan/vk_helpers.hsrc/libANGLE/renderer/vulkan/vk_renderer.cppsrc/libANGLE/renderer/vulkan/vk_utils.cppsrc/libANGLE/renderer/vulkan/vk_utils.hsrc/tests/gl_tests/RobustResourceInitTest.cpp
Patch
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, ©Region);
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
Regression Test / PoC
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
Original Bug Report
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.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_renderer.cppthird_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
- Padding Applied: In
vk_renderer.cpp,padBuffersToMaxVertexAttribStrideis enabled for AMD and Samsung GPUs, settingmMaxVertexAttribStrideto 2048. When allocating buffers,BufferHelper::initSuballocationinflates the size by this amount. - Incomplete Initialization: WebGL enforces robust resource initialization.
Buffer::bufferDataImplhandles 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). - CPU Validation Bypassed: During Context creation, if the Vulkan driver supports
robustBufferAccessBehaviorKHR, ANGLE disables its own CPU-side bounds checking (mBufferAccessValidationEnabled = false). - Padded Size Bound to Vulkan: When preparing a draw call,
VertexArrayVk::syncDirtyEnabledNonStreamingAttribcomputes the bound size asuser_size + 2048. This padded size is passed down tovkCmdBindVertexBuffers2EXT. - 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.
- Initialize a WebGL context on a vulnerable device (AMD or Samsung GPU).
- Create a vertex buffer with a specific size
Susinggl.bufferData. - Configure a WebGL Transform Feedback object to capture the outputs of a vertex shader.
- Issue a
gl.drawArraysorgl.drawElementscall that intentionally fetches vertex indices located past the buffer sizeS, but within theS + 2048boundary. - Because CPU validation is disabled and the hardware considers the read in-bounds, the vertex shader will process the uninitialized GPU memory.
- Read back the leaked memory from the Transform Feedback buffer using
gl.getBufferSubDatainto 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:
- Modifying the ANGLE frontend (
Buffer::bufferDataImpl) to query and include the backend padding size when allocating the zero-filled scratch buffer. - Alternatively, explicitly issuing a Vulkan
vkCmdFillBufferor 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.