CVE-2026-7354
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
VertexAttributeUint8Testsrc/tests/gl_tests/VertexAttributeTest.cpp |
modified |
Files Changed
src/libANGLE/renderer/vulkan/VertexArrayVk.cppsrc/tests/gl_tests/VertexAttributeTest.cpp
Patch
From 0d9ddc506ea7593138ef8e80640f7a70a01a9cad Mon Sep 17 00:00:00 2001 From: Ken Russell <[email protected]> Date: Fri, 17 Apr 2026 16:56:55 -0700 Subject: [PATCH] Vulkan: fix maximum index when converting index buffers on GPU. Correct computation of the remaining size in the buffer when the offset is non-zero. Verified the new test case catches the bug via the manual code changes in the bug report. Fixed: chromium:498746519 Change-Id: I8ee9f2fe2830a3d30e4d19863fbc8a97e1518322 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7774827 Commit-Queue: Kenneth Russell <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> Auto-Submit: Kenneth Russell <[email protected]> Reviewed-by: Charlie Lao <[email protected]> --- diff --git a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp index f36d02c..620822e 100644 --- a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp +++ b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp @@ -529,7 +529,8 @@ UtilsVk::ConvertIndexParameters params = {}; params.srcOffset = static_cast<uint32_t>(offsetIntoSrcData); params.dstOffset = 0; - params.maxIndex = static_cast<uint32_t>(bufferVk->getSize()); + // Remaining space in buffer was already computed above. + params.maxIndex = static_cast<uint32_t>(srcDataSize); ANGLE_TRY(contextVk->getUtils().convertIndexBuffer(contextVk, dst, src, params)); mTranslatedByteIndexData.clearDirty(); diff --git a/src/tests/gl_tests/VertexAttributeTest.cpp b/src/tests/gl_tests/VertexAttributeTest.cpp index 098713a..ebaaf85 100644 --- a/src/tests/gl_tests/VertexAttributeTest.cpp +++ b/src/tests/gl_tests/VertexAttributeTest.cpp @@ -8,6 +8,7 @@ # pragma allow_unsafe_buffers #endif +#include <cmath> #include "anglebase/numerics/safe_conversions.h" #include "common/mathutil.h" #include "test_utils/ANGLETest.h" @@ -5857,6 +5858,111 @@ EXPECT_GL_NO_ERROR(); } +class VertexAttributeUint8Test : public VertexAttributeTestES3 +{}; + +// Regression test for a bug in emulation of 8-bit indices, when the end of +// the index buffer is used. +TEST_P(VertexAttributeUint8Test, ConvertUint8IndexAtEndOfBuffer) +{ + ANGLE_GL_PROGRAM(prog, essl3_shaders::vs::Simple(), essl3_shaders::fs::Red()); + ANGLE_GL_PROGRAM(prog2, essl3_shaders::vs::Simple(), essl3_shaders::fs::Blue()); + glUseProgram(prog); + + GLVertexArray vao; + glBindVertexArray(vao); + + // Vertex buffer: 256 vertices so any uint8 index value is valid for + // robust-access vertex fetch (avoids unrelated OOB on the draw side). + GLBuffer vbo; + glBindBuffer(GL_ARRAY_BUFFER, vbo); + std::vector<float> verts(256 * 2); + for (int i = 0; i < 256; i++) + { + float x, y; + // Vertices 0, 1, 2: cover the right half of the framebuffer + // Vertices 3, 4, 5: cover the left half of the framebuffer + switch (i % 6) + { + case 0: + x = 0; + y = -2; + break; + case 1: + x = 2; + y = 0; + break; + case 2: + x = 0; + y = 2; + break; + case 3: + x = 0; + y = -2; + break; + case 4: + x = 0; + y = 2; + break; + case 5: + x = -2; + y = 0; + break; + } + verts[i * 2] = x; + verts[i * 2 + 1] = y; + } + glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_STATIC_DRAW); + + GLint aLoc = glGetAttribLocation(prog, "a_position"); + glEnableVertexAttribArray(aLoc); + glVertexAttribPointer(aLoc, 2, GL_FLOAT, GL_FALSE, 0, 0); + + // Create a large index buffer, contents of which don't really matter. + // Filled with low values so any vertex fetch is in-range. + const size_t kEboSize = 65536; + GLBuffer ebo; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); + std::vector<uint8_t> indices(kEboSize); + for (size_t i = 0; i < kEboSize - 3; i++) + { + indices[i] = i % 3; + } + // Set indices 65533, 65534, 65535 to 3, 4, 5 to cover the left + // half of the framebuffer. + indices[kEboSize - 3] = 3; + indices[kEboSize - 2] = 4; + indices[kEboSize - 1] = 5; + glBufferData(GL_ELEMENT_ARRAY_BUFFER, kEboSize, indices.data(), GL_STATIC_DRAW); + + // Draw red to the right half of the framebuffer. This draw call ensures that the + // index buffer is in use by the GPU, so the emulation, if any, would prefer the GPU path. + glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, 0); + + // Draw blue to the left half of the framebuffer. The draw call uses an offset to the + // end of the index buffer. This is a regression test for a bug where the 8-bit index + // emulation path miscalculated the range to emulate. + const size_t kOffset = 65533; + const size_t kCount = 3; + glUseProgram(prog2); + GLint aLoc2 = glGetAttribLocation(prog, "a_position"); + glEnableVertexAttribArray(aLoc2); + glVertexAttribPointer(aLoc2, 2, GL_FLOAT, GL_FALSE, 0, 0); + glDrawElements(GL_TRIANGLES, kCount, GL_UNSIGNED_BYTE, reinterpret_cast<void *>(kOffset)); + + const int w = getWindowWidth(); + const int h = getWindowHeight(); + + EXPECT_PIXEL_RECT_EQ(0, 0, w / 2 - 1, h, GLColor::blue); + EXPECT_PIXEL_RECT_EQ(w / 2 + 1, 0, w / 2 - 2, h, GLColor::red); + ASSERT_GL_NO_ERROR(); +} + +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(VertexAttributeUint8Test); +ANGLE_INSTANTIATE_TEST_ES3_AND(VertexAttributeUint8Test, + ES3_VULKAN().disable(Feature::SupportsIndexTypeUint8), + ES3_VULKAN_SWIFTSHADER().disable(Feature::SupportsIndexTypeUint8)); + ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND( VertexAttributeShiftInstancedArrayDataWithOffsetTest, ES2_OPENGL().enable(Feature::ShiftInstancedArrayDataWithOffset),
Regression Test / PoC
diff --git a/src/tests/gl_tests/VertexAttributeTest.cpp b/src/tests/gl_tests/VertexAttributeTest.cpp
index 098713a..ebaaf85 100644
--- a/src/tests/gl_tests/VertexAttributeTest.cpp
+++ b/src/tests/gl_tests/VertexAttributeTest.cpp
@@ -8,6 +8,7 @@
# pragma allow_unsafe_buffers
#endif
+#include <cmath>
#include "anglebase/numerics/safe_conversions.h"
#include "common/mathutil.h"
#include "test_utils/ANGLETest.h"
@@ -5857,6 +5858,111 @@
EXPECT_GL_NO_ERROR();
}
+class VertexAttributeUint8Test : public VertexAttributeTestES3
+{};
+
+// Regression test for a bug in emulation of 8-bit indices, when the end of
+// the index buffer is used.
+TEST_P(VertexAttributeUint8Test, ConvertUint8IndexAtEndOfBuffer)
+{
+ ANGLE_GL_PROGRAM(prog, essl3_shaders::vs::Simple(), essl3_shaders::fs::Red());
+ ANGLE_GL_PROGRAM(prog2, essl3_shaders::vs::Simple(), essl3_shaders::fs::Blue());
+ glUseProgram(prog);
+
+ GLVertexArray vao;
+ glBindVertexArray(vao);
+
+ // Vertex buffer: 256 vertices so any uint8 index value is valid for
+ // robust-access vertex fetch (avoids unrelated OOB on the draw side).
+ GLBuffer vbo;
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+ std::vector<float> verts(256 * 2);
+ for (int i = 0; i < 256; i++)
+ {
+ float x, y;
+ // Vertices 0, 1, 2: cover the right half of the framebuffer
+ // Vertices 3, 4, 5: cover the left half of the framebuffer
+ switch (i % 6)
+ {
+ case 0:
+ x = 0;
+ y = -2;
+ break;
+ case 1:
+ x = 2;
+ y = 0;
+ break;
+ case 2:
+ x = 0;
+ y = 2;
+ break;
+ case 3:
+ x = 0;
+ y = -2;
+ break;
+ case 4:
+ x = 0;
+ y = 2;
+ break;
+ case 5:
+ x = -2;
+ y = 0;
+ break;
+ }
+ verts[i * 2] = x;
+ verts[i * 2 + 1] = y;
+ }
+ glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_STATIC_DRAW);
+
+ GLint aLoc = glGetAttribLocation(prog, "a_position");
+ glEnableVertexAttribArray(aLoc);
+ glVertexAttribPointer(aLoc, 2, GL_FLOAT, GL_FALSE, 0, 0);
+
+ // Create a large index buffer, contents of which don't really matter.
+ // Filled with low values so any vertex fetch is in-range.
+ const size_t kEboSize = 65536;
+ GLBuffer ebo;
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
+ std::vector<uint8_t> indices(kEboSize);
+ for (size_t i = 0; i < kEboSize - 3; i++)
+ {
+ indices[i] = i % 3;
+ }
+ // Set indices 65533, 65534, 65535 to 3, 4, 5 to cover the left
+ // half of the framebuffer.
+ indices[kEboSize - 3] = 3;
+ indices[kEboSize - 2] = 4;
+ indices[kEboSize - 1] = 5;
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, kEboSize, indices.data(), GL_STATIC_DRAW);
+
+ // Draw red to the right half of the framebuffer. This draw call ensures that the
+ // index buffer is in use by the GPU, so the emulation, if any, would prefer the GPU path.
+ glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, 0);
+
+ // Draw blue to the left half of the framebuffer. The draw call uses an offset to the
+ // end of the index buffer. This is a regression test for a bug where the 8-bit index
+ // emulation path miscalculated the range to emulate.
+ const size_t kOffset = 65533;
+ const size_t kCount = 3;
+ glUseProgram(prog2);
+ GLint aLoc2 = glGetAttribLocation(prog, "a_position");
+ glEnableVertexAttribArray(aLoc2);
+ glVertexAttribPointer(aLoc2, 2, GL_FLOAT, GL_FALSE, 0, 0);
+ glDrawElements(GL_TRIANGLES, kCount, GL_UNSIGNED_BYTE, reinterpret_cast<void *>(kOffset));
+
+ const int w = getWindowWidth();
+ const int h = getWindowHeight();
+
+ EXPECT_PIXEL_RECT_EQ(0, 0, w / 2 - 1, h, GLColor::blue);
+ EXPECT_PIXEL_RECT_EQ(w / 2 + 1, 0, w / 2 - 2, h, GLColor::red);
+ ASSERT_GL_NO_ERROR();
+}
+
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(VertexAttributeUint8Test);
+ANGLE_INSTANTIATE_TEST_ES3_AND(VertexAttributeUint8Test,
+ ES3_VULKAN().disable(Feature::SupportsIndexTypeUint8),
+ ES3_VULKAN_SWIFTSHADER().disable(Feature::SupportsIndexTypeUint8));
+
ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
VertexAttributeShiftInstancedArrayDataWithOffsetTest,
ES2_OPENGL().enable(Feature::ShiftInstancedArrayDataWithOffset),
Original Bug Report
OOB Read/Write in ANGLE Vulkan Index Buffer Conversion
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: A potential out-of-bounds read and write vulnerability exists in ANGLE’s Vulkan backend when emulating uint8 index buffers. The convertIndexBufferGPU function incorrectly uses the full source buffer size to set the compute shader’s workload, rather than the remaining size after the draw offset. Because ANGLE uses memory suballocations, this causes the shader to massively overrun the destination suballocation, potentially leading to arbitrary GPU memory corruption.
Affected files:
src/libANGLE/renderer/vulkan/VertexArrayVk.cppsrc/libANGLE/renderer/vulkan/shaders/src/ConvertIndex.compsrc/libANGLE/renderer/vulkan/UtilsVk.cpp
Estimated timestamp from git blame: 2022-01-06
Summary
There is a potential out-of-bounds (OOB) read and write vulnerability in VertexArrayVk::convertIndexBufferGPU within ANGLE’s Vulkan backend. On Vulkan implementations that do not support the VK_EXT_index_type_uint8 extension, ANGLE emulates uint8 indices by converting them to uint16 indices using a compute shader (ConvertIndex.comp).
When calculating the shader parameters, the code incorrectly uses the total size of the original buffer as the maxIndex limit, instead of calculating the limit based on the size of the data actually being converted (total size minus the draw offset). This mismatch results in the shader writing far beyond its allocated destination buffer.
Technical Details
In third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp, the convertIndexBufferGPU function correctly calculates the size of the data that needs to be converted and allocates the destination suballocation accordingly:
// srcDataSize is correctly bounded by the offset
size_t srcDataSize = static_cast<size_t>(bufferVk->getSize()) - offsetIntoSrcData;
// Allocate buffer for results (e.g. if srcDataSize is 3, allocates ~6 bytes)
ANGLE_TRY(contextVk->initBufferForVertexConversion(&mTranslatedByteIndexData,
sizeof(GLushort) * srcDataSize,
vk::MemoryHostVisibility::NonVisible));
However, a few lines later, when preparing the parameters for the compute shader, maxIndex is set to the full size of the original buffer, disregarding the offset:
params.maxIndex = static_cast<uint32_t>(bufferVk->getSize()); // Flawed!
This maxIndex determines how many workgroups are dispatched and dictates the bounds check inside the ConvertIndex.comp compute shader. If an attacker supplies a large buffer but issues a draw call with an offset near the end of the buffer, srcDataSize will be very small, resulting in a tiny destination suballocation. However, maxIndex will remain extremely large.
The compute shader will consequently write maxIndex * 2 bytes to the destination array. Because ANGLE pools allocations into large (e.g., 8MB) BufferBlocks backing a single VkBuffer/VkDeviceMemory, standard Vulkan robustBufferAccess hardware checks (which only validate against the underlying memory object bounds) are bypassed. The out-of-bounds write corrupts adjacent suballocations within the shared buffer pool.
Potential Exploitation Steps
Note: These are suggested steps to trigger the vulnerability based on code analysis. Our tooling agent does not currently have the capability to run a working Proof of Concept (PoC) to verify execution natively.
- Environment: Target a device lacking
VK_EXT_index_type_uint8(common on many Android Vulkan drivers). - Buffer Creation: In WebGL, create a large
ELEMENT_ARRAY_BUFFER(e.g., 65,536 bytes). - Pool Grooming: Carefully create and delete other WebGL buffers to groom the Vulkan
BufferPool. The goal is to position a sensitive object—like an indirect draw command buffer (VkDrawIndexedIndirectCommand) or a Uniform Buffer Object (UBO)—immediately after the destination suballocation for the upcoming conversion. - Force GPU Path: Issue a benign draw call using the source buffer to make it “GPU-busy.” This forces ANGLE to take the asynchronous GPU compute shader conversion path instead of the CPU mapping path.
- Trigger Bug: Call
gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_BYTE, 65533), where65533is the offset. - Result: ANGLE allocates a tiny destination suballocation (~6 bytes) but commands the shader to process 65,536 indices. The shader writes ~131KB out-of-bounds, overwriting the groomed target object. This could allow the attacker to execute arbitrary indirect commands on the GPU, leading to a Renderer-to-GPU Sandbox Escape.
Suggested Fix
Update VertexArrayVk::convertIndexBufferGPU to calculate maxIndex based on srcDataSize rather than the total buffer size.
// Fix in src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
params.srcOffset = static_cast<uint32_t>(offsetIntoSrcData);
params.dstOffset = 0;
- params.maxIndex = static_cast<uint32_t>(bufferVk->getSize());
+ params.maxIndex = static_cast<uint32_t>(srcDataSize);
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.