High chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in GPU
DescriptionUninitialized Use in GPU
ComponentGPU
Bug ClassUninitialized Memory
Tracker521618871
Fix commit7acecde88e9e (chromium/src) +98/-31
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-16

Changed Functions

FunctionChangeNotes
TEST_F
gpu/command_buffer/service/framebuffer_manager_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/framebuffer_manager.cc
  • gpu/command_buffer/service/framebuffer_manager.h
  • gpu/command_buffer/service/framebuffer_manager_unittest.cc
From 7acecde88e9e2bcb416882ae4273dfb88cdb113f Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <[email protected]>
Date: Tue, 09 Jun 2026 17:29:03 -0700
Subject: [PATCH] gpu: Propagate texture clearing failures in GLES2 validating decoder

Previously, the validating decoder discarded the return value of
`TextureManager::ClearTextureLevel` when clearing uncleared
3D/integer/partially-cleared attachments. If a clear failed (e.g. due to
OOM), the attachments were still falsely marked as cleared, potentially
leading to readbacks of uninitialized GPU memory.

This CL fixes the issue by:
1. Propagating the clearing failure.
2. Gracefully losing the WebGL context if a clear fails, aborting the
   draw/read operation.
3. Adding a unit test to verify this behavior.

Bug: 521618871
Test: gpu_unittests --gtest_filter='FramebufferInfoTest.*'
Change-Id: I107e1d345865b5e1f5c8e7be786841ad931f46ea
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7915249
Auto-Submit: Zhenyao Mo <[email protected]>
Commit-Queue: Zhenyao Mo <[email protected]>
Reviewed-by: Kenneth Russell <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1644347}
---

diff --git a/gpu/command_buffer/service/framebuffer_manager.cc b/gpu/command_buffer/service/framebuffer_manager.cc
index 4eaf5fc..430d8a2d6 100644
--- a/gpu/command_buffer/service/framebuffer_manager.cc
+++ b/gpu/command_buffer/service/framebuffer_manager.cc
@@ -610,8 +610,9 @@
   return draw_buffer_type_mask_ != mask;
 }
 
-void Framebuffer::ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-    GLES2Decoder* decoder, TextureManager* texture_manager) {
+bool Framebuffer::ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+    GLES2Decoder* decoder,
+    TextureManager* texture_manager) {
   for (AttachmentMap::const_iterator it = attachments_.begin();
        it != attachments_.end(); ++it) {
     if (!it->second->IsTextureAttachment() || it->second->cleared())
@@ -620,12 +621,14 @@
         reinterpret_cast<TextureAttachment*>(it->second.get());
     if (attachment->IsPartiallyCleared() || attachment->Is3D() ||
         GLES2Util::IsIntegerFormat(attachment->internal_format())) {
-      texture_manager->ClearTextureLevel(decoder,
-                                         attachment->texture(),
-                                         attachment->target(),
-                                         attachment->level());
+      if (!texture_manager->ClearTextureLevel(decoder, attachment->texture(),
+                                              attachment->target(),
+                                              attachment->level())) {
+        return false;
+      }
     }
   }
+  return true;
 }
 
 // TODO([email protected]): when the texture or the renderbuffer in
diff --git a/gpu/command_buffer/service/framebuffer_manager.h b/gpu/command_buffer/service/framebuffer_manager.h
index cbc4319..c77b6bf2 100644
--- a/gpu/command_buffer/service/framebuffer_manager.h
+++ b/gpu/command_buffer/service/framebuffer_manager.h
@@ -96,7 +96,7 @@
   bool HasSRGBAttachments() const;
   bool HasDepthStencilFormatAttachment() const;
 
-  void ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+  bool ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
       GLES2Decoder* decoder,
       TextureManager* texture_manager);
 
diff --git a/gpu/command_buffer/service/framebuffer_manager_unittest.cc b/gpu/command_buffer/service/framebuffer_manager_unittest.cc
index af7b5d347..cf117730 100644
--- a/gpu/command_buffer/service/framebuffer_manager_unittest.cc
+++ b/gpu/command_buffer/service/framebuffer_manager_unittest.cc
@@ -767,8 +767,9 @@
   // Clear it but nothing happens.
   EXPECT_CALL(*decoder_.get(), GetFeatureInfo())
      .WillRepeatedly(Return(feature_info_.get()));
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_FALSE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_FALSE(framebuffer_->IsCleared());
@@ -785,8 +786,9 @@
   EXPECT_FALSE(framebuffer_->HasUnclearedAttachment(GL_COLOR_ATTACHMENT0));
   EXPECT_FALSE(framebuffer_->HasUnclearedColorAttachments());
   // Clear it but nothing happens.
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
@@ -818,8 +820,9 @@
       .WillOnce(Return(true))
       .WillOnce(Return(true))
       .RetiresOnSaturation();
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
@@ -858,8 +861,9 @@
   // Clear it but nothing happens.
   EXPECT_CALL(*decoder_.get(), GetFeatureInfo())
      .WillRepeatedly(Return(feature_info_.get()));
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
@@ -885,8 +889,9 @@
                                             kWidth, kHeight, kDepth))
       .WillOnce(Return(true))
       .RetiresOnSaturation();
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
@@ -894,6 +899,54 @@
   EXPECT_FALSE(framebuffer_->HasUnclearedColorAttachments());
 }
 
