Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker503054174
Fix commitbbcfc3c0f7d3 (angle/angle) +308/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
VertexAttributeShiftInstancedArrayDataWithOffsetTest
src/tests/gl_tests/VertexAttributeTest.cpp
modified

Files Changed

  • src/libANGLE/renderer/vulkan/BufferVk.h
  • src/libANGLE/renderer/vulkan/TextureVk.cpp
  • src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
  • src/tests/gl_tests/VertexAttributeTest.cpp
From bbcfc3c0f7d3c60bd81d85877036541641b3af99 Mon Sep 17 00:00:00 2001
From: Charlie Lao <[email protected]>
Date: Wed, 06 May 2026 10:07:53 -0700
Subject: [PATCH] Vulkan: Limit VertexConversionBuffer to single attribIndex

This change prevents rendering corruption by ensuring that
VertexConversionBuffer instances are not shared across different vertex
attributes.

Key Changes: Unique Cache Key: Adds VertexArrayVk * and attribIndex to
VertexConversionBuffer::CacheKey. This ensures that attributes with
different source offsets but the same format/stride do not attempt to
reuse the same conversion buffer, which previously caused destination
offset conflicts. VertexConversionBuffer::match() is updated to include
these new fields in its comparison.

Regression tests are also added (credit to Shahbaz).

Bug: b/503054174
Change-Id: I40bdc311625342991f3ed6c4c20d1788b97266da
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7818636
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Charlie Lao <[email protected]>
Reviewed-by: Yuxin Hu <[email protected]>
---

diff --git a/src/libANGLE/renderer/vulkan/BufferVk.h b/src/libANGLE/renderer/vulkan/BufferVk.h
index c225574..2239df8 100644
--- a/src/libANGLE/renderer/vulkan/BufferVk.h
+++ b/src/libANGLE/renderer/vulkan/BufferVk.h
@@ -71,6 +71,8 @@
   public:
     struct CacheKey final
     {
+        VertexArrayVk *vertexArrayVk;
+        size_t attribIndex;
         angle::FormatID formatID;
         GLuint stride;
         size_t offset;
@@ -86,7 +88,9 @@
     bool match(const CacheKey &cacheKey)
     {
         // If anything other than offset mismatch, it can't reuse.
-        if (mCacheKey.formatID != cacheKey.formatID || mCacheKey.stride != cacheKey.stride ||
+        if (mCacheKey.vertexArrayVk != cacheKey.vertexArrayVk ||
+            mCacheKey.attribIndex != cacheKey.attribIndex ||
+            mCacheKey.formatID != cacheKey.formatID || mCacheKey.stride != cacheKey.stride ||
             mCacheKey.offsetMustMatchExactly != cacheKey.offsetMustMatchExactly ||
             mCacheKey.hostVisible != cacheKey.hostVisible)
         {
diff --git a/src/libANGLE/renderer/vulkan/TextureVk.cpp b/src/libANGLE/renderer/vulkan/TextureVk.cpp
index 0ad33d8..130abe1 100644
--- a/src/libANGLE/renderer/vulkan/TextureVk.cpp
+++ b/src/libANGLE/renderer/vulkan/TextureVk.cpp
@@ -3752,7 +3752,7 @@
     BufferVk *bufferVk                                        = vk::GetImpl(getBuffer().get());
     const gl::OffsetBindingPointer<gl::Buffer> &bufferBinding = mState.getBuffer();
     const VertexConversionBuffer::CacheKey cacheKey{
-        formatID, 16, static_cast<size_t>(bufferBinding.getOffset()), false, true};
+        nullptr, 0, formatID, 16, static_cast<size_t>(bufferBinding.getOffset()), false, true};
     ConversionBuffer *conversion = bufferVk->getVertexConversionBuffer(renderer, cacheKey);
     return conversion->getBuffer();
 }
@@ -3771,8 +3771,8 @@
     conversionBufferSize = roundUpPow2<size_t>(static_cast<size_t>((bufferSizeFromOffset / 3) * 4),
                                                4 * sizeof(uint32_t));
 
-    const VertexConversionBuffer::CacheKey cacheKey{imageUniformFormat->getIntendedFormatID(), 16,
-                                                    bindingOffset, false, true};
+    const VertexConversionBuffer::CacheKey cacheKey{
+        nullptr, 0, imageUniformFormat->getIntendedFormatID(), 16, bindingOffset, false, true};
     ConversionBuffer *conversion = bufferVk->getVertexConversionBuffer(renderer, cacheKey);
     mBufferContentsObservers->enableForBuffer(getBuffer().get());
     if (!conversion->valid())
diff --git a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
index 620822e..ad7a308 100644
--- a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
+++ b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
@@ -1285,8 +1285,13 @@
     WarnOnVertexFormatConversion(contextVk, vertexFormat);
 
     const VertexConversionBuffer::CacheKey cacheKey{
-        srcFormat.id, srcStride, static_cast<size_t>(binding.getOffset()) + attrib.relativeOffset,
-        !bindingIsAligned, false};
+        this,
+        attribIndex,
+        srcFormat.id,
+        srcStride,
+        static_cast<size_t>(binding.getOffset()) + attrib.relativeOffset,
+        !bindingIsAligned,
+        false};
 
     VertexConversionBuffer *conversion = bufferVk->getVertexConversionBuffer(renderer, cacheKey);
 
diff --git a/src/tests/gl_tests/VertexAttributeTest.cpp b/src/tests/gl_tests/VertexAttributeTest.cpp
index eb4f3f3..3e311ca 100644
--- a/src/tests/gl_tests/VertexAttributeTest.cpp
+++ b/src/tests/gl_tests/VertexAttributeTest.cpp
@@ -5806,7 +5806,7 @@
 class VertexAttributeShiftInstancedArrayDataWithOffsetTest : public VertexAttributeTest
 {};
 
-// Regression test (crbug.com/497928952) for heap-buffer-overflow in streamAttributes() when
+// Regression test (http://crbug.com/497928952) for heap-buffer-overflow in streamAttributes() when
 // shiftInstancedArrayDataWithOffset workaround is enabled.
 TEST_P(VertexAttributeShiftInstancedArrayDataWithOffsetTest,
        ShiftInstancedArrayDataWithOffsetSlowPath)
@@ -5858,6 +5858,298 @@
     EXPECT_GL_NO_ERROR();
 }
 
