Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker522079372
Fix commit51afe011d316 (angle/angle) +174/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
VertexAttributeResizeDefaultTest
src/tests/gl_tests/VertexAttributeTest.cpp
modified
VertexAttributeResizeTest
src/tests/gl_tests/VertexAttributeTest.cpp
modified

Files Changed

  • src/libANGLE/renderer/vulkan/ContextVk.cpp
  • src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
  • src/libANGLE/renderer/vulkan/VertexArrayVk.h
  • src/tests/gl_tests/VertexAttributeTest.cpp
From 51afe011d31610979424e7491bea370ad9152c21 Mon Sep 17 00:00:00 2001
From: kylechar <[email protected]>
Date: Fri, 26 Jun 2026 10:59:44 -0400
Subject: [PATCH] vulkan: Fix UAF in VertexArrayVk

VertexArrayVk::mCurrentArrayBuffers caches pointer for streaming
attributes but the pointed to BufferHelper can be destroyed when a
different VAO is bound. The stale pointer is dereferenced, even if the
attribute isn't used by the current program, leading to a UAF. Ensure
that cache pointers are reset for all streaming attributes when binding
a VAO.

Bug: chromium:522079372
Change-Id: I71999b9455ab632affd3fdf8bbdf16aae472a78b
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7955406
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Charlie Lao <[email protected]>
Commit-Queue: Kyle Charbonneau <[email protected]>
Reviewed-by: Charlie Lao <[email protected]>
Auto-Submit: Kyle Charbonneau <[email protected]>
---

diff --git a/src/libANGLE/renderer/vulkan/ContextVk.cpp b/src/libANGLE/renderer/vulkan/ContextVk.cpp
index 551c4d6..1d35bc6 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.cpp
+++ b/src/libANGLE/renderer/vulkan/ContextVk.cpp
@@ -5813,6 +5813,7 @@
                 invalidateDefaultAttributes(context->getActiveDefaultAttribsMask());
                 ANGLE_TRY(onVertexArrayChange(vertexArrayVk->getCurrentEnabledAttribsMask()));
                 ANGLE_TRY(onIndexBufferChange(vertexArrayVk->getCurrentElementArrayBuffer()));
+                vertexArrayVk->resetInactiveStreamedAttribs(context);
                 break;
             }
             case gl::state::DIRTY_BIT_DRAW_INDIRECT_BUFFER_BINDING:
diff --git a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
index 91dbbc1..48ad828 100644
--- a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
+++ b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
@@ -1635,6 +1635,29 @@
     return angle::Result::Continue;
 }
 
+void VertexArrayVk::resetInactiveStreamedAttribs(const gl::Context *context)
+{
+    ContextVk *contextVk = vk::GetImpl(context);
+    const gl::AttributesMask activeAttribs =
+        context->getActiveClientAttribsMask() | context->getActiveBufferedAttribsMask();
+    const gl::AttributesMask inactiveStreamedAttribs = mStreamingVertexAttribsMask & ~activeAttribs;
+    if (inactiveStreamedAttribs.any())
+    {
+        vk::BufferHelper &emptyBuffer = contextVk->getEmptyBuffer();
+        for (size_t attribIndex : inactiveStreamedAttribs)
+        {
+            if (mCurrentArrayBuffers[attribIndex] != &emptyBuffer)
+            {
+                mCurrentArrayBuffers[attribIndex]       = &emptyBuffer;
+                mCurrentArrayBufferSerial[attribIndex]  = emptyBuffer.getBufferSerial();
+                mCurrentArrayBufferHandles[attribIndex] = emptyBuffer.getBuffer().getHandle();
+                mCurrentArrayBufferOffsets[attribIndex] = emptyBuffer.getOffset();
+                mCurrentArrayBufferSizes[attribIndex]   = emptyBuffer.getSize();
+            }
+        }
+    }
+}
+
 angle::Result VertexArrayVk::handleLineLoop(ContextVk *contextVk,
                                             GLint firstVertex,
                                             GLsizei vertexOrIndexCount,
diff --git a/src/libANGLE/renderer/vulkan/VertexArrayVk.h b/src/libANGLE/renderer/vulkan/VertexArrayVk.h
index 50eecc4..aeecd57 100644
--- a/src/libANGLE/renderer/vulkan/VertexArrayVk.h
+++ b/src/libANGLE/renderer/vulkan/VertexArrayVk.h
@@ -54,6 +54,8 @@
                                         const void *indices,
                                         gl::AttributesMask *strideDirtyAttribMaskOut);
 
