CVE-2026-13975
Overview
Files Changed
src/libANGLE/renderer/metal/mtl_command_buffer.hsrc/libANGLE/renderer/metal/mtl_command_buffer.mmsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/DrawBaseVertexBaseInstanceTest.cpp
Patch
From 6f2c0162c12f50ec9c32fef603a86d332d46f8e9 Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <lehoangquyen@chromium.org>
Date: Tue, 19 May 2026 16:35:00 +0800
Subject: [PATCH] Metal: Fix baseVertex type in RenderCommandEncoder
Change baseVertex type from uint32_t to int32_t in
drawIndexedInstancedBaseVertexBaseInstance to correctly support
negative values. Also fix the stream fetch to use int32_t.
Added a regression test NegativeBaseVertex.
Even before this change, the sign mismatch bug didn't seem to affect
the test results, possibly because the Metal driver performs the
computation in 32 bits.
Bug: chromium:513857658
Change-Id: I351ba1f3a5f84545ab6944686a091dad24b636ba
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7859153
Reviewed-by: Kenneth Russell <kbr@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@google.com>
---
diff --git a/src/libANGLE/renderer/metal/mtl_command_buffer.h b/src/libANGLE/renderer/metal/mtl_command_buffer.h
index 285645a..443f2e4 100644
--- a/src/libANGLE/renderer/metal/mtl_command_buffer.h
+++ b/src/libANGLE/renderer/metal/mtl_command_buffer.h
@@ -560,7 +560,7 @@
const BufferRef &indexBuffer,
size_t bufferOffset,
uint32_t instances,
- uint32_t baseVertex,
+ int32_t baseVertex,
uint32_t baseInstance);
RenderCommandEncoder &setVisibilityResultMode(MTLVisibilityResultMode mode, size_t offset);
diff --git a/src/libANGLE/renderer/metal/mtl_command_buffer.mm b/src/libANGLE/renderer/metal/mtl_command_buffer.mm
index a84b094..0decccb 100644
--- a/src/libANGLE/renderer/metal/mtl_command_buffer.mm
+++ b/src/libANGLE/renderer/metal/mtl_command_buffer.mm
@@ -358,7 +358,7 @@
id<MTLBuffer> indexBuffer = stream->fetch<id<MTLBuffer>>();
size_t bufferOffset = stream->fetch<size_t>();
uint32_t instances = stream->fetch<uint32_t>();
- uint32_t baseVertex = stream->fetch<uint32_t>();
+ int32_t baseVertex = stream->fetch<int32_t>();
uint32_t baseInstance = stream->fetch<uint32_t>();
[encoder drawIndexedPrimitives:primitiveType
indexCount:indexCount
@@ -2199,7 +2199,7 @@
const BufferRef &indexBuffer,
size_t bufferOffset,
uint32_t instances,
- uint32_t baseVertex,
+ int32_t baseVertex,
uint32_t baseInstance)
{
ASSERT(mPipelineStateSet &&
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 6717d56..046f8d0 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -574,6 +574,7 @@
496259841 WIN D3D11 : RobustResourceInitTestES3.DrawThenInvalidateThenVerifyDepthStencil/* = SKIP
512896813 WIN D3D11 : LineLoopTestES3.LineLoopDrawArraysInstancedBaseInstance/* = SKIP
512896813 WIN D3D11 : LineLoopTestES3.LineLoopDrawElementsInstancedBaseVertexBaseInstance/* = SKIP
+514615434 D3D11 : DrawBaseVertexBaseInstanceTest.NegativeBaseVertex/* = SKIP
// Android
42264624 ANDROID GLES : GLSLTest_ES3.InitGlobalComplexConstant/* = SKIP
diff --git a/src/tests/gl_tests/DrawBaseVertexBaseInstanceTest.cpp b/src/tests/gl_tests/DrawBaseVertexBaseInstanceTest.cpp
index bb076171..1ccbebc 100644
--- a/src/tests/gl_tests/DrawBaseVertexBaseInstanceTest.cpp
+++ b/src/tests/gl_tests/DrawBaseVertexBaseInstanceTest.cpp
@@ -835,6 +835,41 @@
setupProgram(p3, false, true);
}
+// Tests that negative baseVertex works properly.
+TEST_P(DrawBaseVertexBaseInstanceTest, NegativeBaseVertex)
+{
+ ANGLE_SKIP_TEST_IF(!requestExtensions());
+
+ GLProgram program;
+ setupProgram(program, false, false);
+
+ GLBuffer vertexBuffer;
+ GLBuffer indexBuffer;
+ setupIndexedBuffers(vertexBuffer, indexBuffer);
+ setupPositionVertexAttribPointer();
+
+ // Create a new index buffer with shifted indices: {4, 5, 6, 4, 6, 7}
+ // These indices point to the second quad (vertices 4, 5, 6, 7).
+ std::vector<GLushort> shiftedIndices = {4, 5, 6, 4, 6, 7};
+ GLBuffer shiftedIndexBuffer;
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, shiftedIndexBuffer);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * shiftedIndices.size(),
+ shiftedIndices.data(), GL_STATIC_DRAW);
+
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ // Draw using baseVertex = -4.
+ // This should shift the indices back to {0, 1, 2, 0, 2, 3}, drawing the first quad!
+ glDrawElementsInstancedBaseVertexBaseInstanceANGLE(
+ GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, reinterpret_cast<GLvoid *>(static_cast<uintptr_t>(0)),
+ 1, -4, 0);
+
+ ASSERT_GL_NO_ERROR();
+
+ // Check that the first quad was drawn as white.
+ EXPECT_PIXEL_NEAR(16, 16, 255, 255, 255, 255, 3);
+}
+
// Tests if baseInstance works properly with instanced array with non-zero divisor
TEST_P(DrawBaseVertexBaseInstanceTest, BaseInstanceDivisor)
{
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 6717d56..046f8d0 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -574,6 +574,7 @@
496259841 WIN D3D11 : RobustResourceInitTestES3.DrawThenInvalidateThenVerifyDepthStencil/* = SKIP
512896813 WIN D3D11 : LineLoopTestES3.LineLoopDrawArraysInstancedBaseInstance/* = SKIP
512896813 WIN D3D11 : LineLoopTestES3.LineLoopDrawElementsInstancedBaseVertexBaseInstance/* = SKIP
+514615434 D3D11 : DrawBaseVertexBaseInstanceTest.NegativeBaseVertex/* = SKIP
// Android
42264624 ANDROID GLES : GLSLTest_ES3.InitGlobalComplexConstant/* = SKIP
diff --git a/src/tests/gl_tests/DrawBaseVertexBaseInstanceTest.cpp b/src/tests/gl_tests/DrawBaseVertexBaseInstanceTest.cpp
index bb076171..1ccbebc 100644
--- a/src/tests/gl_tests/DrawBaseVertexBaseInstanceTest.cpp
+++ b/src/tests/gl_tests/DrawBaseVertexBaseInstanceTest.cpp
@@ -835,6 +835,41 @@
setupProgram(p3, false, true);
}
+// Tests that negative baseVertex works properly.
+TEST_P(DrawBaseVertexBaseInstanceTest, NegativeBaseVertex)
+{
+ ANGLE_SKIP_TEST_IF(!requestExtensions());
+
+ GLProgram program;
+ setupProgram(program, false, false);
+
+ GLBuffer vertexBuffer;
+ GLBuffer indexBuffer;
+ setupIndexedBuffers(vertexBuffer, indexBuffer);
+ setupPositionVertexAttribPointer();
+
+ // Create a new index buffer with shifted indices: {4, 5, 6, 4, 6, 7}
+ // These indices point to the second quad (vertices 4, 5, 6, 7).
+ std::vector<GLushort> shiftedIndices = {4, 5, 6, 4, 6, 7};
+ GLBuffer shiftedIndexBuffer;
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, shiftedIndexBuffer);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * shiftedIndices.size(),
+ shiftedIndices.data(), GL_STATIC_DRAW);
+
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ // Draw using baseVertex = -4.
+ // This should shift the indices back to {0, 1, 2, 0, 2, 3}, drawing the first quad!
+ glDrawElementsInstancedBaseVertexBaseInstanceANGLE(
+ GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, reinterpret_cast<GLvoid *>(static_cast<uintptr_t>(0)),
+ 1, -4, 0);
+
+ ASSERT_GL_NO_ERROR();
+
+ // Check that the first quad was drawn as white.
+ EXPECT_PIXEL_NEAR(16, 16, 255, 255, 255, 255, 3);
+}
+
// Tests if baseInstance works properly with instanced array with non-zero divisor
TEST_P(DrawBaseVertexBaseInstanceTest, BaseInstanceDivisor)
{
Original Bug Report
Potential sign-extension error in ANGLE Metal backend baseVertex leads to OOB vertex fetch
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A type mismatch in ANGLE’s Metal backend causes signed baseVertex values to be zero-extended when passed to the Metal API. This results in incorrect GPU memory offsets during vertex fetching, potentially allowing a compromised renderer to perform out-of-bounds reads from GPU memory.
Affected files:
third_party/angle/src/libANGLE/renderer/metal/mtl_command_buffer.mmthird_party/angle/src/libANGLE/renderer/metal/ContextMtl.mm
Estimated timestamp from git blame: Unknown (Google3 checkout)
Summary
A potential sign-extension vulnerability exists in ANGLE’s Metal backend implementation of indexed draw calls. The baseVertex parameter, which is a signed integer in OpenGL ES and the Metal API, is incorrectly treated as an unsigned 32-bit integer within ANGLE’s internal command stream. On 64-bit platforms, this causes negative baseVertex values to be zero-extended rather than sign-extended, leading to extremely large positive offsets during GPU vertex fetching.
Technical Details
The vulnerability exists in the transition between ANGLE’s front-end and the Metal back-end encoder:
- Entry Point:
ContextMtl::drawElementsImplinthird_party/angle/src/libANGLE/renderer/metal/ContextMtl.mmreceives thebaseVertexparameter as a signedGLint(32-bit signed). - Type Mismatch: It calls
RenderCommandEncoder::drawIndexedInstancedBaseVertexBaseInstanceinthird_party/angle/src/libANGLE/renderer/metal/mtl_command_buffer.mm. The signature of this method (and its declaration inmtl_command_buffer.h) incorrectly definesbaseVertexas auint32_t(line 2202):RenderCommandEncoder &RenderCommandEncoder::drawIndexedInstancedBaseVertexBaseInstance( ..., uint32_t baseVertex, ...) - Command Encoding: The signed value is implicitly cast to
uint32_tand pushed into theIntermediateCommandStream(line 2223). - API Invocation: During command execution,
DrawIndexedInstancedBaseVertexBaseInstanceCmdfetches the value as auint32_t(line 361) and passes it to the Metal API:[encoder drawIndexedPrimitives:primitiveType indexCount:indexCount indexType:indexType indexBuffer:indexBuffer indexBufferOffset:bufferOffset instanceCount:instances baseVertex:baseVertex // baseVertex is uint32_t here baseInstance:baseInstance];
On 64-bit macOS and iOS, the baseVertex parameter of drawIndexedPrimitives is an NSInteger (64-bit signed). When a uint32_t value like 0xFFFFFFFB (-5) is passed to a 64-bit NSInteger, it is zero-extended to 0x00000000FFFFFFFB (4,294,967,291) instead of being sign-extended to 0xFFFFFFFFFFFFFFFB (-5).
Impact
The GPU uses this zero-extended value to calculate vertex fetch addresses. For an index i, the effective index becomes i + 4,294,967,291. This causes the GPU to fetch vertex data from memory far beyond the bounds of the intended buffer (approximately 4GB offset).
Since the Metal backend advertises robustBufferAccessBehaviorKHR = false in DisplayMtl.mm (line 990), the driver is not required to clamp these accesses. This primitive could be used by a compromised renderer to disclose cross-origin GPU process memory.
Reachability
A compromised renderer can potentially trigger this path by enabling the GL_ANGLE_base_vertex_base_instance extension via the RequestExtensionCHROMIUM command. This extension is listed as requestable in the passthrough command decoder (gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc, line 278).
Suggested Potential Reproduction Steps
- From a compromised renderer (or with
--enable-webgl-draft-extensions), enable theGL_ANGLE_base_vertex_base_instanceextension usingglRequestExtensionCHROMIUM. - Create a small vertex buffer and bind it to an attribute.
- Create an index buffer with valid indices (e.g.,
[0, 1, 2]). - Issue a draw call with a negative
baseVertex:glDrawElementsInstancedBaseVertexBaseInstanceANGLE(..., -1, 0). - Observe that the GPU fetches vertices from an out-of-bounds offset (~4GB).
Proposed Fix
Change the baseVertex parameter type from uint32_t to int32_t (or GLint) in the following locations:
RenderCommandEncoder::drawIndexedInstancedBaseVertexBaseInstancesignature inmtl_command_buffer.handmtl_command_buffer.mm.- The
fetchandpushlogic inDrawIndexedInstancedBaseVertexBaseInstanceCmdand its corresponding encoder method inmtl_command_buffer.mm.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.