High chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in WebGL
DescriptionUninitialized Use in WebGL
ComponentWebGL
Bug ClassUninitialized Memory
Tracker500095304
Fix commit60172394d499 (chromium/src) +92/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/gles2_cmd_decoder.cc
modified
TEST_P
gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc
modified
for
gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc
modified
if
gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc
From 60172394d4995bd549b56af10128db080e59f8ca Mon Sep 17 00:00:00 2001
From: Ken Russell <[email protected]>
Date: Tue, 05 May 2026 08:18:12 -0700
Subject: [PATCH] Disable rasterizer discard during implicit clears.

Avoids interference by user-set state.

Added ported unit test from the bug report.

Co-authored with jetski-cli.

Fixed: 500095304
Change-Id: I44267dc1049a0e1a5dd3bed7b33d60c69f72437a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7815560
Reviewed-by: Vasiliy Telezhnikov <[email protected]>
Commit-Queue: Vasiliy Telezhnikov <[email protected]>
Auto-Submit: Kenneth Russell <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1625439}
---

diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index 1710e44..da2a06d 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -7014,6 +7014,11 @@
 // Assumes framebuffer is complete.
 void GLES2DecoderImpl::ClearUnclearedAttachments(
     GLenum target, Framebuffer* framebuffer) {
+  bool rasterizer_discard_enabled = state_.enable_flags.rasterizer_discard;
+  if (rasterizer_discard_enabled) {
+    state_.SetDeviceCapabilityState(GL_RASTERIZER_DISCARD, false);
+  }
+
   // Clear textures that we can't use glClear first. These textures will be
   // marked as cleared after the call and no longer be part of the following
   // code.
@@ -7097,6 +7102,10 @@
 
   framebuffer_manager()->MarkAttachmentsAsCleared(
       framebuffer, renderbuffer_manager(), texture_manager());
+
+  if (rasterizer_discard_enabled) {
+    state_.SetDeviceCapabilityState(GL_RASTERIZER_DISCARD, true);
+  }
 }
 
 void GLES2DecoderImpl::RestoreClearState() {
diff --git a/gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc b/gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc
index 11ce787c..f2d6f562 100644
--- a/gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc
+++ b/gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc
@@ -633,4 +633,87 @@
   ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
 }
 
+TEST_P(ES3ClearBufferTest, RasterizerDiscardIntegerClearBypass) {
+  if (ShouldSkipTest()) {
+    return;
+  }
+
+  const GLsizei kDirtyWidth = 256;
+  const GLsizei kDirtyHeight = 256;
+
+  // Step 0: Dirty VRAM using a separate context.
+  {
+    GLManager gl2;
+    gl2.Initialize(GetGlManagerOptions());
+    gl2.MakeCurrent();
+
+    for (int i = 0; i < 8; ++i) {
+      GLuint rb = 0;
+      glGenRenderbuffers(1, &rb);
+      glBindRenderbuffer(GL_RENDERBUFFER, rb);
+      glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kDirtyWidth,
+                            kDirtyHeight);
+      GLuint fb = 0;
+      glGenFramebuffers(1, &fb);
+      glBindFramebuffer(GL_FRAMEBUFFER, fb);
+      glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                GL_RENDERBUFFER, rb);
+      EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+                glCheckFramebufferStatus(GL_FRAMEBUFFER));
+
+      glClearColor(0.8f, 0.2f, 0.6f, 1.0f);
+      glClear(GL_COLOR_BUFFER_BIT);
+
+      glDeleteFramebuffers(1, &fb);
+      glDeleteRenderbuffers(1, &rb);
+    }
+    glFinish();
+    gl2.Destroy();
+  }
+
+  // Restore main context.
+  gl_.MakeCurrent();
+
+  // Step 1: Trigger the bug.
+  GLuint rb = 0;
+  glGenRenderbuffers(1, &rb);
+  glBindRenderbuffer(GL_RENDERBUFFER, rb);
+  glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8UI, kDirtyWidth, kDirtyHeight);
+
+  GLuint fb = 0;
+  glGenFramebuffers(1, &fb);
+  glBindFramebuffer(GL_FRAMEBUFFER, fb);
+  glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                            GL_RENDERBUFFER, rb);
+  EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+            glCheckFramebufferStatus(GL_FRAMEBUFFER));
+
+  glEnable(GL_RASTERIZER_DISCARD);
+  ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // readPixels to trigger lazy clear.
+  std::vector<GLuint> pixels(kDirtyWidth * kDirtyHeight * 4, 0xAAAAAAAAu);
+  glReadPixels(0, 0, kDirtyWidth, kDirtyHeight, GL_RGBA_INTEGER,
+               GL_UNSIGNED_INT, pixels.data());
+
+  EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // Inspect results.
+  uint32_t nonzero_components = 0;
+  for (GLuint val : pixels) {
+    if (val != 0) {
+      nonzero_components++;
+    }
+  }
+
+  // If bug is present, we expect non-zero components (leak from dirty VRAM).
+  // If fixed, we expect ALL zero.
+  EXPECT_EQ(0u, nonzero_components);
+
+  // Cleanup.
+  glDisable(GL_RASTERIZER_DISCARD);
+  glDeleteFramebuffers(1, &fb);
+  glDeleteRenderbuffers(1, &rb);
+}
+
 }  // namespace gpu
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc b/gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc
index 11ce787c..f2d6f562 100644
--- a/gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc
+++ b/gpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc
@@ -633,4 +633,87 @@
   ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
 }
 
