Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker511737097
Fix commitca1dbd0b011c (angle/angle) +148/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • src/libANGLE/renderer/metal/ContextMtl.mm
  • src/libANGLE/renderer/metal/VertexArrayMtl.h
  • src/libANGLE/renderer/metal/VertexArrayMtl.mm
  • src/tests/gl_tests/StateChangeTest.cpp
From ca1dbd0b011ca3eeaae3a280bdd79cc8e22d211e Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <lehoangquyen@chromium.org>
Date: Tue, 12 May 2026 23:40:16 +0800
Subject: [PATCH] Metal: fix primitive restart with converted indices

When using GL_UNSIGNED_BYTE indices, ANGLE's Metal backend converts
them to GL_UNSIGNED_SHORT in a shared pool.
VertexArrayMtl::getDrawIndices calculates currentIndexOffset by dividing
the pool offset by the index type size, making it relative to the start
of the pool sub-allocation. However, restart ranges from
BufferMtl::getRestartIndices are absolute element indices relative to
the original GL buffer. This mismatch causes incorrect draw splitting
when a non-zero pool offset is present, as currentIndexOffset is
artificially inflated.

Bug: angleproject:511737097
Change-Id: I80e2d0d20d38376aa912c851954701fac510ea46
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7839560
Reviewed-by: Kenneth Russell <kbr@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@google.com>
---

diff --git a/src/libANGLE/renderer/metal/ContextMtl.mm b/src/libANGLE/renderer/metal/ContextMtl.mm
index 21f9753..3fb5453 100644
--- a/src/libANGLE/renderer/metal/ContextMtl.mm
+++ b/src/libANGLE/renderer/metal/ContextMtl.mm
@@ -804,7 +804,7 @@
     // as drawIdxBuffer.
     const std::vector<DrawCommandRange> drawCommands =
         mVertexArray->getDrawIndices(context, type, convertedType, originalMode, mode, idxBuffer,
-                                     (uint32_t)count, convertedOffset);
+                                     (uint32_t)count, indices, convertedOffset);
 
     bool isNoOp = false;
     ANGLE_TRY(setupDraw(context, 0, count, instances, type, indices, false, &isNoOp));
diff --git a/src/libANGLE/renderer/metal/VertexArrayMtl.h b/src/libANGLE/renderer/metal/VertexArrayMtl.h
index f08ff02..66aa2bc 100644
--- a/src/libANGLE/renderer/metal/VertexArrayMtl.h
+++ b/src/libANGLE/renderer/metal/VertexArrayMtl.h
@@ -69,7 +69,8 @@
                                                  gl::PrimitiveMode mode,
                                                  mtl::BufferRef idxBuffer,
                                                  uint32_t indexCount,
-                                                 size_t offset);
+                                                 const void *originalOffsetOrClientPtr,
+                                                 size_t offsetInBytes);
 
   private:
     void reset(ContextMtl *context);
diff --git a/src/libANGLE/renderer/metal/VertexArrayMtl.mm b/src/libANGLE/renderer/metal/VertexArrayMtl.mm
index 7f103f0..3c1baf6 100644
--- a/src/libANGLE/renderer/metal/VertexArrayMtl.mm
+++ b/src/libANGLE/renderer/metal/VertexArrayMtl.mm
@@ -747,7 +747,8 @@
                                                              gl::PrimitiveMode mode,
                                                              mtl::BufferRef clientBuffer,
                                                              uint32_t indexCount,