+    void resetInactiveStreamedAttribs(const gl::Context *context);
+
     angle::Result handleLineLoop(ContextVk *contextVk,
                                  GLint firstVertex,
                                  GLsizei vertexOrIndexCount,
diff --git a/src/tests/gl_tests/VertexAttributeTest.cpp b/src/tests/gl_tests/VertexAttributeTest.cpp
index 442ddf6..adf419e 100644
--- a/src/tests/gl_tests/VertexAttributeTest.cpp
+++ b/src/tests/gl_tests/VertexAttributeTest.cpp
@@ -6426,7 +6426,7 @@
     ASSERT_GL_NO_ERROR();
 }
 
-class VertexAttributeResizeDefaultTest : public ANGLETest<>
+class VertexAttributeResizeTest : public ANGLETest<>
 {
   protected:
     static constexpr char kVS1[] = R"(#version 300 es
@@ -6444,7 +6444,7 @@
 out vec4 col;
 void main() { col = vec4(0, 1, 0, 1); })";
 
-    VertexAttributeResizeDefaultTest()
+    VertexAttributeResizeTest()
     {
         setWindowWidth(128);
         setWindowHeight(128);
@@ -6457,7 +6457,7 @@
 
 // Tests that cached pointers in VertexArrayVk are reset if the DynamicBuffer for default attribute
 // is resized. See crbug.com/502812366.
-TEST_P(VertexAttributeResizeDefaultTest, ResizeAndSwitch)
+TEST_P(VertexAttributeResizeTest, ResizeAndSwitch)
 {
     // Program 1: Uses attribute 0 for vertex coords and draws red.
     ANGLE_GL_PROGRAM(prog1, kVS1, kFS1);
@@ -6517,7 +6517,7 @@
 // Tests that cache pointers in VertexArrayVk are reset if the DynamicBuffer for default attribute
 // is resized. This also ensures there are no default active attributes when next draw after
 // switching VAOs happen. See crbug.com/502812366.
-TEST_P(VertexAttributeResizeDefaultTest, ResizeAndSwitchWithNoDefaultAttribsActive)
+TEST_P(VertexAttributeResizeTest, ResizeAndSwitchWithNoDefaultAttribsActive)
 {
     // Program 1: Uses attribute 0 for vertex coords and draws red.
     ANGLE_GL_PROGRAM(prog1, kVS1, kFS1);
@@ -6574,6 +6574,149 @@
     EXPECT_PIXEL_COLOR_EQ(54, 54, GLColor::green);
 }
 
+// Tests that cached pointers in VertexArrayVk are reset if the DynamicBuffer for a streamed
+// attribute is resized while the VAO is unbound and the attribute is inactive in the program
+// used for the next draw.
+TEST_P(VertexAttributeResizeTest, ResizeStreamedAttribAndSwitchProgram)
+{
+    // Program 1: active 0, 1, 3. FS Red
+    constexpr char kLocalVS1[] = R"(#version 300 es
+layout(location = 0) in vec4 pos;
+layout(location = 1) in vec4 a1;
+layout(location = 3) in vec4 a3;
+void main() { gl_Position = vec4(pos.xyz + a1.xyz + a3.xyz, pos.w); gl_PointSize = 2.0; })";
+
+    // Program 2: active 0, 3. FS Green
+    constexpr char kLocalVS2[] = R"(#version 300 es
+layout(location = 0) in vec4 pos;
+layout(location = 3) in vec4 a3;
+void main() { gl_Position = vec4(pos.xyz + a3.xyz, pos.w); gl_PointSize = 2.0; })";
+
+    // Program 3: active 0, 2, 4. FS Blue
+    constexpr char kLocalVS3[] = R"(#version 300 es
+layout(location = 0) in vec4 pos;
+layout(location = 2) in vec4 a2;
+layout(location = 4) in vec4 a4;
+void main() { gl_Position = vec4(pos.xyz + a2.xyz + a4.xyz, pos.w); gl_PointSize = 2.0; })";
+
+    constexpr char kLocalFS1[] = R"(#version 300 es
+precision mediump float;
+out vec4 col;
+void main() { col = vec4(1, 0, 0, 1); })";
+
+    constexpr char kLocalFS2[] = R"(#version 300 es
+precision mediump float;
+out vec4 col;
+void main() { col = vec4(0, 1, 0, 1); })";
+
+    constexpr char kLocalFS3[] = R"(#version 300 es
+precision mediump float;
+out vec4 col;
+void main() { col = vec4(0, 0, 1, 1); })";
+
+    ANGLE_GL_PROGRAM(prog1, kLocalVS1, kLocalFS1);
+    ANGLE_GL_PROGRAM(prog2, kLocalVS2, kLocalFS2);
+    ANGLE_GL_PROGRAM(prog3, kLocalVS3, kLocalFS3);
+
+    const std::vector<float> positionData = {0.0f, 0.0f, 0.0f, 1.0f};
+    const std::vector<float> zeroData     = {0.0f, 0.0f, 0.0f, 0.0f};
+
+    // Step 1: Setup VAO 0 (default) and draw with prog1.
+    // Attribs 0, 1, 3 will be streamed.
+    glBindVertexArray(0);
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
+
+    glEnableVertexAttribArray(0);
+    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, positionData.data());
+    glEnableVertexAttribArray(1);
+    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, zeroData.data());
+    glEnableVertexAttribArray(2);
+    glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, zeroData.data());
+    glEnableVertexAttribArray(3);
+    glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 0, zeroData.data());
+    glEnableVertexAttribArray(4);
+    glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 0, zeroData.data());
+
+    glUseProgram(prog1);
+    glDrawArraysInstanced(GL_POINTS, 0, 1, 1);
+    ASSERT_GL_NO_ERROR();
+    EXPECT_PIXEL_COLOR_EQ(64, 64, GLColor::red);
+
+    // Step 2: Bind VAO 1, force resize of streamed buffer for attribute 1.
+    GLVertexArray vao1;
+    glBindVertexArray(vao1);
+
+    // Attrib 0: active, enabled, small buffer.
+    GLBuffer buf1;
+    glBindBuffer(GL_ARRAY_BUFFER, buf1);
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 442ddf6..adf419e 100644
--- a/src/tests/gl_tests/VertexAttributeTest.cpp
+++ b/src/tests/gl_tests/VertexAttributeTest.cpp
@@ -6426,7 +6426,7 @@
     ASSERT_GL_NO_ERROR();
 }
 
