CVE-2026-17718
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
VertexAttributeResizeDefaultTestsrc/tests/gl_tests/VertexAttributeTest.cpp |
modified | |
VertexAttributeResizeTestsrc/tests/gl_tests/VertexAttributeTest.cpp |
modified |
Files Changed
src/libANGLE/renderer/vulkan/ContextVk.cppsrc/libANGLE/renderer/vulkan/VertexArrayVk.cppsrc/libANGLE/renderer/vulkan/VertexArrayVk.hsrc/tests/gl_tests/VertexAttributeTest.cpp
Patch
From 51afe011d31610979424e7491bea370ad9152c21 Mon Sep 17 00:00:00 2001
From: kylechar <kylechar@google.com>
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 <syoussefi@chromium.org>
Commit-Queue: Charlie Lao <cclao@google.com>
Commit-Queue: Kyle Charbonneau <kylechar@chromium.org>
Reviewed-by: Charlie Lao <cclao@google.com>
Auto-Submit: Kyle Charbonneau <kylechar@chromium.org>
---
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);
Regression Test / PoC
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,
Original Bug Report
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.cppthird_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cppthird_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:
- Dynamic Buffer Resizing: When
ContextVk::mStreamedVertexBuffersneeds more space, it pushes oldBufferHelperblocks 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::allocateclears the free list, deleting theBufferHelperC++ objects and freeing their memory. - Partial Synchronization: Before a draw call,
VertexArrayVk::updateStreamedAttribsupdates the cached pointers inmCurrentArrayBuffers. However, it only updates attributes that are both enabled and active in the current shader program (mStreamingVertexAttribsMask & activeAttribs). - 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,
updateStreamedAttribsskips it. If the underlyingBufferHelperwas destroyed by another VAO causing a resize,mCurrentArrayBuffers[0]now contains a dangling pointer. - UAF Trigger:
ContextVk::handleDirtyGraphicsVertexBuffersVertexInputDynamicStateDisabledcallsmRenderPassCommands->buffersVertexAttribRead(..., maxAttrib), wheremaxAttribis the maximum active attribute location. If Attribute 1 is active,maxAttribis at least 2. The function iterates from 0 tomaxAttrib - 1, dereferencing the dangling pointer at index 0 to callbufferRead, resulting in a UAF.
Potential Trigger Steps
Note: Our tooling agent does not run code, so these are suggested steps based on static analysis.
- Create a WebGL context with the
ANGLE_instanced_arraysextension. - Create and bind
vao1. Bind a WebGL buffer, enable Attribute 0 and Attribute 1, and configure them. - Call
gl.vertexAttribDivisor(0, 0xFFFFFFFF)to force Attribute 0 into client-side streaming emulation. - Draw using
programA(which uses both attributes). ANGLE allocates aBufferHelperand caches its pointer invao1’smCurrentArrayBuffers[0]. - Create and bind
vao2. Configure it identically tovao1to use streaming on Attribute 0. - Execute numerous, large draw calls using
vao2. This exhausts the per-contextDynamicBufferblock, pushing the originalBufferHelperto the in-flight list. - Force a GPU flush (e.g.,
gl.readPixels()), moving theBufferHelperto the free list. - Execute another draw call with
vao2using a vastly different allocation size.DynamicBuffer::allocateresizes its blocks and clears the free list, destroying the originalBufferHelperC++ object. - Rebind
vao1. ThemCurrentArrayBuffers[0]pointer is now dangling. - Draw using
programB(which uses Attribute 1, but not Attribute 0). updateStreamedAttribsskips Attribute 0 because it is inactive.buffersVertexAttribReaditerates up tomaxAttrib(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.