+TEST_P(ES3ClearBufferTest, RasterizerDiscardIntegerClearBypass) {
+  if (ShouldSkipTest()) {
+    return;
+  }
+
+  const GLsizei kDirtyWidth = 256;
+  const GLsizei kDirtyHeight = 256;
+
+  // Step 0: Dirty VRAM using a separate context.
+  {
+    GLManager gl2;
+    gl2.Initialize(GetGlManagerOptions());
+    gl2.MakeCurrent();
+
+    for (int i = 0; i < 8; ++i) {
+      GLuint rb = 0;
+      glGenRenderbuffers(1, &rb);
+      glBindRenderbuffer(GL_RENDERBUFFER, rb);
+      glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kDirtyWidth,
+                            kDirtyHeight);
+      GLuint fb = 0;
+      glGenFramebuffers(1, &fb);
+      glBindFramebuffer(GL_FRAMEBUFFER, fb);
+      glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                GL_RENDERBUFFER, rb);
+      EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+                glCheckFramebufferStatus(GL_FRAMEBUFFER));
+
+      glClearColor(0.8f, 0.2f, 0.6f, 1.0f);
+      glClear(GL_COLOR_BUFFER_BIT);
+
+      glDeleteFramebuffers(1, &fb);
+      glDeleteRenderbuffers(1, &rb);
+    }
+    glFinish();
+    gl2.Destroy();
+  }
+
+  // Restore main context.
+  gl_.MakeCurrent();
+
+  // Step 1: Trigger the bug.
+  GLuint rb = 0;
+  glGenRenderbuffers(1, &rb);
+  glBindRenderbuffer(GL_RENDERBUFFER, rb);
+  glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8UI, kDirtyWidth, kDirtyHeight);
+
+  GLuint fb = 0;
+  glGenFramebuffers(1, &fb);
+  glBindFramebuffer(GL_FRAMEBUFFER, fb);
+  glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                            GL_RENDERBUFFER, rb);
+  EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+            glCheckFramebufferStatus(GL_FRAMEBUFFER));
+
+  glEnable(GL_RASTERIZER_DISCARD);
+  ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // readPixels to trigger lazy clear.
+  std::vector<GLuint> pixels(kDirtyWidth * kDirtyHeight * 4, 0xAAAAAAAAu);
+  glReadPixels(0, 0, kDirtyWidth, kDirtyHeight, GL_RGBA_INTEGER,
+               GL_UNSIGNED_INT, pixels.data());
+
+  EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  // Inspect results.
+  uint32_t nonzero_components = 0;
+  for (GLuint val : pixels) {
+    if (val != 0) {
+      nonzero_components++;
+    }
+  }
+
+  // If bug is present, we expect non-zero components (leak from dirty VRAM).
+  // If fixed, we expect ALL zero.
+  EXPECT_EQ(0u, nonzero_components);
+
+  // Cleanup.
+  glDisable(GL_RASTERIZER_DISCARD);
+  glDeleteFramebuffers(1, &fb);
+  glDeleteRenderbuffers(1, &rb);
+}
+
 }  // namespace gpu
