Medium watch CVSS 4.7 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.7
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in GPU
DescriptionUninitialized resource 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.
Tracker517337579
Fix commitb4c415c65c31 (chromium/src) +125/-2
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-08-12 (public git)
Disclosed2026-09-08
Patch gap27 days — fix public in source before the advisory

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/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;
     }
   }
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…

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

reported by [email protected]

Potential cross-origin VRAM leak in TextureManager PBO workarounds

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: A potential vulnerability in gpu/command_buffer/service/texture_manager.cc allows bypassing the validating decoder’s lazy-clear protections on affected NVIDIA Android devices. Unconditional calls to SetLevelCleared in the NVIDIA PBO workaround branches mark texture levels as cleared even if driver-level sub-uploads fail due to GPU memory pressure. This can potentially result in the disclosure of uninitialized, cross-origin GPU memory (VRAM) to an attacker’s WebGL context.

Affected files:

  • gpu/command_buffer/service/texture_manager.cc

Estimated timestamp from git blame: 2016-03-14

Description

There is a potential vulnerability in the GLES2 validating command decoder’s texture manager (gpu/command_buffer/service/texture_manager.cc) associated with the NVIDIA Pixel Buffer Object (PBO) workarounds. On affected configurations (such as Android devices running with NVIDIA GPU drivers), specific workaround branches in TextureManager::ValidateAndDoTexImage can unconditionally mark a texture level as fully cleared even if the driver-level sub-uploads fail. This allows an attacker to bypass the validating decoder’s lazy-clear protections and potentially read back uninitialized, cross-origin GPU memory (VRAM).

Root Cause Analysis

In gpu/command_buffer/service/texture_manager.cc, TextureManager::ValidateAndDoTexImage handles NVIDIA PBO-workaround paths for overlapping unpack rows (workaround ID 165) and alignment constraints (workaround ID 217). The implementation carries out the following operations:

  1. It pre-allocates uninitialized backing storage on the driver using ReserveTexImageToBeFilled (which calls DoTexImage with pixels = nullptr and the pixel unpack buffer temporarily unbound). Since pixels is nullptr, DoTexImage sets the level cleared rect to empty, correctly marking the texture level as uncleared.
  2. It attempts to populate this uninitialized storage with the user’s data by calling the helper functions DoTexSubImageRowByRowWorkaround or DoTexSubImageWithAlignmentWorkaround.
  3. Finally, it unconditionally marks the texture level as fully cleared at line 2684 and line 2718 by calling:
    SetLevelCleared(texture_ref, args.target, args.level, true);
    

The potential vulnerability is that the helper sub-upload functions are void and do not perform any GL-error checking. If a driver-level sub-upload fails silently (for example, generating a GL_OUT_OF_MEMORY error under high GPU memory pressure), the uninitialized VRAM allocated in step 1 is not fully overwritten with user-supplied data. However, because SetLevelCleared is called unconditionally, the command decoder assumes the texture is safely initialized. Consequently, the lazy-clear checks (such as those in gles2_cmd_decoder.cc using SafeToRenderFrom()) are bypassed.

This behavior contrasts with secure reference patterns in the codebase, such as GLES2DecoderImpl::DoCopyTexSubImage2D (at gles2_cmd_decoder.cc:13768), where the cleared state commit is deferred and strictly gated on LOCAL_PEEK_GL_ERROR(...) == GL_NO_ERROR.

Potential Trigger Path

Please note that these are potential steps; our automated analysis tooling does not currently have the capability to run code or execute a proof of concept.

  1. An attacker sets up a page containing WebGL2 content, targeting a device using GLES2 validating decoder with an NVIDIA vendor (where workarounds 165 and 217 are active).
  2. The attacker’s script binds a PIXEL_UNPACK_BUFFER and configures overlapping unpack rows via UNPACK_ROW_LENGTH (where row_length < width) or triggers specific alignment constraints.
  3. The attacker allocates a texture using texImage2D or texImage3D to enter the workaround branches in ValidateAndDoTexImage.
  4. If the attacker has previously induced high GPU memory pressure (by allocating large graphics resources), one or more raw glTexSubImage2D/glTexSubImage3D calls within the helper sub-upload loops may fail with GL_OUT_OF_MEMORY.
  5. The driver records the error, but since the decoder does not check for it, the texture level is unconditionally marked as cleared via SetLevelCleared.
  6. The attacker subsequently reads back from the texture (e.g., via shader sampling or framebuffer readbacks with gl.readPixels). Because lazy-clearing is bypassed, the script potentially obtains uninitialized VRAM that contains stale data from other origins sharing the GPU process.

Suggested Remediation

To remediate this issue, the cleared-state commit should be deferred and strictly gated on verifying that no GL errors occurred during the workaround sub-uploads, similar to the pattern used in DoCopyTexSubImage2D:

// Verify no errors occurred before marking the level as cleared
if (error_state->PeekGLError(function_name, __LINE__) == GL_NO_ERROR) {
  SetLevelCleared(texture_ref, args.target, args.level, true);
}

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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