Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in GPU
DescriptionOut of bounds write in GPU
ComponentGPU
Bug ClassOOB
Tracker499005260
Fix commit8617532d2684 (chromium/src) +73/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/gles2_cmd_decoder.cc
modified
TEST_F
gpu/command_buffer/service/program_manager_unittest.cc
modified
Buffer
gpu/command_buffer/service/transform_feedback_manager.h
modified
Program
gpu/command_buffer/service/transform_feedback_manager.h
modified
TransformFeedbackManager
gpu/command_buffer/service/transform_feedback_manager.h
modified

Files Changed

  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/service/program_manager.cc
  • gpu/command_buffer/service/program_manager.h
  • gpu/command_buffer/service/program_manager_unittest.cc
  • gpu/command_buffer/service/transform_feedback_manager.cc
  • gpu/command_buffer/service/transform_feedback_manager.h
From 8617532d26844dc69cbfe200fc6f020a1d2dc388 Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
Date: Fri, 24 Apr 2026 20:48:29 -0700
Subject: [PATCH] Reject program relinks if transform feedback is active.

If there is any active transform feedback against this program
(regardless of whether it's paused), don't allow it to be relinked.
This keeps the state in sync between the GL driver and the command
decoder.

Co-authored with jetski-cli.

Fixed: 499005260
Change-Id: If827f84d3ab6f6a5f3c1da760cd06e3299244847
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7794329
Reviewed-by: Brandon Jones <bajones@chromium.org>
Commit-Queue: Kenneth Russell <kbr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1620632}
---

diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index 52d5cdd..d2132c9 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -5454,6 +5454,8 @@
   }
   transform_feedback->DoBeginTransformFeedback(primitive_mode);
   DCHECK(transform_feedback->active());
+
+  transform_feedback->SetActiveProgram(program);
 }
 
 void GLES2DecoderImpl::DoEndTransformFeedback() {
@@ -5466,6 +5468,11 @@
   }
   // TODO(zmo): Validate binding points.
   state_.bound_transform_feedback->DoEndTransformFeedback();
+
+  Program* program = state_.bound_transform_feedback->active_program();
+  if (program) {
+    state_.bound_transform_feedback->ClearActiveProgram();
+  }
 }
 
 void GLES2DecoderImpl::DoPauseTransformFeedback() {
@@ -8190,6 +8197,12 @@
     return;
   }
 
+  if (program->IsActiveForTransformFeedback()) {
+    LOCAL_SET_GL_ERROR(GL_INVALID_OPERATION, "glLinkProgram",
+                       "program is active for transform feedback");
+    return;
+  }
+
   LogClientServiceForInfo(program, program_id, "glLinkProgram");
   if (program->Link(shader_manager(),
                     client())) {
diff --git a/gpu/command_buffer/service/program_manager.cc b/gpu/command_buffer/service/program_manager.cc
index 49bb9ba..8e457705 100644
--- a/gpu/command_buffer/service/program_manager.cc
+++ b/gpu/command_buffer/service/program_manager.cc
@@ -395,6 +395,7 @@
 Program::Program(ProgramManager* manager, GLuint service_id)
     : manager_(manager),
       use_count_(0),
+      active_transform_feedback_count_(0),
       max_attrib_name_length_(0),
       max_uniform_name_length_(0),
       service_id_(service_id),
@@ -527,6 +528,11 @@
   }
 }
 
+void Program::DecrementActiveTransformFeedbackCount() {
+  CHECK_GT(active_transform_feedback_count_, 0);
+  --active_transform_feedback_count_;
+}
+
 void Program::SetUniformBlockBinding(GLuint index, GLuint binding) {
   DCHECK_GT(uniform_block_size_info_.size(), index);
   uniform_block_size_info_[index].binding = binding;
diff --git a/gpu/command_buffer/service/program_manager.h b/gpu/command_buffer/service/program_manager.h
index dd7f8cf5..c65338c3 100644
--- a/gpu/command_buffer/service/program_manager.h
+++ b/gpu/command_buffer/service/program_manager.h
@@ -335,6 +335,16 @@
     return use_count_ != 0;
   }
 
+  void IncrementActiveTransformFeedbackCount() {
+    ++active_transform_feedback_count_;
+  }
+
+  void DecrementActiveTransformFeedbackCount();
+
+  bool IsActiveForTransformFeedback() const {
+    return active_transform_feedback_count_ > 0;
+  }
+
   // Sets attribute-location binding from a glBindAttribLocation() call.
   void SetAttribLocationBinding(const std::string& attrib, GLint location) {
     bind_attrib_location_map_[attrib] = location;
@@ -546,6 +556,8 @@
 
   int use_count_;
 
+  int active_transform_feedback_count_;
+
   GLsizei max_attrib_name_length_;
 
   // Attrib by index.
diff --git a/gpu/command_buffer/service/program_manager_unittest.cc b/gpu/command_buffer/service/program_manager_unittest.cc
index 8706897..439dcc2 100644
--- a/gpu/command_buffer/service/program_manager_unittest.cc
+++ b/gpu/command_buffer/service/program_manager_unittest.cc
@@ -1077,6 +1077,25 @@
   EXPECT_FALSE(fshader->InUse());
 }
 