-                                                             size_t offset)
+                                                             const void *originalOffsetOrClientPtr,
+                                                             size_t offsetInBytes)
 {
     ContextMtl *contextMtl = mtl::GetImpl(glContext);
     std::vector<DrawCommandRange> drawCommands;
@@ -759,23 +760,31 @@
     bool indicesRewritten = (originalMode != mode);
     if ((!isSimpleType && !indicesRewritten) || !glContext->getState().isPrimitiveRestartEnabled())
     {
-        drawCommands.push_back({indexCount, offset});
+        drawCommands.push_back({indexCount, offsetInBytes});
         return drawCommands;
     }
 
     const std::vector<IndexRange> *restartIndices;
     std::vector<IndexRange> clientIndexRange;
     const gl::Buffer *glElementArrayBuffer = getElementArrayBuffer();
+    size_t startIndex;
     if (glElementArrayBuffer)
     {
         BufferMtl *idxBuffer = mtl::GetImpl(glElementArrayBuffer);
         restartIndices       = &idxBuffer->getRestartIndices(contextMtl, originalIndexType);
+        // restartIndices is relative to the original element buffer, hence we use the original
+        // offset to calculate startIndex.
+        size_t originalOffsetInBytes = reinterpret_cast<size_t>(originalOffsetOrClientPtr);
+        startIndex = originalOffsetInBytes / gl::GetDrawElementsTypeSize(originalIndexType);
     }
     else
     {
         clientIndexRange =
             BufferMtl::getRestartIndicesFromClientData(contextMtl, indexType, clientBuffer);
         restartIndices = &clientIndexRange;
+        // For client indices, restartIndices is relative to the clientBuffer, we use offsetInBytes
+        // to calculate startIndex.
+        startIndex = offsetInBytes / gl::GetDrawElementsTypeSize(indexType);
     }
 
     uint32_t nIndicesPerPrimitive;
@@ -851,8 +860,7 @@
 
     const GLuint indexTypeBytes = gl::GetDrawElementsTypeSize(indexType);
     uint32_t indicesLeft        = indexCount;
-    size_t currentIndexOffset   = offset / indexTypeBytes;
-    const size_t baseOffset     = currentIndexOffset;
+    size_t currentIndexOffset   = startIndex;
 
     auto addDrawCommand = [&](uint32_t count, size_t elementOffset) {
         // Skip slices that don't contain enough indices to form a single primitive.
@@ -862,7 +870,7 @@
             uint32_t transformedCount = (count - stripExclude) * factor;
             // Scale the offset to account for the expansion factor.
             size_t transformedOffset =
-                offset + (elementOffset - baseOffset) * (size_t)factor * indexTypeBytes;
+                offsetInBytes + (elementOffset - startIndex) * (size_t)factor * indexTypeBytes;
             drawCommands.push_back({transformedCount, transformedOffset});
         }
     };
diff --git a/src/tests/gl_tests/StateChangeTest.cpp b/src/tests/gl_tests/StateChangeTest.cpp
index 669c715..73b5e34 100644
--- a/src/tests/gl_tests/StateChangeTest.cpp
+++ b/src/tests/gl_tests/StateChangeTest.cpp
@@ -10448,6 +10448,138 @@
     ASSERT_GL_NO_ERROR();
 }
 
+// Tests that primitive restart works correctly when drawing with an unsigned byte element array
+// buffer that is updated between draw calls. Buffer updates for unsigned bytes might result in
+// faulty index conversion in some backends.
+TEST_P(StateChangeTestES3, PrimitiveRestartWithUnsignedBytesIndexBufferUpdates)
+{
+    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor());
+    glUseProgram(program);
+
+    GLint colorLoc = glGetUniformLocation(program, angle::essl1_shaders::ColorUniform());
+    ASSERT_NE(colorLoc, -1);
+
+    GLint posAttrib = glGetAttribLocation(program, essl1_shaders::PositionAttrib());
+    ASSERT_EQ(0, posAttrib);
+
+    // Two triangles forming a full-screen quad split along the diagonal.
+    // Triangle 1: upper-left half  (-1,-1), (1,1), (-1,1)
+    // Triangle 2: lower-right half (-1,-1), (1,-1), (1,1)
+    std::vector<Vector3> positionData = {
+        {-1.0f, -1.0f, 0.0f}, {1.0f, 1.0f, 0.0f},  {-1.0f, 1.0f, 0.0f},  // Triangle 1
+        {-1.0f, -1.0f, 0.0f}, {1.0f, -1.0f, 0.0f}, {1.0f, 1.0f, 0.0f}    // Triangle 2
+    };
+
+    // Indices for Triangle 1 only
+    std::vector<GLubyte> indices1 = {0, 1, 2};
+
+    GLBuffer posBuffer;
+    glBindBuffer(GL_ARRAY_BUFFER, posBuffer);
+    glBufferData(GL_ARRAY_BUFFER, positionData.size() * sizeof(positionData[0]),
+                 positionData.data(), GL_STATIC_DRAW);
+    glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(posAttrib);
+
+    GLBuffer indexBuffer;
+    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
+    glBufferData(GL_ELEMENT_ARRAY_BUFFER, 7, nullptr, GL_STREAM_DRAW);
+
+    const int w = getWindowWidth();
+    const int h = getWindowHeight();
+
+    glClearColor(0, 0, 0, 1);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
+
+    // Draw 1: Red color, Triangle 1
+    glUniform4f(colorLoc, 1, 0, 0, 1);  // Red
+    glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, indices1.size(), indices1.data());
+    glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, nullptr);
+
+    // Verify after Draw 1
+    EXPECT_PIXEL_COLOR_EQ(w / 4, h / 2, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(3 * w / 4, h / 2, GLColor::black);
+
+    // Update index buffer to have Triangle 1, Restart, Triangle 2
+    std::vector<GLubyte> indices2 = {0, 1, 2, 0xFF, 3, 4, 5};
+    glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, indices2.size(), indices2.data());
+
+    // Draw 2: Blue color, both triangles
+    glUniform4f(colorLoc, 0, 0, 1, 1);  // Blue
+    glDrawElements(GL_TRIANGLES, 7, GL_UNSIGNED_BYTE, nullptr);
+
+    // Verify after Draw 2
+    EXPECT_PIXEL_COLOR_EQ(w / 4, h / 2, GLColor::blue);
+    EXPECT_PIXEL_COLOR_EQ(3 * w / 4, h / 2, GLColor::blue);
+
+    ASSERT_GL_NO_ERROR();
+}
+
+// Tests that primitive restart works correctly when drawing with an unsigned byte client indices
+// that are updated between draw calls.
+TEST_P(StateChangeTestES3, PrimitiveRestartWithUnsignedBytesClientIndexData)
+{
+    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor());
+    glUseProgram(program);
+
+    GLint colorLoc = glGetUniformLocation(program, angle::essl1_shaders::ColorUniform());
+    ASSERT_NE(colorLoc, -1);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/StateChangeTest.cpp b/src/tests/gl_tests/StateChangeTest.cpp
index 669c715..73b5e34 100644
--- a/src/tests/gl_tests/StateChangeTest.cpp
+++ b/src/tests/gl_tests/StateChangeTest.cpp
@@ -10448,6 +10448,138 @@
     ASSERT_GL_NO_ERROR();
 }
 
