Medium chrome Uninitialized Memory 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in GPU
DescriptionUninitialized resource in GPU
ComponentGPU
Bug ClassUninitialized Memory
Tracker517337579
Fix commitb4c415c65c31 (chromium/src) +125/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Background

Lazy clearing
A GPU optimization where a newly allocated texture level is not zeroed immediately but instead flagged as needing a clear before it can be safely read from or rendered.
PBO (Pixel Buffer Object)
A GL buffer bound to GL_PIXEL_UNPACK_BUFFER from which texture pixel data is sourced during uploads instead of client memory.
Driver bug workaround
A code path (e.g. unpack_overlapping_rows_separately_unpack_buffer or unpack_alignment_workaround_with_unpack_buffer) that rewrites a texture upload into multiple TexSubImage2D calls to sidestep buggy driver behavior.
`SetLevelCleared`
A TextureManager call that marks a texture level as fully initialized, allowing it to pass SafeToRenderFrom checks without a lazy clear.

Root Cause Analysis

In TextureManager::ValidateAndDoTexImage, when a PBO-based driver-bug workaround runs — the row-by-row path (DoTexSubImageRowByRowWorkaround) or the alignment path (DoTexSubImageWithAlignmentWorkaround) — the code unconditionally called SetLevelCleared(texture_ref, args.target, args.level, true) immediately after issuing the split TexSubImage2D uploads. The invariant that “a level may only be marked cleared once its entire contents have actually been written” was violated because the workaround could fail partway through (for example a driver returning GL_OUT_OF_MEMORY), leaving some rows of the level never written. Because the level was still flagged as cleared, the lazy-clear step was permanently bypassed and the texture reported SafeToRenderFrom() == true.

The fix wraps each SetLevelCleared call in a check of ERRORSTATE_PEEK_GL_ERROR(error_state, function_name) == GL_NO_ERROR, so the level is marked cleared only when the workaround completed without a GL error; otherwise the level stays uncleared and the normal lazy-clear path will zero it before use.

Key insight
The single core mistake was treating the PBO workaround upload as always successful and marking the texture level cleared without checking for a GL error, which suppressed lazy-clearing of partially-written VRAM. The fix conditions SetLevelCleared on ERRORSTATE_PEEK_GL_ERROR returning GL_NO_ERROR, preserving the uncleared state whenever the uploads fail.

Attack Path

  1. Trigger a workaround-enabled context On hardware where unpack_overlapping_rows_separately_unpack_buffer or unpack_alignment_workaround_with_unpack_buffer is active, a page obtains a WebGL2 context that routes uploads through the vulnerable path.
  2. Bind a PBO and set pixel-store state The attacker binds a buffer to GL_PIXEL_UNPACK_BUFFER and configures GL_UNPACK_ROW_LENGTH/alignment so ValidateAndDoTexImage selects the split-upload workaround.
  3. Force a mid-upload GL error By sizing the texture, buffer, or state such that one of the split TexSubImage2D calls fails (e.g. driver returns GL_OUT_OF_MEMORY), the level is left only partially written.
  4. Bypass lazy clear The pre-patch code still calls SetLevelCleared(..., true), so the level is flagged cleared and never zeroed despite containing uninitialized VRAM.
  5. Read back the stale contents The attacker samples or reads back the texture (which now passes SafeToRenderFrom), disclosing previously-freed GPU memory.

Impact Assessment

An attacker gains disclosure of uninitialized GPU (VRAM) contents — data that may include prior textures or framebuffer memory from other contexts — by reading back a texture level that was flagged cleared but never fully written. The flaw manifests in the GPU process, reachable from a renderer via the WebGL/GLES2 command-buffer interface. Preconditions are a device/driver where the unpack_overlapping_rows_separately_unpack_buffer or unpack_alignment_workaround_with_unpack_buffer workaround is enabled, use of a PBO-sourced upload, and inducing a GL error mid-workaround.

Changed Functions

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

Files Changed

  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
  • gpu/command_buffer/service/texture_manager.cc

