Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in WebGL
DescriptionOut of bounds read in WebGL
ComponentWebGL
Bug ClassOOB
Tracker503464551
Fix commit4b1eea7e0aa9 (chromium/src) +107/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/vertex_attrib_manager.cc
modified
TEST_F
gpu/command_buffer/service/vertex_attrib_manager_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/vertex_attrib_manager.cc
  • gpu/command_buffer/service/vertex_attrib_manager_unittest.cc
From 4b1eea7e0aa97e1b69b8b0202548b6a0fa68defa Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
Date: Thu, 23 Apr 2026 16:27:55 -0700
Subject: [PATCH] Fix VertexAttribManager's stride computation.

It was incorrect when the stride was less than the group size.

Added unit test from the bug as well as one authored by jetski-cli
specifically covering some corner cases.

Fixed: 503464551
Change-Id: Id7a7ac39a823bc2cbb46c09922965ef4c57a3634
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7787505
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
Auto-Submit: Kenneth Russell <kbr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1619827}
---

diff --git a/gpu/command_buffer/service/vertex_attrib_manager.cc b/gpu/command_buffer/service/vertex_attrib_manager.cc
index e1f4f50..8508bd5 100644
--- a/gpu/command_buffer/service/vertex_attrib_manager.cc
+++ b/gpu/command_buffer/service/vertex_attrib_manager.cc
@@ -53,7 +53,7 @@
     GLsizei real_stride,
     GLsizei offset,
     GLboolean integer) {
-  DCHECK_GT(real_stride, 0);
+  CHECK_GT(real_stride, 0);
   buffer_ = buffer;
   size_ = size;
   type_ = type;
@@ -77,9 +77,11 @@
   }
 
   uint32_t usable_size = buffer_size - offset_;
-  GLuint num_elements = usable_size / real_stride_ +
-      ((usable_size % real_stride_) >=
-       (GLES2Util::GetGroupSizeForBufferType(size_, type_)) ? 1 : 0);
+  uint32_t group_size = GLES2Util::GetGroupSizeForBufferType(size_, type_);
+  if (usable_size < group_size) {
+    return false;
+  }
+  GLuint num_elements = (usable_size - group_size) / real_stride_ + 1;
   return index < num_elements;
 }
 
diff --git a/gpu/command_buffer/service/vertex_attrib_manager_unittest.cc b/gpu/command_buffer/service/vertex_attrib_manager_unittest.cc
index 10f362c9..4574ff1f 100644
--- a/gpu/command_buffer/service/vertex_attrib_manager_unittest.cc
+++ b/gpu/command_buffer/service/vertex_attrib_manager_unittest.cc
@@ -179,6 +179,107 @@
   buffer_manager.Destroy();
 }
 