+// Tests that primitive restart works correctly when drawing with an unsigned byte element array
+// buffer that is updated between draw calls. Buffer updates for unsigned bytes might result in
+// faulty index conversion in some backends.
+TEST_P(StateChangeTestES3, PrimitiveRestartWithUnsignedBytesIndexBufferUpdates)
+{
+    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor());
+    glUseProgram(program);
+
+    GLint colorLoc = glGetUniformLocation(program, angle::essl1_shaders::ColorUniform());
+    ASSERT_NE(colorLoc, -1);
+
+    GLint posAttrib = glGetAttribLocation(program, essl1_shaders::PositionAttrib());
+    ASSERT_EQ(0, posAttrib);
+
+    // Two triangles forming a full-screen quad split along the diagonal.
+    // Triangle 1: upper-left half  (-1,-1), (1,1), (-1,1)
+    // Triangle 2: lower-right half (-1,-1), (1,-1), (1,1)
+    std::vector<Vector3> positionData = {
+        {-1.0f, -1.0f, 0.0f}, {1.0f, 1.0f, 0.0f},  {-1.0f, 1.0f, 0.0f},  // Triangle 1
+        {-1.0f, -1.0f, 0.0f}, {1.0f, -1.0f, 0.0f}, {1.0f, 1.0f, 0.0f}    // Triangle 2
+    };
+
+    // Indices for Triangle 1 only
+    std::vector<GLubyte> indices1 = {0, 1, 2};
+
+    GLBuffer posBuffer;
+    glBindBuffer(GL_ARRAY_BUFFER, posBuffer);
+    glBufferData(GL_ARRAY_BUFFER, positionData.size() * sizeof(positionData[0]),
+                 positionData.data(), GL_STATIC_DRAW);
+    glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(posAttrib);
+
+    GLBuffer indexBuffer;
+    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
+    glBufferData(GL_ELEMENT_ARRAY_BUFFER, 7, nullptr, GL_STREAM_DRAW);
+
+    const int w = getWindowWidth();
+    const int h = getWindowHeight();
+
+    glClearColor(0, 0, 0, 1);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
+
+    // Draw 1: Red color, Triangle 1
+    glUniform4f(colorLoc, 1, 0, 0, 1);  // Red
+    glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, indices1.size(), indices1.data());
+    glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, nullptr);
+
+    // Verify after Draw 1
+    EXPECT_PIXEL_COLOR_EQ(w / 4, h / 2, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(3 * w / 4, h / 2, GLColor::black);
+
+    // Update index buffer to have Triangle 1, Restart, Triangle 2
+    std::vector<GLubyte> indices2 = {0, 1, 2, 0xFF, 3, 4, 5};
+    glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, indices2.size(), indices2.data());
+
+    // Draw 2: Blue color, both triangles
+    glUniform4f(colorLoc, 0, 0, 1, 1);  // Blue
+    glDrawElements(GL_TRIANGLES, 7, GL_UNSIGNED_BYTE, nullptr);
+
+    // Verify after Draw 2
+    EXPECT_PIXEL_COLOR_EQ(w / 4, h / 2, GLColor::blue);
+    EXPECT_PIXEL_COLOR_EQ(3 * w / 4, h / 2, GLColor::blue);
+
+    ASSERT_GL_NO_ERROR();
+}
+
+// Tests that primitive restart works correctly when drawing with an unsigned byte client indices
+// that are updated between draw calls.
+TEST_P(StateChangeTestES3, PrimitiveRestartWithUnsignedBytesClientIndexData)
+{
+    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor());
+    glUseProgram(program);
+
+    GLint colorLoc = glGetUniformLocation(program, angle::essl1_shaders::ColorUniform());
+    ASSERT_NE(colorLoc, -1);
+
+    GLint posAttrib = glGetAttribLocation(program, essl1_shaders::PositionAttrib());
+    ASSERT_EQ(0, posAttrib);
+
+    // Two triangles forming a full-screen quad split along the diagonal.
+    // Triangle 1: upper-left half  (-1,-1), (1,1), (-1,1)
+    // Triangle 2: lower-right half (-1,-1), (1,-1), (1,1)
+    std::vector<Vector3> positionData = {
+        {-1.0f, -1.0f, 0.0f}, {1.0f, 1.0f, 0.0f},  {-1.0f, 1.0f, 0.0f},  // Triangle 1
+        {-1.0f, -1.0f, 0.0f}, {1.0f, -1.0f, 0.0f}, {1.0f, 1.0f, 0.0f}    // Triangle 2
+    };
+
+    // Indices for Triangle 1 only
+    std::vector<GLubyte> indices1 = {0, 1, 2};
+
+    GLBuffer posBuffer;
+    glBindBuffer(GL_ARRAY_BUFFER, posBuffer);
+    glBufferData(GL_ARRAY_BUFFER, positionData.size() * sizeof(positionData[0]),
+                 positionData.data(), GL_STATIC_DRAW);
+    glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(posAttrib);
+
+    // No element array buffer bound
+    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+
+    const int w = getWindowWidth();
+    const int h = getWindowHeight();
+
+    glClearColor(0, 0, 0, 1);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
+
+    // Draw 1: Red color, Triangle 1
+    glUniform4f(colorLoc, 1, 0, 0, 1);  // Red
+    glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, indices1.data());
+
+    // Verify after Draw 1
+    EXPECT_PIXEL_COLOR_EQ(w / 4, h / 2, GLColor::red);
+    EXPECT_PIXEL_COLOR_EQ(3 * w / 4, h / 2, GLColor::black);
+
+    // Indices for both triangles
+    std::vector<GLubyte> indices2 = {0, 1, 2, 0xFF, 3, 4, 5};
+
+    // Draw 2: Blue color, both triangles
+    glUniform4f(colorLoc, 0, 0, 1, 1);  // Blue
+    glDrawElements(GL_TRIANGLES, 7, GL_UNSIGNED_BYTE, indices2.data());
+
+    // Verify after Draw 2
+    EXPECT_PIXEL_COLOR_EQ(w / 4, h / 2, GLColor::blue);
+    EXPECT_PIXEL_COLOR_EQ(3 * w / 4, h / 2, GLColor::blue);
+
+    ASSERT_GL_NO_ERROR();
+}
+
 // Tests that primitive restart for patches can be queried when tessellation shaders are available,
 // and that its value is independent of whether primitive restart is enabled.
 TEST_P(StateChangeTestES31, PrimitiveRestartForPatchQuery)
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential GPU OOB Read in ANGLE Metal via getDrawIndices Coordinate Mismatch

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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: ANGLE’s Metal backend incorrectly calculates index buffer slices for primitive restart when an index conversion uses pool allocation. A coordinate mismatch between the pool allocation offset and the original WebGL buffer indices causes the slicing logic to miss the primitive restart sentinel, passing 0xFFFF to Metal. Since Metal lacks primitive restart support for non-strip primitives and bounds checking is disabled, this leads to an out-of-bounds read from the GPU heap.

