Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in GPU
DescriptionOut of bounds write in GPU
ComponentGPU
Bug ClassOOB
Tracker513543143
Fix commit048900b2334c (chromium/src) +143/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/gles2_cmd_decoder.cc
modified
if
gpu/command_buffer/tests/gl_test_service_helper.cc
modified
GLManager
gpu/command_buffer/tests/gl_test_service_helper.h
modified
GL3Test
gpu/command_buffer/tests/gl_unittest.cc
modified
TEST_F
gpu/command_buffer/tests/gl_unittest.cc
modified

Files Changed

  • gpu/BUILD.gn
  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/tests/gl_test_service_helper.cc
  • gpu/command_buffer/tests/gl_test_service_helper.h
  • gpu/command_buffer/tests/gl_unittest.cc
From 048900b2334cf2aeff2fb8a4e20c74b69124a6b7 Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <sunnyps@chromium.org>
Date: Mon, 18 May 2026 15:33:21 -0700
Subject: [PATCH] [gpu] Fix state desync in DoCopyTexImage2D

An incorrect GL target enum used in a workaround path in
DoCopyTexImage2D caused a state desynchronization between the GLES2
validating command decoder and the GPU driver. When copying to a cube-
map face while a different level of the same texture was attached to the
read framebuffer, the decoder incorrectly passed a cube-face target enum
(e.g., GL_TEXTURE_CUBE_MAP_POSITIVE_X) to glTexParameteri. This resulted
in a GL_INVALID_ENUM driver error, causing the decoder to skip updating
its internal LevelInfo state, while the driver-side allocation was
updated.

This change replaces target with texture->target() in the
glTexParameteri calls within the workaround path, ensuring the base
target GL_TEXTURE_CUBE_MAP is used. This prevents the driver error and
keeps the decoder's LevelInfo tracking synchronized with the driver.

Fixed: 513543143
Test: GLTest.CopyTexImage2DWorkaroundStateDesync
Link: https://chromium-review.googlesource.com/id/Id876e00cb0c205a6b7ccb432a0cea8b16a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7853657
Reviewed-by: Kenneth Russell <kbr@chromium.org>
Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1632441}
---

diff --git a/gpu/BUILD.gn b/gpu/BUILD.gn
index 815f367..11b32f5 100644
--- a/gpu/BUILD.gn
+++ b/gpu/BUILD.gn
@@ -257,6 +257,8 @@
     "command_buffer/tests/gl_request_extension_unittest.cc",
     "command_buffer/tests/gl_set_aggressively_free_resources_unittest.cc",
     "command_buffer/tests/gl_stream_draw_unittest.cc",