Audit Directions

  • Unconditional `SetLevelCleared` after uploads
    Search for SetLevelCleared(..., true) calls that follow texture-write helpers without an intervening GL-error check, since any failed or partial write followed by a cleared flag reopens this uninitialized-memory class.
  • Error handling in driver-bug workaround paths
    Review each GpuDriverBugWorkarounds-gated split-upload path (row-by-row, alignment, and similar) to confirm it propagates or peeks GL errors before updating clear/initialized state.
  • Lazy-clear invariant enforcement
    Audit places where SafeToRenderFrom and level-cleared state can diverge from actual written coverage, ensuring partially-completed uploads never leave a level marked as fully initialized.
From b4c415c65c314e40b6b9c65af05ed758e8c26a22 Mon Sep 17 00:00:00 2001
From: Tzarial <[email protected]>
Date: Wed, 12 Aug 2026 09:14:28 -0700
Subject: [PATCH] [agy][gpu] Fix PBO workaround lazy-clear bypass

Do not mark texture level as cleared if an error occurred during
the PBO workarounds (row-by-row or alignment workarounds) in
ValidateAndDoTexImage.

Fixed: 517337579
Test: gpu_unittests
Change-Id: If54f50ff98fb55435fae3ad3cf145caf5fb41d78
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8239634
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Tzarial <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1678093}
---

diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
index 70e377be..6b7d756 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
@@ -594,6 +594,119 @@
       texture->GetLevelSize(GL_TEXTURE_2D, level, &width, &height, nullptr));
 }
 