Loading diff…

Original Bug Report

reported by [email protected]

Potential uninitialized memory leak in GPU via RASTERIZER_DISCARD during clear

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: The validating command decoder fails to disable GL_RASTERIZER_DISCARD before performing lazy clears on uninitialized framebuffer attachments. Because underlying drivers (like ANGLE) ignore clear commands when rasterizer discard is active, the buffers remain uninitialized but are erroneously marked as cleared. A subsequent glReadPixels call can then leak this uninitialized GPU VRAM to web content.

Affected files:

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

Estimated timestamp from git blame: 2017-11-03

Description

In Chrome’s validating command decoder, uninitialized framebuffer attachments (such as renderbuffers) are lazily cleared to zero before their first use. This prevents leaking sensitive GPU memory to web content. The setup for these clear operations is handled by GLES2DecoderImpl::ClearUnclearedAttachments.

While this function correctly temporarily disables states that could interfere with clearing (such as GL_SCISSOR_TEST), it fails to disable GL_RASTERIZER_DISCARD.

When GL_RASTERIZER_DISCARD is enabled, underlying drivers (such as ANGLE) will silently ignore glClear and glClearBuffer* commands. For instance, ANGLE’s Context::noopClearBuffer returns early if rasterizer discard is active.

If an attacker triggers a lazy clear while GL_RASTERIZER_DISCARD is enabled, Chrome will issue the clear commands (e.g., glClearBufferuiv via Framebuffer::ClearUnclearedIntRenderbufferAttachments), which the driver immediately drops. Chrome then unconditionally updates its internal tracking, marking the attachment as initialized via SetCleared(..., true).

Because glReadPixels is not a rasterization command, it is unaffected by GL_RASTERIZER_DISCARD. A subsequent read operation will successfully execute, extracting the uninitialized GPU VRAM and returning it to the sandboxed renderer process.

Potential Reproduction Steps

Note: These are suggested steps based on static code analysis; we do not yet have a working proof of concept that has been successfully run.

  1. On a platform using the validating command decoder (e.g., Chrome for Android), create a WebGL 2 context.
  2. Allocate an integer renderbuffer (e.g., gl.RGBA8UI) and attach it to a newly created framebuffer’s gl.COLOR_ATTACHMENT0.
  3. Enable rasterizer discard by calling gl.enable(gl.RASTERIZER_DISCARD).
  4. Trigger a readback operation that forces a lazy clear, such as gl.readPixels(...) using gl.RGBA_INTEGER and gl.UNSIGNED_INT.
  5. The returned TypedArray will contain uninitialized GPU VRAM (potentially cross-origin data or other processes’ memory) instead of the expected zeros.

Suggested Fix

Modify GLES2DecoderImpl::ClearUnclearedAttachments in gpu/command_buffer/service/gles2_cmd_decoder.cc to temporarily disable GL_RASTERIZER_DISCARD before issuing any clear commands, similar to how it currently handles GL_SCISSOR_TEST. Additionally, ensure that GL_RASTERIZER_DISCARD is properly restored to its previous state at the end of the function (e.g., by updating GLES2DecoderImpl::RestoreClearState()).

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