High watch CVSS 5.3 chrome Uninitialized Memory 🔧 Commit mapped

Overview

High
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in GPU
DescriptionUninitialized Use in GPU
ComponentGPU
Sub-componentGPU
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker523713150
Fix commit870caf4d4873 (chromium/src) +110/-1
CWECWE-457 (Use of uninitialized)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-14 (per vendor advisory)
Fix landed2026-07-15 (public git)
Disclosed2026-08-06
Patch gap22 days — fix public in source before the advisory

Background

`Copy{Sub}TextureCHROMIUM`
Chrome GL command-buffer extensions that copy one texture into another; certain conversions (e.g. flip, cube-map destination) take a DRAW_AND_COPY / DRAW_AND_READBACK path that allocates an intermediate GL_TEXTURE_2D.
`GL_PIXEL_UNPACK_BUFFER` (PBO)
a client-bound GL buffer that, when bound, becomes the implicit data source for glTexImage2D/glTexSubImage2D uploads instead of a client-memory pointer.
Validating command decoder
the GPU-process decoder that services untrusted client GL commands; the bug reproduces only here (not the passthrough decoder).
`IsES3Capable()`
feature check for ES3, the level at which pixel-unpack-buffer bindings exist; used here to gate the fix so it matches RestoreBufferBindings().

Root Cause Analysis

On the DRAW_AND_COPY / DRAW_AND_READBACK paths, DoCopyTexture and DoCopySubTexture allocate an intermediate texture with glTexImage2D(dest_target, 0, ..., nullptr), intending fresh, driver-allocated storage. But the client’s GL_PIXEL_UNPACK_BUFFER binding was still applied to the driver, so with a bound PBO the driver treats the nullptr as an offset into that buffer and sources the texture’s storage from the client buffer rather than allocating new storage. If the client bound an undersized PBO, the glTexImage2D fails with GL_INVALID_OPERATION, leaving the intermediate texture with no storage; the subsequent draw/readback then operates on an incomplete framebuffer / unbacked texture, exposing uninitialized data.

The fix unbinds the buffer with glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) immediately before the intermediate glTexImage2D in both functions, gated on decoder->GetFeatureInfo()->IsES3Capable() so it only runs where a PBO binding can exist and matches the condition under which RestoreBufferBindings() later restores the client’s binding. With the buffer unbound, the intermediate texture is allocated from real storage and the copy proceeds correctly regardless of the client’s PBO.

Key insight
The intermediate-texture allocation implicitly inherited the client’s GL_PIXEL_UNPACK_BUFFER binding, so a glTexImage2D(..., nullptr) sourced storage from (or failed against) the client buffer instead of allocating fresh memory; the fix unbinds the PBO before that allocation, gated on IsES3Capable().

Attack Path

  1. Bind a tiny unpack buffer A renderer (WebGL2 client) binds a small GL_PIXEL_UNPACK_BUFFER via glBindBuffer + glBufferData and leaves it bound.
  2. Trigger an intermediate copy It calls glCopyTextureCHROMIUM/glCopySubTextureCHROMIUM with parameters (e.g. flip_y plus a cube-map destination) that force the DRAW_AND_COPY/DRAW_AND_READBACK path, which allocates an intermediate GL_TEXTURE_2D.
  3. Storage allocation subverted Pre-patch, glTexImage2D(..., nullptr) uses the still-bound undersized PBO as its data source and fails GL_INVALID_OPERATION, so the intermediate texture is left without storage.
  4. Operate on uninitialized/incomplete storage The subsequent draw and copy/readback run against the incomplete framebuffer / unbacked intermediate texture, letting the client observe uninitialized GPU memory in the destination. (The regression test confirms the destination should instead receive the real source pixels and the client PBO binding must be preserved.)

Impact Assessment

A renderer using the validating command decoder (ES3) can, by keeping an undersized pixel-unpack buffer bound across a Copy{Sub}TextureCHROMIUM that needs an intermediate texture, cause the intermediate to lack proper storage and expose uninitialized GPU memory through the copy result, an information disclosure in the GPU process. The advisory classifies it as uninitialized use in GPU and states no exploited-in-the-wild status; the passthrough decoder is not affected. Any cross-context nature of the leaked memory is (inferred).