+    "command_buffer/tests/gl_test_service_helper.cc",
+    "command_buffer/tests/gl_test_service_helper.h",
     "command_buffer/tests/gl_test_setup_helper.cc",
     "command_buffer/tests/gl_test_setup_helper.h",
     "command_buffer/tests/gl_test_utils.cc",
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index 9e699f2..deda1dc 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -13583,9 +13583,9 @@
     }
   }
   if (reset_source_texture_base_level_max_level) {
-    api()->glTexParameteriFn(target, GL_TEXTURE_BASE_LEVEL,
+    api()->glTexParameteriFn(texture->target(), GL_TEXTURE_BASE_LEVEL,
                              attached_texture_level);
-    api()->glTexParameteriFn(target, GL_TEXTURE_MAX_LEVEL,
+    api()->glTexParameteriFn(texture->target(), GL_TEXTURE_MAX_LEVEL,
                              attached_texture_level);
   }
 
@@ -13664,9 +13664,9 @@
     }
   }
   if (reset_source_texture_base_level_max_level) {
-    api()->glTexParameteriFn(target, GL_TEXTURE_BASE_LEVEL,
+    api()->glTexParameteriFn(texture->target(), GL_TEXTURE_BASE_LEVEL,
                              texture->base_level());
-    api()->glTexParameteriFn(target, GL_TEXTURE_MAX_LEVEL,
+    api()->glTexParameteriFn(texture->target(), GL_TEXTURE_MAX_LEVEL,
                              texture->max_level());
   }
   GLenum error = LOCAL_PEEK_GL_ERROR(func_name);
diff --git a/gpu/command_buffer/tests/gl_test_service_helper.cc b/gpu/command_buffer/tests/gl_test_service_helper.cc
new file mode 100644
index 0000000..1fd0358
--- /dev/null
+++ b/gpu/command_buffer/tests/gl_test_service_helper.cc
@@ -0,0 +1,40 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "gpu/command_buffer/tests/gl_test_service_helper.h"
+
+#include "gpu/command_buffer/service/context_group.h"
+#include "gpu/command_buffer/service/gles2_cmd_decoder.h"
+#include "gpu/command_buffer/service/texture_manager.h"
+#include "gpu/command_buffer/tests/gl_manager.h"
+
+namespace gpu {
+
+bool InspectTextureLevelSize(GLManager* gl_manager,
+                             unsigned int client_id,
+                             unsigned int target,
+                             int level,
+                             int* width,
+                             int* height) {
+  gles2::ContextGroup* group = gl_manager->decoder()->GetContextGroup();
+  if (!group) {
+    return false;
+  }
+  gles2::TextureManager* texture_manager = group->texture_manager();
+  if (!texture_manager) {
+    return false;
+  }
+  gles2::TextureRef* texture_ref = texture_manager->GetTexture(client_id);
+  if (!texture_ref) {
+    return false;
+  }
+  gles2::Texture* texture = texture_ref->texture();
+  if (!texture) {
+    return false;
+  }
+
+  return texture->GetLevelSize(target, level, width, height, nullptr);
+}
+
+}  // namespace gpu
diff --git a/gpu/command_buffer/tests/gl_test_service_helper.h b/gpu/command_buffer/tests/gl_test_service_helper.h
new file mode 100644
index 0000000..135f1595
--- /dev/null
+++ b/gpu/command_buffer/tests/gl_test_service_helper.h
@@ -0,0 +1,20 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GPU_COMMAND_BUFFER_TESTS_GL_TEST_SERVICE_HELPER_H_
+#define GPU_COMMAND_BUFFER_TESTS_GL_TEST_SERVICE_HELPER_H_
+
+namespace gpu {
+class GLManager;
+
+bool InspectTextureLevelSize(GLManager* gl_manager,
+                             unsigned int client_id,
+                             unsigned int target,
+                             int level,
+                             int* width,
+                             int* height);
+
+}  // namespace gpu
+
+#endif  // GPU_COMMAND_BUFFER_TESTS_GL_TEST_SERVICE_HELPER_H_
diff --git a/gpu/command_buffer/tests/gl_unittest.cc b/gpu/command_buffer/tests/gl_unittest.cc
index 5ad306a..ffcf252 100644
--- a/gpu/command_buffer/tests/gl_unittest.cc
+++ b/gpu/command_buffer/tests/gl_unittest.cc
@@ -4,14 +4,18 @@
 
 #include <GLES2/gl2.h>
 #include <GLES2/gl2ext.h>
+#include <GLES3/gl3.h>
 #include <stdint.h>
 
 #include "base/containers/heap_array.h"
+#include "build/build_config.h"
 #include "gpu/command_buffer/service/feature_info.h"
 #include "gpu/command_buffer/tests/gl_manager.h"
+#include "gpu/command_buffer/tests/gl_test_service_helper.h"
 #include "gpu/command_buffer/tests/gl_test_utils.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gl/buildflags.h"
 
 namespace gpu {
 
@@ -127,4 +131,77 @@
       reinterpret_cast<const char*>(glGetString(GL_SHADING_LANGUAGE_VERSION)));
 }
 
+// TODO(crbug.com/513543143): Goldfish GLES emulator driver on 32-bit x86
+// Android bots has a known driver bug where it incorrectly rejects
+// glCopyTexImage2D on cubemaps with GL_INVALID_ENUM.
+#if BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER) && \
+    !(BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86))
+class GL3Test : public GLTest {
+ protected:
+  void SetUp() override {
+    if (gl_.gpu_preferences().use_passthrough_cmd_decoder) {
+      GTEST_SKIP() << "Test only applies to validating decoder";
+    }
+    GLManager::Options options;
+    options.context_type = CONTEXT_TYPE_OPENGLES3;
+    gl_.Initialize(options);
+  }
+};
+
+// Tests that glCopyTexImage2D correctly updates the internal service-side
+// texture level size when using the workaround path that temporarily clamps
+// base/max levels to prevent FBO incompleteness. Specifically, it verifies
+// that the workaround uses the correct base target (GL_TEXTURE_CUBE_MAP)
+// instead of the specific face target (e.g., GL_TEXTURE_CUBE_MAP_POSITIVE_X)
+// which would cause driver-side GL_INVALID_ENUM errors and result in a state
+// desynchronization between the decoder and the GPU driver.
+TEST_F(GL3Test, CopyTexImage2DWorkaroundStateDesync) {
+  GLuint tex = 0;
+  glGenTextures(1, &tex);
+  glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
+
+  // We manually specify all mip levels instead of calling glGenerateMipmap
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/tests/gl_test_service_helper.cc b/gpu/command_buffer/tests/gl_test_service_helper.cc
new file mode 100644
index 0000000..1fd0358
--- /dev/null
+++ b/gpu/command_buffer/tests/gl_test_service_helper.cc
@@ -0,0 +1,40 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "gpu/command_buffer/tests/gl_test_service_helper.h"
+
+#include "gpu/command_buffer/service/context_group.h"
+#include "gpu/command_buffer/service/gles2_cmd_decoder.h"
+#include "gpu/command_buffer/service/texture_manager.h"
+#include "gpu/command_buffer/tests/gl_manager.h"
+
+namespace gpu {
+
+bool InspectTextureLevelSize(GLManager* gl_manager,
+                             unsigned int client_id,
+                             unsigned int target,
+                             int level,
+                             int* width,
+                             int* height) {
+  gles2::ContextGroup* group = gl_manager->decoder()->GetContextGroup();
+  if (!group) {
+    return false;
+  }
+  gles2::TextureManager* texture_manager = group->texture_manager();
+  if (!texture_manager) {
+    return false;
+  }
+  gles2::TextureRef* texture_ref = texture_manager->GetTexture(client_id);
+  if (!texture_ref) {
+    return false;
+  }
+  gles2::Texture* texture = texture_ref->texture();
+  if (!texture) {
+    return false;
+  }
+
+  return texture->GetLevelSize(target, level, width, height, nullptr);
+}
+
+}  // namespace gpu
diff --git a/gpu/command_buffer/tests/gl_test_service_helper.h b/gpu/command_buffer/tests/gl_test_service_helper.h
new file mode 100644
index 0000000..135f1595
--- /dev/null
+++ b/gpu/command_buffer/tests/gl_test_service_helper.h
@@ -0,0 +1,20 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GPU_COMMAND_BUFFER_TESTS_GL_TEST_SERVICE_HELPER_H_
+#define GPU_COMMAND_BUFFER_TESTS_GL_TEST_SERVICE_HELPER_H_
+
+namespace gpu {
+class GLManager;
+
+bool InspectTextureLevelSize(GLManager* gl_manager,
+                             unsigned int client_id,
+                             unsigned int target,
+                             int level,
+                             int* width,
+                             int* height);
+
+}  // namespace gpu
+
+#endif  // GPU_COMMAND_BUFFER_TESTS_GL_TEST_SERVICE_HELPER_H_
diff --git a/gpu/command_buffer/tests/gl_unittest.cc b/gpu/command_buffer/tests/gl_unittest.cc
index 5ad306a..ffcf252 100644
--- a/gpu/command_buffer/tests/gl_unittest.cc
+++ b/gpu/command_buffer/tests/gl_unittest.cc
@@ -4,14 +4,18 @@
 
 #include <GLES2/gl2.h>
 #include <GLES2/gl2ext.h>
+#include <GLES3/gl3.h>
 #include <stdint.h>
 
 #include "base/containers/heap_array.h"
+#include "build/build_config.h"
 #include "gpu/command_buffer/service/feature_info.h"
 #include "gpu/command_buffer/tests/gl_manager.h"
+#include "gpu/command_buffer/tests/gl_test_service_helper.h"
 #include "gpu/command_buffer/tests/gl_test_utils.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gl/buildflags.h"
 
 namespace gpu {
 
@@ -127,4 +131,77 @@
       reinterpret_cast<const char*>(glGetString(GL_SHADING_LANGUAGE_VERSION)));
 }
 
