Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker513857658
Fix commit6f2c0162c12f (angle/angle) +39/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • src/libANGLE/renderer/metal/mtl_command_buffer.h
  • src/libANGLE/renderer/metal/mtl_command_buffer.mm
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/DrawBaseVertexBaseInstanceTest.cpp
From 6f2c0162c12f50ec9c32fef603a86d332d46f8e9 Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <[email protected]>
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 <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Quyen Le <[email protected]>
---

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)
 {
Loading diff…

Regression Test / PoC

shipped with the fix
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)
 {
Loading diff…

Original Bug Report

reported by [email protected]

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.mm
  • third_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:

  1. Entry Point: ContextMtl::drawElementsImpl in third_party/angle/src/libANGLE/renderer/metal/ContextMtl.mm receives the baseVertex parameter as a signed GLint (32-bit signed).
  2. Type Mismatch: It calls RenderCommandEncoder::drawIndexedInstancedBaseVertexBaseInstance in third_party/angle/src/libANGLE/renderer/metal/mtl_command_buffer.mm. The signature of this method (and its declaration in mtl_command_buffer.h) incorrectly defines baseVertex as a uint32_t (line 2202):
    RenderCommandEncoder &RenderCommandEncoder::drawIndexedInstancedBaseVertexBaseInstance(
        ...,
        uint32_t baseVertex,
        ...)
    
  3. Command Encoding: The signed value is implicitly cast to uint32_t and pushed into the IntermediateCommandStream (line 2223).
  4. API Invocation: During command execution, DrawIndexedInstancedBaseVertexBaseInstanceCmd fetches the value as a uint32_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

  1. From a compromised renderer (or with --enable-webgl-draft-extensions), enable the GL_ANGLE_base_vertex_base_instance extension using glRequestExtensionCHROMIUM.
  2. Create a small vertex buffer and bind it to an attribute.
  3. Create an index buffer with valid indices (e.g., [0, 1, 2]).
  4. Issue a draw call with a negative baseVertex: glDrawElementsInstancedBaseVertexBaseInstanceANGLE(..., -1, 0).
  5. 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::drawIndexedInstancedBaseVertexBaseInstance signature in mtl_command_buffer.h and mtl_command_buffer.mm.
  • The fetch and push logic in DrawIndexedInstancedBaseVertexBaseInstanceCmd and its corresponding encoder method in mtl_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.

View on issue tracker