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
Sub-componentGPU
Bug ClassUninitialized Memory
Tracker507351786
Fix commitdb872cb9f16e (chromium/src) +210/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
ScopedFramebufferOverride
gpu/command_buffer/service/feature_info.cc
modified
if
gpu/command_buffer/service/feature_info.cc
modified
WebGLDrawBuffersTest
gpu/command_buffer/service/feature_info_unittest.cc
modified
TEST_F
gpu/command_buffer/service/feature_info_unittest.cc
modified
for
gpu/command_buffer/service/feature_info_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/feature_info_unittest.cc
  • gpu/command_buffer/service/feature_info.cc
From db872cb9f16e398dd1000ffb17747bacb46c1d20 Mon Sep 17 00:00:00 2001
From: Tzarial <[email protected]>
Date: Wed, 02 Sep 2026 08:59:38 -0700
Subject: [PATCH] [agy][gpu] Restore FBO bindings after probing

Restore framebuffer bindings after FeatureInfo initialization in
HandleGetRequestableExtensionsCHROMIUM to prevent driver state desync.

FeatureInfo initialization may bind temporary framebuffers via
GL_FRAMEBUFFER while probing driver capabilities, which can leave the
driver's read framebuffer binding pointing at the previous draw
framebuffer.

Fixed: 507351786
Test: gpu_unittests --gtest_filter=*GetRequestableExtensionsCHROMIUMRestoresFramebufferBindings*
Change-Id: I6a790d16501cef47da3a62594106daf49fad8941
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8234559
Commit-Queue: Tzarial <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1690791}
---
diff --git a/gpu/command_buffer/service/feature_info.cc b/gpu/command_buffer/service/feature_info.cc
index d140fcd5..8ee399f 100644
--- a/gpu/command_buffer/service/feature_info.cc
+++ b/gpu/command_buffer/service/feature_info.cc
@@ -58,10 +58,43 @@
   }
 
  private:
-    GLint orig_binding_;
+  GLint orig_binding_;
+};
+
+// Restores both draw and read framebuffer bindings on destruction to prevent
+// driver state desync when GL_READ_FRAMEBUFFER_BINDING is supported.
+// See https://crbug.com/507351786
+class ScopedFramebufferOverride {
+ public:
+  explicit ScopedFramebufferOverride(bool supports_read_framebuffer_binding)
+      : supports_read_framebuffer_binding_(supports_read_framebuffer_binding) {
+    if (supports_read_framebuffer_binding_) {
+      glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw_binding_);
+      glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &read_binding_);
+    } else {
+      glGetIntegerv(GL_FRAMEBUFFER_BINDING, &draw_binding_);
+    }
+  }
+
+  ~ScopedFramebufferOverride() {
+    if (supports_read_framebuffer_binding_) {
+      glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER,
+                           static_cast<GLuint>(draw_binding_));
+      glBindFramebufferEXT(GL_READ_FRAMEBUFFER,
+                           static_cast<GLuint>(read_binding_));
+    } else {
+      glBindFramebufferEXT(GL_FRAMEBUFFER, static_cast<GLuint>(draw_binding_));
+    }
+  }
+
+ private:
+  const bool supports_read_framebuffer_binding_ = false;
+  GLint draw_binding_ = 0;
+  GLint read_binding_ = 0;
 };
 
 bool IsWebGLDrawBuffersSupported(bool webglCompatibilityContext,
+                                 bool supports_read_framebuffer_binding,
                                  GLenum depth_texture_internal_format,
                                  GLenum depth_stencil_texture_internal_format,
                                  GLuint complete_fbo_for_workarounds) {
@@ -74,9 +107,9 @@
     return false;
   }
 
-  GLint fb_binding = 0;
+  ScopedFramebufferOverride scoped_fbo(supports_read_framebuffer_binding);
+
   GLint tex_binding = 0;
