CVE-2026-9917
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/gles2_cmd_decoder.cc |
modified | |
TEST_Pgpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc |
modified | |
forgpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc |
modified | |
ifgpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/gles2_cmd_decoder.ccgpu/command_buffer/tests/gl_clear_framebuffer_unittest.cc
Patch
From 60172394d4995bd549b56af10128db080e59f8ca Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
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 <vasilyt@chromium.org>
Commit-Queue: Vasiliy Telezhnikov <vasilyt@chromium.org>
Auto-Submit: Kenneth Russell <kbr@chromium.org>
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
Regression Test / PoC
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
Original Bug Report
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.ccgpu/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.
- On a platform using the validating command decoder (e.g., Chrome for Android), create a WebGL 2 context.
- Allocate an integer renderbuffer (e.g.,
gl.RGBA8UI) and attach it to a newly created framebuffer’sgl.COLOR_ATTACHMENT0. - Enable rasterizer discard by calling
gl.enable(gl.RASTERIZER_DISCARD). - Trigger a readback operation that forces a lazy clear, such as
gl.readPixels(...)usinggl.RGBA_INTEGERandgl.UNSIGNED_INT. - 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.