+// TODO(crbug.com/513543143): Goldfish GLES emulator driver on 32-bit x86
+// Android bots has a known driver bug where it incorrectly rejects
+// glCopyTexImage2D on cubemaps with GL_INVALID_ENUM.
+#if BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER) && \
+    !(BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86))
+class GL3Test : public GLTest {
+ protected:
+  void SetUp() override {
+    if (gl_.gpu_preferences().use_passthrough_cmd_decoder) {
+      GTEST_SKIP() << "Test only applies to validating decoder";
+    }
+    GLManager::Options options;
+    options.context_type = CONTEXT_TYPE_OPENGLES3;
+    gl_.Initialize(options);
+  }
+};
+
+// Tests that glCopyTexImage2D correctly updates the internal service-side
+// texture level size when using the workaround path that temporarily clamps
+// base/max levels to prevent FBO incompleteness. Specifically, it verifies
+// that the workaround uses the correct base target (GL_TEXTURE_CUBE_MAP)
+// instead of the specific face target (e.g., GL_TEXTURE_CUBE_MAP_POSITIVE_X)
+// which would cause driver-side GL_INVALID_ENUM errors and result in a state
+// desynchronization between the decoder and the GPU driver.
+TEST_F(GL3Test, CopyTexImage2DWorkaroundStateDesync) {
+  GLuint tex = 0;
+  glGenTextures(1, &tex);
+  glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
+
+  // We manually specify all mip levels instead of calling glGenerateMipmap
+  // because various GPU drivers (e.g., the Android x64 emulator) have bugs
+  // when generating cubemap texture mipmaps, which can cause FBO
+  // incompleteness.
+  auto pixels = base::HeapArray<uint8_t>::WithSize(32 * 32 * 4);
+  for (int level = 0; level < 6; ++level) {
+    int size = 32 >> level;
+    for (int i = 0; i < 6; ++i) {
+      glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, level, GL_RGBA8, size,
+                   size, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
+    }
+  }
+
+  GLuint fbo = 0;
+  glGenFramebuffers(1, &fbo);
+  glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                         GL_TEXTURE_CUBE_MAP_POSITIVE_Y, tex, 3);
+  EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+            glCheckFramebufferStatus(GL_FRAMEBUFFER));
+
+  glCopyTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 5, GL_RGBA8, 0, 0, 2, 2, 0);
+
+  EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // Verify that the decoder's internal LevelInfo tracking has been correctly
+  // updated to match the new 2x2 size. If the workaround had triggered a driver
+  // error, the decoder would have skipped updating this state while the driver-
+  // side allocation would still be updated, leading to a security
+  // vulnerability.
+  int tracked_width = 0;
+  int tracked_height = 0;
+  bool defined =
+      InspectTextureLevelSize(&gl_, tex, GL_TEXTURE_CUBE_MAP_POSITIVE_X, 5,
+                              &tracked_width, &tracked_height);
+  EXPECT_TRUE(defined);
+  EXPECT_EQ(2, tracked_width);
+  EXPECT_EQ(2, tracked_height);
+
+  glDeleteFramebuffers(1, &fbo);
+  glDeleteTextures(1, &tex);
+}
+#endif
+
 }  // namespace gpu
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential state desync in GLES2 decoder DoCopyTexImage2D leads to OOB write

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: An incorrect GL target enum used in a workaround path in DoCopyTexImage2D causes a state desynchronization between the GLES2 validating command decoder and the GPU driver. This desynchronization allows attackers to bypass bounds checks, potentially leading to an out-of-bounds write in the GPU process.