-  glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fb_binding);
   glGetIntegerv(GL_TEXTURE_BINDING_2D, &tex_binding);
 
   GLuint fbo;
@@ -163,7 +196,6 @@
   if (complete_fbo_for_workarounds) {
     glBindFramebufferEXT(GL_FRAMEBUFFER, complete_fbo_for_workarounds);
   }
-  glBindFramebufferEXT(GL_FRAMEBUFFER, static_cast<GLuint>(fb_binding));
   glDeleteFramebuffersEXT(1, &fbo);
 
   glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(tex_binding));
@@ -1242,8 +1274,10 @@
       (context_type_ == CONTEXT_TYPE_OPENGLES2 ||
        (context_type_ == CONTEXT_TYPE_WEBGL1 &&
         IsWebGLDrawBuffersSupported(
-            is_webgl_compatibility_context, depth_texture_format,
-            depth_stencil_texture_format, complete_fbo_for_workarounds)));
+            is_webgl_compatibility_context,
+            validators_.g_l_state.IsValid(GL_READ_FRAMEBUFFER_BINDING),
+            depth_texture_format, depth_stencil_texture_format,
+            complete_fbo_for_workarounds)));
   if (have_es2_draw_buffers) {
     AddExtensionString("GL_EXT_draw_buffers");
     feature_flags_.ext_draw_buffers = true;
diff --git a/gpu/command_buffer/service/feature_info_unittest.cc b/gpu/command_buffer/service/feature_info_unittest.cc
index 78619e0..df03cbf2 100644
--- a/gpu/command_buffer/service/feature_info_unittest.cc
+++ b/gpu/command_buffer/service/feature_info_unittest.cc
@@ -1385,5 +1385,175 @@
   EXPECT_TRUE(info_->feature_flags().mesa_framebuffer_flip_y);
 }
 
