CVE-2026-17689
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
RobustResourceInitTestES31src/tests/gl_tests/RobustResourceInitTest.cpp |
modified |
Files Changed
src/libANGLE/renderer/gl/BlitGL.cppsrc/libANGLE/renderer/gl/BlitGL.hsrc/tests/gl_tests/RobustResourceInitTest.cpp
Patch
From ac2ad595b9508f8de80e7c80a36d8ad31c6cf11c Mon Sep 17 00:00:00 2001
From: Stacy Gaikovaia <gaiko@google.com>
Date: Mon, 08 Jun 2026 17:00:37 -0400
Subject: [PATCH] Replace mFunctions->clear in clearRenderbuffer
Bug: angleproject:517045160
Change-Id: Ie38d168d96544e9fd549118cfec9ff03d0e0540d
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7909294
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Stacy Gaikovaia <gaiko@google.com>
---
diff --git a/src/libANGLE/renderer/gl/BlitGL.cpp b/src/libANGLE/renderer/gl/BlitGL.cpp
index 4901801..e4c3c77 100644
--- a/src/libANGLE/renderer/gl/BlitGL.cpp
+++ b/src/libANGLE/renderer/gl/BlitGL.cpp
@@ -1057,6 +1057,89 @@
return angle::Result::Continue;
}
+angle::Result BlitGL::clearAttachment(const gl::Context *context,
+ GLenum attachment,
+ GLenum sizedInternalFormat)
+{
+ const bool isAtLeastES3 = context->getClientVersion() >= gl::ES_3_0;
+ switch (attachment)
+ {
+ case GL_COLOR_ATTACHMENT0:
+ {
+ const gl::InternalFormat &internalFormatInfo =
+ gl::GetSizedInternalFormatInfo(sizedInternalFormat);
+ if (isAtLeastES3)
+ {
+ switch (internalFormatInfo.componentType)
+ {
+ case GL_UNSIGNED_NORMALIZED:
+ case GL_SIGNED_NORMALIZED:
+ case GL_FLOAT:
+ {
+ constexpr GLfloat clearValue[] = {0, 0, 0, 0};
+ ANGLE_GL_TRY(context, mFunctions->clearBufferfv(GL_COLOR, 0, clearValue));
+ }
+ break;
+
+ case GL_INT:
+ {
+ constexpr GLint clearValue[] = {0, 0, 0, 0};
+ ANGLE_GL_TRY(context, mFunctions->clearBufferiv(GL_COLOR, 0, clearValue));
+ }
+ break;
+
+ case GL_UNSIGNED_INT:
+ {
+ constexpr GLuint clearValue[] = {0, 0, 0, 0};
+ ANGLE_GL_TRY(context, mFunctions->clearBufferuiv(GL_COLOR, 0, clearValue));
+ }
+ break;
+
+ default:
+ UNREACHABLE();
+ break;
+ }
+ }
+ else
+ {
+ ANGLE_GL_TRY(context, mFunctions->clear(GL_COLOR_BUFFER_BIT));
+ }
+ }
+ break;
+ case GL_DEPTH_ATTACHMENT:
+ {
+ if (isAtLeastES3)
+ {
+ constexpr GLfloat clearValue[] = {1.0f, 0, 0, 0};
+ ANGLE_GL_TRY(context, mFunctions->clearBufferfv(GL_DEPTH, 0, clearValue));
+ }
+ else
+ {
+ ANGLE_GL_TRY(context, mFunctions->clear(GL_DEPTH_BUFFER_BIT));
+ }
+ }
+ break;
+ case GL_STENCIL_ATTACHMENT:
+ {
+ if (isAtLeastES3)
+ {
+ constexpr GLint clearValue[] = {0, 0, 0, 0};
+ ANGLE_GL_TRY(context, mFunctions->clearBufferiv(GL_STENCIL, 0, clearValue));
+ }
+ else
+ {
+ ANGLE_GL_TRY(context, mFunctions->clear(GL_STENCIL_BUFFER_BIT));
+ }
+ }
+ break;
+ default:
+ UNREACHABLE();
+ break;
+ }
+
+ return angle::Result::Continue;
+}
+
angle::Result BlitGL::clearRenderbuffer(const gl::Context *context,
RenderbufferGL *source,
GLenum sizedInternalFormat)
@@ -1077,8 +1160,8 @@
ANGLE_GL_TRY(context,
mFunctions->framebufferRenderbuffer(
GL_FRAMEBUFFER, bindTarget, GL_RENDERBUFFER, source->getRenderbufferID()));
+ ANGLE_TRY(clearAttachment(context, bindTarget, sizedInternalFormat));
}
- ANGLE_GL_TRY(context, mFunctions->clear(clearMask));
// Unbind
for (GLenum bindTarget : bindTargets)
diff --git a/src/libANGLE/renderer/gl/BlitGL.h b/src/libANGLE/renderer/gl/BlitGL.h
index 632d3a8..885a418 100644
--- a/src/libANGLE/renderer/gl/BlitGL.h
+++ b/src/libANGLE/renderer/gl/BlitGL.h
@@ -146,6 +146,10 @@
const gl::ImageIndex &imageIndex,
bool *clearSucceededOut);
+ angle::Result clearAttachment(const gl::Context *context,
+ GLenum attachment,
+ GLenum sizedInternalFormat);
+
angle::Result clearRenderbuffer(const gl::Context *context,
RenderbufferGL *source,
GLenum sizedInternalFormat);
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 5c54ef1..4f7eede 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -307,6 +307,10 @@
GLenum internalFormatRGBA,
GLenum internalFormatRGB,
GLenum type);
+ template <typename PixelT>
+ void testIntegerRenderbufferInit(GLenum internalFormat, GLenum type);
+ template <typename PixelT>
+ void testFloatRenderbufferInit(GLenum internalFormat, GLenum type);
};
class RobustResourceInitTestES31 : public RobustResourceInitTest
@@ -1985,6 +1989,93 @@
EXPECT_EQ(0, incorrectPixels);
}
+template <typename PixelT>
+void RobustResourceInitTestES3::testIntegerRenderbufferInit(GLenum internalFormat, GLenum type)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ GLRenderbuffer renderbuffer;
+ glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
+ glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kWidth, kHeight);
+ ASSERT_GL_NO_ERROR();
+
+ GLFramebuffer framebuffer;
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
+ ASSERT_GL_NO_ERROR();
+
+ std::array<PixelT, kWidth * kHeight * 4> data;
+ glReadPixels(0, 0, kWidth, kHeight, GL_RGBA_INTEGER, type, data.data());
+ ASSERT_GL_NO_ERROR();
+
+ int incorrectPixels = 0;
+ for (int y = 0; y < kHeight; ++y)
+ {
+ for (int x = 0; x < kWidth; ++x)
+ {
+ int index = (y * kWidth + x) * 4;
+ bool correct = (data[index] == 0 && data[index + 1] == 0 && data[index + 2] == 0 &&
+ data[index + 3] == 0);
+ incorrectPixels += (!correct ? 1 : 0);
+ }
+ }
+
+ EXPECT_EQ(0, incorrectPixels);
+}
+
+template <typename PixelT>
+void RobustResourceInitTestES3::testFloatRenderbufferInit(GLenum internalFormat, GLenum type)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ GLRenderbuffer renderbuffer;
+ glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
+ glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kWidth, kHeight);
+ ASSERT_GL_NO_ERROR();
+
+ GLFramebuffer framebuffer;
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
+ ASSERT_GL_NO_ERROR();
Regression Test / PoC
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 5c54ef1..4f7eede 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -307,6 +307,10 @@
GLenum internalFormatRGBA,
GLenum internalFormatRGB,
GLenum type);
+ template <typename PixelT>
+ void testIntegerRenderbufferInit(GLenum internalFormat, GLenum type);
+ template <typename PixelT>
+ void testFloatRenderbufferInit(GLenum internalFormat, GLenum type);
};
class RobustResourceInitTestES31 : public RobustResourceInitTest
@@ -1985,6 +1989,93 @@
EXPECT_EQ(0, incorrectPixels);
}
+template <typename PixelT>
+void RobustResourceInitTestES3::testIntegerRenderbufferInit(GLenum internalFormat, GLenum type)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ GLRenderbuffer renderbuffer;
+ glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
+ glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kWidth, kHeight);
+ ASSERT_GL_NO_ERROR();
+
+ GLFramebuffer framebuffer;
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
+ ASSERT_GL_NO_ERROR();
+
+ std::array<PixelT, kWidth * kHeight * 4> data;
+ glReadPixels(0, 0, kWidth, kHeight, GL_RGBA_INTEGER, type, data.data());
+ ASSERT_GL_NO_ERROR();
+
+ int incorrectPixels = 0;
+ for (int y = 0; y < kHeight; ++y)
+ {
+ for (int x = 0; x < kWidth; ++x)
+ {
+ int index = (y * kWidth + x) * 4;
+ bool correct = (data[index] == 0 && data[index + 1] == 0 && data[index + 2] == 0 &&
+ data[index + 3] == 0);
+ incorrectPixels += (!correct ? 1 : 0);
+ }
+ }
+
+ EXPECT_EQ(0, incorrectPixels);
+}
+
+template <typename PixelT>
+void RobustResourceInitTestES3::testFloatRenderbufferInit(GLenum internalFormat, GLenum type)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ GLRenderbuffer renderbuffer;
+ glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
+ glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kWidth, kHeight);
+ ASSERT_GL_NO_ERROR();
+
+ GLFramebuffer framebuffer;
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
+ ASSERT_GL_NO_ERROR();
+
+ std::array<PixelT, kWidth * kHeight * 4> data;
+ glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, type, data.data());
+ ASSERT_GL_NO_ERROR();
+
+ int incorrectPixels = 0;
+ for (int y = 0; y < kHeight; ++y)
+ {
+ for (int x = 0; x < kWidth; ++x)
+ {
+ int index = (y * kWidth + x) * 4;
+ bool correct = (data[index] == 0.0f && data[index + 1] == 0.0f &&
+ data[index + 2] == 0.0f && data[index + 3] == 0.0f);
+ incorrectPixels += (!correct ? 1 : 0);
+ }
+ }
+
+ EXPECT_EQ(0, incorrectPixels);
+}
+
+// Test that integer renderbuffers are initialized to zero.
+TEST_P(RobustResourceInitTestES3, RenderbufferInit_IntRGBA8)
+{
+ testIntegerRenderbufferInit<int32_t>(GL_RGBA8I, GL_INT);
+}
+
+// Test that unsigned integer renderbuffers are initialized to zero.
+TEST_P(RobustResourceInitTestES3, RenderbufferInit_UIntRGBA8)
+{
+ testIntegerRenderbufferInit<uint32_t>(GL_RGBA8UI, GL_UNSIGNED_INT);
+}
+
+// Test that floating point renderbuffers are initialized to zero.
+TEST_P(RobustResourceInitTestES3, RenderbufferInit_FloatRGBA32F)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_color_buffer_float"));
+ testFloatRenderbufferInit<GLfloat>(GL_RGBA32F, GL_FLOAT);
+}
+
// Simple tests for integer formats that ANGLE must emulate on D3D11.
TEST_P(RobustResourceInitTestES3, TextureInit_UIntRGB8)
{
Original Bug Report
Potential VRAM leak in ANGLE GL backend via robust initialization of integer attachments
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: In ANGLE’s OpenGL/GLES backend, robust resource initialization of integer-format renderbuffers and textures uses a plain float-based glClear, which conforming drivers ignore. As a result, uninitialized GPU memory is falsely marked as initialized and can potentially be read back by a WebGL context, leading to cross-origin VRAM leaks.
Affected files:
third_party/angle/src/libANGLE/renderer/gl/BlitGL.cppthird_party/angle/src/libANGLE/renderer/gl/RenderbufferGL.cppthird_party/angle/src/libANGLE/renderer/gl/TextureGL.cpp
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
There is a potential cross-origin information disclosure vulnerability in ANGLE’s OpenGL/GLES backend when robust resource initialization is enabled (such as in WebGL2 contexts).
During robust resource initialization, renderbuffers are cleared to zero via RenderbufferGL::initializeContents, which delegates to BlitGL::clearRenderbuffer in third_party/angle/src/libANGLE/renderer/gl/BlitGL.cpp:
angle::Result BlitGL::clearRenderbuffer(const gl::Context *context,
RenderbufferGL *source,
GLenum sizedInternalFormat)
{
...
ANGLE_TRY(PrepareForClear(mStateManager, sizedInternalFormat, &bindTargets, &unbindTargets,
&clearMask));
mStateManager->bindFramebuffer(GL_FRAMEBUFFER, mScratchFBO);
...
ANGLE_GL_TRY(context, mFunctions->clear(clearMask));
...
}
For signed or unsigned integer-format color attachments (e.g., GL_RGBA8UI, GL_RGBA32I), standard glClear(GL_COLOR_BUFFER_BIT) (called via mFunctions->clear) is ineffective because it clears using the floating-point clear color state. Under the OpenGL and OpenGL ES specifications, conforming drivers ignore or do nothing when a standard glClear is executed on integer attachments.
While other parts of ANGLE (such as BlitGL::clearFramebuffer) explicitly check the component type of the attachment and utilize glClearBufferiv/glClearBufferuiv for integer-format attachments, BlitGL::clearRenderbuffer (and similarly BlitGL::clearRenderableTexture at line 953) performs an unconditional float-based glClear.
Because clearRenderbuffer returns success (angle::Result::Continue), ANGLE’s front-end erroneously marks the resource’s initialization state as InitState::Initialized via setInitState(InitState::Initialized) in FramebufferAttachment::initializeContents. Consequently, the uninitialized VRAM remains untouched but is treated as clean, allowing an attacker to read back stale VRAM contents via gl.readPixels.
Potential Trigger Path
Because this analysis was performed statically without running code, the following are potential steps that an attacker could follow to trigger the vulnerability from WebGL2:
- Allocate an integer-format renderbuffer (e.g.,
gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8UI, W, H)), which starts withInitState::MayNeedInit. - Attach the renderbuffer to a custom framebuffer (
gl.framebufferRenderbuffer(...)). - Disable active draw buffers by calling
gl.drawBuffers([gl.NONE])to setenabledDrawBuffersto{}. - Call
gl.readPixels(...)on the color attachment. Because the set of active draw buffers is empty,FramebufferGL::ensureAttachmentsInitializedfalls back to the per-attachment initializer path (FramebufferImpl::ensureAttachmentsInitialized) rather than the fast-path framebuffer clear. - This triggers the per-attachment
initializeContentswhich callsBlitGL::clearRenderbuffer. The ineffectiveglClearis executed but the resource is markedInitialized. gl.readPixelsproceeds, returning raw, uninitialized VRAM contents back to the JavaScript context.
Proposed Fix
In third_party/angle/src/libANGLE/renderer/gl/BlitGL.cpp, refactor BlitGL::clearRenderbuffer and BlitGL::clearRenderableTexture to check the component type of the internal format being cleared. If the attachment is of an integer format (GL_INT or GL_UNSIGNED_INT), use glClearBufferiv or glClearBufferuiv instead of a plain float-based glClear(GL_COLOR_BUFFER_BIT), matching the safe pattern implemented in BlitGL::clearFramebuffer.
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.