-class VertexAttributeResizeDefaultTest : public ANGLETest<>
+class VertexAttributeResizeTest : public ANGLETest<>
 {
   protected:
     static constexpr char kVS1[] = R"(#version 300 es
@@ -6444,7 +6444,7 @@
 out vec4 col;
 void main() { col = vec4(0, 1, 0, 1); })";
 
-    VertexAttributeResizeDefaultTest()
+    VertexAttributeResizeTest()
     {
         setWindowWidth(128);
         setWindowHeight(128);
@@ -6457,7 +6457,7 @@
 
 // Tests that cached pointers in VertexArrayVk are reset if the DynamicBuffer for default attribute
 // is resized. See crbug.com/502812366.
-TEST_P(VertexAttributeResizeDefaultTest, ResizeAndSwitch)
+TEST_P(VertexAttributeResizeTest, ResizeAndSwitch)
 {
     // Program 1: Uses attribute 0 for vertex coords and draws red.
     ANGLE_GL_PROGRAM(prog1, kVS1, kFS1);
@@ -6517,7 +6517,7 @@
 // Tests that cache pointers in VertexArrayVk are reset if the DynamicBuffer for default attribute
 // is resized. This also ensures there are no default active attributes when next draw after
 // switching VAOs happen. See crbug.com/502812366.
-TEST_P(VertexAttributeResizeDefaultTest, ResizeAndSwitchWithNoDefaultAttribsActive)
+TEST_P(VertexAttributeResizeTest, ResizeAndSwitchWithNoDefaultAttribsActive)
 {
     // Program 1: Uses attribute 0 for vertex coords and draws red.
     ANGLE_GL_PROGRAM(prog1, kVS1, kFS1);
@@ -6574,6 +6574,149 @@
     EXPECT_PIXEL_COLOR_EQ(54, 54, GLColor::green);
 }
 
+// Tests that cached pointers in VertexArrayVk are reset if the DynamicBuffer for a streamed
+// attribute is resized while the VAO is unbound and the attribute is inactive in the program
+// used for the next draw.
+TEST_P(VertexAttributeResizeTest, ResizeStreamedAttribAndSwitchProgram)
+{
+    // Program 1: active 0, 1, 3. FS Red
+    constexpr char kLocalVS1[] = R"(#version 300 es
+layout(location = 0) in vec4 pos;
+layout(location = 1) in vec4 a1;
+layout(location = 3) in vec4 a3;
+void main() { gl_Position = vec4(pos.xyz + a1.xyz + a3.xyz, pos.w); gl_PointSize = 2.0; })";
+
+    // Program 2: active 0, 3. FS Green
+    constexpr char kLocalVS2[] = R"(#version 300 es
+layout(location = 0) in vec4 pos;
+layout(location = 3) in vec4 a3;
+void main() { gl_Position = vec4(pos.xyz + a3.xyz, pos.w); gl_PointSize = 2.0; })";
+
+    // Program 3: active 0, 2, 4. FS Blue
+    constexpr char kLocalVS3[] = R"(#version 300 es
+layout(location = 0) in vec4 pos;
+layout(location = 2) in vec4 a2;
+layout(location = 4) in vec4 a4;
+void main() { gl_Position = vec4(pos.xyz + a2.xyz + a4.xyz, pos.w); gl_PointSize = 2.0; })";
+
+    constexpr char kLocalFS1[] = R"(#version 300 es
+precision mediump float;
+out vec4 col;
+void main() { col = vec4(1, 0, 0, 1); })";
+
+    constexpr char kLocalFS2[] = R"(#version 300 es
+precision mediump float;
+out vec4 col;
+void main() { col = vec4(0, 1, 0, 1); })";
+
+    constexpr char kLocalFS3[] = R"(#version 300 es
+precision mediump float;
+out vec4 col;
+void main() { col = vec4(0, 0, 1, 1); })";
+
+    ANGLE_GL_PROGRAM(prog1, kLocalVS1, kLocalFS1);
+    ANGLE_GL_PROGRAM(prog2, kLocalVS2, kLocalFS2);
+    ANGLE_GL_PROGRAM(prog3, kLocalVS3, kLocalFS3);
+
+    const std::vector<float> positionData = {0.0f, 0.0f, 0.0f, 1.0f};
+    const std::vector<float> zeroData     = {0.0f, 0.0f, 0.0f, 0.0f};
+
+    // Step 1: Setup VAO 0 (default) and draw with prog1.
+    // Attribs 0, 1, 3 will be streamed.
+    glBindVertexArray(0);
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
+
+    glEnableVertexAttribArray(0);
+    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, positionData.data());
+    glEnableVertexAttribArray(1);
+    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, zeroData.data());
+    glEnableVertexAttribArray(2);
+    glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, zeroData.data());
+    glEnableVertexAttribArray(3);
+    glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 0, zeroData.data());
+    glEnableVertexAttribArray(4);
+    glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 0, zeroData.data());
+
+    glUseProgram(prog1);
+    glDrawArraysInstanced(GL_POINTS, 0, 1, 1);
+    ASSERT_GL_NO_ERROR();
+    EXPECT_PIXEL_COLOR_EQ(64, 64, GLColor::red);
+
+    // Step 2: Bind VAO 1, force resize of streamed buffer for attribute 1.
+    GLVertexArray vao1;
+    glBindVertexArray(vao1);
+
+    // Attrib 0: active, enabled, small buffer.
+    GLBuffer buf1;
+    glBindBuffer(GL_ARRAY_BUFFER, buf1);
+    std::vector<float> positionData1 = {10.0f / 64.0f, 10.0f / 64.0f, 0.0f, 1.0f};
+    glBufferData(GL_ARRAY_BUFFER, positionData1.size() * sizeof(float), positionData1.data(),
+                 GL_STREAM_DRAW);
+    glEnableVertexAttribArray(0);
+    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glVertexAttribDivisor(1, 1200);
+
+    // Attrib 1: active, enabled, large buffer to force resize.
+    GLBuffer buf2;
+    glBindBuffer(GL_ARRAY_BUFFER, buf2);
+    std::vector<float> largeData(16, 0.0f);
+    glBufferData(GL_ARRAY_BUFFER, largeData.size() * sizeof(float), largeData.data(),
+                 GL_STREAM_DRAW);
+    glEnableVertexAttribArray(1);
+    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glVertexAttribDivisor(1, 300);
+
+    glUseProgram(prog1);
+    glDrawArraysInstanced(GL_POINTS, 0, 1, 1200);
+    ASSERT_GL_NO_ERROR();
+    EXPECT_PIXEL_COLOR_EQ(74, 74, GLColor::red);
+
+    // Step 3: Bind VAO 0, draw with prog2 (active: 0, 3. Inactive: 1, 2, 4).
+    // Attrib 1 (stale) should be reset.
+    glBindVertexArray(0);
+    glUseProgram(prog2);
+    glDrawArrays(GL_POINTS, 0, 1);
+    ASSERT_GL_NO_ERROR();
+    EXPECT_PIXEL_COLOR_EQ(64, 64, GLColor::green);
+
+    // Step 4: Bind VAO 2, force resize of streamed buffer for attribute 3.
+    GLVertexArray vao2;
+    glBindVertexArray(vao2);
+
+    // Attrib 0: active, enabled, small buffer.
+    GLBuffer buf3;
+    glBindBuffer(GL_ARRAY_BUFFER, buf3);
+    std::vector<float> positionData2 = {-10.0f / 64.0f, -10.0f / 64.0f, 0.0f, 1.0f};
+    glBufferData(GL_ARRAY_BUFFER, positionData2.size() * sizeof(float), positionData2.data(),
+                 GL_STREAM_DRAW);
+    glEnableVertexAttribArray(0);
+    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glVertexAttribDivisor(1, 1200);
+
+    // Attrib 3: active, enabled, large buffer.
+    GLBuffer buf4;
+    glBindBuffer(GL_ARRAY_BUFFER, buf4);
+    std::vector<float> largerData(48, 0.0f);
+    glBufferData(GL_ARRAY_BUFFER, largerData.size() * sizeof(float), largerData.data(),
+                 GL_STREAM_DRAW);
+    glEnableVertexAttribArray(3);
+    glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glVertexAttribDivisor(3, 100);
+
+    glUseProgram(prog2);
+    glDrawArraysInstanced(GL_POINTS, 0, 1, 1200);
+    ASSERT_GL_NO_ERROR();
+    EXPECT_PIXEL_COLOR_EQ(54, 54, GLColor::green);
+
+    // Step 5: Bind VAO 0, draw with prog3 (active: 0, 2, 4. Inactive: 1, 3).
+    // Attrib 3 (stale) should be reset.
+    glBindVertexArray(0);
+    glUseProgram(prog3);
+    glDrawArrays(GL_POINTS, 0, 1);
+    ASSERT_GL_NO_ERROR();
+    EXPECT_PIXEL_COLOR_EQ(64, 64, GLColor::blue);
+}
+
 // Ensure a large offset is not interpreted as negative.
 TEST_P(VertexAttributeTestES3, LargeAttribPointerOffsetNoCrash)
 {
@@ -6599,7 +6742,7 @@
     swapBuffers();
 }
 