+class WebGLDrawBuffersTest : public GpuServiceTest {
+ public:
+  WebGLDrawBuffersTest() = default;
+  ~WebGLDrawBuffersTest() override = default;
+
+  void SetUp() override {
+    SetUpWithGLVersion("OpenGL ES 3.0", "GL_EXT_draw_buffers");
+  }
+};
+
+TEST_F(WebGLDrawBuffersTest, RestoresFramebufferBindings) {
+  const char* extensions = "GL_EXT_draw_buffers";
+  const char* version = "OpenGL ES 3.0";
+
+  InSequence sequence;
+  EXPECT_CALL(*gl_, GetString(GL_EXTENSIONS))
+      .WillOnce(Return(reinterpret_cast<const uint8_t*>(extensions)))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetString(GL_VERSION))
+      .WillOnce(Return(reinterpret_cast<const uint8_t*>(version)))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetString(GL_RENDERER))
+      .WillOnce(Return(reinterpret_cast<const uint8_t*>("")))
+      .RetiresOnSaturation();
+
+  EXPECT_CALL(*gl_, GetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, _))
+      .WillOnce(SetArgPointee<1>(0))
+      .RetiresOnSaturation();
+
+  // IsWebGLDrawBuffersSupported probing expectations:
+  EXPECT_CALL(*gl_, GetIntegerv(GL_MAX_DRAW_BUFFERS, _))
+      .WillOnce(SetArgPointee<1>(8))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetIntegerv(GL_MAX_COLOR_ATTACHMENTS, _))
+      .WillOnce(SetArgPointee<1>(8))
+      .RetiresOnSaturation();
+
+  // ScopedFramebufferOverride queries both draw and read bindings on ES3:
+  const GLuint kDrawFBO = 10;
+  const GLuint kReadFBO = 20;
+  EXPECT_CALL(*gl_, GetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, _))
+      .WillOnce(SetArgPointee<1>(kDrawFBO))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetIntegerv(GL_READ_FRAMEBUFFER_BINDING, _))
+      .WillOnce(SetArgPointee<1>(kReadFBO))
+      .RetiresOnSaturation();
+
+  EXPECT_CALL(*gl_, GetIntegerv(GL_TEXTURE_BINDING_2D, _))
+      .WillOnce(SetArgPointee<1>(0))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GenFramebuffersEXT(1, _))
+      .WillOnce(SetArgPointee<1>(100))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, 100))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  static const auto color_ids = std::to_array<GLuint>({1, 2, 3, 4, 5, 6, 7, 8});
+  EXPECT_CALL(*gl_, GenTextures(8, _))
+      .WillOnce(SetArrayArgument<1>(base::span<const GLuint>(color_ids).begin(),
+                                    base::span<const GLuint>(color_ids).end()))
+      .RetiresOnSaturation();
+  for (int i = 0; i < 8; ++i) {
+    EXPECT_CALL(*gl_, BindTexture(GL_TEXTURE_2D, color_ids[i]))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA,
+                                 GL_UNSIGNED_BYTE, nullptr))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(
+        *gl_, FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i,
+                                      GL_TEXTURE_2D, color_ids[i], 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+        .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+        .RetiresOnSaturation();
+  }
+
+  EXPECT_CALL(*gl_, DeleteFramebuffersEXT(1, _)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindTexture(GL_TEXTURE_2D, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, DeleteTextures(1, _)).Times(2).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, DeleteTextures(8, _)).Times(1).RetiresOnSaturation();
+#if DCHECK_IS_ON()
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
+#endif
+
+  // On destruction, ScopedFramebufferOverride restores both bindings.
+  // See https://crbug.com/507351786
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_DRAW_FRAMEBUFFER, kDrawFBO))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_READ_FRAMEBUFFER, kReadFBO))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  EXPECT_CALL(*gl_, GetIntegerv(GL_MAX_COLOR_ATTACHMENTS_EXT, _))
+      .WillOnce(SetArgPointee<1>(8))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetIntegerv(GL_MAX_DRAW_BUFFERS, _))
+      .WillOnce(SetArgPointee<1>(8))
+      .RetiresOnSaturation();
+
+#if !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_ANDROID)
+#if DCHECK_IS_ON()
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
+#endif
+  static const auto red_tx_ids = std::to_array<GLuint>({101, 102});
+  static const auto red_fb_ids = std::to_array<GLuint>({103, 104});
+  EXPECT_CALL(*gl_, GetIntegerv(GL_FRAMEBUFFER_BINDING, _))
+      .WillOnce(SetArgPointee<1>(red_fb_ids[0]))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetIntegerv(GL_TEXTURE_BINDING_2D, _))
+      .WillOnce(SetArgPointee<1>(red_tx_ids[0]))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GenTextures(1, _))
+      .WillOnce(SetArrayArgument<1>(
+          base::span<const GLuint>(red_tx_ids).subspan(1u).data(),
+          base::span<const GLuint>(red_tx_ids).subspan(2u).data()))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindTexture(GL_TEXTURE_2D, red_tx_ids[1]))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, _, 8, 8, 0, GL_RED_EXT,
+                               GL_UNSIGNED_BYTE, _))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GenFramebuffersEXT(1, _))
+      .WillOnce(SetArrayArgument<1>(
+          base::span<const GLuint>(red_fb_ids).subspan(1u).data(),
+          base::span<const GLuint>(red_fb_ids).subspan(2u).data()))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, red_fb_ids[1]))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_,
+              FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                      GL_TEXTURE_2D, red_tx_ids[1], 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, DeleteFramebuffersEXT(1, _)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, DeleteTextures(1, _)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, red_fb_ids[0]))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindTexture(GL_TEXTURE_2D, red_tx_ids[0]))
+      .Times(1)
+      .RetiresOnSaturation();
+#if DCHECK_IS_ON()
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
+#endif
+#endif  // !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_ANDROID)
+
+  auto info = base::MakeRefCounted<FeatureInfo>();
+  info->Initialize(CONTEXT_TYPE_WEBGL1, false, DisallowedFeatures());
+  EXPECT_TRUE(info->feature_flags().ext_draw_buffers);
+}
+
 }  // namespace gles2
 }  // namespace gpu
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/feature_info_unittest.cc b/gpu/command_buffer/service/feature_info_unittest.cc
index 78619e0..df03cbf2 100644
--- a/gpu/command_buffer/service/feature_info_unittest.cc
+++ b/gpu/command_buffer/service/feature_info_unittest.cc
@@ -1385,5 +1385,175 @@
   EXPECT_TRUE(info_->feature_flags().mesa_framebuffer_flip_y);
 }
 
