Medium watch CVSS 5.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Sub-componentMetal
Bug ClassOOB
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513857658
Fix commit6f2c0162c12f (angle/angle) +39/-3
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-16 (per vendor advisory)
Fix landed2026-05-19 (public git)
Disclosed2026-06-30
Patch gap42 days — fix public in source before the advisory

Background

`baseVertex`
A signed offset added to every index fetched from the element array buffer before the vertex is looked up, allowing draws such as glDrawElementsInstancedBaseVertexBaseInstanceANGLE to shift into or (with negative values) back from a region of the vertex buffer.
`RenderCommandEncoder`
The ANGLE Metal-backend object that records draw commands into a serialized byte stream that is later replayed against a real id<MTLRenderCommandEncoder>.
Command stream `fetch`
The stream->fetch<T>() helper that deserializes a value of type T from the recorded command buffer in the exact order and width it was written.
`drawIndexedInstancedBaseVertexBaseInstance`
The RenderCommandEncoder method that records an indexed, instanced draw carrying baseVertex and baseInstance parameters down to Metal.

Root Cause Analysis

The RenderCommandEncoder::drawIndexedInstancedBaseVertexBaseInstance method declared baseVertex as uint32_t, and the corresponding replay path in mtl_command_buffer.mm deserialized it with stream->fetch<uint32_t>(). Because the GL entry point accepts a signed GLint baseVertex, a legitimate negative value (used to shift shifted indices back into range) was reinterpreted as a large unsigned integer when it crossed the ANGLE Metal boundary. The invariant violated is that the signedness/width of the value written into the command stream must match the type read back out and the semantic sign of the API parameter.

The fix changes both the declaration in mtl_command_buffer.h and the fetch in mtl_command_buffer.mm to int32_t, so a negative baseVertex is carried through and applied with the correct sign. The commit notes the mismatch happened not to change results in practice because the Metal driver performs the index+baseVertex computation in 32 bits, but relying on that is unsound and the correct signed type removes the latent out-of-bounds index computation.

Key insight
The single core mistake is a signed-to-unsigned type mismatch: a GLint baseVertex was stored and re-read as uint32_t, so negative offsets became huge positive ones; the fix makes the storage and fetch types int32_t to preserve the sign.

Attack Path

  1. Issue a draw with a negative `baseVertex` An application (or a compromised renderer via WebGL) calls glDrawElementsInstancedBaseVertexBaseInstanceANGLE with a negative baseVertex, as the new NegativeBaseVertex regression test does with -4.
  2. Sign is lost at record time ANGLE records the parameter through drawIndexedInstancedBaseVertexBaseInstance, whose uint32_t baseVertex sign-extends the negative value into a large unsigned integer.
  3. Replay reads the wrong width/sign The Metal replay path in mtl_command_buffer.mm deserializes the value with stream->fetch<uint32_t>(), producing an incorrect base-vertex offset for the indexed draw.
  4. Out-of-bounds index computation (inferred) The incorrect large offset combined with the fetched indices can produce vertex indices outside the bound vertex buffer, matching the advisory’s out-of-bounds read classification, though the diff does not show the concrete Metal read sink.

Impact Assessment

An attacker able to drive GPU draw calls (for example through WebGL in the GPU/ANGLE process) could cause an out-of-bounds read in ANGLE’s Metal backend by supplying a negative baseVertex that is mishandled as unsigned, per the advisory’s “Out of bounds read in ANGLE” classification. The precondition is issuing an indexed instanced draw with a negative base-vertex through the affected entry point on the Metal backend. The advisory states no exploited-in-the-wild status, and any escalation of this read into an information leak or further compromise is (inferred) rather than demonstrated by the patch, which notes the sign mismatch did not even alter test results because the Metal driver computes in 32 bits.

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

Audit Directions

  • Signed/unsigned parameter crossings
    Audit every ANGLE backend entry that carries a GLint-typed value (offsets, base indices, base vertex/instance) into internal uint32_t fields or stream->fetch<> calls for sign mismatches.
  • Command-stream serialize/fetch width parity
    Verify that every value written into a RenderCommandEncoder command stream is read back with the identical type and width in the corresponding replay path.
  • Base-vertex and base-instance handling across backends
    Review the D3D11, Vulkan, and GL backends’ handling of negative baseVertex/baseInstance for the same class of sign-loss and out-of-bounds index computation.
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)
 {
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 vm...@google.com

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