CVE-2026-106370
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/gles2_cmd_decoder.cc |
modified |
Files Changed
gpu/command_buffer/service/gles2_cmd_decoder.ccgpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
Patch
From 235c973badccd2475b121ab1474b332df570435b Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <lehoangquyen@chromium.org>
Date: Mon, 14 Sep 2026 07:15:02 -0700
Subject: [PATCH] [validating GL]: Skip driver invalidation on default framebuffer
In GLES2DecoderImpl::InvalidateFramebufferImpl, invalidating the default
framebuffer forwarded the discard/invalidation command to the underlying
GL driver. If subsequent lazy clear operations were dropped (e.g. via
GL_RASTERIZER_DISCARD), the driver's discarded memory could be exposed.
Because the default framebuffer is already zeroed upon creation,
skipping driver-level invalidation ensures that the underlying buffer
never enters an uninitialized state.
Bug: 517033396
Change-Id: I9810ad21a70a24b9144c27692e8518df4d29966b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8397468
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@google.com>
Cr-Commit-Position: refs/heads/main@{#1697239}
---
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index 55e3382..e452317 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -5817,6 +5817,10 @@
}
bool skip_api_call = false;
+ if (!framebuffer) {
+ // Don't invalidate backbuffer.
+ skip_api_call = true;
+ }
if (workarounds().dont_invalidate_incomplete_fbos) {
if (DoCheckFramebufferStatus(target) != GL_FRAMEBUFFER_COMPLETE) {
skip_api_call = 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 46667a1..e3efc70 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
@@ -3455,9 +3455,8 @@
const GLsizei count = 1;
GLenum attachments[] = {GL_COLOR_EXT};
- EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _))
- .Times(1)
- .RetiresOnSaturation();
+ // We skip driver's DiscardFramebuffer call on default fbo.
+ EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _)).Times(0);
auto& cmd = *GetImmediateAs<cmds::DiscardFramebufferEXTImmediate>();
cmd.Init(target, count, attachments);
EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(cmd, sizeof(attachments)));
@@ -3466,9 +3465,7 @@
GetAndClearBackbufferClearBitsForTest());
attachments[0] = GL_DEPTH_EXT;
- EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _))
- .Times(1)
- .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _)).Times(0);
cmd.Init(target, count, attachments);
EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(cmd, sizeof(attachments)));
EXPECT_EQ(GL_NO_ERROR, GetGLError());
@@ -3476,9 +3473,7 @@
GetAndClearBackbufferClearBitsForTest());
attachments[0] = GL_STENCIL_EXT;
- EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _))
- .Times(1)
- .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _)).Times(0);
cmd.Init(target, count, attachments);
EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(cmd, sizeof(attachments)));
EXPECT_EQ(GL_NO_ERROR, GetGLError());
@@ -3487,9 +3482,7 @@
const GLsizei count0 = 3;
const GLenum attachments0[] = {GL_COLOR_EXT, GL_DEPTH_EXT, GL_STENCIL_EXT};
- EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count0, _))
- .Times(1)
- .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count0, _)).Times(0);
cmd.Init(target, count0, attachments0);
EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(cmd, sizeof(attachments0)));
EXPECT_EQ(GL_NO_ERROR, GetGLError());
@@ -3504,9 +3497,8 @@
// 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();
+ // We skip driver's InvalidateFramebuffer call on default fbo.
+ EXPECT_CALL(*gl_, InvalidateFramebuffer(GL_FRAMEBUFFER, count, _)).Times(0);
auto& invalidate_cmd =
*GetImmediateAs<cmds::InvalidateFramebufferImmediate>();
invalidate_cmd.Init(GL_FRAMEBUFFER, count, attachments);
Regression Test / PoC
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 46667a1..e3efc70 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc
@@ -3455,9 +3455,8 @@
const GLsizei count = 1;
GLenum attachments[] = {GL_COLOR_EXT};
- EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _))
- .Times(1)
- .RetiresOnSaturation();
+ // We skip driver's DiscardFramebuffer call on default fbo.
+ EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _)).Times(0);
auto& cmd = *GetImmediateAs<cmds::DiscardFramebufferEXTImmediate>();
cmd.Init(target, count, attachments);
EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(cmd, sizeof(attachments)));
@@ -3466,9 +3465,7 @@
GetAndClearBackbufferClearBitsForTest());
attachments[0] = GL_DEPTH_EXT;
- EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _))
- .Times(1)
- .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _)).Times(0);
cmd.Init(target, count, attachments);
EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(cmd, sizeof(attachments)));
EXPECT_EQ(GL_NO_ERROR, GetGLError());
@@ -3476,9 +3473,7 @@
GetAndClearBackbufferClearBitsForTest());
attachments[0] = GL_STENCIL_EXT;
- EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _))
- .Times(1)
- .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count, _)).Times(0);
cmd.Init(target, count, attachments);
EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(cmd, sizeof(attachments)));
EXPECT_EQ(GL_NO_ERROR, GetGLError());
@@ -3487,9 +3482,7 @@
const GLsizei count0 = 3;
const GLenum attachments0[] = {GL_COLOR_EXT, GL_DEPTH_EXT, GL_STENCIL_EXT};
- EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count0, _))
- .Times(1)
- .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, DiscardFramebufferEXT(target, count0, _)).Times(0);
cmd.Init(target, count0, attachments0);
EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(cmd, sizeof(attachments0)));
EXPECT_EQ(GL_NO_ERROR, GetGLError());
@@ -3504,9 +3497,8 @@
// 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();
+ // We skip driver's InvalidateFramebuffer call on default fbo.
+ EXPECT_CALL(*gl_, InvalidateFramebuffer(GL_FRAMEBUFFER, count, _)).Times(0);
auto& invalidate_cmd =
*GetImmediateAs<cmds::InvalidateFramebufferImmediate>();
invalidate_cmd.Init(GL_FRAMEBUFFER, count, attachments);
Original Bug Report
GL_RASTERIZER_DISCARD bypass of backbuffer lazy-clearing in GLES2 validating decoder
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The validating GLES2 command decoder’s default backbuffer lazy-clear mechanism fails to temporarily disable GL_RASTERIZER_DISCARD. Consequently, if a client enables rasterizer discard, the driver-side clear commands are ignored while the decoder’s internal lazy-clear tracking state is unconditionally disarmed. This could potentially allow a compromised renderer with direct command buffer access to read uninitialized GPU VRAM.
Affected files:
gpu/command_buffer/service/gles2_cmd_decoder.cc
Estimated timestamp from git blame: 2012-12-12
Root Cause Analysis
In GLES2DecoderImpl::CheckFramebufferValid (located in gpu/command_buffer/service/gles2_cmd_decoder.cc), a lazy clear of the default framebuffer (FBO 0) is executed when backbuffer_needs_clear_bits_ is set. While this block neutralizes various state settings (such as the scissor test, window rectangles, color/stencil/depth masks, and the active draw buffer), it never disables GL_RASTERIZER_DISCARD before calling the clear command:
if (!framebuffer) {
if (surfaceless_)
return false;
if (backbuffer_needs_clear_bits_) {
api()->glClearColorFn(0, 0, 0, 1.0f);
state_.SetDeviceColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// ... [sets other states] ...
if (workarounds().gl_clear_broken) {
ClearFramebufferForWorkaround(backbuffer_needs_clear_bits_);
} else {
api()->glClearFn(backbuffer_needs_clear_bits_);
}
// ...
backbuffer_needs_clear_bits_ = 0; // Unconditionally disarmed
RestoreClearState();
}
return true;
}
According to the OpenGL ES 3.0 specification (§3.1): > “When RASTERIZER_DISCARD is enabled, the Clear and ClearBuffer commands are also effectively ignored.”*
If GL_RASTERIZER_DISCARD is enabled, the underlying driver-level clear operation is ignored. However, the decoder still unconditionally resets backbuffer_needs_clear_bits_ to 0, leaving the backbuffer uncleared but marked as cleared. This differs from the user-FBO lazy-clear path in ClearUnclearedAttachments, which properly handles this capability by temporarily disabling and restoring it:
bool rasterizer_discard_enabled = state_.enable_flags.rasterizer_discard;
if (rasterizer_discard_enabled) {
state_.SetDeviceCapabilityState(GL_RASTERIZER_DISCARD, false);
}
// ... [performs clear] ...
if (rasterizer_discard_enabled) {
state_.SetDeviceCapabilityState(GL_RASTERIZER_DISCARD, true);
}
Potential Attack Path
Note: The following steps represent a potential attack sequence. Our tooling does not currently have the capability to execute dynamic proof-of-concept code, but the sequence follows directly from static analysis of the decoder’s state machine.
On platforms using the GLES2 validating decoder (such as Android devices where the validating decoder is still compiled in and active as a fallback), a compromised renderer with direct command buffer access could potentially leak uninitialized VRAM contents as follows:
- Bind Backbuffer: Bind the default framebuffer (
BindFramebuffer(GL_FRAMEBUFFER, 0)). - Arm Lazy-Clear: Invalidate the default framebuffer color buffer (
InvalidateFramebuffer(GL_FRAMEBUFFER, 1, {GL_COLOR_EXT})), which setsbackbuffer_needs_clear_bits_ |= GL_COLOR_BUFFER_BITinside the decoder. - Enable Discard: Enable rasterizer discard (
Enable(GL_RASTERIZER_DISCARD)), which updatesstate_.enable_flags.rasterizer_discardtotrueand propagates the state to the driver. - Trigger Lazy-Clear Bypass: Issue any command that triggers draw validation (e.g.,
Clear(GL_COLOR_BUFFER_BIT)). The decoder entersCheckFramebufferValid, executes the driver-level no-op clear (which is ignored due toGL_RASTERIZER_DISCARD), and unconditionally resetsbackbuffer_needs_clear_bits_to0. - Disable Discard: Disable discard (
Disable(GL_RASTERIZER_DISCARD)). - Read Back VRAM: Issue
ReadPixelson FBO 0. Sincebackbuffer_needs_clear_bits_is already0, the validation checks pass without executing a clear, and the driver returns the uninitialized/recycled VRAM contents corresponding to the default framebuffer.
Security Impact
This results in a potential uninitialized GPU memory disclosure in the GPU process. Because the GPU process is shared across origins, a compromised renderer could exploit this vulnerability to read uninitialized memory from the GPU’s memory allocator, potentially disclosing cross-origin pixel data from other browser tabs or processes.
Suggested Fix
In GLES2DecoderImpl::CheckFramebufferValid (gpu/command_buffer/service/gles2_cmd_decoder.cc), temporarily disable GL_RASTERIZER_DISCARD during the lazy clear of the default framebuffer if it is enabled, and restore it afterward, mirroring the logic used in ClearUnclearedAttachments:
if (backbuffer_needs_clear_bits_) {
bool rasterizer_discard_enabled = state_.enable_flags.rasterizer_discard;
if (rasterizer_discard_enabled) {
state_.SetDeviceCapabilityState(GL_RASTERIZER_DISCARD, false);
}
// ... [existing clear logic] ...
if (rasterizer_discard_enabled) {
state_.SetDeviceCapabilityState(GL_RASTERIZER_DISCARD, true);
}
backbuffer_needs_clear_bits_ = 0;
RestoreClearState();
}
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.