Changed Functions

FunctionChangeNotes
TEST_P
gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc
modified
for
gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc
modified
if
gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc
modified

Files Changed

  • gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc
  • gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc

Audit Directions

  • Implicit binding leakage into internal allocations
    Look for GPU-process code that issues glTexImage2D/glTexSubImage2D/glReadPixels with a nullptr/offset while a client GL_PIXEL_UNPACK_BUFFER/GL_PIXEL_PACK_BUFFER may be bound, since the client binding silently alters the operation’s data source.
  • Intermediate/scratch resource setup
    Audit paths that create temporary textures, buffers, or framebuffers on behalf of a client command to ensure they reset all relevant client GL state (bindings, pixel-store params) before allocating.
  • Unchecked allocation success
    Verify that internal glTexImage2D-style allocations either check for GL_INVALID_OPERATION/errors or cannot silently produce an unbacked object that later draws/readbacks treat as valid.
From 870caf4d48738256593ab12d80db2f0d8012b116 Mon Sep 17 00:00:00 2001
From: Ken Russell <[email protected]>
Date: Tue, 14 Jul 2026 19:04:38 -0700
Subject: [PATCH] gpu: Unbind unpack buffer for CopyTextureCHROMIUM intermediate

When Copy{Sub}TextureCHROMIUM takes the DRAW_AND_COPY or
DRAW_AND_READBACK path, the validating decoder allocates an intermediate
GL_TEXTURE_2D via glTexImage2D(..., nullptr). The client's
GL_PIXEL_UNPACK_BUFFER binding is still applied to the driver at this
point, so the driver sources the intermediate texture from the client's
buffer instead of allocating fresh storage. With an undersized client
buffer this fails with GL_INVALID_OPERATION, leaving the intermediate
texture without storage and the subsequent draw / readback operating on
an incomplete framebuffer.

Unbind GL_PIXEL_UNPACK_BUFFER before allocating the intermediate texture
in DoCopyTexture and DoCopySubTexture, gated on IsES3Capable() to match
the condition under which RestoreBufferBindings() restores the binding
afterwards.

Add a gl_tests regression test that binds a tiny unpack buffer, performs
a copy that requires an intermediate texture, verifies the destination
receives the source pixels, and checks that the client binding is
preserved.

Fixed: 523713150
Change-Id: I42989c54ecb1a71be228bd457a1378d003feb1e4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8088997
Reviewed-by: Zhenyao Mo <[email protected]>
Commit-Queue: Kenneth Russell <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1662326}
---
diff --git a/gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc b/gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc
index fda1e42..16d4f37 100644
--- a/gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc
+++ b/gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc
@@ -1119,7 +1119,11 @@
         adjusted_internal_format);
     GLenum type =
         TextureManager::ExtractTypeFromStorageFormat(adjusted_internal_format);
-
+    // Allocate from client memory, not from any currently bound unpack
+    // buffer. The binding is restored by the calls below.
+    if (decoder->GetFeatureInfo()->IsES3Capable()) {
+      glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+    }
     glTexImage2D(dest_target, 0, adjusted_internal_format, width, height, 0,
                  format, type, nullptr);
     dest_texture = intermediate_texture;
@@ -1204,6 +1208,11 @@
         adjusted_internal_format);
     GLenum type =
         TextureManager::ExtractTypeFromStorageFormat(adjusted_internal_format);
+    // Allocate from client memory, not from any currently bound unpack
+    // buffer. The binding is restored by the calls below.
+    if (decoder->GetFeatureInfo()->IsES3Capable()) {
+      glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+    }
     glTexImage2D(dest_target, 0, adjusted_internal_format, width, height, 0,
                  format, type, nullptr);
     dest_texture = intermediate_texture;
diff --git a/gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc b/gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc
index 28135403..6d564708 100644
--- a/gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc
+++ b/gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc
@@ -1948,4 +1948,104 @@
 #endif  // !BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER)
 }
 