+TEST_F(ProgramManagerWithShaderTest, ProgramInfoActiveTransformFeedbackCount) {
+  Program* program =
+      manager_->CreateProgram(kClientProgramId, kServiceProgramId);
+  ASSERT_TRUE(program != nullptr);
+  EXPECT_FALSE(program->IsActiveForTransformFeedback());
+
+  program->IncrementActiveTransformFeedbackCount();
+  EXPECT_TRUE(program->IsActiveForTransformFeedback());
+
+  program->IncrementActiveTransformFeedbackCount();
+  EXPECT_TRUE(program->IsActiveForTransformFeedback());
+
+  program->DecrementActiveTransformFeedbackCount();
+  EXPECT_TRUE(program->IsActiveForTransformFeedback());
+
+  program->DecrementActiveTransformFeedbackCount();
+  EXPECT_FALSE(program->IsActiveForTransformFeedback());
+}
+
 TEST_F(ProgramManagerWithShaderTest, ProgramInfoUseCount2) {
   Program* program =
       manager_->CreateProgram(kClientProgramId, kServiceProgramId);
diff --git a/gpu/command_buffer/service/transform_feedback_manager.cc b/gpu/command_buffer/service/transform_feedback_manager.cc
index 0c9dd74..684c2bf 100644
--- a/gpu/command_buffer/service/transform_feedback_manager.cc
+++ b/gpu/command_buffer/service/transform_feedback_manager.cc
@@ -4,9 +4,11 @@
 
 #include "gpu/command_buffer/service/transform_feedback_manager.h"
 
+#include "base/check.h"
 #include "base/notreached.h"
 #include "base/numerics/checked_math.h"
 #include "gpu/command_buffer/service/buffer_manager.h"
+#include "gpu/command_buffer/service/program_manager.h"
 #include "ui/gl/gl_version_info.h"
 
 namespace gpu {
@@ -71,6 +73,19 @@
   }
 }
 