+TEST_F(VertexAttribManagerTest, CanAccessStrideSmallerThanGroup) {
+  // Regression test for bug: CanAccess() over-counts elements when
+  // real_stride < group_size, allowing the validating decoder to
+  // accept draw calls whose last vertex fetch reads past the end of
+  // the bound GL_ARRAY_BUFFER.
+  const GLenum kTarget = GL_ARRAY_BUFFER;
+  MockErrorState error_state;
+  BufferManager buffer_manager(nullptr, nullptr);
+  buffer_manager.CreateBuffer(1, 2);
+  Buffer* buffer = buffer_manager.GetBuffer(1);
+  ASSERT_TRUE(buffer != nullptr);
+
+  VertexAttrib* attrib = manager_->GetVertexAttrib(1);
+  manager_->Enable(1, true);
+
+  // size=4, type=GL_FLOAT -> group_size = 16 bytes per vertex fetch.
+  // gl_stride=4, real_stride=4 -> overlapping fetches (legal per GLES).
+  manager_->SetAttribInfo(1, buffer, 4, GL_FLOAT, GL_FALSE, 4, 4, 0, GL_FALSE);
+
+  EXPECT_TRUE(buffer_manager.SetTarget(buffer, kTarget));
+  // 20-byte buffer.
+  TestHelper::DoBufferData(gl_.get(), &error_state, &buffer_manager, buffer,
+                           kTarget, 20, GL_STATIC_DRAW, nullptr, GL_NO_ERROR);
+
+  // Vertex i fetches bytes [i*4, i*4 + 16). With a 20-byte buffer:
+  //   i=0 -> [0,16)  in bounds
+  //   i=1 -> [4,20)  in bounds
+  //   i=2 -> [8,24)  4 bytes OOB
+  //   i=3 -> [12,28) 8 bytes OOB
+  //   i=4 -> [16,32) 12 bytes OOB
+  EXPECT_TRUE(attrib->CanAccess(0));
+  EXPECT_TRUE(attrib->CanAccess(1));
+  // *** These three assertions FAIL on unpatched code: CanAccess() incorrectly
+  // returns true because num_elements is computed as 20/4 + (0>=16?1:0) = 5.
+  // ***
+  EXPECT_FALSE(attrib->CanAccess(2));
+  EXPECT_FALSE(attrib->CanAccess(3));
+  EXPECT_FALSE(attrib->CanAccess(4));
+  // Index 5 is rejected even by the buggy formula.
+  EXPECT_FALSE(attrib->CanAccess(5));
+
+  // Edge case: buffer smaller than one element. group_size=16, buffer=12.
+  // No vertex can be fetched at all, but the buggy formula computes
+  // 12/4 + (0>=16?1:0) = 3.
+  TestHelper::DoBufferData(gl_.get(), &error_state, &buffer_manager, buffer,
+                           kTarget, 12, GL_STATIC_DRAW, nullptr, GL_NO_ERROR);
+  EXPECT_FALSE(attrib->CanAccess(0));
+  EXPECT_FALSE(attrib->CanAccess(1));
+  EXPECT_FALSE(attrib->CanAccess(2));
+  EXPECT_FALSE(attrib->CanAccess(3));
+
+  manager_ = nullptr;
+  buffer_manager.MarkContextLost();
+  buffer_manager.Destroy();
+}
+
+TEST_F(VertexAttribManagerTest, CanAccessRemainderAndEdgeCases) {
+  const GLenum kTarget = GL_ARRAY_BUFFER;
+  MockErrorState error_state;
+  BufferManager buffer_manager(nullptr, nullptr);
+  buffer_manager.CreateBuffer(1, 2);
+  Buffer* buffer = buffer_manager.GetBuffer(1);
+  ASSERT_TRUE(buffer != nullptr);
+
+  VertexAttrib* attrib = manager_->GetVertexAttrib(1);
+  manager_->Enable(1, true);
+
+  EXPECT_TRUE(buffer_manager.SetTarget(buffer, kTarget));
+
+  // Case 1: offset > buffer_size
+  manager_->SetAttribInfo(1, buffer, 4, GL_FLOAT, GL_FALSE, 16, 16, 20,
+                          GL_FALSE);
+  TestHelper::DoBufferData(gl_.get(), &error_state, &buffer_manager, buffer,
+                           kTarget, 16, GL_STATIC_DRAW, nullptr, GL_NO_ERROR);
+  EXPECT_FALSE(attrib->CanAccess(0));
+
+  // Case 2: Remainder logic (usable_size % real_stride_ >= group_size)
+  // real_stride = 16, group_size = 8 (size=2, GL_FLOAT).
+  manager_->SetAttribInfo(1, buffer, 2, GL_FLOAT, GL_FALSE, 16, 16, 0,
+                          GL_FALSE);
+
+  // Subcase 2a: Remainder is enough.
+  // usable_size = 24. 24 % 16 = 8 >= 8. Should allow 2 elements.
+  TestHelper::DoBufferData(gl_.get(), &error_state, &buffer_manager, buffer,
+                           kTarget, 24, GL_STATIC_DRAW, nullptr, GL_NO_ERROR);
+  EXPECT_TRUE(attrib->CanAccess(0));
+  EXPECT_TRUE(attrib->CanAccess(1));
+  EXPECT_FALSE(attrib->CanAccess(2));
+
+  // Subcase 2b: Remainder is NOT enough.
+  // usable_size = 20. 20 % 16 = 4 < 8. Should allow 1 element.
+  TestHelper::DoBufferData(gl_.get(), &error_state, &buffer_manager, buffer,
+                           kTarget, 20, GL_STATIC_DRAW, nullptr, GL_NO_ERROR);
+  EXPECT_TRUE(attrib->CanAccess(0));
+  EXPECT_FALSE(attrib->CanAccess(1));
+
+  manager_ = nullptr;
+  buffer_manager.MarkContextLost();
+  buffer_manager.Destroy();
+}
+
 TEST_F(VertexAttribManagerTest, Unbind) {
   BufferManager buffer_manager(nullptr, nullptr);
   buffer_manager.CreateBuffer(1, 2);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/vertex_attrib_manager_unittest.cc b/gpu/command_buffer/service/vertex_attrib_manager_unittest.cc
index 10f362c9..4574ff1f 100644
--- a/gpu/command_buffer/service/vertex_attrib_manager_unittest.cc
+++ b/gpu/command_buffer/service/vertex_attrib_manager_unittest.cc
@@ -179,6 +179,107 @@
   buffer_manager.Destroy();
 }
 