+// A bound GL_PIXEL_UNPACK_BUFFER must not be picked up when allocating the
+// intermediate texture used by the DRAW_AND_COPY / DRAW_AND_READBACK paths.
+TEST_P(GLCopyTextureCHROMIUMES3Test, PixelUnpackBufferDoesNotInterfere) {
+#if !BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER)
+  GTEST_SKIP() << "Test only reproduces with validating decoder";
+#else
+  if (gl_.gpu_preferences().use_passthrough_cmd_decoder) {
+    GTEST_SKIP() << "Skipping test because it's run with the passthrough "
+                    "command decoder";
+  }
+
+  if (!gl_.IsInitialized()) {
+    GTEST_SKIP() << "ES3 context unavailable";
+  }
+
+#if BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86_FAMILY)
+  GTEST_SKIP() << "Skipping test on Android x86/x64";
+#else
+
+  constexpr GLsizei kW = 8, kH = 8;
+  const uint8_t kGreen[4] = {0u, 255u, 0u, 255u};
+
+  GLuint src;
+  glGenTextures(1, &src);
+  glBindTexture(GL_TEXTURE_2D, src);
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+  std::vector<uint8_t> green(kW * kH * 4);
+  for (size_t i = 0; i < green.size(); i += 4) {
+    green[i + 0] = kGreen[0];
+    green[i + 1] = kGreen[1];
+    green[i + 2] = kGreen[2];
+    green[i + 3] = kGreen[3];
+  }
+  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kW, kH, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+               green.data());
+  ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // Cube-map destination so the copy goes through an intermediate texture.
+  GLuint dest;
+  glGenTextures(1, &dest);
+  glBindTexture(GL_TEXTURE_CUBE_MAP, dest);
+  glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+  glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+  std::vector<uint8_t> zeros(kW * kH * 4, 0);
+  for (int face = 0; face < 6; ++face) {
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA8, kW, kH, 0,
+                 GL_RGBA, GL_UNSIGNED_BYTE, zeros.data());
+  }
+  ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // Bind a tiny pixel-unpack buffer and leave it bound across the copy. The
+  // copy must not source the intermediate texture's storage from it.
+  GLuint pbo;
+  glGenBuffers(1, &pbo);
+  glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+  const uint8_t kByte = 0;
+  glBufferData(GL_PIXEL_UNPACK_BUFFER, sizeof(kByte), &kByte, GL_STATIC_DRAW);
+  ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // flip_y forces a draw-based path; cube-map dest requires an intermediate.
+  CopyType copy_type = GetParam();
+  if (copy_type == TexImage) {
+    glCopyTextureCHROMIUM(src, 0, GL_TEXTURE_CUBE_MAP_POSITIVE_X, dest, 0,
+                          GL_RGBA8, GL_UNSIGNED_BYTE, GL_TRUE, GL_FALSE,
+                          GL_FALSE);
+  } else {
+    glCopySubTextureCHROMIUM(src, 0, GL_TEXTURE_CUBE_MAP_POSITIVE_X, dest, 0, 0,
+                             0, 0, 0, kW, kH, GL_TRUE, GL_FALSE, GL_FALSE);
+  }
+  EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // The client binding must be preserved.
+  GLint bound_pbo = 0;
+  glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &bound_pbo);
+  EXPECT_EQ(pbo, static_cast<GLuint>(bound_pbo));
+
+  glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+
+  GLuint fbo;
+  glGenFramebuffers(1, &fbo);
+  glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                         GL_TEXTURE_CUBE_MAP_POSITIVE_X, dest, 0);
+  ASSERT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+            glCheckFramebufferStatus(GL_FRAMEBUFFER));
+
+  EXPECT_TRUE(GLTestHelper::CheckPixels(0, 0, kW, kH, 0, kGreen, nullptr));
+  EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  glDeleteFramebuffers(1, &fbo);
+  glDeleteBuffers(1, &pbo);
+  glDeleteTextures(1, &src);
+  glDeleteTextures(1, &dest);
+#endif  // BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86_FAMILY)
+#endif  // !BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER)
+}
+
 }  // namespace gpu
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc b/gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc
index 28135403..6d564708 100644
--- a/gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc
+++ b/gpu/command_buffer/tests/gl_copy_texture_CHROMIUM_unittest.cc
@@ -1948,4 +1948,104 @@
 #endif  // !BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER)
 }
 
