Medium chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in GPU
DescriptionUninitialized resource in GPU
ComponentGPU
Bug ClassUninitialized Memory
Tracker506385755
Fix commit08d25878ae2d (chromium/src) +68/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

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

Files Changed

  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
From 08d25878ae2d2a3000654a593824ff03e0a9261a Mon Sep 17 00:00:00 2001
From: Brandon Jones <[email protected]>
Date: Wed, 29 Jul 2026 13:57:52 -0700
Subject: [PATCH] Ensure default framebuffer is bound when clearing

This changes checks to make sure the default framebuffer is bound
to GL_DRAW_FRAMEBUFFER prior to clearing in CheckFramebufferValid
to avoid accidentally clearing a user-bound framebuffer.

Bug: 506385755
Change-Id: Ib4d2494ea32eb10e7c9a953bf8cacaaf21857c2f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8162608
Reviewed-by: Zhenyao Mo <[email protected]>
Commit-Queue: Brandon Jones <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1670566}
---

diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index c768c87d..7360f83 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -4153,6 +4153,12 @@
     if (surfaceless_)
       return false;
     if (backbuffer_needs_clear_bits_) {
+      // glClear and glDrawBuffers operate on GL_DRAW_FRAMEBUFFER, so make
+      // sure the backbuffer is bound there before clearing it.
+      Framebuffer* draw_framebuffer = GetBoundDrawFramebuffer();
+      if (draw_framebuffer) {
+        BindFramebuffer(GL_DRAW_FRAMEBUFFER, GetBackbufferServiceId());
+      }
       api()->glClearColorFn(0, 0, 0, 1.0f);
       state_.SetDeviceColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
       api()->glClearStencilFn(0);
@@ -4182,6 +4188,10 @@
       }
       backbuffer_needs_clear_bits_ = 0;
       RestoreClearState();
+      // Restore any previously bound GL_DRAW_FRAMEBUFFER.
+      if (draw_framebuffer) {
+        BindFramebuffer(GL_DRAW_FRAMEBUFFER, draw_framebuffer->service_id());
+      }
     }
     return true;
   }
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
index fb69f96..949087f 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
@@ -3499,6 +3499,64 @@
             GetAndClearBackbufferClearBitsForTest());
 }
 
+TEST_P(GLES3DecoderTest,
+       InvalidatedDefaultReadFramebufferClearedBeforeCopyTexImage2D) {
+  // Invalidate the color buffer of the default framebuffer.
+  const GLsizei count = 1;
+  GLenum attachments[] = {GL_COLOR_EXT};
+  EXPECT_CALL(*gl_, InvalidateFramebuffer(GL_FRAMEBUFFER, count, _))
+      .Times(1)
+      .RetiresOnSaturation();
+  auto& invalidate_cmd =
+      *GetImmediateAs<cmds::InvalidateFramebufferImmediate>();
+  invalidate_cmd.Init(GL_FRAMEBUFFER, count, attachments);
+  EXPECT_EQ(error::kNoError,
+            ExecuteImmediateCmd(invalidate_cmd, sizeof(attachments)));
+  EXPECT_EQ(GL_NO_ERROR, GetGLError());
+
+  // Bind a client framebuffer as the draw framebuffer; the default
+  // framebuffer remains bound as the read framebuffer.
+  DoBindFramebuffer(GL_DRAW_FRAMEBUFFER, client_framebuffer_id_,
+                    kServiceFramebufferId);
+
+  DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+
+  // CopyTexImage2D reads from the default framebuffer. The decoder must
+  // bind it to GL_DRAW_FRAMEBUFFER while clearing it after invalidation
+  // and restore the client draw framebuffer afterwards.
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, ClearColor(0, 0, 0, 1.0f)).Times(1).RetiresOnSaturation();
+  SetupExpectationsForColorMask(true, true, true, true);
+  EXPECT_CALL(*gl_, ClearStencil(0)).Times(1).RetiresOnSaturation();
+  SetupExpectationsForStencilMask(GLES2Decoder::kDefaultStencilMask,
+                                  GLES2Decoder::kDefaultStencilMask);
+  EXPECT_CALL(*gl_, ClearDepth(1.0f)).Times(1).RetiresOnSaturation();
+  SetupExpectationsForDepthMask(true);
+  SetupExpectationsForEnableDisable(GL_SCISSOR_TEST, false);
+  EXPECT_CALL(*gl_, Clear(GL_COLOR_BUFFER_BIT)).Times(1).RetiresOnSaturation();
+  SetupExpectationsForRestoreClearState(0.0f, 0.0f, 0.0f, 0.0f, 0, 1.0f, false,
+                                        0, 0, kBackBufferWidth,
+                                        kBackBufferHeight);
+  EXPECT_CALL(*gl_,
+              BindFramebufferEXT(GL_DRAW_FRAMEBUFFER, kServiceFramebufferId))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  EXPECT_CALL(*gl_, CopyTexImage2D(GL_TEXTURE_2D, 0, _, 0, 0, 1, 1, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
+  cmds::CopyTexImage2D copy_cmd;
+  copy_cmd.Init(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, 1, 1);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(copy_cmd));
+  EXPECT_EQ(GL_NO_ERROR, GetGLError());
+}
+
 TEST_P(GLES2DecoderTest, DiscardFramebufferEXTUnsupported) {
   const GLenum target = GL_FRAMEBUFFER;
   const GLsizei count = 1;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
index fb69f96..949087f 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
@@ -3499,6 +3499,64 @@
             GetAndClearBackbufferClearBitsForTest());
 }
 