Affected files:

  • third_party/angle/src/libANGLE/renderer/metal/VertexArrayMtl.mm
  • third_party/angle/src/libANGLE/renderer/metal/ContextMtl.mm
  • third_party/angle/src/libANGLE/renderer/metal/BufferMtl.mm

Estimated timestamp from git blame: 2026-05-06

Overview

A potential vulnerability exists in ANGLE’s Metal backend where a coordinate system mismatch in the index buffer slicing logic allows the primitive restart index to be passed to Metal drawing commands for non-strip primitives. This results in an out-of-bounds (OOB) vertex fetch from the GPU heap, which can be observed by the web page to leak cross-origin GPU data.

Vulnerability Details

When a WebGL2 application uses gl.drawElements with UNSIGNED_BYTE indices, ANGLE’s Metal backend must convert these indices to UNSIGNED_SHORT (u16) because Metal’s support for u8 indices is limited. This is handled by VertexArrayMtl::convertIndexBuffer.

The converted indices are allocated from a shared mtl::BufferPool. The offset into this pool is returned via idxBufferOffsetOut, which includes the sub-allocation offset (e.g., X bytes) within the shared pool and any alignment padding.

Subsequently, VertexArrayMtl::getDrawIndices is called to slice the draw call into multiple ranges to handle primitive restart, as Metal does not natively support primitive restart for non-strip primitives (e.g., GL_POINTS, GL_LINES, GL_TRIANGLES).

