CVE-2026-106290
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ScopedFramebufferOverridegpu/command_buffer/service/feature_info.cc |
modified | |
ifgpu/command_buffer/service/feature_info.cc |
modified | |
WebGLDrawBuffersTestgpu/command_buffer/service/feature_info_unittest.cc |
modified | |
TEST_Fgpu/command_buffer/service/feature_info_unittest.cc |
modified | |
forgpu/command_buffer/service/feature_info_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/feature_info_unittest.ccgpu/command_buffer/service/feature_info.cc
Patch
From db872cb9f16e398dd1000ffb17747bacb46c1d20 Mon Sep 17 00:00:00 2001
From: Tzarial <zork@google.com>
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 <zork@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
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
Regression Test / PoC
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
Original Bug Report
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.ccgpu/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:
- Context Creation: Request a
CONTEXT_TYPE_WEBGL1context on a system using a native OpenGL ES 3.0+ driver that supports theGL_EXT_framebuffer_multisampleextension. This extension allows separate READ/DRAW bindings in the command buffer validator. - Allocation: Create an uncleared texture (
texA) usingglTexImage2Dwith aNULLdata pointer. Attach it tofboA(the leak source). - Bypass Creation: Create a cleared texture (
texB) usingglClearto explicitly initialize its contents. Attach it tofboB. - Binding: Send command buffer IPCs to bind
fboAtoGL_DRAW_FRAMEBUFFERandfboBtoGL_READ_FRAMEBUFFER.- Decoder State: DRAW =
fboA, READ =fboB. - Driver State: DRAW =
fboA, READ =fboB.
- Decoder State: DRAW =
- 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)
- Decoder State: DRAW =
- Leak: Issue
ReadPixels.- The decoder’s
CheckBoundReadFramebufferValidvalidates its tracked READ FBO (fboB). SincefboBis cleared, theClearUnclearedAttachmentssafety 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 oftexAinto renderer-accessible shared memory.
- The decoder’s
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.