Medium chrome Integer Overflow πŸ“„ Reporter bug report πŸ”§ Commit mapped

Overview

Medium
Severity
β€”
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in ANGLE
DescriptionInteger overflow in ANGLE
ComponentANGLE
Bug ClassInteger Overflow
Tracker506212452
Fix commit13738a97e6a1 (angle/angle) +81/-3
CISA KEVNot listed
CreditedQuac Tran
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
VertexAttributeUint8Test
src/tests/gl_tests/VertexAttributeTest.cpp
modified

Files Changed

  • src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp
  • src/tests/gl_tests/VertexAttributeTest.cpp
From 13738a97e6a19fbaf280a9f9c67cfde5f714963e Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <zmo@chromium.org>
Date: Mon, 04 May 2026 12:37:02 -0700
Subject: [PATCH] D3D11: Defend against potential integer overflow in a function.

VertexBuffer11::storeVertexAttributes().

Also, added a regression test for this.

Bug: b/506212452
Change-Id: Id82847ef730287515e64a3836e3479fc208ed704
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7814021
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Auto-Submit: Zhenyao Mo <zmo@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---

diff --git a/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp b/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp
index 62de655..43444b8 100644
--- a/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp
@@ -12,6 +12,9 @@
 
 #include "libANGLE/renderer/d3d/d3d11/VertexBuffer11.h"
 
+#include <cstddef>
+
+#include "common/mathutil.h"
 #include "libANGLE/Buffer.h"
 #include "libANGLE/Context.h"
 #include "libANGLE/VertexAttribute.h"