+TEST_P(GLES2DecoderManualInitTest,
+       TexImage2DUnpackOverlappingRowsWorkaroundGLError) {
+  gpu::GpuDriverBugWorkarounds workarounds;
+  workarounds.unpack_overlapping_rows_separately_unpack_buffer = true;
+  InitState init;
+  init.gl_version = "OpenGL ES 3.0";
+  init.context_type = CONTEXT_TYPE_OPENGLES3;
+  InitDecoderWithWorkarounds(init, workarounds);
+
+  const GLsizei kWidth = 2;
+  const GLsizei kHeight = 2;
+  const GLint kRowLength = 1;
+  const GLenum kFormat = GL_RGBA;
+  const GLenum kType = GL_UNSIGNED_BYTE;
+  const GLint kBufferSize = 16;
+
+  DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+
+  cmds::PixelStorei pixel_store_cmd;
+  pixel_store_cmd.Init(GL_UNPACK_ROW_LENGTH, kRowLength);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(pixel_store_cmd));
+
+  EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ROW_LENGTH, kRowLength))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId);
+  DoBufferData(GL_PIXEL_UNPACK_BUFFER, kBufferSize);
+
+  TextureManager* manager = group().texture_manager();
+  TextureRef* texture_ref = manager->GetTexture(client_texture_id_);
+  ASSERT_TRUE(texture_ref != nullptr);
+  Texture* texture = texture_ref->texture();
+
+  EXPECT_CALL(*gl_, PixelStorei(_, _)).Times(AnyNumber());
+  EXPECT_CALL(*gl_, BindBuffer(GL_PIXEL_UNPACK_BUFFER, _)).Times(AnyNumber());
+  EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0,
+                               kFormat, kType, nullptr))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(
+      *gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, kWidth, 1, kFormat, kType, _))
+      .Times(kHeight)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_OUT_OF_MEMORY))
+      .RetiresOnSaturation();
+  cmds::TexImage2D cmd;
+  cmd.Init(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, kFormat, kType, 0, 0);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+  EXPECT_FALSE(texture->IsLevelCleared(GL_TEXTURE_2D, 0));
+  EXPECT_FALSE(texture->SafeToRenderFrom());
+  EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError());
+}
+
+TEST_P(GLES2DecoderManualInitTest, TexImage2DUnpackAlignmentWorkaroundGLError) {
+  gpu::GpuDriverBugWorkarounds workarounds;
+  workarounds.unpack_alignment_workaround_with_unpack_buffer = true;
+  InitState init;
+  init.gl_version = "OpenGL ES 3.0";
+  init.context_type = CONTEXT_TYPE_OPENGLES3;
+  InitDecoderWithWorkarounds(init, workarounds);
+
+  const GLsizei kWidth = 2;
+  const GLsizei kHeight = 2;
+  const GLenum kFormat = GL_RGB;
+  const GLenum kType = GL_UNSIGNED_BYTE;
+  // Padded row size 8 (alignment 4), unpadded row size 6; pixels_size = 14.
+  const GLint kBufferSize = 14;
+
+  DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+
+  EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ROW_LENGTH, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId);
+  DoBufferData(GL_PIXEL_UNPACK_BUFFER, kBufferSize);
+
+  TextureManager* manager = group().texture_manager();
+  TextureRef* texture_ref = manager->GetTexture(client_texture_id_);
+  ASSERT_TRUE(texture_ref != nullptr);
+  Texture* texture = texture_ref->texture();
+
+  EXPECT_CALL(*gl_, PixelStorei(_, _)).Times(AnyNumber());
+  EXPECT_CALL(*gl_, BindBuffer(GL_PIXEL_UNPACK_BUFFER, _)).Times(AnyNumber());
+  EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, 0,
+                               kFormat, kType, nullptr))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(
+      *gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, kWidth, _, kFormat, kType, _))
+      .Times(2)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_OUT_OF_MEMORY))
+      .RetiresOnSaturation();
+  cmds::TexImage2D cmd;
+  cmd.Init(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, kFormat, kType, 0, 0);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+  EXPECT_FALSE(texture->IsLevelCleared(GL_TEXTURE_2D, 0));
+  EXPECT_FALSE(texture->SafeToRenderFrom());
+  EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError());
+}
+
 TEST_P(GLES2DecoderTest, TexSubImage2DGLErrorDoesNotMarkLevelAsCleared) {
   DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
   DoTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0,
diff --git a/gpu/command_buffer/service/texture_manager.cc b/gpu/command_buffer/service/texture_manager.cc
index 5c9b27bc9..c9a764c 100644
--- a/gpu/command_buffer/service/texture_manager.cc
+++ b/gpu/command_buffer/service/texture_manager.cc
@@ -2688,7 +2688,12 @@
       DoTexSubImageRowByRowWorkaround(texture_state, state, sub_args,
                                       unpack_params);
 
-      SetLevelCleared(texture_ref, args.target, args.level, true);
+      // https://crbug.com/517337579: Only mark the level as cleared if the
+      // workaround succeeded, to prevent leaking uninitialized VRAM if
+      // sub-image uploads failed.
+      if (ERRORSTATE_PEEK_GL_ERROR(error_state, function_name) == GL_NO_ERROR) {
+        SetLevelCleared(texture_ref, args.target, args.level, true);
+      }
       return;
     }
   }
@@ -2722,7 +2727,12 @@
               : DoTexSubImageArguments::CommandType::kTexSubImage2D};
       DoTexSubImageWithAlignmentWorkaround(texture_state, state, sub_args);
 
-      SetLevelCleared(texture_ref, args.target, args.level, true);
+      // https://crbug.com/517337579: Only mark the level as cleared if the
+      // workaround succeeded, to prevent leaking uninitialized VRAM if
+      // sub-image uploads failed.
+      if (ERRORSTATE_PEEK_GL_ERROR(error_state, function_name) == GL_NO_ERROR) {
+        SetLevelCleared(texture_ref, args.target, args.level, true);
+      }
       return;
     }
   }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
index 70e377be..6b7d756 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
@@ -594,6 +594,119 @@
       texture->GetLevelSize(GL_TEXTURE_2D, level, &width, &height, nullptr));
 }
 