The bug lies in how VertexArrayMtl::getDrawIndices calculates the current index and compares it against known restart index positions:

// VertexArrayMtl.mm line 854
size_t currentIndexOffset = offset / indexTypeBytes;
// ... later in a loop
if (range.restartBegin > currentIndexOffset)

Here, offset is the byte offset into the converted pool buffer. Therefore, currentIndexOffset is an element offset relative to the start of the Metal pool’s sub-allocation (inflated by X).

However, the restart ranges (range.restartBegin) are obtained from BufferMtl::getRestartIndices:

// BufferMtl.mm line 510
angle::Span<const uint8_t> data = getBufferDataReadOnly(ctx, 0);
// ...
ranges = CalculateRestartRanges<uint8_t>(data);

These restart ranges are calculated from byte 0 of the original WebGL ELEMENT_ARRAY_BUFFER. They are absolute element indices relative to the WebGL buffer, not the Metal pool buffer.

Because of this mismatch, if a pool allocation offset is present, currentIndexOffset will be artificially inflated. The comparison range.restartBegin > currentIndexOffset fails to correctly identify the restart index’s position. Consequently, the restart sentinel (promoted from 0xFF to 0xFFFF) is not sliced out and is included in the draw command’s range.

Impact

Since Metal is told to draw with a range that includes 0xFFFF, and it does not natively recognize 0xFFFF as a restart index for non-strip primitives, it treats it as a valid vertex index.

Furthermore, ANGLE disables robustBufferAccessBehaviorKHR on Metal (in DisplayMtl.mm), meaning there are no backend-level bounds checks for vertex fetches. The GPU will attempt to fetch vertex 65535. If the bound vertex buffer is smaller than this, it results in an out-of-bounds read of the GPU heap.

An attacker can leak cross-origin GPU memory by rendering this OOB data to a floating-point render target and reading the pixels back into JavaScript.

Potential Reproduction Steps

Please note: These are suggested steps to trigger the vulnerability. Our tooling agent cannot run code to verify them.

  1. On macOS with ANGLE-Metal (default), obtain a WebGL2 context.
  2. Create an ARRAY_BUFFER with a small number of vertices (e.g. 4) and bind it to a vertex attribute; configure the vertex shader to pass this attribute to the fragment shader to be written to a render target.
  3. Create an ELEMENT_ARRAY_BUFFER with UNSIGNED_BYTE indices, intentionally including the restart index 0xFF (e.g., [0, 1, 2, 0xFF, 4]).
  4. Call gl.drawElements(gl.POINTS, count, gl.UNSIGNED_BYTE, offset) where offset does not align with kIndexBufferOffsetAlignment (4), e.g., offset 7. This forces conversion and pool allocation.
  5. Observe that the draw call includes vertex 65535 by reading back the rendered pixels from the render target via gl.readPixels().
  6. To guarantee a non-zero pool offset, perform multiple index conversions or sub-data updates prior to the draw call to advance the BufferPool allocation pointer.

Suggested Fix

Ensure that the slicing logic uses the same coordinate space for comparisons. One approach is to calculate the slices based on the original WebGL buffer’s element index and then apply the pool offset and alignment translation to the generated DrawCommandRange offset values, rather than attempting to slice using the post-conversion pool offsets.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


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.

View on issue tracker