Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in GPU
DescriptionOut of bounds read in GPU
ComponentGPU
Bug ClassOOB
Tracker514508415
Fix commit761a8d44b9ce (chromium/src) +39/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
TEST_P
gpu/command_buffer/service/gles2_cmd_decoder_unittest_drawing.cc
modified

Files Changed

  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_drawing.cc
From 761a8d44b9cef31e7846b16c6417c3e9883e811a Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Mon, 27 Jul 2026 13:37:09 -0700
Subject: [PATCH] gpu: Validate largest uniform block at a shared binding point

When more than one active uniform block is mapped to the same binding
point via glUniformBlockBinding, the validating command decoder computed
the per-binding required size by overwriting rather than accumulating,
so the last block at a given binding won. Keep the maximum data size per
binding so the bound buffer is checked against every block that uses it.

Add a GLES3DecoderWithShaderTest covering an undersized buffer that
backs two uniform blocks sharing one binding.

Fixed: 514508415
Change-Id: I1ab279d32ee6c3772a973e5448b5207fc5455af6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8156780
Reviewed-by: Brandon Jones <[email protected]>
Commit-Queue: Andrew Paseltiner <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1668937}
---

diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index af237a1..c768c87d 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -8758,7 +8758,8 @@
     uniform_block_sizes[ii] = 0;
   for (auto info : state_.current_program->uniform_block_size_info()) {
     uint32_t index = info.binding;
-    uniform_block_sizes[index] = static_cast<GLsizeiptr>(info.data_size);
+    uniform_block_sizes[index] = std::max(
+        uniform_block_sizes[index], static_cast<GLsizeiptr>(info.data_size));
   }
   return buffer_manager()->RequestBuffersAccess(
       error_state_.get(), state_.indexed_uniform_buffer_bindings.get(),
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_drawing.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_drawing.cc
index 7200abb..1b738ab 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_drawing.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_drawing.cc
@@ -2147,6 +2147,43 @@
   EXPECT_EQ(GL_NO_ERROR, GetGLError());
 }
 
+// Regression test for crbug.com/514508415.
+TEST_P(GLES3DecoderWithShaderTest,
+       DrawArraysUniformBlockSharedBindingTooSmall) {
+  SetupTexture();
+
+  // The default ES3 program has two uniform blocks: index 0 (data size 32)
+  // bound at binding 0 and index 1 (data size 16) bound at binding 1. Move
+  // index 1 to binding 0 so that both blocks share the same binding point.
+  EXPECT_CALL(*gl_, UniformBlockBinding(kServiceProgramId, 1, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  cmds::UniformBlockBinding ubb_cmd;
+  ubb_cmd.Init(client_program_id_, 1, 0);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(ubb_cmd));
+  EXPECT_EQ(GL_NO_ERROR, GetGLError());
+
+  // Back the shared binding with a buffer that is large enough for the
+  // smaller block but not for the larger one.
+  DoBindBuffer(GL_UNIFORM_BUFFER, client_element_buffer_id_,
+               kServiceElementBufferId);
+  DoBufferData(GL_UNIFORM_BUFFER, 16);
+  EXPECT_CALL(*gl_,
+              BindBufferBase(GL_UNIFORM_BUFFER, 0, kServiceElementBufferId))
+      .Times(1)
+      .RetiresOnSaturation();
+  cmds::BindBufferBase bbb_cmd;
+  bbb_cmd.Init(GL_UNIFORM_BUFFER, 0, client_element_buffer_id_);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(bbb_cmd));
+  EXPECT_EQ(GL_NO_ERROR, GetGLError());
+
+  EXPECT_CALL(*gl_, DrawArrays(_, _, _)).Times(0);
+  cmds::DrawArrays cmd;
+  cmd.Init(GL_TRIANGLES, 0, kNumVertices);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+  EXPECT_EQ(GL_INVALID_OPERATION, GetGLError());
+}
+
 TEST_P(GLES2DecoderTest, ClearInvalidValue) {
   EXPECT_CALL(*gl_, Clear(_)).Times(0);
   cmds::Clear cmd;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_drawing.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_drawing.cc
index 7200abb..1b738ab 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_drawing.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_drawing.cc
@@ -2147,6 +2147,43 @@
   EXPECT_EQ(GL_NO_ERROR, GetGLError());
 }
 
