High watch CVSS 4.3 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in WebGL
DescriptionUninitialized Use in WebGL
ComponentWebGL
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.
Tracker500150338
Fix commitf786ca0ffae2 (chromium/src) +48/-3
CWECWE-457 (Use of uninitialized)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-07 (per vendor advisory)
Fix landed2026-05-02 (public git)
Disclosed2026-05-27
Patch gap25 days — fix public in source before the advisory

Background

`DoCopyTexSubImage2D`
the GLES2DecoderImpl handler for the WebGL/GLES2 copyTexSubImage2D command, which copies a rectangle of pixels from the current framebuffer into an existing texture level.
Level cleared flag
per-level texture state in TextureManager (SetLevelCleared / SetLevelClearedRect) marking a texture level’s storage as fully or partially initialized, so the decoder can skip zero-clearing before a read/sample.
`LOCAL_PEEK_GL_ERROR` / `LOCAL_COPY_REAL_GL_ERRORS_TO_WRAPPER`
decoder macros that inspect and drain the underlying driver’s GL error state, letting the handler know whether the real allocation/copy succeeded.
Uninitialized use
reading GPU memory that was never written by the app or cleared by the service, disclosing residual data from prior allocations.

Root Cause Analysis

In the vulnerable DoCopyTexSubImage2D, the code called SetLevelCleared / SetLevelClearedRect on the texture level before or without confirming that the underlying GL texture allocation and copy actually succeeded. When allocation or the copy raised a GL error, the driver left the texture level’s storage untouched (still holding uninitialized GPU memory), yet the TextureManager recorded the level as cleared/initialized. Because the cleared flag is the signal the decoder uses to decide whether lazy zero-clearing is needed, a level wrongly marked cleared bypasses that safety clear on subsequent operations. A later read, sample, or copy of that texture therefore consumed uninitialized memory.

The fix drains pending errors first with LOCAL_COPY_REAL_GL_ERRORS_TO_WRAPPER(func_name), records the intended cleared state only in locals (set_cleared / set_cleared_rect), and applies them via SetLevelCleared / SetLevelClearedRect only when LOCAL_PEEK_GL_ERROR(func_name) == GL_NO_ERROR. This ties the cleared-flag update to genuine success of the GL allocation and copy.

Key insight
The cleared flag was updated on the control-flow assumption of success rather than on the observed outcome, so any GL error during allocation/copy left uninitialized storage labelled as initialized.

Attack Path

  1. Provoke a failing allocation Craft a WebGL copyTexSubImage2D against a texture level whose backing allocation or copy triggers a GL error (e.g. resource/format conditions that make the driver fail).
  2. Skip the clear The vulnerable path marks the level cleared despite the error, leaving its GPU storage uninitialized while TextureManager believes it is initialized.
  3. Read back the texture Sample or read the texture from a shader or via a readback/copy path, which no longer triggers lazy zero-clearing because the level is flagged cleared.
  4. Exfiltrate residual memory Recover the returned pixel values into script, disclosing uninitialized GPU memory contents.

Impact Assessment

This is an uninitialized-memory disclosure in the GPU process reachable from a WebGL context, allowing a malicious page to read residual GPU memory that may contain data from other allocations or contexts. Such leaks can defeat memory randomization and expose sensitive content, and they are a valuable primitive when chained with other GPU-process bugs.

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/gles2_cmd_decoder.cc
modified

Files Changed

  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_1.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc

Audit Directions

  • Success-assumed state updates
    Audit decoder handlers that set a level’s cleared/initialized flag; ensure the flag is applied only after checking LOCAL_PEEK_GL_ERROR for the real GL outcome, not on the mere fact that the code path ran.
  • Copy/allocation error handling
    Review other DoCopyTex* / DoTex*Image* paths in gles2_cmd_decoder.cc for cases where allocation or copy can fail while TextureManager state is still advanced as if it succeeded.
  • Stale error state
    Check that handlers drain prior driver errors (e.g. LOCAL_COPY_REAL_GL_ERRORS_TO_WRAPPER) before peeking, so a later LOCAL_PEEK_GL_ERROR reflects only this operation’s result.
From f786ca0ffae2c4740af8c81063e85c588f1b8a9d Mon Sep 17 00:00:00 2001
From: Ken Russell <[email protected]>
Date: Fri, 01 May 2026 17:54:21 -0700
Subject: [PATCH] Don't prematurely set texture's cleared flag.

In DoCopyTexSubImage2D, watch for GL errors during texture allocation
and do not set the texture's cleared flag in this case.

Co-authored with jetski-cli.

Fixed: 500150338
Change-Id: I76a363f5c241d50c9c697bbea596b1b0cdb8df64
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7807563
Reviewed-by: Brandon Jones <[email protected]>
Commit-Queue: Kenneth Russell <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1624199}
---

diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index 7ea21c6..1710e44 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -13653,6 +13653,9 @@
   GLint dy = src.y() - y;
   GLint destX = xoffset + dx;
   GLint destY = yoffset + dy;