@@ -111,18 +114,24 @@
 {
     ASSERT(mBuffer.valid());
 
-    int inputStride = static_cast<int>(ComputeVertexAttributeStride(attrib, binding));
+    size_t inputStride = ComputeVertexAttributeStride(attrib, binding);
 
     // This will map the resource if it isn't already mapped.
     ANGLE_TRY(mapResource(context));
 
-    uint8_t *output = mMappedResourceData + offset;
+    angle::CheckedNumeric<ptrdiff_t> checkedOffset(static_cast<ptrdiff_t>(offset));
+    ANGLE_CHECK_GL_MATH(GetImplAs<Context11>(context), checkedOffset.IsValid());
+
+    uint8_t *output = mMappedResourceData + static_cast<ptrdiff_t>(checkedOffset.ValueOrDie());
 
     const uint8_t *input = sourceData;
 
     if (instances == 0 || binding.getDivisor() == 0)
     {
-        input += inputStride * start;
+        angle::CheckedNumeric<ptrdiff_t> checkedInputOffset(static_cast<ptrdiff_t>(start));
+        checkedInputOffset *= static_cast<ptrdiff_t>(inputStride);
+        ANGLE_CHECK_GL_MATH(GetImplAs<Context11>(context), checkedInputOffset.IsValid());
+        input += static_cast<ptrdiff_t>(checkedInputOffset.ValueOrDie());
     }
 
     angle::FormatID vertexFormatID       = gl::GetVertexFormatID(attrib, currentValueType);
diff --git a/src/tests/gl_tests/VertexAttributeTest.cpp b/src/tests/gl_tests/VertexAttributeTest.cpp
index ebaaf85..eb4f3f3 100644
--- a/src/tests/gl_tests/VertexAttributeTest.cpp
+++ b/src/tests/gl_tests/VertexAttributeTest.cpp
@@ -5861,6 +5861,75 @@
 class VertexAttributeUint8Test : public VertexAttributeTestES3
 {};
 
+// Regression test for a bug in VertexBuffer11::storeVertexAttributes where an integer overflow
+// could occur when calculating input/output pointers during vertex attribute conversion.
+TEST_P(VertexAttributeTest, StoreVertexAttributesIntegerOverflow)
+{
+    // This test specifically targets the D3D11 backend's vertex attribute storage logic.
+    // It triggers a path where attributes are copied/converted, which is common when
+    // strides or formats require translation.
+    // TODO(zmo): Make this test run and pass in all platforms.
+    ANGLE_SKIP_TEST_IF(!IsD3D11());
+
+    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+    glUseProgram(program);
+    GLint posLoc = glGetAttribLocation(program, essl1_shaders::PositionAttrib());
+    ASSERT_NE(-1, posLoc);
+
+    // Create a buffer for position data.
+    auto quadVertices = GetQuadVertices();
+    GLBuffer posBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, posBuf);
+    glBufferData(GL_ARRAY_BUFFER, quadVertices.size() * sizeof(Vector3), quadVertices.data(),
+                 GL_STATIC_DRAW);
+    glEnableVertexAttribArray(posLoc);
+    glVertexAttribPointer(posLoc, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
+
+    // Create a second attribute that will trigger the overflow.
+    // We use a large stride and a large 'first' (start) index to trigger the overflow
+    // in 'input += inputStride * start'.
+    // inputStride = 255, start = (2^32 / 255) + 1 should overflow a 32-bit size_t.
+    // On 64-bit it won't overflow size_t unless we use much larger values, but the
+    // D3D11 buffer offset 'unsigned int offset' can also be targeted.
+
+    // Target the 'input += inputStride * start' overflow.
+    // We'll use a stride that's not a multiple of 4 to force the slow path (conversion) in D3D11.
+    const GLsizei srcStride = 255;
+    // On 32-bit systems, this will overflow: 0x10000000 * 255 > 2^32.
+    // On 64-bit systems, we'd need a much larger value, but this is a good start
+    // for testing the overflow logic.
+    const GLint first = 0x10000000;
+
+    // We don't need a huge buffer if we're just testing the overflow check.
+    // But we need to make sure the draw call is "valid" enough to reach the renderer.
+    GLBuffer instBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, instBuf);
+    glBufferData(GL_ARRAY_BUFFER, 1024, nullptr, GL_STATIC_DRAW);
+
+    GLint testLoc = 1;  // Use an attribute location that's likely available.
+    glEnableVertexAttribArray(testLoc);
+    glVertexAttribPointer(testLoc, 4, GL_UNSIGNED_BYTE, GL_FALSE, srcStride, nullptr);
+
+    // This draw call will trigger VertexBuffer11::storeVertexAttributes.
+    glDrawArrays(GL_TRIANGLES, first, 3);
+
+    // If the fix is working, we shouldn't crash.
+    // We check if the calculation would have overflowed for the current architecture's ptrdiff_t.
+    angle::CheckedNumeric<ptrdiff_t> checkedInputOffset(static_cast<ptrdiff_t>(first));
+    checkedInputOffset *= static_cast<ptrdiff_t>(srcStride);
+
+    if (!checkedInputOffset.IsValid())
+    {
+        // If it overflowed, ANGLE_CHECK_GL_MATH should have triggered a GL_INVALID_OPERATION.
+        EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+    }
+    else
+    {
+        // If it didn't overflow (e.g. on 64-bit), we just ensure no crash and no GL error.
+        EXPECT_GL_NO_ERROR();
+    }
+}
+
 // Regression test for a bug in emulation of 8-bit indices, when the end of
 // the index buffer is used.
 TEST_P(VertexAttributeUint8Test, ConvertUint8IndexAtEndOfBuffer)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/VertexAttributeTest.cpp b/src/tests/gl_tests/VertexAttributeTest.cpp
index ebaaf85..eb4f3f3 100644
--- a/src/tests/gl_tests/VertexAttributeTest.cpp
+++ b/src/tests/gl_tests/VertexAttributeTest.cpp
@@ -5861,6 +5861,75 @@
 class VertexAttributeUint8Test : public VertexAttributeTestES3
 {};
 