+// Regression test for crbug.com/514508415.
+TEST_P(GLES3DecoderWithShaderTest,
+       DrawArraysUniformBlockSharedBindingTooSmall) {
+  SetupTexture();
+
+  // The default ES3 program has two uniform blocks: index 0 (data size 32)
+  // bound at binding 0 and index 1 (data size 16) bound at binding 1. Move
+  // index 1 to binding 0 so that both blocks share the same binding point.
+  EXPECT_CALL(*gl_, UniformBlockBinding(kServiceProgramId, 1, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  cmds::UniformBlockBinding ubb_cmd;
+  ubb_cmd.Init(client_program_id_, 1, 0);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(ubb_cmd));
+  EXPECT_EQ(GL_NO_ERROR, GetGLError());
+
+  // Back the shared binding with a buffer that is large enough for the
+  // smaller block but not for the larger one.
+  DoBindBuffer(GL_UNIFORM_BUFFER, client_element_buffer_id_,
+               kServiceElementBufferId);
+  DoBufferData(GL_UNIFORM_BUFFER, 16);
+  EXPECT_CALL(*gl_,
+              BindBufferBase(GL_UNIFORM_BUFFER, 0, kServiceElementBufferId))
+      .Times(1)
+      .RetiresOnSaturation();
+  cmds::BindBufferBase bbb_cmd;
+  bbb_cmd.Init(GL_UNIFORM_BUFFER, 0, client_element_buffer_id_);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(bbb_cmd));
+  EXPECT_EQ(GL_NO_ERROR, GetGLError());
+
+  EXPECT_CALL(*gl_, DrawArrays(_, _, _)).Times(0);
+  cmds::DrawArrays cmd;
+  cmd.Init(GL_TRIANGLES, 0, kNumVertices);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+  EXPECT_EQ(GL_INVALID_OPERATION, GetGLError());
+}
+
 TEST_P(GLES2DecoderTest, ClearInvalidValue) {
   EXPECT_CALL(*gl_, Clear(_)).Times(0);
   cmds::Clear cmd;
Loading diff…

Original Bug Report

reported by [email protected]

Potential GPU OOB read via uniform block binding flaw in validating decoder

Flapjack, 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 logic error in the GLES2 validating command decoder fails to properly validate buffer sizes when multiple uniform blocks share the same binding point. This allows an attacker to bypass buffer size checks, potentially leading to an out-of-bounds read of GPU memory. This issue affects configurations using the validating decoder without hardware robust buffer access.

Affected files:

  • gpu/command_buffer/service/gles2_cmd_decoder.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential logic flaw in GLES2DecoderImpl::ValidateUniformBlockBackings within the GLES2 validating command decoder could allow an attacker to trigger an out-of-bounds (OOB) read on the GPU. The decoder incorrectly calculates the required buffer size when multiple uniform blocks are mapped to the same binding point, allowing an attacker to bypass validation by ensuring a smaller block’s size requirement overwrites a larger block’s requirement.

Root Cause Analysis

During WebGL 2 draw calls, the validating command decoder calls GLES2DecoderImpl::ValidateUniformBlockBackings (gpu/command_buffer/service/gles2_cmd_decoder.cc) to ensure that all bound uniform buffers are large enough for the active shader’s uniform blocks.

The function iterates through all active uniform blocks and populates a vector (uniform_block_sizes), indexed by the binding point, with the required buffer size:

  for (auto info : state_.current_program->uniform_block_size_info()) {
    uint32_t index = info.binding;
    uniform_block_sizes[index] = static_cast<GLsizeiptr>(info.data_size);
  }

The OpenGL ES 3.0 specification explicitly permits multiple uniform blocks within the same program to be mapped to the same binding point (e.g., using glUniformBlockBinding).

Because the uniform_block_size_info() array is typically ordered by the driver’s assignment of uniform block indices (often corresponding to declaration order in the shader source), an attacker can declare a large uniform block followed by a small uniform block.

If both blocks are bound to the same index, the loop will process the large block first, assigning its size to uniform_block_sizes[index]. However, it will then process the small block, and execute the assignment again, overwriting the required size with the smaller value. The decoder then erroneously validates the bound buffer against this smaller requirement.

Potential Attack Vector

Note: These are suggested steps; a working proof-of-concept has not been executed.

  1. Context Creation: An attacker obtains a WebGL 2 rendering context.
  2. Shader Authoring: The attacker authors a GLSL shader containing a large uniform block (e.g., 1024 bytes) declared before a small uniform block (e.g., 16 bytes).
  3. Program Linking: The attacker compiles and links the shader program.
  4. Binding Blocks: The attacker uses gl.uniformBlockBinding() to map both the large and small blocks to the same binding point (e.g., binding point 0).
  5. Buffer Allocation: The attacker allocates a WebGLBuffer sized exactly for the small block (16 bytes) and binds it to binding point 0 using gl.bindBufferBase().
  6. Draw Call: The attacker issues a draw command (e.g., gl.drawArrays()).
  7. Validation Bypass: The validating decoder calculates the required size for binding point 0. Because the small block is processed after the large block in uniform_block_size_info_, the requirement is incorrectly calculated as 16 bytes. Validation passes.
  8. OOB Read: The GPU executes the shader. When the shader accesses data from the large uniform block, the GPU attempts to read up to 1024 bytes from the 16-byte buffer, resulting in a 1008-byte out-of-bounds read in VRAM.
  9. Exfiltration: The attacker’s shader logic writes the out-of-bounds data to a fragment color output, allowing the attacker to read it back via gl.readPixels().

Impact

If successfully exploited, an attacker could read arbitrary contiguous GPU memory. This memory may contain sensitive information, including cross-origin textures, images, or UI elements, representing a High Severity (S1) cross-origin data leak.

Exploitation is limited to configurations utilizing the validating command decoder (as opposed to the passthrough decoder) and lacking hardware-level robust buffer access extensions.

Suggested Fix

Modify the assignment in GLES2DecoderImpl::ValidateUniformBlockBackings to keep the maximum required size for any given binding point.

  for (auto info : state_.current_program->uniform_block_size_info()) {
    uint32_t index = info.binding;
    uniform_block_sizes[index] = std::max(
        uniform_block_sizes[index],
        static_cast<GLsizeiptr>(info.data_size));
  }

Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0


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