CVE-2026-14391
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
VertexAttributeUint8Testsrc/tests/gl_tests/VertexAttributeTest.cpp |
modified |
Files Changed
src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cppsrc/tests/gl_tests/VertexAttributeTest.cpp
Patch
From 13738a97e6a19fbaf280a9f9c67cfde5f714963e Mon Sep 17 00:00:00 2001 From: Zhenyao Mo <[email protected]> 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 <[email protected]> Auto-Submit: Zhenyao Mo <[email protected]> Commit-Queue: Geoff Lang <[email protected]> --- 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)
Regression Test / PoC
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)
Original Bug Report
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):
- https://chromium.googlesource.com/angle/angle/+/641c0d0
- https://chromium-review.googlesource.com/c/angle/angle/+/7736785
// 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_shellvalidation_bypass.patchβ 3 patches to simulate compromised rendererfix.patchβ Suggested fix (CheckedNumeric)VertexBuffer11OverflowTest.cppβ ANGLE standalone regression testasan_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 (
glDrawArraysfirst) β multiplied with stride, controls total read offset - count (
glDrawArrayscount) β 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.)
Is the security issue related to a crash?
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