Affected files:

  • gpu/command_buffer/service/gles2_cmd_decoder.cc

Estimated timestamp from git blame: 2018-01-08

Summary

A potential state desynchronization vulnerability exists in the GLES2 validating command decoder’s implementation of DoCopyTexImage2D in gpu/command_buffer/service/gles2_cmd_decoder.cc. When performing a copy operation that triggers a feedback loop workaround (specifically when copying to a cube-map face while a different level of the same texture is attached to the read framebuffer), the decoder incorrectly passes a cube-face target enum to glTexParameteri. This results in a GL_INVALID_ENUM error from the driver, which causes the decoder to skip updating its internal state for that texture level. Subsequent operations can then bypass the decoder’s validation, potentially leading to out-of-bounds writes in the GPU process.

Root Cause Analysis

In GLES2DecoderImpl::DoCopyTexImage2D, there is a workaround for cases where the read framebuffer’s attachment is the same texture as the destination, but at a different level. To avoid mipmap incompleteness during the copy, the code attempts to temporarily clamp GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to the attached level.

However, the code incorrectly uses the target parameter (which, for cube maps, is a specific face enum like GL_TEXTURE_CUBE_MAP_POSITIVE_X) in the calls to api()->glTexParameteriFn (lines 13586 and 13588). According to the GLES specification, glTexParameteri accepts base targets like GL_TEXTURE_CUBE_MAP, but not individual cube-face targets. This mismatch causes the driver to generate a GL_INVALID_ENUM error.