+TEST_P(GLES3DecoderTest,
+       InvalidatedDefaultReadFramebufferClearedBeforeCopyTexImage2D) {
+  // Invalidate the color buffer of the default framebuffer.
+  const GLsizei count = 1;
+  GLenum attachments[] = {GL_COLOR_EXT};
+  EXPECT_CALL(*gl_, InvalidateFramebuffer(GL_FRAMEBUFFER, count, _))
+      .Times(1)
+      .RetiresOnSaturation();
+  auto& invalidate_cmd =
+      *GetImmediateAs<cmds::InvalidateFramebufferImmediate>();
+  invalidate_cmd.Init(GL_FRAMEBUFFER, count, attachments);
+  EXPECT_EQ(error::kNoError,
+            ExecuteImmediateCmd(invalidate_cmd, sizeof(attachments)));
+  EXPECT_EQ(GL_NO_ERROR, GetGLError());
+
+  // Bind a client framebuffer as the draw framebuffer; the default
+  // framebuffer remains bound as the read framebuffer.
+  DoBindFramebuffer(GL_DRAW_FRAMEBUFFER, client_framebuffer_id_,
+                    kServiceFramebufferId);
+
+  DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+
+  // CopyTexImage2D reads from the default framebuffer. The decoder must
+  // bind it to GL_DRAW_FRAMEBUFFER while clearing it after invalidation
+  // and restore the client draw framebuffer afterwards.
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, ClearColor(0, 0, 0, 1.0f)).Times(1).RetiresOnSaturation();
+  SetupExpectationsForColorMask(true, true, true, true);
+  EXPECT_CALL(*gl_, ClearStencil(0)).Times(1).RetiresOnSaturation();
+  SetupExpectationsForStencilMask(GLES2Decoder::kDefaultStencilMask,
+                                  GLES2Decoder::kDefaultStencilMask);
+  EXPECT_CALL(*gl_, ClearDepth(1.0f)).Times(1).RetiresOnSaturation();
+  SetupExpectationsForDepthMask(true);
+  SetupExpectationsForEnableDisable(GL_SCISSOR_TEST, false);
+  EXPECT_CALL(*gl_, Clear(GL_COLOR_BUFFER_BIT)).Times(1).RetiresOnSaturation();
+  SetupExpectationsForRestoreClearState(0.0f, 0.0f, 0.0f, 0.0f, 0, 1.0f, false,
+                                        0, 0, kBackBufferWidth,
+                                        kBackBufferHeight);
+  EXPECT_CALL(*gl_,
+              BindFramebufferEXT(GL_DRAW_FRAMEBUFFER, kServiceFramebufferId))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  EXPECT_CALL(*gl_, CopyTexImage2D(GL_TEXTURE_2D, 0, _, 0, 0, 1, 1, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
+  cmds::CopyTexImage2D copy_cmd;
+  copy_cmd.Init(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, 1, 1);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(copy_cmd));
+  EXPECT_EQ(GL_NO_ERROR, GetGLError());
+}
+
 TEST_P(GLES2DecoderTest, DiscardFramebufferEXTUnsupported) {
   const GLenum target = GL_FRAMEBUFFER;
   const GLsizei count = 1;
Loading diff…

Original Bug Report

reported by [email protected]

GPU info leak via misdirected lazy clear of default framebuffer

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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic flaw in the GLES2 validating decoder allows a compromised renderer to read uninitialized GPU memory. When the default backbuffer requires a lazy clear during a read validation, the clear command mistakenly targets the currently bound draw framebuffer. This allows an attacker to bypass the safety clear and extract stale pixel data from the backbuffer.

Affected files:

  • gpu/command_buffer/service/gles2_cmd_decoder.cc

Estimated timestamp from git blame: 2024-06-13

Summary

A logic flaw exists in GLES2DecoderImpl::CheckFramebufferValid within the GLES2 validating command decoder. When the default framebuffer (backbuffer) is invalidated, the decoder defers clearing it until it is used. However, if the backbuffer is used as a read target while a different Framebuffer Object (FBO) is bound to the draw target, the deferred clear operation incorrectly clears the draw FBO instead of the backbuffer. This leaves the backbuffer uninitialized, allowing an attacker to read potentially sensitive or cross-origin stale GPU memory.

Technical Details

In gpu/command_buffer/service/gles2_cmd_decoder.cc, CheckFramebufferValid handles the deferred clear for the default framebuffer (when framebuffer == nullptr):

  if (!framebuffer) {
    // ... 
    if (backbuffer_needs_clear_bits_) {
      // ...
      api()->glClearFn(backbuffer_needs_clear_bits_); 
      // ...
      backbuffer_needs_clear_bits_ = 0;
    }
    return true;
  }

The function calls api()->glClearFn(), which in OpenGL strictly operates on the currently bound GL_DRAW_FRAMEBUFFER.

In ES3 or WebGL 2 contexts, GL_READ_FRAMEBUFFER and GL_DRAW_FRAMEBUFFER can be bound to different targets. If CheckBoundReadFramebufferValid invokes this logic (e.g., during a glBlitFramebuffer or glReadPixels call) while the backbuffer is bound to GL_READ_FRAMEBUFFER and a user FBO is bound to GL_DRAW_FRAMEBUFFER, the glClear will erroneously target the user FBO. The backbuffer_needs_clear_bits_ flag is then reset to 0, despite the backbuffer remaining uninitialized.

Potential Attacker Steps

Note: Our tooling agent does not have the ability to run code, so the following exploit steps are theoretical, though strongly supported by static analysis of the state machine.

An attacker starting from a compromised renderer could theoretically trigger this vulnerability by sending direct GLES2 IPC commands:

  1. Initialize a WebGL 2 / GLES 3 context to enable separate read/draw framebuffer bindings.
  2. Call glInvalidateFramebuffer(GL_FRAMEBUFFER, ...) targeting the default framebuffer. This sets backbuffer_needs_clear_bits_ and flags the memory as discarded to the GPU driver.
  3. Generate a user FBO and bind it strictly to GL_DRAW_FRAMEBUFFER.
  4. Ensure the default framebuffer is bound to GL_READ_FRAMEBUFFER.
  5. Call glBlitFramebuffer to copy from the default framebuffer to the user FBO.
  6. During validation, the decoder notices the backbuffer needs a clear and calls glClear. Due to the bug, this clears the user FBO instead of the backbuffer, but clears the pending flag.
  7. The blit operation proceeds, copying uninitialized, stale GPU memory from the backbuffer into the user FBO.
  8. The attacker binds the user FBO to GL_READ_FRAMEBUFFER and calls glReadPixels to extract the uninitialized memory into shared memory.

Suggested Fix

In GLES2DecoderImpl::CheckFramebufferValid, the backbuffer clearing block should be updated to ensure the backbuffer is actively bound to GL_DRAW_FRAMEBUFFER before calling glClearFn or glDrawBuffersARBFn.

If the currently bound draw framebuffer is not the backbuffer (e.g., GetBoundDrawFramebuffer() != nullptr), the code should temporarily bind the backbuffer’s service ID to GL_DRAW_FRAMEBUFFER, execute the clear, and then restore the original draw framebuffer binding. This pattern is already correctly implemented for user FBOs in GLES2DecoderImpl::ClearUnclearedAttachments.

Evaluated with Chrome root at commit: a1e33f5848218e21d4a16ae2c1bc94e815c30c7f


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