+
+  LOCAL_COPY_REAL_GL_ERRORS_TO_WRAPPER(func_name);
+
   // It's only legal to skip clearing the level of the target texture
   // if the entire level is being redefined.
   GLsizei level_width = 0;
@@ -13662,10 +13665,15 @@
       target, level, &level_width, &level_height, &level_depth);
   // Validated above.
   DCHECK(have_level);
+
+  bool set_cleared = false;
+  gfx::Rect cleared_rect_to_set;
+  bool set_cleared_rect = false;
+
   if (destX == 0 && destY == 0 &&
       src.width() == level_width && src.height() == level_height) {
     // Write all pixels in below.
-    texture_manager()->SetLevelCleared(texture_ref, target, level, true);
+    set_cleared = true;
   } else {
     gfx::Rect cleared_rect;
     if (TextureManager::CombineAdjacentRects(
@@ -13674,8 +13682,8 @@
             &cleared_rect)) {
       DCHECK_GE(cleared_rect.size().GetArea(),
                 texture->GetLevelClearedRect(target, level).size().GetArea());
-      texture_manager()->SetLevelClearedRect(texture_ref, target, level,
-                                             cleared_rect);
+      cleared_rect_to_set = cleared_rect;
+      set_cleared_rect = true;
     } else {
       // Otherwise clear part of texture level that is not already cleared.
       if (!texture_manager()->ClearTextureLevel(this, texture_ref, target,
@@ -13702,6 +13710,15 @@
                                  src.width(), src.height());
   }
 
+  if (LOCAL_PEEK_GL_ERROR(func_name) == GL_NO_ERROR) {
+    if (set_cleared) {
+      texture_manager()->SetLevelCleared(texture_ref, target, level, true);
+    } else if (set_cleared_rect) {
+      texture_manager()->SetLevelClearedRect(texture_ref, target, level,
+                                             cleared_rect_to_set);
+    }
+  }
+
   // This may be a slow command.  Exit command processing to allow for
   // context preemption and GPU watchdog checks.
   ExitCommandProcessingEarly();
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_1.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_1.cc
index 55849419..d33e425 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_1.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_1.cc
@@ -110,6 +110,10 @@
     DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
     DoTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 16, 16, 0, GL_RGBA,
                  GL_UNSIGNED_BYTE, shared_memory_id_, kSharedMemoryOffset);
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
   }
 }
 
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 6c63c9d323..def979a3d 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
@@ -450,6 +450,10 @@
               CopyTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 0, 0, kWidth, kHeight))
       .Times(1)
       .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
   cmds::CopyTexSubImage2D cmd;
   cmd.Init(GL_TEXTURE_2D, 1, 0, 0, 0, 0, kWidth, kHeight);
   EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
@@ -2765,6 +2769,10 @@
     EXPECT_CALL(*gl_, CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 1))
         .Times(1)
         .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
     cmds::CopyTexSubImage2D cmd;
     cmd.Init(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 1);
     EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
@@ -2775,6 +2783,10 @@
     EXPECT_CALL(*gl_, CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 1, 0, 0, 2, 1))
         .Times(1)
         .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
     cmds::CopyTexSubImage2D cmd;
     cmd.Init(GL_TEXTURE_2D, 0, 0, 1, 0, 0, 2, 1);
     EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
@@ -2797,6 +2809,10 @@
     EXPECT_CALL(*gl_, CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 1))
         .Times(1)
         .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
     cmds::CopyTexSubImage2D cmd;
     cmd.Init(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 1);
     EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
@@ -2812,6 +2828,10 @@
     EXPECT_CALL(*gl_, CopyTexSubImage2D(GL_TEXTURE_2D, 0, 1, 1, 0, 0, 1, 1))
         .Times(1)
         .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
     cmds::CopyTexSubImage2D cmd;
     cmd.Init(GL_TEXTURE_2D, 0, 1, 1, 0, 0, 1, 1);
     EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
@@ -2833,6 +2853,10 @@
                                       kBackBufferWidth, kBackBufferHeight))
       .Times(1)
       .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
   cmds::CopyTexSubImage2D cmd;
   cmd.Init(GL_TEXTURE_2D, 0, 0, 0, 0, 0, kBackBufferWidth, kBackBufferHeight);
   EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_1.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_1.cc
index 55849419..d33e425 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_1.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_1.cc
@@ -110,6 +110,10 @@
     DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
     DoTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 16, 16, 0, GL_RGBA,
                  GL_UNSIGNED_BYTE, shared_memory_id_, kSharedMemoryOffset);
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
   }
 }
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 6c63c9d323..def979a3d 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
@@ -450,6 +450,10 @@
               CopyTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 0, 0, kWidth, kHeight))
       .Times(1)
       .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
   cmds::CopyTexSubImage2D cmd;
   cmd.Init(GL_TEXTURE_2D, 1, 0, 0, 0, 0, kWidth, kHeight);
   EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
@@ -2765,6 +2769,10 @@
     EXPECT_CALL(*gl_, CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 1))
         .Times(1)
         .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
     cmds::CopyTexSubImage2D cmd;
     cmd.Init(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 1);
     EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
