Critical watch CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in GPU
DescriptionOut of bounds write in GPU
ComponentGPU
Sub-componentGPU
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513543143
Fix commit048900b2334c (chromium/src) +143/-4
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-15 (per vendor advisory)
Fix landed2026-05-18 (public git)
Disclosed2026-06-02
Patch gap15 days — fix public in source before the advisory

Background

Validating command decoder
the GPU-process component that mirrors GL state in its own LevelInfo tracking so it can bounds-check later commands against the sizes it believes each texture level has.
`LevelInfo`
the decoder’s per-level record of a texture’s dimensions and format, kept in sync with the driver’s actual allocation and used for subsequent validation.
Cube-map targets
a cube-map texture is bound with the base target GL_TEXTURE_CUBE_MAP, but its six faces are addressed with per-face enums such as GL_TEXTURE_CUBE_MAP_POSITIVE_X.
`glTexParameteri` base/max level workaround
a path in DoCopyTexImage2D that temporarily clamps GL_TEXTURE_BASE_LEVEL/GL_TEXTURE_MAX_LEVEL to avoid framebuffer incompleteness when a different level of the same texture is attached to the read framebuffer.

Root Cause Analysis

DoCopyTexImage2D copies framebuffer pixels into a texture level and, when the same texture has another level attached to the read framebuffer, enters a workaround that resets the source texture’s base/max levels via glTexParameteriFn. The buggy code passed target — the specific cube-map face enum such as GL_TEXTURE_CUBE_MAP_POSITIVE_X — to glTexParameteriFn, but GL_TEXTURE_BASE_LEVEL/GL_TEXTURE_MAX_LEVEL are only valid against the base target GL_TEXTURE_CUBE_MAP, so the driver raised GL_INVALID_ENUM. That pending driver error caused the decoder to abort updating its LevelInfo for the destination level while the driver had already performed the real allocation/copy at the new (smaller) size — leaving the decoder’s tracked size and the driver’s actual allocation desynchronized.

The fix replaces target with texture->target() in both glTexParameteriFn calls so the base target GL_TEXTURE_CUBE_MAP is used, the driver call succeeds, and the decoder’s LevelInfo stays synchronized (verified by the regression test, which confirms the tracked level size becomes 2x2).

Key insight
The workaround passed a per-face cube-map enum where glTexParameteri’s base/max-level pname requires the base target, producing a GL_INVALID_ENUM that made the decoder skip its LevelInfo update while the driver allocation went ahead; using texture->target() restores the correct base target and keeps decoder and driver in agreement.

Attack Path

  1. Build a cube-map with mip levels The renderer creates a GL_TEXTURE_CUBE_MAP and defines its faces/levels (e.g. per-face glTexImage2D for levels 0–5).
  2. Attach a level to a read framebuffer It attaches one face/level of that texture as the read framebuffer’s color attachment, so DoCopyTexImage2D will enter the base/max-level workaround.
  3. Copy into another face at a new size It calls glCopyTexImage2D targeting a cube-map face (e.g. GL_TEXTURE_CUBE_MAP_POSITIVE_X) with a smaller size, triggering the workaround’s glTexParameteriFn(target, …) and the resulting GL_INVALID_ENUM.
  4. Induce the state desync The driver error makes the decoder skip updating LevelInfo while the driver reallocates the level at the new size, so the decoder now bounds-checks later commands against a stale/incorrect size that no longer matches the driver’s allocation.
  5. Trigger out-of-bounds access A subsequent texture operation validated against the mismatched tracked size drives a read or write beyond the driver’s real allocation (inferred) — the patch shows the desync but not the specific downstream sink.

Impact Assessment

An attacker controlling the renderer’s command stream can desynchronize the validating decoder’s texture-size bookkeeping from the driver’s actual allocation, enabling an out-of-bounds write in the GPU process as classified by the advisory. The advisory does not report exploitation in the wild, and progression from the state desync to a concrete memory-corruption primitive or sandbox escape is (inferred). Preconditions are use of the validating command decoder (the test skips the passthrough decoder) plus a cube-map with a same-texture level attached to the read framebuffer during a glCopyTexImage2D.

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

Audit Directions

  • Face vs. base target enums
    Audit all cube-map handling for calls that must use the base target (glTexParameteri, level queries) but are passed a per-face target variable, which drivers reject with GL_INVALID_ENUM.
  • Error-swallowing that skips state updates
    Look for decoder paths where a driver error causes the code to skip updating internal tracking (LevelInfo, size caches) while the driver-side operation still takes effect, creating a desync exploitable for OOB.
  • Temporary-state workarounds
    Review base/max-level and other temporary GL state adjustments in copy/blit paths, checking that the temporary calls cannot fail silently and that state is restored consistently on both success and error paths.
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