+TEST_F(FramebufferInfoTest, Clear3DTextureAttachmentsFails) {
+  const GLuint kTextureClientId = 33;
+  const GLuint kTextureServiceId = 333;
+  texture_manager_->CreateTexture(kTextureClientId, kTextureServiceId);
+  scoped_refptr<TextureRef> texture(
+      texture_manager_->GetTexture(kTextureClientId));
+  ASSERT_TRUE(texture.get() != nullptr);
+  texture_manager_->SetTarget(texture.get(), GL_TEXTURE_3D);
+  framebuffer_->AttachTexture(GL_COLOR_ATTACHMENT0, texture.get(),
+                              GL_TEXTURE_3D, 0, 0);
+  const Framebuffer::Attachment* attachment =
+      framebuffer_->GetAttachment(GL_COLOR_ATTACHMENT0);
+  ASSERT_TRUE(attachment != nullptr);
+
+  const int kWidth = 4;
+  const int kHeight = 8;
+  const int kDepth = 2;
+
+  // Not cleared at all.
+  texture_manager_->SetLevelInfo(texture.get(), GL_TEXTURE_3D, 0, GL_RGBA8,
+                                 kWidth, kHeight, kDepth, 0, GL_RGBA,
+                                 GL_UNSIGNED_BYTE, gfx::Rect());
+  EXPECT_FALSE(attachment->cleared());
+  EXPECT_FALSE(attachment->IsPartiallyCleared());
+  EXPECT_FALSE(framebuffer_->IsCleared());
+  EXPECT_TRUE(framebuffer_->HasUnclearedAttachment(GL_COLOR_ATTACHMENT0));
+  EXPECT_TRUE(framebuffer_->HasUnclearedColorAttachments());
+
+  // Now clear it, but make it fail (return false).
+  EXPECT_CALL(*decoder_.get(), GetFeatureInfo())
+      .WillRepeatedly(Return(feature_info_.get()));
+  EXPECT_CALL(*decoder_.get(),
+              ClearLevel3D(texture->texture(), GL_TEXTURE_3D, 0, GL_RGBA,
+                           GL_UNSIGNED_BYTE, kWidth, kHeight, kDepth))
+      .WillOnce(Return(false))
+      .RetiresOnSaturation();
+
+  // Clearing should fail (return false).
+  EXPECT_FALSE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
+
+  // The attachment should still be uncleared.
+  EXPECT_FALSE(attachment->cleared());
+  EXPECT_FALSE(framebuffer_->IsCleared());
+  EXPECT_TRUE(framebuffer_->HasUnclearedAttachment(GL_COLOR_ATTACHMENT0));
+}
+
 TEST_F(FramebufferInfoTest, Clear3DOutsideRenderableRange) {
   const GLuint kTextureClientId = 33;
   const GLuint kTextureServiceId = 333;
@@ -936,8 +989,9 @@
       .RetiresOnSaturation();
   EXPECT_CALL(*decoder_.get(), GetFeatureInfo())
      .WillRepeatedly(Return(feature_info_.get()));
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/framebuffer_manager_unittest.cc b/gpu/command_buffer/service/framebuffer_manager_unittest.cc
index af7b5d347..cf117730 100644
--- a/gpu/command_buffer/service/framebuffer_manager_unittest.cc
+++ b/gpu/command_buffer/service/framebuffer_manager_unittest.cc
@@ -767,8 +767,9 @@
   // Clear it but nothing happens.
   EXPECT_CALL(*decoder_.get(), GetFeatureInfo())
      .WillRepeatedly(Return(feature_info_.get()));
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_FALSE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_FALSE(framebuffer_->IsCleared());
@@ -785,8 +786,9 @@
   EXPECT_FALSE(framebuffer_->HasUnclearedAttachment(GL_COLOR_ATTACHMENT0));
   EXPECT_FALSE(framebuffer_->HasUnclearedColorAttachments());
   // Clear it but nothing happens.
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
@@ -818,8 +820,9 @@
       .WillOnce(Return(true))
       .WillOnce(Return(true))
       .RetiresOnSaturation();
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
@@ -858,8 +861,9 @@
   // Clear it but nothing happens.
   EXPECT_CALL(*decoder_.get(), GetFeatureInfo())
      .WillRepeatedly(Return(feature_info_.get()));
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
@@ -885,8 +889,9 @@
                                             kWidth, kHeight, kDepth))
       .WillOnce(Return(true))
       .RetiresOnSaturation();
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
@@ -894,6 +899,54 @@
   EXPECT_FALSE(framebuffer_->HasUnclearedColorAttachments());
 }
 