+class WebGLDrawBuffersTest : public GpuServiceTest {
+ public:
+  WebGLDrawBuffersTest() = default;
+  ~WebGLDrawBuffersTest() override = default;
+
+  void SetUp() override {
+    SetUpWithGLVersion("OpenGL ES 3.0", "GL_EXT_draw_buffers");
+  }
+};
+
+TEST_F(WebGLDrawBuffersTest, RestoresFramebufferBindings) {
+  const char* extensions = "GL_EXT_draw_buffers";
+  const char* version = "OpenGL ES 3.0";
+
+  InSequence sequence;
+  EXPECT_CALL(*gl_, GetString(GL_EXTENSIONS))
+      .WillOnce(Return(reinterpret_cast<const uint8_t*>(extensions)))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetString(GL_VERSION))
+      .WillOnce(Return(reinterpret_cast<const uint8_t*>(version)))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetString(GL_RENDERER))
+      .WillOnce(Return(reinterpret_cast<const uint8_t*>("")))
+      .RetiresOnSaturation();
+
+  EXPECT_CALL(*gl_, GetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, _))
+      .WillOnce(SetArgPointee<1>(0))
+      .RetiresOnSaturation();
+
+  // IsWebGLDrawBuffersSupported probing expectations:
+  EXPECT_CALL(*gl_, GetIntegerv(GL_MAX_DRAW_BUFFERS, _))
+      .WillOnce(SetArgPointee<1>(8))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetIntegerv(GL_MAX_COLOR_ATTACHMENTS, _))
+      .WillOnce(SetArgPointee<1>(8))
+      .RetiresOnSaturation();
+
+  // ScopedFramebufferOverride queries both draw and read bindings on ES3:
+  const GLuint kDrawFBO = 10;
+  const GLuint kReadFBO = 20;
+  EXPECT_CALL(*gl_, GetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, _))
+      .WillOnce(SetArgPointee<1>(kDrawFBO))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetIntegerv(GL_READ_FRAMEBUFFER_BINDING, _))
+      .WillOnce(SetArgPointee<1>(kReadFBO))
+      .RetiresOnSaturation();
+
+  EXPECT_CALL(*gl_, GetIntegerv(GL_TEXTURE_BINDING_2D, _))
+      .WillOnce(SetArgPointee<1>(0))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GenFramebuffersEXT(1, _))
+      .WillOnce(SetArgPointee<1>(100))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, 100))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  static const auto color_ids = std::to_array<GLuint>({1, 2, 3, 4, 5, 6, 7, 8});
+  EXPECT_CALL(*gl_, GenTextures(8, _))
+      .WillOnce(SetArrayArgument<1>(base::span<const GLuint>(color_ids).begin(),
+                                    base::span<const GLuint>(color_ids).end()))
+      .RetiresOnSaturation();
+  for (int i = 0; i < 8; ++i) {
+    EXPECT_CALL(*gl_, BindTexture(GL_TEXTURE_2D, color_ids[i]))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA,
+                                 GL_UNSIGNED_BYTE, nullptr))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(
+        *gl_, FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i,
+                                      GL_TEXTURE_2D, color_ids[i], 0))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+        .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+        .RetiresOnSaturation();
+  }
+
+  EXPECT_CALL(*gl_, DeleteFramebuffersEXT(1, _)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindTexture(GL_TEXTURE_2D, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, DeleteTextures(1, _)).Times(2).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, DeleteTextures(8, _)).Times(1).RetiresOnSaturation();
+#if DCHECK_IS_ON()
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
+#endif
+
+  // On destruction, ScopedFramebufferOverride restores both bindings.
+  // See https://crbug.com/507351786
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_DRAW_FRAMEBUFFER, kDrawFBO))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_READ_FRAMEBUFFER, kReadFBO))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  EXPECT_CALL(*gl_, GetIntegerv(GL_MAX_COLOR_ATTACHMENTS_EXT, _))
+      .WillOnce(SetArgPointee<1>(8))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetIntegerv(GL_MAX_DRAW_BUFFERS, _))
+      .WillOnce(SetArgPointee<1>(8))
+      .RetiresOnSaturation();
+
+#if !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_ANDROID)
+#if DCHECK_IS_ON()
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
+#endif
+  static const auto red_tx_ids = std::to_array<GLuint>({101, 102});
+  static const auto red_fb_ids = std::to_array<GLuint>({103, 104});
+  EXPECT_CALL(*gl_, GetIntegerv(GL_FRAMEBUFFER_BINDING, _))
+      .WillOnce(SetArgPointee<1>(red_fb_ids[0]))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GetIntegerv(GL_TEXTURE_BINDING_2D, _))
+      .WillOnce(SetArgPointee<1>(red_tx_ids[0]))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GenTextures(1, _))
+      .WillOnce(SetArrayArgument<1>(
+          base::span<const GLuint>(red_tx_ids).subspan(1u).data(),
+          base::span<const GLuint>(red_tx_ids).subspan(2u).data()))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindTexture(GL_TEXTURE_2D, red_tx_ids[1]))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, _, 8, 8, 0, GL_RED_EXT,
+                               GL_UNSIGNED_BYTE, _))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, GenFramebuffersEXT(1, _))
+      .WillOnce(SetArrayArgument<1>(
+          base::span<const GLuint>(red_fb_ids).subspan(1u).data(),
+          base::span<const GLuint>(red_fb_ids).subspan(2u).data()))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, red_fb_ids[1]))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_,
+              FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                      GL_TEXTURE_2D, red_tx_ids[1], 0))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, DeleteFramebuffersEXT(1, _)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, DeleteTextures(1, _)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, red_fb_ids[0]))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindTexture(GL_TEXTURE_2D, red_tx_ids[0]))
+      .Times(1)
+      .RetiresOnSaturation();
+#if DCHECK_IS_ON()
+  EXPECT_CALL(*gl_, GetError())
+      .WillOnce(Return(GL_NO_ERROR))
+      .RetiresOnSaturation();
+#endif
+#endif  // !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_ANDROID)
+
+  auto info = base::MakeRefCounted<FeatureInfo>();
+  info->Initialize(CONTEXT_TYPE_WEBGL1, false, DisallowedFeatures());
+  EXPECT_TRUE(info->feature_flags().ext_draw_buffers);
+}
+
 }  // namespace gles2
 }  // namespace gpu