+TEST_P(GLES2DecoderManualInitTest,
+       TexImage2DUnpackOverlappingRowsWorkaroundGLError) {
+  gpu::GpuDriverBugWorkarounds workarounds;
+  workarounds.unpack_overlapping_rows_separately_unpack_buffer = true;
+  InitState init;
+  init.gl_version = "OpenGL ES 3.0";
+  init.context_type = CONTEXT_TYPE_OPENGLES3;
+  InitDecoderWithWorkarounds(init, workarounds);
+
+  const GLsizei kWidth = 2;
+  const GLsizei kHeight = 2;
+  const GLint kRowLength = 1;
+  const GLenum kFormat = GL_RGBA;
+  const GLenum kType = GL_UNSIGNED_BYTE;
+  const GLint kBufferSize = 16;
+
+  DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+
+  cmds::PixelStorei pixel_store_cmd;
+  pixel_store_cmd.Init(GL_UNPACK_ROW_LENGTH, kRowLength);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(pixel_store_cmd));
+
+  EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ROW_LENGTH, kRowLength))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId);
+  DoBufferData(GL_PIXEL_UNPACK_BUFFER, kBufferSize);
+
+  TextureManager* manager = group().texture_manager();
+  TextureRef* texture_ref = manager->GetTexture(client_texture_id_);
+  ASSERT_TRUE(texture_ref != nullptr);
+  Texture* texture = texture_ref->texture();
+
+  EXPECT_CALL(*gl_, PixelStorei(_, _)).Times(AnyNumber());
+  EXPECT_CALL(*gl_, BindBuffer(GL_PIXEL_UNPACK_BUFFER, _)).Times(AnyNumber());
+  EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0,
+                               kFormat, kType, nullptr))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(
+      *gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, kWidth, 1, kFormat, kType, _))
+      .Times(kHeight)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_OUT_OF_MEMORY))
+      .RetiresOnSaturation();
+  cmds::TexImage2D cmd;
+  cmd.Init(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, kFormat, kType, 0, 0);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+  EXPECT_FALSE(texture->IsLevelCleared(GL_TEXTURE_2D, 0));
+  EXPECT_FALSE(texture->SafeToRenderFrom());
+  EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError());
+}
+
+TEST_P(GLES2DecoderManualInitTest, TexImage2DUnpackAlignmentWorkaroundGLError) {
+  gpu::GpuDriverBugWorkarounds workarounds;
+  workarounds.unpack_alignment_workaround_with_unpack_buffer = true;
+  InitState init;
+  init.gl_version = "OpenGL ES 3.0";
+  init.context_type = CONTEXT_TYPE_OPENGLES3;
+  InitDecoderWithWorkarounds(init, workarounds);
+
+  const GLsizei kWidth = 2;
+  const GLsizei kHeight = 2;
+  const GLenum kFormat = GL_RGB;
+  const GLenum kType = GL_UNSIGNED_BYTE;
+  // Padded row size 8 (alignment 4), unpadded row size 6; pixels_size = 14.
+  const GLint kBufferSize = 14;
+
+  DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+
+  EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ROW_LENGTH, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId);
+  DoBufferData(GL_PIXEL_UNPACK_BUFFER, kBufferSize);
+
+  TextureManager* manager = group().texture_manager();
+  TextureRef* texture_ref = manager->GetTexture(client_texture_id_);
+  ASSERT_TRUE(texture_ref != nullptr);
+  Texture* texture = texture_ref->texture();
+
+  EXPECT_CALL(*gl_, PixelStorei(_, _)).Times(AnyNumber());
+  EXPECT_CALL(*gl_, BindBuffer(GL_PIXEL_UNPACK_BUFFER, _)).Times(AnyNumber());
+  EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, 0,
+                               kFormat, kType, nullptr))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(
+      *gl_, TexSubImage2D(GL_TEXTURE_2D, 0, 0, _, kWidth, _, kFormat, kType, _))
+      .Times(2)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_OUT_OF_MEMORY))
+      .RetiresOnSaturation();
+  cmds::TexImage2D cmd;
+  cmd.Init(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, kFormat, kType, 0, 0);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+  EXPECT_FALSE(texture->IsLevelCleared(GL_TEXTURE_2D, 0));
+  EXPECT_FALSE(texture->SafeToRenderFrom());
+  EXPECT_EQ(GL_OUT_OF_MEMORY, GetGLError());
+}
+
 TEST_P(GLES2DecoderTest, TexSubImage2DGLErrorDoesNotMarkLevelAsCleared) {
   DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
   DoTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0,
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.