+TEST_F(FramebufferInfoTest, Clear3DTextureAttachmentsFails) {
+  const GLuint kTextureClientId = 33;
+  const GLuint kTextureServiceId = 333;
+  texture_manager_->CreateTexture(kTextureClientId, kTextureServiceId);
+  scoped_refptr<TextureRef> texture(
+      texture_manager_->GetTexture(kTextureClientId));
+  ASSERT_TRUE(texture.get() != nullptr);
+  texture_manager_->SetTarget(texture.get(), GL_TEXTURE_3D);
+  framebuffer_->AttachTexture(GL_COLOR_ATTACHMENT0, texture.get(),
+                              GL_TEXTURE_3D, 0, 0);
+  const Framebuffer::Attachment* attachment =
+      framebuffer_->GetAttachment(GL_COLOR_ATTACHMENT0);
+  ASSERT_TRUE(attachment != nullptr);
+
+  const int kWidth = 4;
+  const int kHeight = 8;
+  const int kDepth = 2;
+
+  // Not cleared at all.
+  texture_manager_->SetLevelInfo(texture.get(), GL_TEXTURE_3D, 0, GL_RGBA8,
+                                 kWidth, kHeight, kDepth, 0, GL_RGBA,
+                                 GL_UNSIGNED_BYTE, gfx::Rect());
+  EXPECT_FALSE(attachment->cleared());
+  EXPECT_FALSE(attachment->IsPartiallyCleared());
+  EXPECT_FALSE(framebuffer_->IsCleared());
+  EXPECT_TRUE(framebuffer_->HasUnclearedAttachment(GL_COLOR_ATTACHMENT0));
+  EXPECT_TRUE(framebuffer_->HasUnclearedColorAttachments());
+
+  // Now clear it, but make it fail (return false).
+  EXPECT_CALL(*decoder_.get(), GetFeatureInfo())
+      .WillRepeatedly(Return(feature_info_.get()));
+  EXPECT_CALL(*decoder_.get(),
+              ClearLevel3D(texture->texture(), GL_TEXTURE_3D, 0, GL_RGBA,
+                           GL_UNSIGNED_BYTE, kWidth, kHeight, kDepth))
+      .WillOnce(Return(false))
+      .RetiresOnSaturation();
+
+  // Clearing should fail (return false).
+  EXPECT_FALSE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
+
+  // The attachment should still be uncleared.
+  EXPECT_FALSE(attachment->cleared());
+  EXPECT_FALSE(framebuffer_->IsCleared());
+  EXPECT_TRUE(framebuffer_->HasUnclearedAttachment(GL_COLOR_ATTACHMENT0));
+}
+
 TEST_F(FramebufferInfoTest, Clear3DOutsideRenderableRange) {
   const GLuint kTextureClientId = 33;
   const GLuint kTextureServiceId = 333;
@@ -936,8 +989,9 @@
       .RetiresOnSaturation();
   EXPECT_CALL(*decoder_.get(), GetFeatureInfo())
      .WillRepeatedly(Return(feature_info_.get()));
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
@@ -975,8 +1029,9 @@
   // Clear it but nothing happens.
   EXPECT_CALL(*decoder_.get(), GetFeatureInfo())
      .WillRepeatedly(Return(feature_info_.get()));
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
@@ -1005,8 +1060,9 @@
                                           0, 0, kWidth, kHeight))
       .WillOnce(Return(true))
       .RetiresOnSaturation();
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
@@ -1059,8 +1115,9 @@
       .RetiresOnSaturation();
   EXPECT_CALL(*decoder_.get(), GetFeatureInfo())
      .WillRepeatedly(Return(feature_info_.get()));
-  framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
-      decoder_.get(), texture_manager_.get());
+  EXPECT_TRUE(
+      framebuffer_->ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
+          decoder_.get(), texture_manager_.get()));
   EXPECT_TRUE(attachment->cleared());
   EXPECT_FALSE(attachment->IsPartiallyCleared());
   EXPECT_TRUE(framebuffer_->IsCleared());
Loading diff…

Original Bug Report

reported by [email protected]