Loading diff…

Original Bug Report

reported by [email protected]

Uninitialized GPU memory disclosure via FBO desync in GetRequestableExtensionsCHROMIUM

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 state desynchronization in the GLES2 validating command decoder can potentially allow a compromised renderer to bypass the lazy-clear mechanism and read uninitialized GPU memory. The HandleGetRequestableExtensionsCHROMIUM command triggers feature probes that clobber the driver’s GL_READ_FRAMEBUFFER binding without updating internal state tracking.

Affected files:

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

Estimated timestamp from git blame: 2026-02-11

Summary

The GLES2 validating command decoder is vulnerable to a potential uninitialized GPU memory disclosure. The HandleGetRequestableExtensionsCHROMIUM command triggers driver probes that desynchronize the hardware’s GL_READ_FRAMEBUFFER binding from the decoder’s internal state tracking. This desynchronization allows an attacker to bypass the “lazy-clear” mechanism, which is intended to prevent WebGL from reading stale GPU memory from other origins or the system compositor.

Root Cause Analysis

When GLES2DecoderImpl::HandleGetRequestableExtensionsCHROMIUM is called (gpu/command_buffer/service/gles2_cmd_decoder.cc), it allocates a temporary FeatureInfo object and calls InitializeWithCompleteFramebufferForWorkarounds() to probe for supported extensions against the live GL context.

