Overview

High
Severity
6.5
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.
Tracker500095304
Fix commit60172394d499 (chromium/src) +92/-0
CWECWE-457 (Use of uninitialized)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-06 (per vendor advisory)
Fix landed2026-05-05 (public git)
Disclosed2026-05-27
Patch gap22 days — fix public in source before the advisory

Background

Robust resource initialization
Chrome’s guarantee that GPU resources (framebuffer attachments) are zero-cleared before a client can read them, preventing exposure of stale memory.
Lazy/implicit clear
The deferred zero-clearing of uninitialized framebuffer attachments performed by ClearUnclearedAttachments at first read/use, rather than at allocation.
`GL_RASTERIZER_DISCARD`
A client-settable GL capability that, when enabled, makes the driver discard all primitives before rasterization so no fragments (and no draw-based clears) are ever produced.
`MarkAttachmentsAsCleared`
The FramebufferManager bookkeeping call that flags attachments as initialized so they will not be cleared again.

Root Cause Analysis

ClearUnclearedAttachments implements the lazy zero-clear for framebuffer attachments that cannot be cleared with a plain glClear (such as integer-format renderbuffers) by issuing glClear-style draw operations. The command decoder honored the client’s persistent state_.enable_flags.rasterizer_discard when performing this implicit clear, meaning the driver-level GL_RASTERIZER_DISCARD capability remained enabled throughout. When a client had enabled GL_RASTERIZER_DISCARD, the driver discarded all rasterization, including the implicit clear’s own draws, so no pixels were actually written. Critically, the code still called MarkAttachmentsAsCleared afterward, marking the attachments as initialized even though they were never zeroed.

The result is that uninitialized GPU memory (whatever residual VRAM contents the attachment aliased) remained readable, breaking the robust-resource-init guarantee.

The fix saves and clears GL_RASTERIZER_DISCARD before the clear and restores it afterward, ensuring the implicit clear’s draws are not discarded.

Key insight
User-controllable device state (GL_RASTERIZER_DISCARD) silently suppressed an internal security-critical clear operation, while the bookkeeping (MarkAttachmentsAsCleared) unconditionally recorded success — decoupling “marked cleared” from “actually cleared.”

Attack Path

  1. Prime VRAM The attacker uses a secondary GL context (or prior allocations) to leave sensitive/dirty contents in GPU memory that a fresh attachment may alias.
  2. Create uninitialized FBO On the target context, allocate a framebuffer with an integer-format attachment (e.g. RGBA8UI renderbuffer) that requires draw-based lazy clearing.
  3. Enable discard Call glEnable(GL_RASTERIZER_DISCARD) so all subsequent rasterization, including the pending implicit clear, is discarded.
  4. Trigger lazy clear Call glReadPixels (or otherwise read the attachment), invoking ClearUnclearedAttachments, whose discarded draws leave the attachment un-zeroed yet marked as cleared.
  5. Exfiltrate Read back the leaked, uninitialized VRAM contents through the same glReadPixels result.

Impact Assessment

This is an information-disclosure flaw that exposes uninitialized GPU memory to a web page via WebGL, potentially leaking pixels, textures, or other residual data from other contexts, processes, or prior GPU allocations. Because it requires only standard WebGL calls with no memory corruption, it is reliably reachable from untrusted JavaScript in the renderer/GPU sandbox. Its severity is bounded by disclosure (no direct code execution), but leaked cross-origin or cross-process GPU contents can undermine site isolation and aid further exploitation.

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

Audit Directions

  • Client state leaking into internal clears
    Audit every internal/implicit clear, blit, or copy path (e.g. other ClearUnclearedAttachments callers, glClearBuffer fallbacks) for reliance on client-controlled capabilities like GL_RASTERIZER_DISCARD, GL_SCISSOR_TEST, color/stencil masks, or GL_COLOR_WRITEMASK that can silently suppress the operation.
  • Clear-then-mark ordering
    Flag any code that calls MarkAttachmentsAsCleared (or equivalent init-tracking) without verifying the clear actually executed; the mark must be conditioned on effective, un-discarded, un-masked writes.
  • Draw-based initialization under user state
    Review robust-resource-init paths that rely on draw operations rather than glClear, since draws are subject to the full pipeline (rasterizer discard, scissor, masks, viewport) and are easier for a client to neuter than fixed-function clears.
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