+// A bound GL_PIXEL_UNPACK_BUFFER must not be picked up when allocating the
+// intermediate texture used by the DRAW_AND_COPY / DRAW_AND_READBACK paths.
+TEST_P(GLCopyTextureCHROMIUMES3Test, PixelUnpackBufferDoesNotInterfere) {
+#if !BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER)
+  GTEST_SKIP() << "Test only reproduces with validating decoder";
+#else
+  if (gl_.gpu_preferences().use_passthrough_cmd_decoder) {
+    GTEST_SKIP() << "Skipping test because it's run with the passthrough "
+                    "command decoder";
+  }
+
+  if (!gl_.IsInitialized()) {
+    GTEST_SKIP() << "ES3 context unavailable";
+  }
+
+#if BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86_FAMILY)
+  GTEST_SKIP() << "Skipping test on Android x86/x64";
+#else
+
+  constexpr GLsizei kW = 8, kH = 8;
+  const uint8_t kGreen[4] = {0u, 255u, 0u, 255u};
+
+  GLuint src;
+  glGenTextures(1, &src);
+  glBindTexture(GL_TEXTURE_2D, src);
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+  std::vector<uint8_t> green(kW * kH * 4);
+  for (size_t i = 0; i < green.size(); i += 4) {
+    green[i + 0] = kGreen[0];
+    green[i + 1] = kGreen[1];
+    green[i + 2] = kGreen[2];
+    green[i + 3] = kGreen[3];
+  }
+  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kW, kH, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+               green.data());
+  ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // Cube-map destination so the copy goes through an intermediate texture.
+  GLuint dest;
+  glGenTextures(1, &dest);
+  glBindTexture(GL_TEXTURE_CUBE_MAP, dest);
+  glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+  glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+  std::vector<uint8_t> zeros(kW * kH * 4, 0);
+  for (int face = 0; face < 6; ++face) {
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA8, kW, kH, 0,
+                 GL_RGBA, GL_UNSIGNED_BYTE, zeros.data());
+  }
+  ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // Bind a tiny pixel-unpack buffer and leave it bound across the copy. The
+  // copy must not source the intermediate texture's storage from it.
+  GLuint pbo;
+  glGenBuffers(1, &pbo);
+  glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+  const uint8_t kByte = 0;
+  glBufferData(GL_PIXEL_UNPACK_BUFFER, sizeof(kByte), &kByte, GL_STATIC_DRAW);
+  ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // flip_y forces a draw-based path; cube-map dest requires an intermediate.
+  CopyType copy_type = GetParam();
+  if (copy_type == TexImage) {
+    glCopyTextureCHROMIUM(src, 0, GL_TEXTURE_CUBE_MAP_POSITIVE_X, dest, 0,
+                          GL_RGBA8, GL_UNSIGNED_BYTE, GL_TRUE, GL_FALSE,
+                          GL_FALSE);
+  } else {
+    glCopySubTextureCHROMIUM(src, 0, GL_TEXTURE_CUBE_MAP_POSITIVE_X, dest, 0, 0,
+                             0, 0, 0, kW, kH, GL_TRUE, GL_FALSE, GL_FALSE);
+  }
+  EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // The client binding must be preserved.
+  GLint bound_pbo = 0;
+  glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &bound_pbo);
+  EXPECT_EQ(pbo, static_cast<GLuint>(bound_pbo));
+
+  glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+
+  GLuint fbo;
+  glGenFramebuffers(1, &fbo);
+  glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                         GL_TEXTURE_CUBE_MAP_POSITIVE_X, dest, 0);
+  ASSERT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+            glCheckFramebufferStatus(GL_FRAMEBUFFER));
+
+  EXPECT_TRUE(GLTestHelper::CheckPixels(0, 0, kW, kH, 0, kGreen, nullptr));
+  EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  glDeleteFramebuffers(1, &fbo);
+  glDeleteBuffers(1, &pbo);
+  glDeleteTextures(1, &src);
+  glDeleteTextures(1, &dest);
+#endif  // BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_X86_FAMILY)
+#endif  // !BUILDFLAG(ENABLE_VALIDATING_COMMAND_DECODER)
+}
+
 }  // namespace gpu
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.