+// Regression test for a bug in VertexBuffer11::storeVertexAttributes where an integer overflow
+// could occur when calculating input/output pointers during vertex attribute conversion.
+TEST_P(VertexAttributeTest, StoreVertexAttributesIntegerOverflow)
+{
+    // This test specifically targets the D3D11 backend's vertex attribute storage logic.
+    // It triggers a path where attributes are copied/converted, which is common when
+    // strides or formats require translation.
+    // TODO(zmo): Make this test run and pass in all platforms.
+    ANGLE_SKIP_TEST_IF(!IsD3D11());
+
+    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+    glUseProgram(program);
+    GLint posLoc = glGetAttribLocation(program, essl1_shaders::PositionAttrib());
+    ASSERT_NE(-1, posLoc);
+
+    // Create a buffer for position data.
+    auto quadVertices = GetQuadVertices();
+    GLBuffer posBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, posBuf);
+    glBufferData(GL_ARRAY_BUFFER, quadVertices.size() * sizeof(Vector3), quadVertices.data(),
+                 GL_STATIC_DRAW);
+    glEnableVertexAttribArray(posLoc);
+    glVertexAttribPointer(posLoc, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
+
+    // Create a second attribute that will trigger the overflow.
+    // We use a large stride and a large 'first' (start) index to trigger the overflow
+    // in 'input += inputStride * start'.
+    // inputStride = 255, start = (2^32 / 255) + 1 should overflow a 32-bit size_t.
+    // On 64-bit it won't overflow size_t unless we use much larger values, but the
+    // D3D11 buffer offset 'unsigned int offset' can also be targeted.
+
+    // Target the 'input += inputStride * start' overflow.
+    // We'll use a stride that's not a multiple of 4 to force the slow path (conversion) in D3D11.
+    const GLsizei srcStride = 255;
+    // On 32-bit systems, this will overflow: 0x10000000 * 255 > 2^32.
+    // On 64-bit systems, we'd need a much larger value, but this is a good start
+    // for testing the overflow logic.
+    const GLint first = 0x10000000;
+
+    // We don't need a huge buffer if we're just testing the overflow check.
+    // But we need to make sure the draw call is "valid" enough to reach the renderer.
+    GLBuffer instBuf;
+    glBindBuffer(GL_ARRAY_BUFFER, instBuf);
+    glBufferData(GL_ARRAY_BUFFER, 1024, nullptr, GL_STATIC_DRAW);
+
+    GLint testLoc = 1;  // Use an attribute location that's likely available.
+    glEnableVertexAttribArray(testLoc);
+    glVertexAttribPointer(testLoc, 4, GL_UNSIGNED_BYTE, GL_FALSE, srcStride, nullptr);
+
+    // This draw call will trigger VertexBuffer11::storeVertexAttributes.
+    glDrawArrays(GL_TRIANGLES, first, 3);
+
+    // If the fix is working, we shouldn't crash.
+    // We check if the calculation would have overflowed for the current architecture's ptrdiff_t.
+    angle::CheckedNumeric<ptrdiff_t> checkedInputOffset(static_cast<ptrdiff_t>(first));
+    checkedInputOffset *= static_cast<ptrdiff_t>(srcStride);
+
+    if (!checkedInputOffset.IsValid())
+    {
+        // If it overflowed, ANGLE_CHECK_GL_MATH should have triggered a GL_INVALID_OPERATION.
+        EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+    }
+    else
+    {
+        // If it didn't overflow (e.g. on 64-bit), we just ensure no crash and no GL error.
+        EXPECT_GL_NO_ERROR();
+    }
+}
+
 // Regression test for a bug in emulation of 8-bit indices, when the end of
 // the index buffer is used.
 TEST_P(VertexAttributeUint8Test, ConvertUint8IndexAtEndOfBuffer)
Loading diff…

Original Bug Report

reported by tr...@gmail.com

Integer Overflow to OOB Heap Read in GPU Process (VertexBuffer11, D3D11)


Report description

Integer Overflow to OOB Heap Read in GPU Process (VertexBuffer11, D3D11)


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

third_party/angle/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp


The problem

Please describe the technical details of the vulnerability

Summary

VertexBuffer11::storeVertexAttributes (VertexBuffer11.cpp:114) truncates a size_t stride to int via static_cast<int>. When stride exceeds INT_MAX, it wraps to a negative value. This negative stride is used in pointer arithmetic, causing the read pointer to move backward past the buffer allocation β€” an out-of-bounds heap read in the GPU process.

This is the same bug class as b/489369089, which was fixed with CheckedNumeric in VertexDataManager.cpp (commit 641c0d0). The fix was NOT applied to VertexBuffer11.cpp β€” the static_cast<int> truncation remains.