Potential uninitialized VRAM leakage in validating decoder 3D texture clearing

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: In the Android WebView validating decoder (WebGL2), a failure to check the return value of TextureManager::ClearTextureLevel can cause uninitialized texture levels to be falsely marked as cleared. Under specific GPU memory pressure conditions where 3D texture clears fail, only the single layer attached to the framebuffer is cleared via glClear, while the remaining uninitialized layers are committed as cleared. This allows a standard WebGL2 context to sample and read back uninitialized, cross-origin GPU memory slices.

Affected files:

  • gpu/command_buffer/service/framebuffer_manager.cc
  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/service/texture_manager.cc

Estimated timestamp from git blame: 2016-02-22

Root Cause Analysis

In the Android WebView validating decoder (GLES2DecoderImpl), Framebuffer::ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures is called to initialize special texture attachments (e.g., 3D textures, integer formats, or partially cleared levels) prior to performing rendering operations or readbacks.

However, in gpu/command_buffer/service/framebuffer_manager.cc:

void Framebuffer::ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures(
    GLES2Decoder* decoder, TextureManager* texture_manager) {
  for (...) {
    if (!it->second->IsTextureAttachment() || it->second->cleared()) continue;
    TextureAttachment* attachment = ...;
    if (attachment->IsPartiallyCleared() || attachment->Is3D() ||
        GLES2Util::IsIntegerFormat(attachment->internal_format())) {
      texture_manager->ClearTextureLevel(decoder,            // <-- Return value is discarded!
                                         attachment->texture(),
                                         attachment->target(),
                                         attachment->level());
    }
  }
}

When a 3D texture level clear fails inside GLES2DecoderImpl::ClearLevel3D (for example, due to a GL_OUT_OF_MEMORY error during temporary PBO allocation), the helper returns false (in gles2_cmd_decoder.cc). Because the return value is discarded by the caller above, the failure status is swallowed.

Subsequently, inside GLES2DecoderImpl::ClearUnclearedAttachments:

  1. The helper ClearUnclearedIntOr3DTextures... completes without reporting the failure.
  2. The decoder checks if there are still uncleared attachments. Since the 3D clear failed, HasUnclearedColorAttachments() is still true.
  3. In production release builds, the DCHECK(!it.second->Is3D()) check is compiled out, so the draw buffer is prepared for clearing.
  4. The decoder issues a physical clear call: api()->glClearFn(clear_bits).
  5. Since only a single slice (layer k) of the 3D texture was attached to the framebuffer via glFramebufferTextureLayer, only that single layer is cleared by the driver.
  6. The decoder unconditionally calls MarkAttachmentsAsCleared which invokes SetLevelCleared on the texture attachment, marking the entire level (all slices) as cleared despite only one slice being initialized.

Bypassing lose_context_when_out_of_memory_

While Chromium employs a lose_context_when_out_of_memory_ mitigation to destroy the context when an OOM is encountered, it can be bypassed due to driver-specific GL error ordering. Inside PeekGLError (in error_state.cc), the error is retrieved via glGetError() exactly once. If the driver returns GL_INVALID_OPERATION (generated by the subsequent glTexSubImage3D calls on the invalid PBO) before GL_OUT_OF_MEMORY, PeekGLError will register the error as GL_INVALID_OPERATION, bypass the context-loss handler, and return false, allowing execution to continue.

Potential Steps to Reproduce

Note: These steps are based on static analysis; our tooling does not currently have the capability to execute code to verify them.

  1. Initialize a WebGL2 context on Android WebView (which forces the validating GLES2 decoder).
  2. Create a 3D texture and allocate uninitialized storage for level 0 using gl.texImage3D.
  3. Bind a framebuffer and attach exactly one slice of the 3D texture to color attachment 0 using gl.framebufferTextureLayer.
  4. From a separate/sibling WebGL context, allocate large resources to exhaust GPU VRAM to a state where a ~2 MiB PBO buffer allocation fails with OOM.
  5. Trigger a draw or readPixels targeting the bound framebuffer to force FBO validation and clearing.
  6. Release the sibling context’s pressure resources, and read back the remaining slices of the 3D texture using a fragment shader sampler.
  7. Slices other than the attached slice k should contain uninitialized cross-origin graphics memory (such as contents from other pages/WebViews).

Suggested Fix

Do not discard the boolean return value from ClearTextureLevel inside Framebuffer::ClearUnclearedIntOr3DTexturesOrPartiallyClearedTextures. Instead, propagate the return status up to ClearUnclearedAttachments. If a clearing operation fails, abort the execution or gracefully lose the WebGL context.

Evaluated with Chrome root at commit: 3947e01999a53d4e2382e39736cb79d79c7dffcf


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