+// Test that two attributes with the same format can be on the same buffer.  Uses a common format
+// that won't need any emulation.
+TEST_P(VertexAttributeTestES3, MultipleAttributesSameBufferCommonFormat)
+{
+    constexpr char kVS[] = R"(attribute vec2 position;
+attribute vec4 attrib1;
+attribute vec4 attrib2;
+varying vec4 color;
+void main() {
+    gl_Position = vec4(position, 0, 1);
+    color = attrib1 + attrib2;
+})";
+    constexpr char kFS[] = R"(varying lowp vec4 color;
+void main() {
+    gl_FragColor = color;
+})";
+
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    glUseProgram(program);
+    const GLint posLoc     = glGetAttribLocation(program, "position");
+    const GLint attrib1Loc = glGetAttribLocation(program, "attrib1");
+    const GLint attrib2Loc = glGetAttribLocation(program, "attrib2");
+
+    // Set up position in its own buffer, it's unrelated to what's being tested.
+    constexpr std::array<float, 6> kTriangle = {-1, -1, 3, -1, -1, 3};
+    GLBuffer posBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, posBuf);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(kTriangle), kTriangle.data(), GL_STATIC_DRAW);
+    glEnableVertexAttribArray(posLoc);
+    glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
+
+    std::vector<float> attribs(200 * 4, 0);
+    constexpr uint32_t kAttrib1Offset = 128;
+    constexpr uint32_t kAttrib2Offset = 64;
+    // Blue:
+    attribs[kAttrib1Offset * 4 + 0]  = 0;
+    attribs[kAttrib1Offset * 4 + 1]  = 0;
+    attribs[kAttrib1Offset * 4 + 2]  = 1;
+    attribs[kAttrib1Offset * 4 + 3]  = 1;
+    attribs[kAttrib1Offset * 4 + 4]  = 0;
+    attribs[kAttrib1Offset * 4 + 5]  = 0;
+    attribs[kAttrib1Offset * 4 + 6]  = 1;
+    attribs[kAttrib1Offset * 4 + 7]  = 1;
+    attribs[kAttrib1Offset * 4 + 8]  = 0;
+    attribs[kAttrib1Offset * 4 + 9]  = 0;
+    attribs[kAttrib1Offset * 4 + 10] = 1;
+    attribs[kAttrib1Offset * 4 + 11] = 1;
+    // Green, no alpha:
+    attribs[kAttrib2Offset * 4 + 0]  = 0;
+    attribs[kAttrib2Offset * 4 + 1]  = 1;
+    attribs[kAttrib2Offset * 4 + 2]  = 0;
+    attribs[kAttrib2Offset * 4 + 3]  = 0;
+    attribs[kAttrib2Offset * 4 + 4]  = 0;
+    attribs[kAttrib2Offset * 4 + 5]  = 1;
+    attribs[kAttrib2Offset * 4 + 6]  = 0;
+    attribs[kAttrib2Offset * 4 + 7]  = 0;
+    attribs[kAttrib2Offset * 4 + 8]  = 0;
+    attribs[kAttrib2Offset * 4 + 9]  = 1;
+    attribs[kAttrib2Offset * 4 + 10] = 0;
+    attribs[kAttrib2Offset * 4 + 11] = 0;
+
+    GLBuffer attribBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, attribBuf);
+    glBufferData(GL_ARRAY_BUFFER, attribs.size() * sizeof(uint32_t), attribs.data(),
+                 GL_STATIC_DRAW);
+    glEnableVertexAttribArray(attrib1Loc);
+    glVertexAttribPointer(attrib1Loc, 4, GL_FLOAT, GL_FALSE, 0,
+                          reinterpret_cast<const void *>(kAttrib1Offset * sizeof(float) * 4));
+    glEnableVertexAttribArray(attrib2Loc);
+    glVertexAttribPointer(attrib2Loc, 4, GL_FLOAT, GL_FALSE, 0,
+                          reinterpret_cast<const void *>(kAttrib2Offset * sizeof(float) * 4));
+
+    // Draw once, let everything sync
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    // The resulting color should be blue + green
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::cyan);
+
+    // Draw and verify again
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::cyan);
+    ASSERT_GL_NO_ERROR();
+}
+
+// Test that multiple attributes can reference the same buffer at different offsets.  The test uses
+// a vertex format that can be emulated in the Vulkan backend.  Regression test for
+// http://crbug.com/503054174.
+TEST_P(VertexAttributeTestES3, MultipleAttributesSameBuffer)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_OES_vertex_type_10_10_10_2"));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/VertexAttributeTest.cpp b/src/tests/gl_tests/VertexAttributeTest.cpp
index eb4f3f3..3e311ca 100644
--- a/src/tests/gl_tests/VertexAttributeTest.cpp
+++ b/src/tests/gl_tests/VertexAttributeTest.cpp
@@ -5806,7 +5806,7 @@
 class VertexAttributeShiftInstancedArrayDataWithOffsetTest : public VertexAttributeTest
 {};
 