Inside FeatureInfo (gpu/command_buffer/service/feature_info.cc), probes such as IsWebGLDrawBuffersSupported attempt to save and restore the framebuffer binding using the following pattern:

GLint fb_binding = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fb_binding); // On ES3+, this evaluates to GL_DRAW_FRAMEBUFFER_BINDING
// ... probe binds a temporary FBO ...
glBindFramebufferEXT(GL_FRAMEBUFFER, fbo);
// ...
glBindFramebufferEXT(GL_FRAMEBUFFER, static_cast<GLuint>(fb_binding)); // Restores BOTH to the saved DRAW binding

On OpenGL ES 3.0+ drivers, GL_FRAMEBUFFER_BINDING is an alias for GL_DRAW_FRAMEBUFFER_BINDING. When the probe restores the binding using the generic GL_FRAMEBUFFER target, it inadvertently sets both the driver’s GL_DRAW_FRAMEBUFFER and GL_READ_FRAMEBUFFER to the original draw binding.

Because HandleGetRequestableExtensionsCHROMIUM finishes executing without calling RestoreFramebufferBindings(), the hardware’s read binding becomes desynchronized from the decoder’s state tracking. The decoder remains unaware that the driver’s read binding has been overwritten.

Potential Exploitation Path

An attacker with control over a renderer process could potentially exploit this state desynchronization to leak uninitialized GPU memory. Note: Our tooling agent does not have the ability to run code, so these are suggested steps based on static analysis:

  1. Context Creation: Request a CONTEXT_TYPE_WEBGL1 context on a system using a native OpenGL ES 3.0+ driver that supports the GL_EXT_framebuffer_multisample extension. This extension allows separate READ/DRAW bindings in the command buffer validator.
  2. Allocation: Create an uncleared texture (texA) using glTexImage2D with a NULL data pointer. Attach it to fboA (the leak source).
  3. Bypass Creation: Create a cleared texture (texB) using glClear to explicitly initialize its contents. Attach it to fboB.
  4. Binding: Send command buffer IPCs to bind fboA to GL_DRAW_FRAMEBUFFER and fboB to GL_READ_FRAMEBUFFER.
    • Decoder State: DRAW = fboA, READ = fboB.
    • Driver State: DRAW = fboA, READ = fboB.
  5. Clobber: Issue GetRequestableExtensionsCHROMIUM. The internal probes clobber the driver’s read binding to match the draw binding.
    • Decoder State: DRAW = fboA, READ = fboB.
    • Driver State: DRAW = fboA, READ = fboA. (Desynchronized)
  6. Leak: Issue ReadPixels.
    • The decoder’s CheckBoundReadFramebufferValid validates its tracked READ FBO (fboB). Since fboB is cleared, the ClearUnclearedAttachments safety check is successfully bypassed.
    • The decoder calls the driver’s glReadPixels.
    • The driver executes the read using its actual READ binding (fboA), reading the uninitialized contents of texA into renderer-accessible shared memory.

Suggested Fix

Ensure that GLES2DecoderImpl::HandleGetRequestableExtensionsCHROMIUM restores the framebuffer state before returning. This can be achieved by utilizing gl::ScopedFramebufferBinder or explicitly calling RestoreFramebufferBindings() at the end of the handler.

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