+TEST_F(VertexAttribManagerTest, CanAccessStrideSmallerThanGroup) {
+  // Regression test for bug: CanAccess() over-counts elements when
+  // real_stride < group_size, allowing the validating decoder to
+  // accept draw calls whose last vertex fetch reads past the end of
+  // the bound GL_ARRAY_BUFFER.
+  const GLenum kTarget = GL_ARRAY_BUFFER;
+  MockErrorState error_state;
+  BufferManager buffer_manager(nullptr, nullptr);
+  buffer_manager.CreateBuffer(1, 2);
+  Buffer* buffer = buffer_manager.GetBuffer(1);
+  ASSERT_TRUE(buffer != nullptr);
+
+  VertexAttrib* attrib = manager_->GetVertexAttrib(1);
+  manager_->Enable(1, true);
+
+  // size=4, type=GL_FLOAT -> group_size = 16 bytes per vertex fetch.
+  // gl_stride=4, real_stride=4 -> overlapping fetches (legal per GLES).
+  manager_->SetAttribInfo(1, buffer, 4, GL_FLOAT, GL_FALSE, 4, 4, 0, GL_FALSE);
+
+  EXPECT_TRUE(buffer_manager.SetTarget(buffer, kTarget));
+  // 20-byte buffer.
+  TestHelper::DoBufferData(gl_.get(), &error_state, &buffer_manager, buffer,
+                           kTarget, 20, GL_STATIC_DRAW, nullptr, GL_NO_ERROR);
+
+  // Vertex i fetches bytes [i*4, i*4 + 16). With a 20-byte buffer:
+  //   i=0 -> [0,16)  in bounds
+  //   i=1 -> [4,20)  in bounds
+  //   i=2 -> [8,24)  4 bytes OOB
+  //   i=3 -> [12,28) 8 bytes OOB
+  //   i=4 -> [16,32) 12 bytes OOB
+  EXPECT_TRUE(attrib->CanAccess(0));
+  EXPECT_TRUE(attrib->CanAccess(1));
+  // *** These three assertions FAIL on unpatched code: CanAccess() incorrectly
+  // returns true because num_elements is computed as 20/4 + (0>=16?1:0) = 5.
+  // ***
+  EXPECT_FALSE(attrib->CanAccess(2));
+  EXPECT_FALSE(attrib->CanAccess(3));
+  EXPECT_FALSE(attrib->CanAccess(4));
+  // Index 5 is rejected even by the buggy formula.
+  EXPECT_FALSE(attrib->CanAccess(5));
+
+  // Edge case: buffer smaller than one element. group_size=16, buffer=12.
+  // No vertex can be fetched at all, but the buggy formula computes
+  // 12/4 + (0>=16?1:0) = 3.
+  TestHelper::DoBufferData(gl_.get(), &error_state, &buffer_manager, buffer,
+                           kTarget, 12, GL_STATIC_DRAW, nullptr, GL_NO_ERROR);
+  EXPECT_FALSE(attrib->CanAccess(0));
+  EXPECT_FALSE(attrib->CanAccess(1));
+  EXPECT_FALSE(attrib->CanAccess(2));
+  EXPECT_FALSE(attrib->CanAccess(3));
+
+  manager_ = nullptr;
+  buffer_manager.MarkContextLost();
+  buffer_manager.Destroy();
+}
+
+TEST_F(VertexAttribManagerTest, CanAccessRemainderAndEdgeCases) {
+  const GLenum kTarget = GL_ARRAY_BUFFER;
+  MockErrorState error_state;
+  BufferManager buffer_manager(nullptr, nullptr);
+  buffer_manager.CreateBuffer(1, 2);
+  Buffer* buffer = buffer_manager.GetBuffer(1);
+  ASSERT_TRUE(buffer != nullptr);
+
+  VertexAttrib* attrib = manager_->GetVertexAttrib(1);
+  manager_->Enable(1, true);
+
+  EXPECT_TRUE(buffer_manager.SetTarget(buffer, kTarget));
+
+  // Case 1: offset > buffer_size
+  manager_->SetAttribInfo(1, buffer, 4, GL_FLOAT, GL_FALSE, 16, 16, 20,
+                          GL_FALSE);
+  TestHelper::DoBufferData(gl_.get(), &error_state, &buffer_manager, buffer,
+                           kTarget, 16, GL_STATIC_DRAW, nullptr, GL_NO_ERROR);
+  EXPECT_FALSE(attrib->CanAccess(0));
+
+  // Case 2: Remainder logic (usable_size % real_stride_ >= group_size)
+  // real_stride = 16, group_size = 8 (size=2, GL_FLOAT).
+  manager_->SetAttribInfo(1, buffer, 2, GL_FLOAT, GL_FALSE, 16, 16, 0,
+                          GL_FALSE);
+
+  // Subcase 2a: Remainder is enough.
+  // usable_size = 24. 24 % 16 = 8 >= 8. Should allow 2 elements.
+  TestHelper::DoBufferData(gl_.get(), &error_state, &buffer_manager, buffer,
+                           kTarget, 24, GL_STATIC_DRAW, nullptr, GL_NO_ERROR);
+  EXPECT_TRUE(attrib->CanAccess(0));
+  EXPECT_TRUE(attrib->CanAccess(1));
+  EXPECT_FALSE(attrib->CanAccess(2));
+
+  // Subcase 2b: Remainder is NOT enough.
+  // usable_size = 20. 20 % 16 = 4 < 8. Should allow 1 element.
+  TestHelper::DoBufferData(gl_.get(), &error_state, &buffer_manager, buffer,
+                           kTarget, 20, GL_STATIC_DRAW, nullptr, GL_NO_ERROR);
+  EXPECT_TRUE(attrib->CanAccess(0));
+  EXPECT_FALSE(attrib->CanAccess(1));
+
+  manager_ = nullptr;
+  buffer_manager.MarkContextLost();
+  buffer_manager.Destroy();
+}
+
 TEST_F(VertexAttribManagerTest, Unbind) {
   BufferManager buffer_manager(nullptr, nullptr);
   buffer_manager.CreateBuffer(1, 2);
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential out-of-bounds read in validating command decoder due to flawed CanAccess logic

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 go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A logic error in VertexAttrib::CanAccess within the GLES2 validating command decoder allows WebGL on Android to over-count accessible vertex elements when attribute stride is smaller than attribute size. This can lead to out-of-bounds reads from GPU memory during draw calls. An attacker could potentially leak cross-origin GPU data on platforms where the GPU process is shared.

Affected files:

  • gpu/command_buffer/service/vertex_attrib_manager.cc
  • gpu/command_buffer/service/gles2_cmd_decoder.cc

Estimated timestamp from git blame: 2016-02-26

Summary

A logic error in the GLES2 validating command decoder’s vertex attribute validation allows a renderer to potentially trigger out-of-bounds reads from GPU memory. When a vertex attribute is configured with overlapping elements (a stride smaller than its total component size), the VertexAttrib::CanAccess function over-counts the number of elements that can safely be accessed within the buffer. This allows a compromised or malicious renderer to perform out-of-bounds reads of adjacent GPU memory during draw calls, leading to a potential cross-origin information leak on platforms where the validating decoder is still active (primarily Android).

Technical Details

In gpu/command_buffer/service/vertex_attrib_manager.cc, the VertexAttrib::CanAccess function validates if a specific vertex index is within the bounds of a bound buffer. The calculation is:

uint32_t usable_size = buffer_size - offset_;
GLuint num_elements = usable_size / real_stride_ +
    ((usable_size % real_stride_) >= (GLES2Util::GetGroupSizeForBufferType(size_, type_)) ? 1 : 0);
return index < num_elements;

This formula incorrectly assumes that the stride (real_stride_) is always greater than or equal to the attribute’s element size (group_size).

The OpenGL ES specification allows a non-zero stride to be smaller than the element size, which creates overlapping attributes. When real_stride_ < group_size, the term usable_size % real_stride_ will mathematically always be strictly less than real_stride_, and consequently always less than group_size. This causes the second addition term to always evaluate to 0, simplifying the formula to floor(usable_size / real_stride_).

This is incorrect for overlapping attributes. The last valid index i must satisfy i * stride + group_size <= usable_size. The current logic validates an index i based purely on the start offset of the element (i * stride), without verifying that the entire footprint of that final element (i * stride + group_size) fits within the buffer.

Example Scenario: Consider a buffer of 20 bytes, an attribute with a group size of 16 bytes (size=4, type=GL_FLOAT), and a stride of 4 bytes.

  • Current logic: num_elements = 20 / 4 + 0 = 5. It considers indices 0, 1, 2, 3, 4 to be valid.
  • Index 4 access: The driver will fetch 16 bytes starting at offset 4 * 4 = 16. The access range is [16, 32).
  • Result: Since the buffer size is only 20 bytes, this results in a 12-byte out-of-bounds read from GPU memory.

Furthermore, because the Validating Command Decoder does not explicitly request robust buffer access from the underlying ANGLE driver during initialization (relying instead on its own validation), ANGLE bypasses its internal bounds checks (ValidateDrawArraysAttribs). The driver executes the flawed draw call natively on the GPU hardware.

Impact

This issue is reachable via standard WebGL APIs from a renderer. The validating decoder is still compiled in and used by default on Android (ui/gl/features.gni:23). On Android, the GPU process is typically shared across all origins and is unsandboxed. An attacker could potentially exfiltrate the leaked bytes by passing the out-of-bounds attribute data through a vertex shader and observing the results via glReadPixels, leading to a High-severity cross-origin information leak (S1).

Potential Reproduction Steps

Please note: These are theoretical steps based on code analysis. We do not currently have a live proof-of-concept for this issue.

  1. Use an Android device where the validating command decoder is active.
  2. Create a WebGL context and a shader program that captures an attribute’s value (e.g., writing it to gl_FragColor or transform feedback).
  3. Bind a small buffer (e.g., 20 bytes).
  4. Configure a vertex attribute with overlapping elements (e.g., size=4, type=gl.FLOAT, stride=4).
  5. Perform a draw call (gl.drawArrays) with a count that includes the over-counted index (e.g., count=5).
  6. Read back the rendered pixels to observe data fetched from adjacent memory beyond the buffer’s boundaries.

Suggested Fix

The calculation for accessible elements in VertexAttrib::CanAccess should be updated to correctly handle overlapping attributes.

A robust calculation (similar to ANGLE’s VertexAttribute::updateCachedElementLimit) would be:

uint32_t usable_size = buffer_size - offset_;
uint32_t group_size = GLES2Util::GetGroupSizeForBufferType(size_, type_);

if (usable_size < group_size) {
    // Even the first element doesn't fit.
    return false; 
}

// Calculate remaining space after the first full element is accounted for
uint32_t remaining_space = usable_size - group_size;
GLuint num_elements = (remaining_space / real_stride_) + 1;
return index < num_elements;

Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09


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