-// Regression test (crbug.com/497928952) for heap-buffer-overflow in streamAttributes() when
+// Regression test (http://crbug.com/497928952) for heap-buffer-overflow in streamAttributes() when
 // shiftInstancedArrayDataWithOffset workaround is enabled.
 TEST_P(VertexAttributeShiftInstancedArrayDataWithOffsetTest,
        ShiftInstancedArrayDataWithOffsetSlowPath)
@@ -5858,6 +5858,298 @@
     EXPECT_GL_NO_ERROR();
 }
 
+// Test that two attributes with the same format can be on the same buffer.  Uses a common format
+// that won't need any emulation.
+TEST_P(VertexAttributeTestES3, MultipleAttributesSameBufferCommonFormat)
+{
+    constexpr char kVS[] = R"(attribute vec2 position;
+attribute vec4 attrib1;
+attribute vec4 attrib2;
+varying vec4 color;
+void main() {
+    gl_Position = vec4(position, 0, 1);
+    color = attrib1 + attrib2;
+})";
+    constexpr char kFS[] = R"(varying lowp vec4 color;
+void main() {
+    gl_FragColor = color;
+})";
+
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    glUseProgram(program);
+    const GLint posLoc     = glGetAttribLocation(program, "position");
+    const GLint attrib1Loc = glGetAttribLocation(program, "attrib1");
+    const GLint attrib2Loc = glGetAttribLocation(program, "attrib2");
+
+    // Set up position in its own buffer, it's unrelated to what's being tested.
+    constexpr std::array<float, 6> kTriangle = {-1, -1, 3, -1, -1, 3};
+    GLBuffer posBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, posBuf);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(kTriangle), kTriangle.data(), GL_STATIC_DRAW);
+    glEnableVertexAttribArray(posLoc);
+    glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
+
+    std::vector<float> attribs(200 * 4, 0);
+    constexpr uint32_t kAttrib1Offset = 128;
+    constexpr uint32_t kAttrib2Offset = 64;
+    // Blue:
+    attribs[kAttrib1Offset * 4 + 0]  = 0;
+    attribs[kAttrib1Offset * 4 + 1]  = 0;
+    attribs[kAttrib1Offset * 4 + 2]  = 1;
+    attribs[kAttrib1Offset * 4 + 3]  = 1;
+    attribs[kAttrib1Offset * 4 + 4]  = 0;
+    attribs[kAttrib1Offset * 4 + 5]  = 0;
+    attribs[kAttrib1Offset * 4 + 6]  = 1;
+    attribs[kAttrib1Offset * 4 + 7]  = 1;
+    attribs[kAttrib1Offset * 4 + 8]  = 0;
+    attribs[kAttrib1Offset * 4 + 9]  = 0;
+    attribs[kAttrib1Offset * 4 + 10] = 1;
+    attribs[kAttrib1Offset * 4 + 11] = 1;
+    // Green, no alpha:
+    attribs[kAttrib2Offset * 4 + 0]  = 0;
+    attribs[kAttrib2Offset * 4 + 1]  = 1;
+    attribs[kAttrib2Offset * 4 + 2]  = 0;
+    attribs[kAttrib2Offset * 4 + 3]  = 0;
+    attribs[kAttrib2Offset * 4 + 4]  = 0;
+    attribs[kAttrib2Offset * 4 + 5]  = 1;
+    attribs[kAttrib2Offset * 4 + 6]  = 0;
+    attribs[kAttrib2Offset * 4 + 7]  = 0;
+    attribs[kAttrib2Offset * 4 + 8]  = 0;
+    attribs[kAttrib2Offset * 4 + 9]  = 1;
+    attribs[kAttrib2Offset * 4 + 10] = 0;
+    attribs[kAttrib2Offset * 4 + 11] = 0;
+
+    GLBuffer attribBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, attribBuf);
+    glBufferData(GL_ARRAY_BUFFER, attribs.size() * sizeof(uint32_t), attribs.data(),
+                 GL_STATIC_DRAW);
+    glEnableVertexAttribArray(attrib1Loc);
+    glVertexAttribPointer(attrib1Loc, 4, GL_FLOAT, GL_FALSE, 0,
+                          reinterpret_cast<const void *>(kAttrib1Offset * sizeof(float) * 4));
+    glEnableVertexAttribArray(attrib2Loc);
+    glVertexAttribPointer(attrib2Loc, 4, GL_FLOAT, GL_FALSE, 0,
+                          reinterpret_cast<const void *>(kAttrib2Offset * sizeof(float) * 4));
+
+    // Draw once, let everything sync
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    // The resulting color should be blue + green
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::cyan);
+
+    // Draw and verify again
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::cyan);
+    ASSERT_GL_NO_ERROR();
+}
+
+// Test that multiple attributes can reference the same buffer at different offsets.  The test uses
+// a vertex format that can be emulated in the Vulkan backend.  Regression test for
+// http://crbug.com/503054174.
+TEST_P(VertexAttributeTestES3, MultipleAttributesSameBuffer)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_OES_vertex_type_10_10_10_2"));
+
+    constexpr char kVS[] = R"(attribute vec2 position;
+attribute vec4 attrib1;
+attribute vec4 attrib2;
+varying vec4 color;
+void main() {
+    gl_Position = vec4(position, 0, 1);
+    color = attrib1 + attrib2;
+})";
+    constexpr char kFS[] = R"(varying lowp vec4 color;
+void main() {
+    gl_FragColor = color;
+})";
+
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    glUseProgram(program);
+    const GLint posLoc     = glGetAttribLocation(program, "position");
+    const GLint attrib1Loc = glGetAttribLocation(program, "attrib1");
+    const GLint attrib2Loc = glGetAttribLocation(program, "attrib2");
+
+    // Set up position in its own buffer, it's unrelated to what's being tested.
+    constexpr std::array<float, 6> kTriangle = {-1, -1, 3, -1, -1, 3};
+    GLBuffer posBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, posBuf);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(kTriangle), kTriangle.data(), GL_STATIC_DRAW);
+    glEnableVertexAttribArray(posLoc);
+    glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
+
+    // Set up the other two attribs.  They use the same format and are on the same buffer.  The
+    // format used is RGB10_A2_SNORM which might get emulated in some backends.  The first attribute
+    // is placed at a later offset.
+    std::vector<uint32_t> attribs(200, 0);
+    constexpr uint32_t kAttrib1Offset = 128;
+    constexpr uint32_t kAttrib2Offset = 64;
+    // Blue:
+    attribs[kAttrib1Offset + 0] = 0x000007FD;
+    attribs[kAttrib1Offset + 1] = 0x000007FD;
+    attribs[kAttrib1Offset + 2] = 0x000007FD;
+    // Green, no alpha:
+    attribs[kAttrib2Offset + 0] = 0x001FF000;
+    attribs[kAttrib2Offset + 1] = 0x001FF000;
+    attribs[kAttrib2Offset + 2] = 0x001FF000;
+
+    GLBuffer attribBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, attribBuf);
+    glBufferData(GL_ARRAY_BUFFER, attribs.size() * sizeof(uint32_t), attribs.data(),
+                 GL_STATIC_DRAW);
+    glEnableVertexAttribArray(attrib1Loc);
+    glVertexAttribPointer(attrib1Loc, 4, GL_INT_10_10_10_2_OES, GL_TRUE, 0,
+                          reinterpret_cast<const void *>(kAttrib1Offset * sizeof(uint32_t)));
+    glEnableVertexAttribArray(attrib2Loc);
+    glVertexAttribPointer(attrib2Loc, 4, GL_INT_10_10_10_2_OES, GL_TRUE, 0,
+                          reinterpret_cast<const void *>(kAttrib2Offset * sizeof(uint32_t)));
+
+    // Draw once, let everything sync
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    // The resulting color should be blue + green
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::cyan);
+
+    // Draw and verify again
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::cyan);
+    ASSERT_GL_NO_ERROR();
+}
+
+// Same as MultipleAttributesSameBuffer, but the attributes offsets in the buffer are swapped.
+TEST_P(VertexAttributeTestES3, MultipleAttributesSameBuffer2)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_OES_vertex_type_10_10_10_2"));
+
+    constexpr char kVS[] = R"(attribute vec2 position;
+attribute vec4 attrib1;
+attribute vec4 attrib2;
+varying vec4 color;
+void main() {
+    gl_Position = vec4(position, 0, 1);
+    color = attrib1 + attrib2;
+})";
+    constexpr char kFS[] = R"(varying lowp vec4 color;
+void main() {
+    gl_FragColor = color;
+})";
+
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    glUseProgram(program);
+    const GLint posLoc     = glGetAttribLocation(program, "position");
+    const GLint attrib1Loc = glGetAttribLocation(program, "attrib1");
+    const GLint attrib2Loc = glGetAttribLocation(program, "attrib2");
+
+    // Set up position in its own buffer, it's unrelated to what's being tested.
+    constexpr std::array<float, 6> kTriangle = {-1, -1, 3, -1, -1, 3};
+    GLBuffer posBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, posBuf);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(kTriangle), kTriangle.data(), GL_STATIC_DRAW);
+    glEnableVertexAttribArray(posLoc);
+    glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
+
+    // Set up the other two attribs.  They use the same format and are on the same buffer.  The
+    // format used is RGB10_A2_SNORM which might get emulated in some backends.  The first attribute
+    // is placed at an earlier offset.
+    std::vector<uint32_t> attribs(200, 0);
+    constexpr uint32_t kAttrib1Offset = 64;
+    constexpr uint32_t kAttrib2Offset = 128;
+    // Blue:
+    attribs[kAttrib1Offset + 0] = 0x000007FD;
+    attribs[kAttrib1Offset + 1] = 0x000007FD;
+    attribs[kAttrib1Offset + 2] = 0x000007FD;
+    // Green, no alpha:
+    attribs[kAttrib2Offset + 0] = 0x001FF000;
+    attribs[kAttrib2Offset + 1] = 0x001FF000;
+    attribs[kAttrib2Offset + 2] = 0x001FF000;
+
+    GLBuffer attribBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, attribBuf);
+    glBufferData(GL_ARRAY_BUFFER, attribs.size() * sizeof(uint32_t), attribs.data(),
+                 GL_STATIC_DRAW);
+    glEnableVertexAttribArray(attrib1Loc);
+    glVertexAttribPointer(attrib1Loc, 4, GL_INT_10_10_10_2_OES, GL_TRUE, 0,
+                          reinterpret_cast<const void *>(kAttrib1Offset * sizeof(uint32_t)));
+    glEnableVertexAttribArray(attrib2Loc);
+    glVertexAttribPointer(attrib2Loc, 4, GL_INT_10_10_10_2_OES, GL_TRUE, 0,
+                          reinterpret_cast<const void *>(kAttrib2Offset * sizeof(uint32_t)));
+
+    // Draw once, let everything sync
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    // The resulting color should be blue + green
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::cyan);
+
+    // Draw and verify again
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::cyan);
+    ASSERT_GL_NO_ERROR();
+}
+
+// Same test as MultipleAttributesSameBuffer, but with unaligned data
+TEST_P(VertexAttributeTestES3, MultipleAttributesSameBufferUnaligned)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_OES_vertex_type_10_10_10_2"));
+
+    constexpr char kVS[] = R"(attribute vec2 position;
+attribute vec4 attrib1;
+attribute vec4 attrib2;
+varying vec4 color;
+void main() {
+    gl_Position = vec4(position, 0, 1);
+    color = attrib1 + attrib2;
+})";
+    constexpr char kFS[] = R"(varying lowp vec4 color;
+void main() {
+    gl_FragColor = color;
+})";
+
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    glUseProgram(program);
+    const GLint posLoc     = glGetAttribLocation(program, "position");
+    const GLint attrib1Loc = glGetAttribLocation(program, "attrib1");
+    const GLint attrib2Loc = glGetAttribLocation(program, "attrib2");
+
+    // Set up position in its own buffer, it's unrelated to what's being tested.
+    constexpr std::array<float, 6> kTriangle = {-1, -1, 3, -1, -1, 3};
+    GLBuffer posBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, posBuf);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(kTriangle), kTriangle.data(), GL_STATIC_DRAW);
+    glEnableVertexAttribArray(posLoc);
+    glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
+
+    // Set up the other two attribs.  They use the same format and are on the same buffer.  The
+    // format used is RGB10_A2_SNORM which might get emulated in some backends.  The first attribute
+    // is placed at a later offset.
+    std::vector<uint32_t> attribs(200, 0);
+    constexpr uint32_t kAttrib1Offset = 100;
+    constexpr uint32_t kAttrib2Offset = 10;
+    // Blue:
+    attribs[kAttrib1Offset + 0] = 0x000007FD;
+    attribs[kAttrib1Offset + 1] = 0x000007FD;
+    attribs[kAttrib1Offset + 2] = 0x000007FD;
+    // Green, no alpha:
+    attribs[kAttrib2Offset + 0] = 0x001FF000;
+    attribs[kAttrib2Offset + 1] = 0x001FF000;
+    attribs[kAttrib2Offset + 2] = 0x001FF000;
+
+    GLBuffer attribBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, attribBuf);
+    glBufferData(GL_ARRAY_BUFFER, attribs.size() * sizeof(uint32_t), attribs.data(),
+                 GL_STATIC_DRAW);
+    glEnableVertexAttribArray(attrib1Loc);
+    glVertexAttribPointer(attrib1Loc, 4, GL_INT_10_10_10_2_OES, GL_TRUE, 0,
+                          reinterpret_cast<const void *>(kAttrib1Offset * sizeof(uint32_t)));
+    glEnableVertexAttribArray(attrib2Loc);
+    glVertexAttribPointer(attrib2Loc, 4, GL_INT_10_10_10_2_OES, GL_TRUE, 0,
+                          reinterpret_cast<const void *>(kAttrib2Offset * sizeof(uint32_t)));
+
+    // Draw once, let everything sync
+    glDrawArrays(GL_TRIANGLES, 0, 3);
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Potential Heap-use-after-free in ANGLE Vulkan vertex conversion via stale buffer handles

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. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A potential logic error in ANGLE’s Vulkan backend can lead to a heap-use-after-free (UAF) vulnerability during vertex attribute conversion. When multiple attributes share a conversion buffer that requires reallocation, previously cached Vulkan buffer handles are not updated, leaving them pointing to freed memory. Reusing these stale handles in subsequent draw calls after garbage collection triggers a UAF in the GPU process.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
  • third_party/swiftshader/src/Vulkan/VkBuffer.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cpp