Component: ANGLE (D3D11 backend) File: third_party/angle/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp, line 114 Type: Integer overflow β†’ out-of-bounds heap read Process: GPU process Platform: Windows (D3D11 backend) Attack model: Compromised renderer β†’ GPU process

Vulnerable Code

// VertexBuffer11.cpp:114
int inputStride = static_cast<int>(ComputeVertexAttributeStride(attrib, binding));
// ...
// Line 125 β€” OOB pointer arithmetic
input += inputStride * start;  // negative stride * start β†’ pointer goes BACKWARD
// Line 133 β€” reads from OOB memory
vertexFormatInfo.copyFunction(input, inputStride, count, output);

Sibling Fix (same class, already applied to different file)

Commit 641c0d0 β€” “D3D11: Fix potential OOB read in StoreStaticAttrib” (b/489369089):

// VertexDataManager.cpp β€” FIXED
-const int offset = static_cast<int>(ComputeVertexAttributeOffset(attrib, binding));
+angle::CheckedNumeric<GLintptr> offset = ComputeVertexAttributeOffset(attrib, binding);

The identical static_cast<int> pattern in VertexBuffer11.cpp was NOT fixed.

Web PoC

Attached: poc.html β€” an HTML page with WebGL that triggers the OOB read.

The PoC uses GL_UNSIGNED_BYTE normalized attributes (forces D3D11 format conversion β†’ streaming path through storeVertexAttributes) with stride = -4 and glDrawArrays(GL_TRIANGLES, first=1, count=3).

Reproduction

# 1. Build Chromium with ASAN (release mode, Windows)
gn gen out/AsanRelease --args='is_asan=true is_debug=false is_component_build=false dcheck_always_on=false'
autoninja -C out/AsanRelease content_shell

# 2. Apply validation_bypass.patch (simulates compromised renderer β€” see below)

# 3. Rebuild (incremental, ~1 min)
autoninja -C out/AsanRelease content_shell

# 4. Run
set ASAN_OPTIONS=detect_leaks=0:symbolize=1:halt_on_error=1
out\AsanRelease\content_shell.exe --enable-gpu --use-angle=d3d11 --no-sandbox --single-process poc.html

ASAN Output (from content_shell)

==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x1245f3685cbc
READ of size 4 at 0x1245f3685cbc thread T15
    #0 rx::CopyNativeVertexData<signed char,4,4,0>    copyvertex.inc.h:84
    #1 rx::VertexBuffer11::storeVertexAttributes       VertexBuffer11.cpp:133
    #2 rx::StreamingVertexBufferInterface::storeDynamicAttribute  VertexBuffer.cpp:206
    #3 rx::VertexDataManager::storeDynamicAttrib        VertexDataManager.cpp:595
    #4 rx::VertexDataManager::storeDynamicAttribs       VertexDataManager.cpp:462
    #5 rx::VertexArray11::updateDynamicAttribs          VertexArray11.cpp:330
    #6 rx::VertexArray11::syncStateForDraw              VertexArray11.cpp:163
    #7 rx::StateManager11::updateState                  StateManager11.cpp:2001
    #8 rx::Context11::drawArrays                        Context11.cpp:285
    #9 GL_DrawArrays                                    entry_points_gles_2_0_autogen.cpp:1819
    #10 gl::RealGLApi::glDrawArraysFn                   gl_gl_api_implementation.cc:390
    #11 gpu::gles2::GLES2DecoderPassthroughImpl::DoDrawArrays  gles2_cmd_decoder_passthrough_doers.cc:1149
    #12 gpu::gles2::GLES2DecoderPassthroughImpl::DoCommandsImpl  gles2_cmd_decoder_passthrough.cc:745
    #13 gpu::CommandBufferService::Flush                 command_buffer_service.cc:267

0x1245f3685cbc is located 4 bytes before 256-byte region [0x1245f3685cc0,0x1245f3685dc0)
SUMMARY: AddressSanitizer: heap-buffer-overflow copyvertex.inc.h:84

Why Validation Bypass Patches Are Needed

This is a compromised renderer β†’ GPU process bug. The validation_bypass.patch contains 3 changes that simulate what a compromised renderer does:

Patch 1: Stride < 0 check (validationES2.h)

A compromised renderer writes directly to GPU command buffer shared memory. The passthrough command decoder (DoVertexAttribPointer in gles2_cmd_decoder_passthrough_doers.cc:3474) performs zero validation on stride β€” it reads the value and calls ANGLE directly:

// Passthrough decoder β€” NO validation on stride
error::Error GLES2DecoderPassthroughImpl::DoVertexAttribPointer(
    GLuint indx, GLint size, GLenum type, GLboolean normalized,
    GLsizei stride, const void* ptr) {
  api()->glVertexAttribPointerFn(indx, size, type, normalized, stride, ptr);
  return error::kNoError;
}

Patch 2: WebGL stride > 255 limit (validationES.cpp)

The stride ≀ 255 limit is WebGL spec validation enforced in the renderer process (Blink/ANGLE). A compromised renderer bypasses all renderer-side validation.

Patch 3: Buffer bounds check (VertexDataManager.cpp)

A compromised renderer issues glVertexAttribPointer with buffer=0 (no VBO bound), creating client-side vertex arrays. The buffer bounds check in reserveSpaceForAttrib (line 517) only runs when bufferD3D != nullptr:

if (bufferD3D)  // Skipped for client-side arrays (bufferD3D == nullptr)
{
    // bounds check here β€” NOT reached with client-side arrays
}

Client-side arrays bypass this check entirely, allowing the malformed stride to reach storeVertexAttributes.

Data Flow (Compromised Renderer)

Compromised renderer β†’ GPU command buffer shared memory: stride = 0xFFFFFFFC
  β†’ Passthrough decoder: ZERO validation, calls ANGLE directly
  β†’ ANGLE stores as GLuint mStride = 0xFFFFFFFC
  β†’ ComputeVertexAttributeStride() returns size_t(0xFFFFFFFC)
  β†’ VertexBuffer11.cpp:114: static_cast<int>(0xFFFFFFFC) = -4
  β†’ Line 125: input += (-4) * start β†’ pointer goes BACKWARD
  β†’ Line 133: copyFunction reads OOB memory β†’ HEAP BUFFER OVERFLOW

Suggested Fix

Apply the same CheckedNumeric pattern used in the sibling fix (b/489369089):

--- a/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp
@@ -111,7 +111,9 @@
 {
     ASSERT(mBuffer.valid());

-    int inputStride = static_cast<int>(ComputeVertexAttributeStride(attrib, binding));
+    angle::CheckedNumeric<int> checkedInputStride = ComputeVertexAttributeStride(attrib, binding);
+    ANGLE_CHECK_GL_MATH(GetImplAs<ContextD3D>(context), checkedInputStride.IsValid());
+    int inputStride = checkedInputStride.ValueOrDie();

Attached Files

  • poc.html β€” Web PoC (HTML + WebGL)
  • web_poc_asan_output.txt β€” Full ASAN output from content_shell
  • validation_bypass.patch β€” 3 patches to simulate compromised renderer
  • fix.patch β€” Suggested fix (CheckedNumeric)
  • VertexBuffer11OverflowTest.cpp β€” ANGLE standalone regression test
  • asan_output.txt β€” ANGLE standalone ASAN output

Impact analysis

Impact

The attacker (compromised renderer) controls three parameters:

  • stride β€” direction and step size of pointer displacement
  • start (glDrawArrays first) β€” multiplied with stride, controls total read offset
  • count (glDrawArrays count) β€” controls volume of data read from OOB memory

This enables arbitrary read windows into the GPU process heap, which contains cross-origin rendering data, GPU command buffers, and D3D11 resource metadata from all renderer processes.


The cause

What version of Chrome have you found the security issue in?

149.0.7810.0 (Chromium source at HEAD, built with ASAN. Vulnerable code confirmed present at ANGLE commit 3c125a0. The bug also affects current Chrome Stable on Windows β€” the vulnerable static_cast<int> at VertexBuffer11.cpp:114 has β–Ž not been patched in any release.)

No, it is not related to a crash.

Choose the type of vulnerability

Memory Corruption

How would you like to be publicly acknowledged for your report?

Quac Tran

View on issue tracker