-ANGLE_INSTANTIATE_TEST_ES3(VertexAttributeResizeDefaultTest);
+ANGLE_INSTANTIATE_TEST_ES3(VertexAttributeResizeTest);
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(VertexAttributeUint8Test);
 ANGLE_INSTANTIATE_TEST_ES3_AND(VertexAttributeUint8Test,
Loading diff…

Original Bug Report

reported by [email protected]

Potential Heap-Use-After-Free in ANGLE Vulkan backend via dangling pointers in VertexArrayVk

Flapjack, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential heap-use-after-free vulnerability exists in ANGLE’s Vulkan backend because VertexArrayVk caches raw pointers to vk::BufferHelper objects that can be destroyed during DynamicBuffer resizing. If a previously streamed attribute is inactive in a subsequent draw call, its cached pointer is not updated, leaving a dangling pointer. When vertex buffers are synchronized, iterating up to the maximum active attribute index causes the dangling pointer to be dereferenced.

Affected files:

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

Estimated timestamp from git blame: 2025-09-22

Technical Details

A potential heap-use-after-free (UAF) vulnerability has been identified in the Vulkan backend of ANGLE. The issue stems from VertexArrayVk maintaining dangling raw pointers to vk::BufferHelper objects that have been destroyed.

Root Cause Analysis

In ANGLE’s Vulkan backend, VertexArrayVk caches pointers to vertex buffer helpers in its mCurrentArrayBuffers array. For vertex attributes that use client-side memory streaming, these BufferHelper objects are dynamically allocated from ContextVk::mStreamedVertexBuffers.

While WebGL normally restricts client-side memory pointers, an attacker can force ANGLE to use client-side streaming by enabling the ANGLE_instanced_arrays extension and setting a vertexAttribDivisor that exceeds the Vulkan backend’s maximum limit (which is capped at 255). This flags the attribute for divisor emulation via streaming.

The vulnerability occurs due to a lifecycle mismatch between the DynamicBuffer managing the memory and the cached pointers in VertexArrayVk:

  1. Dynamic Buffer Resizing: When ContextVk::mStreamedVertexBuffers needs more space, it pushes old BufferHelper blocks to an in-flight list. After a GPU flush, these blocks move to a free list. If the block size needs to change significantly, DynamicBuffer::allocate clears the free list, deleting the BufferHelper C++ objects and freeing their memory.
  2. Partial Synchronization: Before a draw call, VertexArrayVk::updateStreamedAttribs updates the cached pointers in mCurrentArrayBuffers. However, it only updates attributes that are both enabled and active in the current shader program (mStreamingVertexAttribsMask & activeAttribs).
  3. Dangling Pointer: If a VAO has a streamed attribute (e.g., Attribute 0) that was previously cached, but the current draw call uses a shader program where Attribute 0 is inactive, updateStreamedAttribs skips it. If the underlying BufferHelper was destroyed by another VAO causing a resize, mCurrentArrayBuffers[0] now contains a dangling pointer.
  4. UAF Trigger: ContextVk::handleDirtyGraphicsVertexBuffersVertexInputDynamicStateDisabled calls mRenderPassCommands->buffersVertexAttribRead(..., maxAttrib), where maxAttrib is the maximum active attribute location. If Attribute 1 is active, maxAttrib is at least 2. The function iterates from 0 to maxAttrib - 1, dereferencing the dangling pointer at index 0 to call bufferRead, resulting in a UAF.

Potential Trigger Steps

Note: Our tooling agent does not run code, so these are suggested steps based on static analysis.

  1. Create a WebGL context with the ANGLE_instanced_arrays extension.
  2. Create and bind vao1. Bind a WebGL buffer, enable Attribute 0 and Attribute 1, and configure them.
  3. Call gl.vertexAttribDivisor(0, 0xFFFFFFFF) to force Attribute 0 into client-side streaming emulation.
  4. Draw using programA (which uses both attributes). ANGLE allocates a BufferHelper and caches its pointer in vao1’s mCurrentArrayBuffers[0].
  5. Create and bind vao2. Configure it identically to vao1 to use streaming on Attribute 0.
  6. Execute numerous, large draw calls using vao2. This exhausts the per-context DynamicBuffer block, pushing the original BufferHelper to the in-flight list.
  7. Force a GPU flush (e.g., gl.readPixels()), moving the BufferHelper to the free list.
  8. Execute another draw call with vao2 using a vastly different allocation size. DynamicBuffer::allocate resizes its blocks and clears the free list, destroying the original BufferHelper C++ object.
  9. Rebind vao1. The mCurrentArrayBuffers[0] pointer is now dangling.
  10. Draw using programB (which uses Attribute 1, but not Attribute 0).
  11. updateStreamedAttribs skips Attribute 0 because it is inactive. buffersVertexAttribRead iterates up to maxAttrib (which is >= 2), accessing index 0 and dereferencing the freed pointer.

Impact

This vulnerability can be triggered via WebGL, allowing a malicious website to achieve memory corruption in the GPU process. Because the GPU process is unsandboxed on Android, this could allow for a full system compromise.

Suggested Fix

To address this issue, VertexArrayVk::updateStreamedAttribs should ensure that all cached pointers in VertexArrayVk are reset to a safe fallback (such as ContextVk::mEmptyBuffer) if they are inactive but their underlying BufferHelper might have been invalidated. Alternatively, buffersVertexAttribRead could iterate over the active attribute mask instead of all indices up to maxAttrib.

Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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