Estimated timestamp from git blame: 2025-09-25

Description

A potential heap-use-after-free (UAF) exists in ANGLE’s Vulkan backend due to stale VkBuffer handle caching within VertexArrayVk.

During a draw call, VertexArrayVk::syncState processes vertex attributes that require format conversion sequentially based on their index. If two attributes (e.g., attribute 0 and attribute 1) share the same VertexConversionBuffer (which occurs if their formats match and the offset gap is a multiple of their stride), a vulnerability can be triggered. When attribute 0 is processed, a vk::BufferHelper is allocated and its VkBuffer handle is cached in mCurrentArrayBufferHandles[0].

If attribute 1 is subsequently processed and has a smaller offset, it requires a larger buffer to cover the extended range. ContextVk::initBufferForVertexConversion will release the existing vk::BufferHelper to the garbage list and allocate a new one. However, the previously cached handle in mCurrentArrayBufferHandles[0] is never updated.

If a pipeline flush is forced (e.g., via glReadPixels), Renderer::cleanupGarbage executes and permanently destroys the released buffer. A subsequent draw call with no VAO state changes will bypass syncState and directly pass the stale VkBuffer handle to the Vulkan driver (e.g., vkCmdBindVertexBuffers), resulting in a UAF in the GPU process.

Potential Exploitation Steps