Following the GL calls, the decoder uses LOCAL_PEEK_GL_ERROR (line 13672) to check for errors. Because the previous glTexParameteri call generated an error, the decoder skips calling texture_manager()->SetLevelInfo (line 13674). While the internal state update is skipped, the driver-side texture dimensions have already been updated by the preceding glCopyTexImage2D call. This leads to a state desynchronization: the decoder’s tracked LevelInfo for the texture level retains its previous size, while the actual allocation in the driver has been redefined with new dimensions.

Impact

The validating decoder enforces security invariants by maintaining an accurate model of GL state. When LevelInfo becomes stale, bounds checking for subsequent commands like glTexSubImage2D is compromised. An attacker could shrink a texture allocation on the driver side while the decoder still believes it is large, allowing a subsequent glTexSubImage2D to perform an out-of-bounds write in the GPU process.

This is potentially web-reachable via WebGL2 on configurations using the validating decoder (common on some Android devices). On Android, the GPU process is often unsandboxed, making this a high-impact vulnerability.

Potential Reproduction Steps

Note: These steps are based on code analysis and have not been executed.

  1. Initialize a cube map texture and define a large level (e.g., 512x512) for GL_TEXTURE_CUBE_MAP_POSITIVE_X at level 5.
  2. Define a smaller level (e.g., 64x64) for another face of the same texture (e.g., GL_TEXTURE_CUBE_MAP_POSITIVE_Y) at level 3.
  3. Attach the level 3 face to the READ_FRAMEBUFFER.
  4. Call gl.copyTexImage2D targeting the level 5 face. This triggers the workaround and the invalid glTexParameteri call.
  5. The decoder skips SetLevelInfo due to the error, but the driver redefines the level 5 allocation to 64x64.
  6. Execute gl.texSubImage2D on the level 5 face with 512x512 data. The decoder validates against the stale 512x512 dimensions and forwards the out-of-bounds write to the driver.

In gpu/command_buffer/service/gles2_cmd_decoder.cc, the workaround path in DoCopyTexImage2D should use the base texture target instead of the face target for glTexParameteri calls. Replace target with texture->target() in the calls at lines 13586, 13588, 13667, and 13669.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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