+void TransformFeedback::SetActiveProgram(Program* program) {
+  CHECK(!active_program_);
+  CHECK(program);
+  active_program_ = program;
+  program->IncrementActiveTransformFeedbackCount();
+}
+
+void TransformFeedback::ClearActiveProgram() {
+  CHECK(active_program_);
+  active_program_->DecrementActiveTransformFeedbackCount();
+  active_program_ = nullptr;
+}
+
 void TransformFeedback::DoBeginTransformFeedback(GLenum primitive_mode) {
   DCHECK(!active_);
   DCHECK(primitive_mode == GL_POINTS ||
diff --git a/gpu/command_buffer/service/transform_feedback_manager.h b/gpu/command_buffer/service/transform_feedback_manager.h
index 0eb5014..f2f333e 100644
--- a/gpu/command_buffer/service/transform_feedback_manager.h
+++ b/gpu/command_buffer/service/transform_feedback_manager.h
@@ -18,6 +18,7 @@
 namespace gles2 {
 
 class Buffer;
+class Program;
 class TransformFeedbackManager;
 
 // Info about TransformFeedbacks currently in the system.
@@ -58,6 +59,11 @@
     return paused_;
   }
 
+  void SetActiveProgram(Program* program);
+  void ClearActiveProgram();
+
+  Program* active_program() const { return active_program_.get(); }
+
   GLenum primitive_mode() const {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/program_manager_unittest.cc b/gpu/command_buffer/service/program_manager_unittest.cc
index 8706897..439dcc2 100644
--- a/gpu/command_buffer/service/program_manager_unittest.cc
+++ b/gpu/command_buffer/service/program_manager_unittest.cc
@@ -1077,6 +1077,25 @@
   EXPECT_FALSE(fshader->InUse());
 }
 
+TEST_F(ProgramManagerWithShaderTest, ProgramInfoActiveTransformFeedbackCount) {
+  Program* program =
+      manager_->CreateProgram(kClientProgramId, kServiceProgramId);
+  ASSERT_TRUE(program != nullptr);
+  EXPECT_FALSE(program->IsActiveForTransformFeedback());
+
+  program->IncrementActiveTransformFeedbackCount();
+  EXPECT_TRUE(program->IsActiveForTransformFeedback());
+
+  program->IncrementActiveTransformFeedbackCount();
+  EXPECT_TRUE(program->IsActiveForTransformFeedback());
+
+  program->DecrementActiveTransformFeedbackCount();
+  EXPECT_TRUE(program->IsActiveForTransformFeedback());
+
+  program->DecrementActiveTransformFeedbackCount();
+  EXPECT_FALSE(program->IsActiveForTransformFeedback());
+}
+
 TEST_F(ProgramManagerWithShaderTest, ProgramInfoUseCount2) {
   Program* program =
       manager_->CreateProgram(kClientProgramId, kServiceProgramId);
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential GPU OOB Write via Transform Feedback Program Link Desynchronization

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 security team.

Overview: The GLES2 validating command decoder fails to verify if Transform Feedback is active during program linking. This allows a compromised renderer to desynchronize Chrome’s internal size calculations from the native GPU driver’s state. By bypassing buffer size validation, an attacker can supply undersized buffers to the native driver, potentially causing an out-of-bounds write in the GPU process.

Affected files:

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

Estimated timestamp from git blame: 2019-04-03

Vulnerability Description

The GLES2 validating command decoder (used on Android and certain desktop GL configurations) contains a state desynchronization vulnerability involving Transform Feedback (TF) and program linking.

According to the OpenGL ES 3.0 specification (§2.12.6), if glLinkProgram is called on a program object that is active for Transform Feedback, the link should fail, a GL_INVALID_OPERATION error should be generated, and the program’s executable state should remain unchanged.

However, Chrome’s command decoder (GLES2DecoderImpl::DoLinkProgram and Program::Link) does not check if the program is active for TF before passing the glLinkProgram call to the native driver. Furthermore, it blindly trusts glGetProgramiv(..., GL_LINK_STATUS, ...) to determine if the native link succeeded, without checking for GL errors. If a program was previously linked successfully, its GL_LINK_STATUS remains GL_TRUE even after a rejected link attempt. Chrome misinterprets this leftover GL_TRUE status as a successful re-link and updates its internal metadata to match the newly attached shaders, while the native driver retains the old shaders.

Potential Exploitation Steps

(Note: Our tooling cannot execute code yet, so these are suggested/potential steps an attacker would follow to trigger this vulnerability.)

  1. Initial Link: A compromised renderer creates a program (P1), attaches a vertex shader outputting a very large varying (e.g., 64 bytes per vertex), and links it. Chrome correctly records the TF requirement as 64 bytes per vertex.
  2. Start Transform Feedback: The attacker allocates a large buffer, binds it, calls glUseProgram(P1), and begins Transform Feedback.
  3. Pause Transform Feedback: The attacker calls glPauseTransformFeedback().
  4. Malicious Reconfiguration: The attacker detaches the original vertex shader and attaches a new one with a very small varying (e.g., 4 bytes per vertex). They then call glTransformFeedbackVaryings for the new small varying.
  5. The Desynchronization: The attacker calls glLinkProgram(P1).
    • The native driver correctly rejects the link because TF is active, generating an error and keeping the 64-byte shader active.
    • Chrome queries GL_LINK_STATUS. Because the driver didn’t change the program state, the status remains GL_TRUE from step 1.
    • Chrome assumes the link succeeded and updates its internal TF metadata to the new 4-byte size.
  6. Bypassing Validation: The attacker allocates a tiny 400-byte TF buffer, binds it, resumes TF, and issues a draw call for 100 vertices.
  7. OOB Write Trigger: Chrome validates the draw call using the corrupted 4-byte metadata (100 * 4 = 400 bytes), approving it. The native driver executes the draw using the actual 64-byte shader, attempting to write 6400 bytes into the 400-byte buffer.

While perfectly conformant drivers should natively clamp this write, many real-world mobile GPU drivers (e.g., Adreno, Mali) historically contain bounds-checking flaws and trust the application (Chrome) to size buffers correctly. This can result in a massive out-of-bounds write in the GPU process or kernel driver, leading to a potential sandbox escape.

Suggested Fix

Modify GLES2DecoderImpl::DoLinkProgram (in gpu/command_buffer/service/gles2_cmd_decoder.cc) to explicitly check if the program is active for Transform Feedback before proceeding. If it is active and not paused, or active and paused, the function should immediately generate a GL_INVALID_OPERATION and return early, preventing the state update.

if (state_.bound_transform_feedback.get() &&
    state_.bound_transform_feedback->active()) {
  // Must check if the program being linked is actually the active one
  if (program == state_.current_program.get()) {
    LOCAL_SET_GL_ERROR(GL_INVALID_OPERATION, "glLinkProgram",
                       "program is active for transform feedback");
    return;
  }
}

Alternatively, Program::Link should check glGetError() after calling glLinkProgram() natively, and fail the link operation if GL_INVALID_OPERATION was generated.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


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