Note: These are suggested potential steps derived from code analysis, as our tooling agent cannot execute code to provide a working proof-of-concept.

  1. An attacker executes a malicious WebGL 2.0 script in the sandboxed renderer.
  2. The script creates a large vertex buffer and enables two vertex attributes (Attribute 0 and 1).
  3. Attribute 0 is configured with a format that requires Vulkan conversion, a specific stride (e.g., 16), and a large offset (e.g., 10MB).
  4. Attribute 1 is configured with the same format and stride, but with a 0 offset.
  5. The script calls gl.drawArrays(). In ANGLE’s GPU process backend, syncState processes Attribute 0 first, allocating a conversion buffer and caching its VkBuffer handle. It then processes Attribute 1, determines they can share a conversion buffer, but reallocates it to accommodate the larger range. Attribute 0’s cached handle becomes stale.
  6. The script synchronously calls gl.readPixels(). This forces ANGLE to flush the command queue and wait for the GPU, which consequently triggers Renderer::cleanupGarbage, freeing the old conversion buffer memory.
  7. The script calls gl.drawArrays() a second time. Because the VAO state hasn’t changed, ANGLE reuses the cached handles, passing the stale, freed VkBuffer handle to the Vulkan driver.
  8. The Vulkan driver (or a software fallback like SwiftShader) dereferences the freed handle/object during vertex fetching, triggering a UAF. An attacker could potentially groom the GPU process heap between step 6 and 7 to control the freed memory, turning this into an arbitrary memory read/disclosure primitive. Since the GPU process is unsandboxed on Android, this is highly privileged.

Suggested Fix

When ContextVk::initBufferForVertexConversion reallocates a VertexConversionBuffer, ANGLE must ensure that any other attributes currently sharing that buffer have their cached VkBuffer handles updated. This could be achieved by tracking all attributes bound to a given VertexConversionBuffer and updating their handles in mCurrentArrayBufferHandles synchronously upon reallocation, or by marking those specific bindings as dirty so they are re-evaluated before the draw call proceeds.

Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f


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