@@ -2775,6 +2783,10 @@
     EXPECT_CALL(*gl_, CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 1, 0, 0, 2, 1))
         .Times(1)
         .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
     cmds::CopyTexSubImage2D cmd;
     cmd.Init(GL_TEXTURE_2D, 0, 0, 1, 0, 0, 2, 1);
     EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
@@ -2797,6 +2809,10 @@
     EXPECT_CALL(*gl_, CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 1))
         .Times(1)
         .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
     cmds::CopyTexSubImage2D cmd;
     cmd.Init(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 2, 1);
     EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
@@ -2812,6 +2828,10 @@
     EXPECT_CALL(*gl_, CopyTexSubImage2D(GL_TEXTURE_2D, 0, 1, 1, 0, 0, 1, 1))
         .Times(1)
         .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, GetError())
+        .WillOnce(Return(GL_NO_ERROR))
+        .WillOnce(Return(GL_NO_ERROR))
+        .RetiresOnSaturation();
     cmds::CopyTexSubImage2D cmd;
     cmd.Init(GL_TEXTURE_2D, 0, 1, 1, 0, 0, 1, 1);
     EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
@@ -2833,6 +2853,10 @@
                                       kBackBufferWidth, kBackBufferHeight))
       .Times(1)
       .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
   cmds::CopyTexSubImage2D cmd;
   cmd.Init(GL_TEXTURE_2D, 0, 0, 0, 0, 0, kBackBufferWidth, kBackBufferHeight);
   EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
Loading diff…

Original Bug Report

reported by [email protected]

Potential VRAM Info Leak via Premature Cleared Flag in DoCopyTexSubImage2D

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 without the security team.

Overview: GLES2DecoderImpl::DoCopyTexSubImage2D updates a texture’s cleared state before ensuring the copy operation succeeds. If the copy fails (e.g., due to an induced GPU out-of-memory error), the texture remains uninitialized but is marked as cleared, allowing an attacker to read stale, potentially cross-origin VRAM.

Affected files:

  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/service/gles2_cmd_copy_tex_image.cc

Estimated timestamp from git blame: 2016-10-12

Description

There is a logic error in GLES2DecoderImpl::DoCopyTexSubImage2D where the ‘cleared’ state of a destination texture is updated prematurely. The function updates the cleared_rect (via SetLevelCleared or SetLevelClearedRect) before performing the actual copy operation.

If the subsequent setup for the copy or the copy itself fails, the function exits early without rolling back the cleared state. Because WebGL contexts are created with lose_context_when_out_of_memory set to false, an attacker can survive an induced Out-Of-Memory (OOM) error and subsequently sample from the uninitialized texture, leading to a cross-origin leakage of GPU memory.

Potential Trigger Steps

  1. Initialize Context: The attacker creates a WebGL context. By default, lose_context_when_out_of_memory is false for WebGL contexts (gpu/ipc/common/gpu_channel.mojom), allowing the context to survive a GL_OUT_OF_MEMORY event.
  2. Induce Memory Pressure: The attacker aggressively allocates large WebGL textures to push the GPU driver close to its memory limit.
  3. Allocate Target Texture: The attacker calls gl.texImage2D(..., null) with a format like GL_LUMINANCE (which requires blit emulation on GLES3 devices). TextureManager::DoTexImage allocates VRAM but correctly leaves its cleared_rect empty, marking it uncleared.
  4. Execute Copy: The attacker calls gl.copyTexSubImage2D with dimensions that exactly cover the full size of the destination texture level.
  5. Premature State Change: In GLES2DecoderImpl::DoCopyTexSubImage2D, the code evaluates the bounds (destX == 0 && destY == 0 && src.width() == level_width && src.height() == level_height) and immediately executes texture_manager()->SetLevelCleared(..., true). The texture is now formally considered initialized and safe to read from.
  6. OOM Failure: Because GL_LUMINANCE requires a blit, the code calls InitializeCopyTexImageBlitter. Inside CopyTexImageResourceManager::Initialize, GL resource allocations (e.g., shaders, framebuffers) fail due to the induced memory pressure, triggering a GL_OUT_OF_MEMORY error.
  7. Early Exit: InitializeCopyTexImageBlitter detects the error via LOCAL_PEEK_GL_ERROR and returns false. DoCopyTexSubImage2D then immediately exits via an early return;.
  8. VRAM Leak: The actual copy is skipped. The texture remains physically uninitialized in VRAM but is tracked as cleared. The attacker binds the texture and reads its contents (e.g., via glReadPixels), successfully leaking cross-origin VRAM.

Suggested Fix

Defer the state updates to the Texture’s cleared state until after the copy operation has been successfully executed. Specifically, move the calls to texture_manager()->SetLevelCleared and texture_manager()->SetLevelClearedRect to occur only after InitializeCopyTexImageBlitter (if applicable) and the subsequent copy commands (DoCopyTexSubImageToLUMACompatibilityTexture or glCopyTexSubImage2DFn) have completed without critical errors. This aligns the logic with GLES2DecoderImpl::DoCopyTexImage2D, which safely sets the cleared state only after verifying GL_NO_ERROR.

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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